From f83631250e0b21f9a6669f436443547aee04c0f8 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Thu, 15 Feb 2024 10:30:03 +0100 Subject: [PATCH 01/14] TASK: Introduce `LegacyNodePathNormalizer` Converts legacy paths like "/absolute/path", "~/site-relative/path" and "~" to the corresponding AbsoluteNodePath depending on the passed base node. The following syntax is not implemented and handled here: - node:// - //my-site/main - some/relative/path Also while legacy and previously allowed, node path traversal like ./neos/info or ../foo/../../bar is not handled. --- .../Utility/LegacyNodePathNormalizer.php | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 Neos.Neos/Classes/Utility/LegacyNodePathNormalizer.php diff --git a/Neos.Neos/Classes/Utility/LegacyNodePathNormalizer.php b/Neos.Neos/Classes/Utility/LegacyNodePathNormalizer.php new file mode 100644 index 00000000000..baf3f9cb444 --- /dev/null +++ b/Neos.Neos/Classes/Utility/LegacyNodePathNormalizer.php @@ -0,0 +1,78 @@ +/my-site/main + * - some/relative/path + * + * Also while legacy and previously allowed, node path traversal like ./neos/info or ../foo/../../bar is not handled. + */ + public function resolveLegacyPathSyntaxToAbsoluteNodePath( + string $path, + Node $baseNode + ): ?AbsoluteNodePath { + if (str_contains($path, '../') || str_contains($path, './')) { + throw new \InvalidArgumentException(sprintf('NodePath traversal via /../ is not allowed. Got: "%s"', $path), 1707732065); + } + + if (!str_starts_with($path, '~') && !str_starts_with($path, '/')) { + return null; + } + + $subgraph = $this->contentRepositoryRegistry->subgraphForNode($baseNode); + + $siteNode = $subgraph->findClosestNode($baseNode->aggregateId, FindClosestNodeFilter::create(nodeTypes: NodeTypeNameFactory::NAME_SITE)); + if ($siteNode === null) { + throw new \RuntimeException(sprintf( + 'Failed to determine site node for aggregate node "%s" in workspace "%s" and dimension "%s"', + $baseNode->aggregateId->value, + $subgraph->getWorkspaceName()->value, + $subgraph->getDimensionSpacePoint()->toJson() + ), 1601366598); + } + if ($siteNode->name === null) { + throw new \RuntimeException(sprintf( + 'Site node "%s" does not have a node name', + $siteNode->aggregateId->value, + ), 1719947246); + } + if ($path === '~') { + return AbsoluteNodePath::fromRootNodeTypeNameAndRelativePath( + NodeTypeNameFactory::forSites(), + NodePath::fromNodeNames($siteNode->name) + ); + } else { + return AbsoluteNodePath::fromRootNodeTypeNameAndRelativePath( + NodeTypeNameFactory::forSites(), + NodePath::fromPathSegments( + [$siteNode->name->value, ...explode('/', substr($path, 1))] + ) + ); + } + } +} From e21b037702905a534fb78dec154d3faea83aa9b2 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Tue, 2 Jul 2024 22:27:35 +0200 Subject: [PATCH 02/14] TASK: Introduce `NodeAddressNormalizer::resolveNodeAddressFromPath` --- .../Utility/LegacyNodePathNormalizer.php | 57 +++++++----- .../Classes/Utility/NodeAddressNormalizer.php | 69 ++++++++++++++ .../ViewHelpers/Link/NodeViewHelper.php | 84 +++++------------ .../ViewHelpers/Uri/NodeViewHelper.php | 89 +++++-------------- 4 files changed, 142 insertions(+), 157 deletions(-) create mode 100644 Neos.Neos/Classes/Utility/NodeAddressNormalizer.php diff --git a/Neos.Neos/Classes/Utility/LegacyNodePathNormalizer.php b/Neos.Neos/Classes/Utility/LegacyNodePathNormalizer.php index baf3f9cb444..1d55de7d13a 100644 --- a/Neos.Neos/Classes/Utility/LegacyNodePathNormalizer.php +++ b/Neos.Neos/Classes/Utility/LegacyNodePathNormalizer.php @@ -21,7 +21,7 @@ final class LegacyNodePathNormalizer protected ContentRepositoryRegistry $contentRepositoryRegistry; /** - * Converts legacy paths like "/absolute/path", "~/site-relative/path" and "~" to the corresponding + * Converts legacy paths like "/sites/site/absolute/path", "~/site-relative/path" and "~" to the corresponding * AbsoluteNodePath depending on the passed base node. * * The following syntax is not implemented and handled here: @@ -32,7 +32,7 @@ final class LegacyNodePathNormalizer * * Also while legacy and previously allowed, node path traversal like ./neos/info or ../foo/../../bar is not handled. */ - public function resolveLegacyPathSyntaxToAbsoluteNodePath( + public function tryResolveLegacyPathSyntaxToAbsoluteNodePath( string $path, Node $baseNode ): ?AbsoluteNodePath { @@ -40,33 +40,39 @@ public function resolveLegacyPathSyntaxToAbsoluteNodePath( throw new \InvalidArgumentException(sprintf('NodePath traversal via /../ is not allowed. Got: "%s"', $path), 1707732065); } - if (!str_starts_with($path, '~') && !str_starts_with($path, '/')) { - return null; - } - - $subgraph = $this->contentRepositoryRegistry->subgraphForNode($baseNode); + $isSiteRelative = str_starts_with($path, '~'); + $isLegacyAbsolute = str_starts_with($path, '/'); - $siteNode = $subgraph->findClosestNode($baseNode->aggregateId, FindClosestNodeFilter::create(nodeTypes: NodeTypeNameFactory::NAME_SITE)); - if ($siteNode === null) { - throw new \RuntimeException(sprintf( - 'Failed to determine site node for aggregate node "%s" in workspace "%s" and dimension "%s"', - $baseNode->aggregateId->value, - $subgraph->getWorkspaceName()->value, - $subgraph->getDimensionSpacePoint()->toJson() - ), 1601366598); + if ($isLegacyAbsolute && !str_starts_with($path, '/sites/')) { + throw new \InvalidArgumentException(sprintf('Legacy absolute paths are only supported when starting with "/sites" like "/sites/my-site". Got: "%s"', $path), 1719949067); } - if ($siteNode->name === null) { - throw new \RuntimeException(sprintf( - 'Site node "%s" does not have a node name', - $siteNode->aggregateId->value, - ), 1719947246); - } - if ($path === '~') { + + if ($isLegacyAbsolute) { + $pathWithoutSitesRoot = substr($path, strlen('/sites/')); return AbsoluteNodePath::fromRootNodeTypeNameAndRelativePath( NodeTypeNameFactory::forSites(), - NodePath::fromNodeNames($siteNode->name) + NodePath::fromString($pathWithoutSitesRoot) ); - } else { + } + + if ($isSiteRelative) { + $subgraph = $this->contentRepositoryRegistry->subgraphForNode($baseNode); + + $siteNode = $subgraph->findClosestNode($baseNode->aggregateId, FindClosestNodeFilter::create(nodeTypes: NodeTypeNameFactory::NAME_SITE)); + if ($siteNode === null) { + throw new \RuntimeException(sprintf( + 'Failed to determine site node for aggregate node "%s" in workspace "%s" and dimension "%s"', + $baseNode->aggregateId->value, + $subgraph->getWorkspaceName()->value, + $subgraph->getDimensionSpacePoint()->toJson() + ), 1601366598); + } + if ($siteNode->name === null) { + throw new \RuntimeException(sprintf( + 'Site node "%s" does not have a node name', + $siteNode->aggregateId->value, + ), 1719947246); + } return AbsoluteNodePath::fromRootNodeTypeNameAndRelativePath( NodeTypeNameFactory::forSites(), NodePath::fromPathSegments( @@ -74,5 +80,8 @@ public function resolveLegacyPathSyntaxToAbsoluteNodePath( ) ); } + + // not a legacy absolute node path + return null; } } diff --git a/Neos.Neos/Classes/Utility/NodeAddressNormalizer.php b/Neos.Neos/Classes/Utility/NodeAddressNormalizer.php new file mode 100644 index 00000000000..94c96439cac --- /dev/null +++ b/Neos.Neos/Classes/Utility/NodeAddressNormalizer.php @@ -0,0 +1,69 @@ +withAggregateId( + NodeAggregateId::fromString(substr($path, strlen('node://'))) + ); + } + + $subgraph = $this->contentRepositoryRegistry->subgraphForNode($documentNode); + + if (is_string($path) && AbsoluteNodePath::patternIsMatchedByString($path)) { + $path = AbsoluteNodePath::fromString($path); + } + if ($path instanceof AbsoluteNodePath) { + $targetNode = $subgraph->findNodeByAbsolutePath($path); + if ($targetNode === null) { + throw new \RuntimeException(sprintf( + 'Node on absolute path "%s" could not be found in workspace "%s" and dimension %s', + $path->serializeToString(), + $subgraph->getWorkspaceName()->value, + $subgraph->getDimensionSpacePoint()->toJson() + ), 1719950354); + } + return NodeAddress::fromNode($targetNode); + } + + if (is_string($path)) { + $path = NodePath::fromString($path); + } + $targetNode = $subgraph->findNodeByPath($path, $documentNode->aggregateId); + + if ($targetNode === null) { + throw new \RuntimeException(sprintf( + 'Node on path "%s" could not be found for base node "%s" in workspace "%s" and dimension %s', + $path->serializeToString(), + $documentNode->aggregateId->value, + $subgraph->getWorkspaceName()->value, + $subgraph->getDimensionSpacePoint()->toJson() + ), 1719950342); + } + return NodeAddress::fromNode($targetNode); + } +} diff --git a/Neos.Neos/Classes/ViewHelpers/Link/NodeViewHelper.php b/Neos.Neos/Classes/ViewHelpers/Link/NodeViewHelper.php index d7990646735..5cefbe1a84c 100644 --- a/Neos.Neos/Classes/ViewHelpers/Link/NodeViewHelper.php +++ b/Neos.Neos/Classes/ViewHelpers/Link/NodeViewHelper.php @@ -14,12 +14,8 @@ namespace Neos\Neos\ViewHelpers\Link; -use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindClosestNodeFilter; 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\Log\ThrowableStorageInterface; @@ -34,6 +30,8 @@ use Neos\Neos\FrontendRouting\NodeShortcutResolver; use Neos\Neos\FrontendRouting\NodeUriBuilderFactory; use Neos\Neos\FrontendRouting\Options; +use Neos\Neos\Utility\LegacyNodePathNormalizer; +use Neos\Neos\Utility\NodeAddressNormalizer; use Neos\Neos\Utility\NodeTypeWithFallbackProvider; /** @@ -152,6 +150,18 @@ class NodeViewHelper extends AbstractTagBasedViewHelper */ protected $nodeUriBuilderFactory; + /** + * @Flow\Inject + * @var NodeAddressNormalizer + */ + protected $nodeAddressNormalizer; + + /** + * @Flow\Inject + * @var LegacyNodePathNormalizer + */ + protected $legacyNodePathNormalizer; + /** * @Flow\Inject * @var NodeLabelGeneratorInterface @@ -252,9 +262,13 @@ public function render(): string if ($node instanceof Node) { $nodeAddress = NodeAddress::fromNode($node); } elseif (is_string($node)) { + /* @var Node $documentNode */ $documentNode = $this->getContextVariable('documentNode'); - assert($documentNode instanceof Node); - $nodeAddress = $this->resolveNodeAddressFromString($node, $documentNode); + $possibleAbsoluteNodePath = $this->legacyNodePathNormalizer->tryResolveLegacyPathSyntaxToAbsoluteNodePath($node, $documentNode); + $nodeAddress = $this->nodeAddressNormalizer->resolveNodeAddressFromPath( + $possibleAbsoluteNodePath ?? $node, + $documentNode + ); $node = $documentNode; } else { throw new ViewHelperException(sprintf( @@ -338,62 +352,4 @@ public function render(): string $this->tag->forceClosingTag(true); return $this->tag->render(); } - - /** - * Converts strings like "relative/path", "/absolute/path", "~/site-relative/path" - * and "~" to the corresponding NodeAddress - * - * @param string $path - * @throws ViewHelperException - */ - 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->withAggregateId( - NodeAggregateId::fromString(\mb_substr($path, 7)) - ); - } - $subgraph = $contentRepository->getContentGraph($documentNodeAddress->workspaceName)->getSubgraph( - $documentNodeAddress->dimensionSpacePoint, - VisibilityConstraints::withoutRestrictions() - ); - if (strncmp($path, '~', 1) === 0) { - $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->aggregateId->value, - $subgraph->getWorkspaceName()->value, - $subgraph->getDimensionSpacePoint()->toJson() - ), 1601366598); - } - if ($path === '~') { - $targetNode = $siteNode; - } else { - $targetNode = $subgraph->findNodeByPath( - NodePath::fromString(substr($path, 1)), - $siteNode->aggregateId - ); - } - } else { - $targetNode = $subgraph->findNodeByPath( - NodePath::fromString($path), - $documentNode->aggregateId - ); - } - if ($targetNode === null) { - throw new ViewHelperException(sprintf( - 'Node on path "%s" could not be found for aggregate node "%s" in workspace "%s" and dimension %s', - $path, - $documentNodeAddress->aggregateId->value, - $subgraph->getWorkspaceName()->value, - $subgraph->getDimensionSpacePoint()->toJson() - ), 1601311789); - } - return $documentNodeAddress->withAggregateId($targetNode->aggregateId); - } } diff --git a/Neos.Neos/Classes/ViewHelpers/Uri/NodeViewHelper.php b/Neos.Neos/Classes/ViewHelpers/Uri/NodeViewHelper.php index 79e7e997a26..211a5927388 100644 --- a/Neos.Neos/Classes/ViewHelpers/Uri/NodeViewHelper.php +++ b/Neos.Neos/Classes/ViewHelpers/Uri/NodeViewHelper.php @@ -14,12 +14,8 @@ namespace Neos\Neos\ViewHelpers\Uri; -use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindClosestNodeFilter; 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\Log\ThrowableStorageInterface; @@ -27,9 +23,10 @@ use Neos\FluidAdaptor\Core\ViewHelper\AbstractViewHelper; 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\Options; +use Neos\Neos\Utility\LegacyNodePathNormalizer; +use Neos\Neos\Utility\NodeAddressNormalizer; /** * A view helper for creating URIs pointing to nodes. @@ -47,8 +44,6 @@ * The given path is treated as a path relative to the current node. * Examples: given that the current node is ``/sites/acmecom/products/``, * ``stapler`` results in ``/sites/acmecom/products/stapler``, - * ``../about`` results in ``/sites/acmecom/about/``, - * ``./neos/info`` results in ``/sites/acmecom/products/neos/info``. * * *``node`` starts with a tilde character (``~``):* * The given path is treated as a path relative to the current site node. @@ -121,6 +116,17 @@ class NodeViewHelper extends AbstractViewHelper */ protected $nodeUriBuilderFactory; + /** + * @Flow\Inject + * @var NodeAddressNormalizer + */ + protected $nodeAddressNormalizer; + + /** + * @Flow\Inject + * @var LegacyNodePathNormalizer + */ + protected $legacyNodePathNormalizer; /** * Initialize arguments @@ -196,13 +202,16 @@ public function render(): string $node = $this->getContextVariable($this->arguments['baseNodeName']); } - /* @var Node $documentNode */ - $documentNode = $this->getContextVariable('documentNode'); - if ($node instanceof Node) { $nodeAddress = NodeAddress::fromNode($node); } elseif (is_string($node)) { - $nodeAddress = $this->resolveNodeAddressFromString($node, $documentNode); + /* @var Node $documentNode */ + $documentNode = $this->getContextVariable('documentNode'); + $possibleAbsoluteNodePath = $this->legacyNodePathNormalizer->tryResolveLegacyPathSyntaxToAbsoluteNodePath($node, $documentNode); + $nodeAddress = $this->nodeAddressNormalizer->resolveNodeAddressFromPath( + $possibleAbsoluteNodePath ?? $node, + $documentNode + ); } else { throw new ViewHelperException(sprintf( 'The "node" argument can only be a string or an instance of %s. Given: %s', @@ -238,62 +247,4 @@ public function render(): string } return (string)$uri; } - - /** - * Converts strings like "relative/path", "/absolute/path", "~/site-relative/path" and "~" - * to the corresponding NodeAddress - * - * @param string $path - * @throws ViewHelperException - */ - 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->withAggregateId( - NodeAggregateId::fromString(\mb_substr($path, 7)) - ); - } - $subgraph = $contentRepository->getContentGraph($documentNodeAddress->workspaceName)->getSubgraph( - $documentNodeAddress->dimensionSpacePoint, - VisibilityConstraints::withoutRestrictions() - ); - if (strncmp($path, '~', 1) === 0) { - $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->aggregateId->value, - $subgraph->getWorkspaceName()->value, - $subgraph->getDimensionSpacePoint()->toJson() - ), 1601366598); - } - if ($path === '~') { - $targetNode = $siteNode; - } else { - $targetNode = $subgraph->findNodeByPath( - NodePath::fromString(substr($path, 1)), - $siteNode->aggregateId - ); - } - } else { - $targetNode = $subgraph->findNodeByPath( - NodePath::fromString($path), - $documentNode->aggregateId - ); - } - if ($targetNode === null) { - throw new ViewHelperException(sprintf( - 'Node on path "%s" could not be found for aggregate node "%s" in workspace "%s" and dimension %s', - $path, - $documentNodeAddress->aggregateId->value, - $subgraph->getWorkspaceName()->value, - $subgraph->getDimensionSpacePoint()->toJson() - ), 1601311789); - } - return $documentNodeAddress->withAggregateId($targetNode->aggregateId); - } } From 33c62773944e5c6f483fab0dc27c2798d0b7364b Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Tue, 2 Jul 2024 22:38:04 +0200 Subject: [PATCH 03/14] TASK: Simplify `NodeViewHelper` Previously the `linkedNode` would contain the node the shortcut points to actually. But as this is not the Neos 8.3 behaviour and introduces complexity that should be handled by the uri builder it will be removed again. --- .../ViewHelpers/Link/NodeViewHelper.php | 33 ++----------------- 1 file changed, 3 insertions(+), 30 deletions(-) diff --git a/Neos.Neos/Classes/ViewHelpers/Link/NodeViewHelper.php b/Neos.Neos/Classes/ViewHelpers/Link/NodeViewHelper.php index 5cefbe1a84c..90a183b5721 100644 --- a/Neos.Neos/Classes/ViewHelpers/Link/NodeViewHelper.php +++ b/Neos.Neos/Classes/ViewHelpers/Link/NodeViewHelper.php @@ -24,10 +24,6 @@ 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\NodeUriBuilderFactory; use Neos\Neos\FrontendRouting\Options; use Neos\Neos\Utility\LegacyNodePathNormalizer; @@ -132,12 +128,6 @@ class NodeViewHelper extends AbstractTagBasedViewHelper */ protected $tagName = 'a'; - /** - * @Flow\Inject - * @var NodeShortcutResolver - */ - protected $nodeShortcutResolver; - /** * @Flow\Inject * @var ThrowableStorageInterface @@ -261,6 +251,7 @@ public function render(): string if ($node instanceof Node) { $nodeAddress = NodeAddress::fromNode($node); + $currentVisibility = $node->visibilityConstraints; } elseif (is_string($node)) { /* @var Node $documentNode */ $documentNode = $this->getContextVariable('documentNode'); @@ -269,7 +260,7 @@ public function render(): string $possibleAbsoluteNodePath ?? $node, $documentNode ); - $node = $documentNode; + $currentVisibility = $documentNode->visibilityConstraints; } else { throw new ViewHelperException(sprintf( 'The "node" argument can only be a string or an instance of %s. Given: %s', @@ -282,7 +273,7 @@ public function render(): string $subgraph = $contentRepository->getContentGraph($nodeAddress->workspaceName) ->getSubgraph( $nodeAddress->dimensionSpacePoint, - $node->visibilityConstraints + $currentVisibility ); $resolvedNode = $subgraph->findNodeById($nodeAddress->aggregateId); @@ -294,24 +285,6 @@ public function render(): string $subgraph->getDimensionSpacePoint()->toJson() ), 1601372444)); } - if ($resolvedNode && $this->getNodeType($resolvedNode)->isOfType(NodeTypeNameFactory::NAME_SHORTCUT)) { - try { - $shortcutNodeAddress = $this->nodeShortcutResolver->resolveShortcutTarget( - $nodeAddress - ); - if ($shortcutNodeAddress instanceof NodeAddress) { - $resolvedNode = $subgraph - ->findNodeById($shortcutNodeAddress->aggregateId); - } - } catch (NodeNotFoundException | InvalidShortcutException $e) { - $this->throwableStorage->logThrowable(new ViewHelperException(sprintf( - 'Failed to resolve shortcut node "%s" in workspace "%s" and dimension %s', - $resolvedNode->aggregateId->value, - $subgraph->getWorkspaceName()->value, - $subgraph->getDimensionSpacePoint()->toJson() - ), 1601370239, $e)); - } - } $nodeUriBuilder = $this->nodeUriBuilderFactory->forActionRequest($this->controllerContext->getRequest()); From 509ea9a7ab0e2f697ecf6e44d38568490caee57f Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Tue, 2 Jul 2024 22:49:04 +0200 Subject: [PATCH 04/14] BUGFIX: Correctly use `baseNodeName` in `NodeViewHelper`'s --- .../ViewHelpers/Link/NodeViewHelper.php | 61 +++++++++---------- .../ViewHelpers/Uri/NodeViewHelper.php | 28 +++++---- 2 files changed, 44 insertions(+), 45 deletions(-) diff --git a/Neos.Neos/Classes/ViewHelpers/Link/NodeViewHelper.php b/Neos.Neos/Classes/ViewHelpers/Link/NodeViewHelper.php index 90a183b5721..26ac1d35462 100644 --- a/Neos.Neos/Classes/ViewHelpers/Link/NodeViewHelper.php +++ b/Neos.Neos/Classes/ViewHelpers/Link/NodeViewHelper.php @@ -244,48 +244,45 @@ public function initializeArguments() */ public function render(): string { + $resolvedNode = null; $node = $this->arguments['node']; - if (!$node instanceof Node) { - $node = $this->getContextVariable($this->arguments['baseNodeName']); - } + if (is_string($node)) { + $baseNode = $this->getContextVariable($this->arguments['baseNodeName']); + if (!$baseNode instanceof Node) { + throw new ViewHelperException(sprintf( + 'If "node" is passed as string a base node in must be set in "%s". Given: %s', + $this->arguments['baseNodeName'], + get_debug_type($baseNode) + ), 1719953186); + } - if ($node instanceof Node) { - $nodeAddress = NodeAddress::fromNode($node); - $currentVisibility = $node->visibilityConstraints; - } elseif (is_string($node)) { - /* @var Node $documentNode */ - $documentNode = $this->getContextVariable('documentNode'); - $possibleAbsoluteNodePath = $this->legacyNodePathNormalizer->tryResolveLegacyPathSyntaxToAbsoluteNodePath($node, $documentNode); + $possibleAbsoluteNodePath = $this->legacyNodePathNormalizer->tryResolveLegacyPathSyntaxToAbsoluteNodePath($node, $baseNode); $nodeAddress = $this->nodeAddressNormalizer->resolveNodeAddressFromPath( $possibleAbsoluteNodePath ?? $node, - $documentNode + $baseNode ); - $currentVisibility = $documentNode->visibilityConstraints; + + $subgraph = $this->contentRepositoryRegistry->subgraphForNode($baseNode); + $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->aggregateId->value, + $subgraph->getWorkspaceName()->value, + $subgraph->getDimensionSpacePoint()->toJson() + ), 1601372444)); + } + + } elseif ($node instanceof Node) { + $nodeAddress = NodeAddress::fromNode($node); + $resolvedNode = $node; } else { throw new ViewHelperException(sprintf( - 'The "node" argument can only be a string or an instance of %s. Given: %s', - Node::class, - is_object($node) ? get_class($node) : gettype($node) + 'The "node" argument can only be a string or an instance of `Node`. Given: %s', + get_debug_type($node) ), 1601372376); } - $contentRepository = $this->contentRepositoryRegistry->get($nodeAddress->contentRepositoryId); - $subgraph = $contentRepository->getContentGraph($nodeAddress->workspaceName) - ->getSubgraph( - $nodeAddress->dimensionSpacePoint, - $currentVisibility - ); - - $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->aggregateId->value, - $subgraph->getWorkspaceName()->value, - $subgraph->getDimensionSpacePoint()->toJson() - ), 1601372444)); - } - $nodeUriBuilder = $this->nodeUriBuilderFactory->forActionRequest($this->controllerContext->getRequest()); $options = $this->arguments['absolute'] ? Options::createForceAbsolute() : Options::createEmpty(); diff --git a/Neos.Neos/Classes/ViewHelpers/Uri/NodeViewHelper.php b/Neos.Neos/Classes/ViewHelpers/Uri/NodeViewHelper.php index 211a5927388..862a507e12d 100644 --- a/Neos.Neos/Classes/ViewHelpers/Uri/NodeViewHelper.php +++ b/Neos.Neos/Classes/ViewHelpers/Uri/NodeViewHelper.php @@ -198,25 +198,27 @@ public function initializeArguments() public function render(): string { $node = $this->arguments['node']; - if (!$node instanceof Node) { - $node = $this->getContextVariable($this->arguments['baseNodeName']); - } + if (is_string($node)) { + $baseNode = $this->getContextVariable($this->arguments['baseNodeName']); + if (!$baseNode instanceof Node) { + throw new ViewHelperException(sprintf( + 'If "node" is passed as string a base node in must be set in "%s". Given: %s', + $this->arguments['baseNodeName'], + get_debug_type($baseNode) + ), 1719953186); + } - if ($node instanceof Node) { - $nodeAddress = NodeAddress::fromNode($node); - } elseif (is_string($node)) { - /* @var Node $documentNode */ - $documentNode = $this->getContextVariable('documentNode'); - $possibleAbsoluteNodePath = $this->legacyNodePathNormalizer->tryResolveLegacyPathSyntaxToAbsoluteNodePath($node, $documentNode); + $possibleAbsoluteNodePath = $this->legacyNodePathNormalizer->tryResolveLegacyPathSyntaxToAbsoluteNodePath($node, $baseNode); $nodeAddress = $this->nodeAddressNormalizer->resolveNodeAddressFromPath( $possibleAbsoluteNodePath ?? $node, - $documentNode + $baseNode ); + } elseif ($node instanceof Node) { + $nodeAddress = NodeAddress::fromNode($node); } else { throw new ViewHelperException(sprintf( - 'The "node" argument can only be a string or an instance of %s. Given: %s', - Node::class, - is_object($node) ? get_class($node) : gettype($node) + 'The "node" argument can only be a string or an instance of `Node`. Given: %s', + get_debug_type($node) ), 1601372376); } From e1b438c94617b273462fc2d8895d501dc2411230 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Wed, 3 Jul 2024 10:50:28 +0200 Subject: [PATCH 05/14] !!! TASK: Remove dead option `resolveShortcuts` from `neos:uri.node` --- Neos.Neos/Classes/ViewHelpers/Uri/NodeViewHelper.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/Neos.Neos/Classes/ViewHelpers/Uri/NodeViewHelper.php b/Neos.Neos/Classes/ViewHelpers/Uri/NodeViewHelper.php index 862a507e12d..278a186c9af 100644 --- a/Neos.Neos/Classes/ViewHelpers/Uri/NodeViewHelper.php +++ b/Neos.Neos/Classes/ViewHelpers/Uri/NodeViewHelper.php @@ -182,14 +182,6 @@ public function initializeArguments() false, 'linkedNode' ); - $this->registerArgument( - 'resolveShortcuts', - 'boolean', - 'INTERNAL Parameter - if false, shortcuts are not redirected to their target.' - . ' Only needed on rare backend occasions when we want to link to the shortcut itself', - false, - true - ); } /** From bf0412943ae2b690b626eb7bfa02138d29c3f9c8 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Wed, 3 Jul 2024 10:51:18 +0200 Subject: [PATCH 06/14] BUGFIX: Reintroduce relative node paths for `Neos.Neos:NodeUri` --- .../Classes/Fusion/NodeUriImplementation.php | 67 ++++++++++--------- 1 file changed, 37 insertions(+), 30 deletions(-) diff --git a/Neos.Neos/Classes/Fusion/NodeUriImplementation.php b/Neos.Neos/Classes/Fusion/NodeUriImplementation.php index 69ad10218f8..32528f5fa5c 100644 --- a/Neos.Neos/Classes/Fusion/NodeUriImplementation.php +++ b/Neos.Neos/Classes/Fusion/NodeUriImplementation.php @@ -25,6 +25,8 @@ use Neos\Fusion\FusionObjects\AbstractFusionObject; use Neos\Neos\FrontendRouting\NodeUriBuilderFactory; use Neos\Neos\FrontendRouting\Options; +use Neos\Neos\Utility\LegacyNodePathNormalizer; +use Neos\Neos\Utility\NodeAddressNormalizer; use Psr\Log\LoggerInterface; /** @@ -51,12 +53,16 @@ class NodeUriImplementation extends AbstractFusionObject protected $nodeUriBuilderFactory; /** - * A node object or a string node path or NULL to resolve the current document node + * @Flow\Inject + * @var NodeAddressNormalizer */ - public function getNode(): Node|string|null - { - return $this->fusionValue('node'); - } + protected $nodeAddressNormalizer; + + /** + * @Flow\Inject + * @var LegacyNodePathNormalizer + */ + protected $legacyNodePathNormalizer; /** * The requested format, for example "html" @@ -103,42 +109,43 @@ public function isAbsolute() * * @return string */ - public function getBaseNodeName() + public function getBaseNodeName(): string { - return $this->fusionValue('baseNodeName'); + return $this->fusionValue('baseNodeName') ?: 'documentNode'; } /** * Render the Uri. * * @return string The rendered URI or NULL if no URI could be resolved for the given node - * @throws \Neos\Flow\Mvc\Routing\Exception\MissingActionNameException */ public function evaluate() { - $baseNode = null; - $baseNodeName = $this->getBaseNodeName() ?: 'documentNode'; - $currentContext = $this->runtime->getCurrentContext(); - if (isset($currentContext[$baseNodeName])) { - $baseNode = $currentContext[$baseNodeName]; + $node = $this->fusionValue('node'); + if (is_string($node)) { + $currentContext = $this->runtime->getCurrentContext(); + $baseNode = $currentContext[$this->getBaseNodeName()] ?? null; + if (!$baseNode instanceof Node) { + throw new \RuntimeException(sprintf( + 'If "node" is passed as string a base node in must be set in "%s". Given: %s', + $this->getBaseNodeName(), + get_debug_type($baseNode) + ), 1719996392); + } + + $possibleAbsoluteNodePath = $this->legacyNodePathNormalizer->tryResolveLegacyPathSyntaxToAbsoluteNodePath($node, $baseNode); + $nodeAddress = $this->nodeAddressNormalizer->resolveNodeAddressFromPath( + $possibleAbsoluteNodePath ?? $node, + $baseNode + ); + } elseif ($node instanceof Node) { + $nodeAddress = NodeAddress::fromNode($node); } else { - 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) { - throw new \RuntimeException(sprintf('Passing node as %s is not supported yet.', get_debug_type($node))); + throw new \RuntimeException(sprintf( + 'The "node" argument can only be a string or an instance of `Node`. Given: %s', + get_debug_type($node) + ), 1719996456); } - /* TODO implement us see https://github.com/neos/neos-development-collection/issues/4524 {@see \Neos\Neos\ViewHelpers\Uri\NodeViewHelper::resolveNodeAddressFromString} for an example implementation - elseif ($node === '~') { - $nodeAddress = $this->nodeAddressFactory->createFromNode($node); - $nodeAddress = $nodeAddress->withNodeAggregateId( - $siteNode->nodeAggregateId - ); - } elseif (is_string($node) && substr($node, 0, 7) === 'node://') { - $nodeAddress = $this->nodeAddressFactory->createFromNode($node); - $nodeAddress = $nodeAddress->withNodeAggregateId( - NodeAggregateId::fromString(\mb_substr($node, 7)) - );*/ $possibleRequest = $this->runtime->fusionGlobals->get('request'); if ($possibleRequest instanceof ActionRequest) { @@ -160,7 +167,7 @@ public function evaluate() } try { - $resolvedUri = $nodeUriBuilder->uriFor(NodeAddress::fromNode($node), $options); + $resolvedUri = $nodeUriBuilder->uriFor($nodeAddress, $options); } catch (NoMatchingRouteException) { // todo log arguments? $this->systemLogger->warning(sprintf('Could not resolve "%s" to a node uri.', $node->aggregateId->value), LogEnvironment::fromMethodName(__METHOD__)); From 7fa5a9fb8546c0f12f33c9874209d55832de9c89 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Wed, 3 Jul 2024 11:59:31 +0200 Subject: [PATCH 07/14] TASK: Deprecate `LinkingService` and correct its logic --- Neos.Neos/Classes/Service/LinkingService.php | 135 +++++++++---------- 1 file changed, 60 insertions(+), 75 deletions(-) diff --git a/Neos.Neos/Classes/Service/LinkingService.php b/Neos.Neos/Classes/Service/LinkingService.php index 6354c69b937..957dda45d9e 100644 --- a/Neos.Neos/Classes/Service/LinkingService.php +++ b/Neos.Neos/Classes/Service/LinkingService.php @@ -17,8 +17,6 @@ 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; @@ -36,7 +34,9 @@ use Neos\Neos\Domain\Model\Site; use Neos\Neos\Domain\Repository\SiteRepository; use Neos\Neos\Exception as NeosException; -use Neos\Neos\FrontendRouting\SiteDetection\SiteDetectionResult; +use Neos\Neos\FrontendRouting\NodeUriBuilder; +use Neos\Neos\Utility\LegacyNodePathNormalizer; +use Neos\Neos\Utility\NodeAddressNormalizer; use Neos\Utility\Arrays; use Psr\Http\Message\UriInterface; use Psr\Log\LoggerInterface; @@ -57,8 +57,6 @@ * The given path is treated as a path relative to the current node. * Examples: given that the current node is ``/sites/acmecom/products/``, * ``stapler`` results in ``/sites/acmecom/products/stapler``, - * ``../about`` results in ``/sites/acmecom/about/``, - * ``./neos/info`` results in ``/sites/acmecom/products/neos/info``. * * *``node`` starts with a tilde character (``~``):* * The given path is treated as a path relative to the current site node. @@ -66,6 +64,7 @@ * ``~/about/us`` results in ``/sites/acmecom/about/us``, * ``~`` results in ``/sites/acmecom``. * + * @deprecated with Neos 9. Please use the new {@see NodeUriBuilder} instead and for resolving a relative node path {@see NodeAddressNormalizer::resolveNodeAddressFromPath()} * @Flow\Scope("singleton") */ class LinkingService @@ -116,6 +115,18 @@ class LinkingService */ protected $baseUriProvider; + /** + * @Flow\Inject + * @var NodeAddressNormalizer + */ + protected $nodeAddressNormalizer; + + /** + * @Flow\Inject + * @var LegacyNodePathNormalizer + */ + protected $legacyNodePathNormalizer; + #[Flow\Inject] protected ContentRepositoryRegistry $contentRepositoryRegistry; @@ -168,17 +179,7 @@ public function resolveNodeUri( ControllerContext $controllerContext, bool $absolute = false ): ?string { - $targetObject = $this->convertUriToObject($uri, $contextNode); - if ($targetObject === null) { - $this->systemLogger->info( - sprintf('Could not resolve "%s" to an existing node; The node was probably deleted.', $uri), - LogEnvironment::fromMethodName(__METHOD__) - ); - - return null; - } - - return $this->createNodeUri($controllerContext, $targetObject, null, null, $absolute); + return $this->createNodeUri($controllerContext, $uri, $contextNode, null, $absolute); } /** @@ -248,9 +249,9 @@ public function convertUriToObject($uri, Node $contextNode = null) * Renders the URI to a given node instance or -path. * * @param ControllerContext $controllerContext - * @param mixed $node A node object or a string node path, + * @param Node|string|null $node A node object or a string node path, * if a relative path is provided the baseNode argument is required - * @param Node $baseNode + * @param Node|null $baseNode * @param string $format Format to use for the URL, for example "html" or "json" * @param boolean $absolute If set, an absolute URI is rendered * @param array $arguments Additional arguments to be passed to the UriBuilder @@ -289,72 +290,56 @@ public function createNodeUri( __METHOD__ )); } - if (!($node instanceof Node || is_string($node) || $baseNode instanceof Node)) { - throw new \InvalidArgumentException( - 'Expected an instance of Node or a string for the node argument,' - . ' or alternatively a baseNode argument.', - 1373101025 - ); - } + $resolvedNode = null; if (is_string($node)) { - $nodeString = $node; - if ($nodeString === '') { - throw new NeosException(sprintf('Empty strings can not be resolved to nodes.'), 1415709942); + if (!$baseNode instanceof Node) { + throw new \RuntimeException('If "node" is passed as string a base node in must be given', 1719999788); } - try { - // if we get a node string, we need to assume it links to the current site - $contentRepositoryId = SiteDetectionResult::fromRequest( - $controllerContext->getRequest()->getHttpRequest() - )->contentRepositoryId; - $contentRepository = $this->contentRepositoryRegistry->get($contentRepositoryId); - $nodeAddress = NodeAddress::fromJsonString($nodeString); - $workspace = $contentRepository->getWorkspaceFinder()->findOneByName($nodeAddress->workspaceName); - $subgraph = $contentRepository->getContentGraph($nodeAddress->workspaceName)->getSubgraph( - $nodeAddress->dimensionSpacePoint, - $workspace && !$workspace->isPublicWorkspace() - ? VisibilityConstraints::withoutRestrictions() - : VisibilityConstraints::frontend() - ); - $node = $subgraph->findNodeById($nodeAddress->aggregateId); - } catch (\Throwable $exception) { - if ($baseNode === null) { - throw new NeosException( - 'The baseNode argument is required for linking to nodes with a relative path.', - 1407879905 - ); - } - $node = $this->contentRepositoryRegistry->subgraphForNode($baseNode) - ->findNodeByPath(NodePath::fromString($nodeString), $baseNode->aggregateId); + + $possibleAbsoluteNodePath = $this->legacyNodePathNormalizer->tryResolveLegacyPathSyntaxToAbsoluteNodePath($node, $baseNode); + $nodeAddress = $this->nodeAddressNormalizer->resolveNodeAddressFromPath( + $possibleAbsoluteNodePath ?? $node, + $baseNode + ); + + $subgraph = $this->contentRepositoryRegistry->subgraphForNode($baseNode); + $resolvedNode = $subgraph->findNodeById($nodeAddress->aggregateId); + if ($resolvedNode === null) { + throw new \RuntimeException(sprintf( + 'Failed to resolve node "%s" (path %s) in workspace "%s" and dimension %s', + $nodeAddress->aggregateId->value, + $node, + $subgraph->getWorkspaceName()->value, + $subgraph->getDimensionSpacePoint()->toJson() + ), 1720000002); } - if (!$node instanceof Node) { - throw new NeosException(sprintf( - 'The string "%s" could not be resolved to an existing node.', - $nodeString - ), 1415709674); + } elseif ($node instanceof Node) { + $nodeAddress = NodeAddress::fromNode($node); + $resolvedNode = $node; + } elseif ($node === null) { + if (!$baseNode instanceof Node) { + throw new \RuntimeException('If "node" is is NULL a base node in must be given', 1719999803); } - } elseif (!$node instanceof Node) { - $node = $baseNode; + $nodeAddress = NodeAddress::fromNode($baseNode); + $resolvedNode = $baseNode; + } else { + throw new \RuntimeException(sprintf( + 'The "node" argument can only be a string or an instance of `Node`. Given: %s', + get_debug_type($node) + ), 1601372376); } - if (!$node instanceof Node) { - throw new NeosException(sprintf( - 'Node must be an instance of Node or string, given "%s".', - gettype($node) - ), 1414772029); - } - $this->lastLinkedNode = $node; + $this->lastLinkedNode = $resolvedNode; + + $contentRepository = $this->contentRepositoryRegistry->get($nodeAddress->contentRepositoryId); + $workspace = $contentRepository->getWorkspaceFinder()->findOneByName($nodeAddress->workspaceName); - $contentRepository = $this->contentRepositoryRegistry->get( - $node->contentRepositoryId - ); - $workspace = $contentRepository->getWorkspaceFinder()->findOneByName( - $node->workspaceName - ); $mainRequest = $controllerContext->getRequest()->getMainRequest(); $uriBuilder = clone $controllerContext->getUriBuilder(); $uriBuilder->setRequest($mainRequest); - $createLiveUri = $workspace && $workspace->isPublicWorkspace() && $node->tags->contain(SubtreeTag::disabled()); + // todo why do we evaluate the hidden state here? We dont do it in the new uri builder. + $createLiveUri = $workspace && $workspace->isPublicWorkspace() && $resolvedNode->tags->contain(SubtreeTag::disabled()); if ($addQueryString === true) { // legacy feature see https://github.com/neos/neos-development-collection/issues/5076 @@ -377,7 +362,7 @@ public function createNodeUri( ->uriFor('preview', [], 'Frontend\Node', 'Neos.Neos'); return (string)UriHelper::uriWithAdditionalQueryParameters( new Uri($previewActionUri), - ['node' => NodeAddress::fromNode($node)->toJson()] + ['node' => $nodeAddress->toJson()] ); } @@ -387,7 +372,7 @@ public function createNodeUri( ->setArguments($arguments) ->setFormat($format ?: $mainRequest->getFormat()) ->setCreateAbsoluteUri($absolute) - ->uriFor('show', ['node' => NodeAddress::fromNode($node)], 'Frontend\Node', 'Neos.Neos'); + ->uriFor('show', ['node' => $nodeAddress], 'Frontend\Node', 'Neos.Neos'); } /** From 4040ae5352a0ba906bb80f9767c3bab3fb73e7df Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Wed, 3 Jul 2024 11:59:53 +0200 Subject: [PATCH 08/14] TASK: Fix `NodeAddressNormalizer` to handle empty paths --- Neos.Neos/Classes/Utility/NodeAddressNormalizer.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Neos.Neos/Classes/Utility/NodeAddressNormalizer.php b/Neos.Neos/Classes/Utility/NodeAddressNormalizer.php index 94c96439cac..36b7de73fca 100644 --- a/Neos.Neos/Classes/Utility/NodeAddressNormalizer.php +++ b/Neos.Neos/Classes/Utility/NodeAddressNormalizer.php @@ -26,6 +26,10 @@ final class NodeAddressNormalizer */ public function resolveNodeAddressFromPath(AbsoluteNodePath|NodePath|string $path, Node $documentNode): NodeAddress { + if ($path === '') { + throw new \RuntimeException('Empty strings can not be resolved to nodes.', 1719999872); + } + if (is_string($path) && str_starts_with($path, 'node://')) { return NodeAddress::fromNode($documentNode)->withAggregateId( NodeAggregateId::fromString(substr($path, strlen('node://'))) From 8295c3b0702367855b4d66559055da0e4ff09e1a Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Wed, 3 Jul 2024 12:00:27 +0200 Subject: [PATCH 09/14] TASK: Improve exceptions --- Neos.Neos/Classes/Fusion/NodeUriImplementation.php | 2 +- Neos.Neos/Classes/ViewHelpers/Link/NodeViewHelper.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Neos.Neos/Classes/Fusion/NodeUriImplementation.php b/Neos.Neos/Classes/Fusion/NodeUriImplementation.php index 32528f5fa5c..8779c742d18 100644 --- a/Neos.Neos/Classes/Fusion/NodeUriImplementation.php +++ b/Neos.Neos/Classes/Fusion/NodeUriImplementation.php @@ -170,7 +170,7 @@ public function evaluate() $resolvedUri = $nodeUriBuilder->uriFor($nodeAddress, $options); } catch (NoMatchingRouteException) { // todo log arguments? - $this->systemLogger->warning(sprintf('Could not resolve "%s" to a node uri.', $node->aggregateId->value), LogEnvironment::fromMethodName(__METHOD__)); + $this->systemLogger->warning(sprintf('Could not resolve "%s" to a node uri.', $nodeAddress->aggregateId->value), LogEnvironment::fromMethodName(__METHOD__)); return ''; } diff --git a/Neos.Neos/Classes/ViewHelpers/Link/NodeViewHelper.php b/Neos.Neos/Classes/ViewHelpers/Link/NodeViewHelper.php index 26ac1d35462..bd715d35ec5 100644 --- a/Neos.Neos/Classes/ViewHelpers/Link/NodeViewHelper.php +++ b/Neos.Neos/Classes/ViewHelpers/Link/NodeViewHelper.php @@ -266,13 +266,13 @@ public function render(): string $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', + 'Failed to resolve node "%s" (path %s) in workspace "%s" and dimension %s', $nodeAddress->aggregateId->value, + $node, $subgraph->getWorkspaceName()->value, $subgraph->getDimensionSpacePoint()->toJson() ), 1601372444)); } - } elseif ($node instanceof Node) { $nodeAddress = NodeAddress::fromNode($node); $resolvedNode = $node; From e7ab19d8a9b63228b21dedf8d2fcd867926697a1 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Wed, 3 Jul 2024 12:52:48 +0200 Subject: [PATCH 10/14] TASK: Simplify LinkHelper and deprecate `resolveNodeUri` --- .../Classes/Fusion/Helper/LinkHelper.php | 127 +++++++----------- 1 file changed, 52 insertions(+), 75 deletions(-) diff --git a/Neos.Neos/Classes/Fusion/Helper/LinkHelper.php b/Neos.Neos/Classes/Fusion/Helper/LinkHelper.php index d6c92994f74..24763cbdc0e 100644 --- a/Neos.Neos/Classes/Fusion/Helper/LinkHelper.php +++ b/Neos.Neos/Classes/Fusion/Helper/LinkHelper.php @@ -16,25 +16,24 @@ 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\Log\Utility\LogEnvironment; use Neos\Flow\Mvc\Controller\ControllerContext; -use Neos\Flow\Mvc\Exception\NoMatchingRouteException; 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\Options; -use Neos\Neos\Fusion\ConvertUrisImplementation; +use Neos\Neos\Service\LinkingService; use Psr\Http\Message\UriInterface; use Psr\Log\LoggerInterface; class LinkHelper implements ProtectedContextAwareInterface { + private const NODE_SCHEME = 'node'; + private const ASSET_SCHEME = 'asset'; + /** * @Flow\Inject * @var LoggerInterface @@ -65,68 +64,46 @@ class LinkHelper implements ProtectedContextAwareInterface */ protected $nodeUriBuilderFactory; + /** + * @Flow\Inject + * @var LinkingService + */ + protected $linkingService; + /** * @param string|Uri $uri * @return boolean */ public function hasSupportedScheme($uri): bool { - return in_array($this->getScheme($uri), ['node', 'asset'], true); + $scheme = $this->getScheme($uri); + return $scheme === self::NODE_SCHEME || $scheme === self::ASSET_SCHEME; } /** * @param string|UriInterface $uri - * @return string */ - public function getScheme($uri): string + public function getScheme($uri): ?string { - if ($uri instanceof UriInterface) { - return $uri->getScheme(); - } - - if (is_string($uri) && preg_match(ConvertUrisImplementation::PATTERN_SUPPORTED_URIS, $uri, $matches) === 1) { - return $matches[1]; - } - - return ''; - } - - public function resolveNodeUri( - string|Uri $uri, - Node $contextNode, - ControllerContext $controllerContext - ): ?string { - $targetNode = $this->convertUriToObject($uri, $contextNode); - if (!$targetNode instanceof Node) { - $this->systemLogger->info( - sprintf( - 'Could not resolve "%s" to an existing node; The node was probably deleted.', - $uri - ), - LogEnvironment::fromMethodName(__METHOD__) - ); + if ($uri === null || $uri === '') { return null; } - - $nodeUriBuilder = $this->nodeUriBuilderFactory->forActionRequest($controllerContext->getRequest()); - - $options = Options::createEmpty(); - $format = $controllerContext->getRequest()->getFormat(); - if ($format && $format !== 'html') { - $options = $options->withCustomFormat($format); - } - try { - $targetUri = $nodeUriBuilder->uriFor(NodeAddress::fromNode($targetNode), $options); - } catch (NoMatchingRouteException $e) { - $this->systemLogger->info(sprintf( - 'Failed to build URI for node "%s": %e', - $targetNode->aggregateId->value, - $e->getMessage() - ), LogEnvironment::fromMethodName(__METHOD__)); - return null; + if (is_string($uri)) { + $uri = new Uri($uri); } + return $uri->getScheme(); + } - return (string)$targetUri; + /** + * @param string|UriInterface $uri + * @param Node $contextNode + * @param ControllerContext $controllerContext + * @return string + * @deprecated with Neos 9 as the linking service is deprecated and this helper cannot be invoked from Fusion either way as the $controllerContext is not available. + */ + public function resolveNodeUri(string|UriInterface $uri, Node $contextNode, ControllerContext $controllerContext) + { + return $this->linkingService->resolveNodeUri($uri, $contextNode, $controllerContext); } public function resolveAssetUri(string|Uri $uri): string @@ -134,9 +111,16 @@ public function resolveAssetUri(string|Uri $uri): string if (!$uri instanceof UriInterface) { $uri = new Uri($uri); } + if ($uri->getScheme() !== self::ASSET_SCHEME) { + throw new \RuntimeException(sprintf( + 'Invalid asset uri "%s" provided. It must start with asset://', + $uri + ), 1720003716); + } + $asset = $this->assetRepository->findByIdentifier($uri->getHost()); if (!$asset instanceof AssetInterface) { - throw new \InvalidArgumentException(sprintf( + throw new \RuntimeException(sprintf( 'Failed to resolve asset from URI "%s", probably the corresponding asset was deleted', $uri ), 1601373937); @@ -151,30 +135,23 @@ public function convertUriToObject( string|Uri $uri, Node $contextNode = null ): Node|AssetInterface|null { - if (empty($uri)) { - return null; - } - if ($uri instanceof UriInterface) { - $uri = (string)$uri; + if (is_string($uri)) { + $uri = new Uri($uri); } - - if (preg_match(ConvertUrisImplementation::PATTERN_SUPPORTED_URIS, $uri, $matches) === 1) { - switch ($matches[1]) { - case 'node': - if ($contextNode === null) { - throw new \RuntimeException( - 'node:// URI conversion requires a context node to be passed', - 1409734235 - ); - } - return $this->contentRepositoryRegistry->subgraphForNode($contextNode) - ->findNodeById(NodeAggregateId::fromString($matches[2])); - case 'asset': - /** @var AssetInterface|null $asset */ - /** @noinspection OneTimeUseVariablesInspection */ - $asset = $this->assetRepository->findByIdentifier($matches[2]); - return $asset; - } + switch ($uri->getScheme()) { + case self::NODE_SCHEME: + if ($contextNode === null) { + throw new \RuntimeException( + sprintf('node:// URI conversion like "%s" requires a context node to be passed', $uri), + 1409734235 + ); + } + return $this->contentRepositoryRegistry->subgraphForNode($contextNode) + ->findNodeById(NodeAggregateId::fromString($uri->getHost())); + case self::ASSET_SCHEME: + /** @var AssetInterface|null $asset */ + $asset = $this->assetRepository->findByIdentifier($uri->getHost()); + return $asset; } return null; } From e770476df46db400b1a3506e5afcd5793f4a24e4 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Wed, 3 Jul 2024 13:01:50 +0200 Subject: [PATCH 11/14] TASK: Use the `LinkHelper` als b/c layer in the `LinkingService` --- Neos.Neos/Classes/Service/LinkingService.php | 122 ++++++------------- 1 file changed, 38 insertions(+), 84 deletions(-) diff --git a/Neos.Neos/Classes/Service/LinkingService.php b/Neos.Neos/Classes/Service/LinkingService.php index 957dda45d9e..673efa4aa63 100644 --- a/Neos.Neos/Classes/Service/LinkingService.php +++ b/Neos.Neos/Classes/Service/LinkingService.php @@ -18,7 +18,6 @@ use Neos\ContentRepository\Core\Feature\SubtreeTagging\Dto\SubtreeTag; 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\Http\BaseUriProvider; @@ -26,15 +25,12 @@ use Neos\Flow\Http\Helper\UriHelper; use Neos\Flow\Log\Utility\LogEnvironment; use Neos\Flow\Mvc\Controller\ControllerContext; -use Neos\Flow\Property\PropertyMapper; -use Neos\Flow\ResourceManagement\ResourceManager; -use Neos\Media\Domain\Model\Asset; use Neos\Media\Domain\Model\AssetInterface; -use Neos\Media\Domain\Repository\AssetRepository; use Neos\Neos\Domain\Model\Site; use Neos\Neos\Domain\Repository\SiteRepository; use Neos\Neos\Exception as NeosException; use Neos\Neos\FrontendRouting\NodeUriBuilder; +use Neos\Neos\Fusion\Helper\LinkHelper; use Neos\Neos\Utility\LegacyNodePathNormalizer; use Neos\Neos\Utility\NodeAddressNormalizer; use Neos\Utility\Arrays; @@ -64,37 +60,11 @@ * ``~/about/us`` results in ``/sites/acmecom/about/us``, * ``~`` results in ``/sites/acmecom``. * - * @deprecated with Neos 9. Please use the new {@see NodeUriBuilder} instead and for resolving a relative node path {@see NodeAddressNormalizer::resolveNodeAddressFromPath()} + * @deprecated with Neos 9. Please use the new {@see NodeUriBuilder} instead and for resolving a relative node path {@see NodeAddressNormalizer::resolveNodeAddressFromPath()} or utilize the {@see LinkHelper} from Fusion * @Flow\Scope("singleton") */ class LinkingService { - /** - * Pattern to match supported URIs. - * - * @var string - */ - public const PATTERN_SUPPORTED_URIS - = '/(node|asset):\/\/([a-z0-9\-]+|([a-f0-9]){8}-([a-f0-9]){4}-([a-f0-9]){4}-([a-f0-9]){4}-([a-f0-9]){12})/'; - - /** - * @Flow\Inject - * @var AssetRepository - */ - protected $assetRepository; - - /** - * @Flow\Inject - * @var ResourceManager - */ - protected $resourceManager; - - /** - * @Flow\Inject - * @var PropertyMapper - */ - protected $propertyMapper; - protected ?Node $lastLinkedNode; /** @@ -127,37 +97,33 @@ class LinkingService */ protected $legacyNodePathNormalizer; + /** + * @Flow\Inject + * @var LinkHelper + */ + protected $newLinkHelper; + #[Flow\Inject] protected ContentRepositoryRegistry $contentRepositoryRegistry; /** * @param string|UriInterface $uri * @return boolean + * @deprecated with Neos 9 */ public function hasSupportedScheme($uri): bool { - if ($uri instanceof UriInterface) { - $uri = (string)$uri; - } - - return $uri !== null && preg_match(self::PATTERN_SUPPORTED_URIS, $uri) === 1; + return $this->newLinkHelper->hasSupportedScheme($uri); } /** * @param string|UriInterface $uri * @return string + * @deprecated with Neos 9 */ public function getScheme($uri): string { - if ($uri instanceof UriInterface) { - return $uri->getScheme(); - } - - if ($uri !== null && preg_match(self::PATTERN_SUPPORTED_URIS, $uri, $matches) === 1) { - return $matches[1]; - } - - return ''; + return $this->newLinkHelper->getScheme($uri); } /** @@ -172,6 +138,7 @@ public function getScheme($uri): string * @throws \Neos\Flow\Mvc\Routing\Exception\MissingActionNameException * @throws \Neos\Flow\Property\Exception * @throws \Neos\Flow\Security\Exception + * @deprecated with Neos 9 */ public function resolveNodeUri( string $uri, @@ -179,7 +146,21 @@ public function resolveNodeUri( ControllerContext $controllerContext, bool $absolute = false ): ?string { - return $this->createNodeUri($controllerContext, $uri, $contextNode, null, $absolute); + try { + if ($this->newLinkHelper->getScheme($uri) !== 'node') { + throw new \RuntimeException(sprintf( + 'Invalid node uri "%s" provided. It must start with node://', + $uri + ), 1720004437); + } + return $this->createNodeUri($controllerContext, $uri, $contextNode, null, $absolute); + } catch (\RuntimeException $e) { + $this->systemLogger->info( + sprintf('Could not resolve "%s" to an existing node; %s', $uri, $e->getMessage()), + LogEnvironment::fromMethodName(__METHOD__) + ); + return null; + } } /** @@ -187,24 +168,19 @@ public function resolveNodeUri( * * @param string $uri * @return string|null If the URI cannot be resolved, null is returned + * @deprecated with Neos 9 */ public function resolveAssetUri(string $uri): ?string { - $targetObject = $this->convertUriToObject($uri); - if (!$targetObject instanceof Asset) { + try { + return $this->newLinkHelper->resolveAssetUri($uri); + } catch (\RuntimeException $e) { $this->systemLogger->info( - sprintf('Could not resolve "%s" to an existing asset; The asset was probably deleted.', $uri), + sprintf('Could not resolve "%s" to an existing asset; %s', $uri, $e->getMessage()), LogEnvironment::fromMethodName(__METHOD__) ); - return null; } - - $assetUri = $this->resourceManager->getPublicPersistentResourceUri($targetObject->getResource()); - - return is_string($assetUri) - ? $assetUri - : null; } /** @@ -213,36 +189,11 @@ public function resolveAssetUri(string $uri): ?string * @param string|UriInterface $uri * @param Node $contextNode * @return Node|AssetInterface|NULL + * @deprecated with Neos 9 */ public function convertUriToObject($uri, Node $contextNode = null) { - if ($uri instanceof UriInterface) { - $uri = (string)$uri; - } - - if (preg_match(self::PATTERN_SUPPORTED_URIS, $uri, $matches) === 1) { - switch ($matches[1]) { - case 'node': - if (!$contextNode instanceof Node) { - throw new \RuntimeException( - 'node:// URI conversion requires a context node to be passed', - 1409734235 - ); - } - return $this->contentRepositoryRegistry->subgraphForNode($contextNode) - ->findNodeById( - NodeAggregateId::fromString($matches[2]) - ); - case 'asset': - /** @var ?AssetInterface $asset */ - $asset = $this->assetRepository->findByIdentifier($matches[2]); - - return $asset; - default: - } - } - - return null; + return $this->newLinkHelper->convertUriToObject($uri, $contextNode); } /** @@ -269,6 +220,7 @@ public function convertUriToObject($uri, Node $contextNode = null) * @throws \Neos\Flow\Security\Exception * @throws HttpException * @throws \Neos\Flow\Persistence\Exception\IllegalObjectTypeException + * @deprecated with Neos 9 */ public function createNodeUri( ControllerContext $controllerContext, @@ -381,6 +333,7 @@ public function createNodeUri( * @return string * @throws NeosException * @throws HttpException + * @deprecated with Neos 9 - todo find alternative */ public function createSiteUri(ControllerContext $controllerContext, Site $site): string { @@ -411,6 +364,7 @@ public function createSiteUri(ControllerContext $controllerContext, Site $site): * May return NULL in case no link has been generated or an error occurred on the last linking run. * * @return Node + * @deprecated with Neos 9 */ public function getLastLinkedNode(): ?Node { From 753127128e2fc84771c656776735fbf5b4d7463a Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Wed, 3 Jul 2024 14:11:05 +0200 Subject: [PATCH 12/14] TASK: Adjust phpstan types --- .../Classes/Fusion/Helper/LinkHelper.php | 21 +++++++------------ Neos.Neos/Classes/Service/LinkingService.php | 2 +- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/Neos.Neos/Classes/Fusion/Helper/LinkHelper.php b/Neos.Neos/Classes/Fusion/Helper/LinkHelper.php index 24763cbdc0e..3c44b5a613a 100644 --- a/Neos.Neos/Classes/Fusion/Helper/LinkHelper.php +++ b/Neos.Neos/Classes/Fusion/Helper/LinkHelper.php @@ -70,20 +70,13 @@ class LinkHelper implements ProtectedContextAwareInterface */ protected $linkingService; - /** - * @param string|Uri $uri - * @return boolean - */ - public function hasSupportedScheme($uri): bool + public function hasSupportedScheme(string|UriInterface|null $uri): bool { $scheme = $this->getScheme($uri); return $scheme === self::NODE_SCHEME || $scheme === self::ASSET_SCHEME; } - /** - * @param string|UriInterface $uri - */ - public function getScheme($uri): ?string + public function getScheme(string|UriInterface|null $uri): ?string { if ($uri === null || $uri === '') { return null; @@ -98,15 +91,15 @@ public function getScheme($uri): ?string * @param string|UriInterface $uri * @param Node $contextNode * @param ControllerContext $controllerContext - * @return string + * @return string|null * @deprecated with Neos 9 as the linking service is deprecated and this helper cannot be invoked from Fusion either way as the $controllerContext is not available. */ - public function resolveNodeUri(string|UriInterface $uri, Node $contextNode, ControllerContext $controllerContext) + public function resolveNodeUri(string|UriInterface $uri, Node $contextNode, ControllerContext $controllerContext): ?string { - return $this->linkingService->resolveNodeUri($uri, $contextNode, $controllerContext); + return $this->linkingService->resolveNodeUri((string)$uri, $contextNode, $controllerContext); } - public function resolveAssetUri(string|Uri $uri): string + public function resolveAssetUri(string|UriInterface $uri): string { if (!$uri instanceof UriInterface) { $uri = new Uri($uri); @@ -132,7 +125,7 @@ public function resolveAssetUri(string|Uri $uri): string } public function convertUriToObject( - string|Uri $uri, + string|UriInterface $uri, Node $contextNode = null ): Node|AssetInterface|null { if (is_string($uri)) { diff --git a/Neos.Neos/Classes/Service/LinkingService.php b/Neos.Neos/Classes/Service/LinkingService.php index 673efa4aa63..ca18dfbdfd7 100644 --- a/Neos.Neos/Classes/Service/LinkingService.php +++ b/Neos.Neos/Classes/Service/LinkingService.php @@ -123,7 +123,7 @@ public function hasSupportedScheme($uri): bool */ public function getScheme($uri): string { - return $this->newLinkHelper->getScheme($uri); + return $this->newLinkHelper->getScheme($uri) ?? ''; } /** From a191ff15e9f7313c49dbd5f150e476ab2ead7e9e Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Wed, 3 Jul 2024 14:14:16 +0200 Subject: [PATCH 13/14] TASK: Simplify regex of `PATTERN_SUPPORTED_URIS` Previously it was a union of `[a-z0-9-]+` and a UUID pattern, but the character set already covers the UUID case --- Neos.Neos/Classes/Fusion/ConvertUrisImplementation.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Neos.Neos/Classes/Fusion/ConvertUrisImplementation.php b/Neos.Neos/Classes/Fusion/ConvertUrisImplementation.php index fc27ab0b5c9..81ca8c74433 100644 --- a/Neos.Neos/Classes/Fusion/ConvertUrisImplementation.php +++ b/Neos.Neos/Classes/Fusion/ConvertUrisImplementation.php @@ -67,8 +67,7 @@ */ class ConvertUrisImplementation extends AbstractFusionObject { - public const PATTERN_SUPPORTED_URIS - = '/(node|asset):\/\/([a-z0-9\-]+|([a-f0-9]){8}-([a-f0-9]){4}-([a-f0-9]){4}-([a-f0-9]){4}-([a-f0-9]){12})/'; + private const PATTERN_SUPPORTED_URIS = '/(node|asset):\/\/([a-z0-9\-]+)/'; /** * @Flow\Inject From 2b2564f010f78625d587c36e599426100a42102a Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Wed, 3 Jul 2024 14:58:03 +0200 Subject: [PATCH 14/14] TASK: Document `NodeAddressNormalizer` --- .../Classes/Fusion/NodeUriImplementation.php | 19 ++++++++++++ .../Utility/LegacyNodePathNormalizer.php | 11 +++++-- .../Classes/Utility/NodeAddressNormalizer.php | 30 +++++++++++++------ 3 files changed, 48 insertions(+), 12 deletions(-) diff --git a/Neos.Neos/Classes/Fusion/NodeUriImplementation.php b/Neos.Neos/Classes/Fusion/NodeUriImplementation.php index 8779c742d18..961a870196c 100644 --- a/Neos.Neos/Classes/Fusion/NodeUriImplementation.php +++ b/Neos.Neos/Classes/Fusion/NodeUriImplementation.php @@ -31,6 +31,25 @@ /** * Create a link to a node + * + * If the node is passed as string the base node is required. + * Following string syntax is allowed: + * + * - node://my-node-identifier + * - //my-site/main + * - some/relative/path + * + * Deprecated syntax: + * + * - /sites/site/absolute/path + * - ~/site-relative/path + * - ~ + * + * Not supported syntax: + * + * - ./neos/info + * - ../foo/../../bar + * */ class NodeUriImplementation extends AbstractFusionObject { diff --git a/Neos.Neos/Classes/Utility/LegacyNodePathNormalizer.php b/Neos.Neos/Classes/Utility/LegacyNodePathNormalizer.php index 1d55de7d13a..4ade6c05f99 100644 --- a/Neos.Neos/Classes/Utility/LegacyNodePathNormalizer.php +++ b/Neos.Neos/Classes/Utility/LegacyNodePathNormalizer.php @@ -21,12 +21,17 @@ final class LegacyNodePathNormalizer protected ContentRepositoryRegistry $contentRepositoryRegistry; /** - * Converts legacy paths like "/sites/site/absolute/path", "~/site-relative/path" and "~" to the corresponding - * AbsoluteNodePath depending on the passed base node. + * Converts legacy paths to the corresponding AbsoluteNodePath depending on the passed base node. + * + * Supported legacy syntax: + * + * - /sites/site/absolute/path + * - ~/site-relative/path + * - ~ * * The following syntax is not implemented and handled here: * - * - node:// + * - node://my-node-identifier * - //my-site/main * - some/relative/path * diff --git a/Neos.Neos/Classes/Utility/NodeAddressNormalizer.php b/Neos.Neos/Classes/Utility/NodeAddressNormalizer.php index 36b7de73fca..2738cf23787 100644 --- a/Neos.Neos/Classes/Utility/NodeAddressNormalizer.php +++ b/Neos.Neos/Classes/Utility/NodeAddressNormalizer.php @@ -18,25 +18,37 @@ final class NodeAddressNormalizer protected ContentRepositoryRegistry $contentRepositoryRegistry; /** - * Converts strings like "relative/path", ... - * to the corresponding NodeAddress + * Converts string-like node representations based the base node to the corresponding NodeAddress. * - * For handling legacy paths like "/sites/site/absolute/path", "~/site-relative/path" and "~", - * please combine with {@see LegacyNodePathNormalizer::tryResolveLegacyPathSyntaxToAbsoluteNodePath()} + * Following string syntax is allowed, as well as passing a NodePath or AbsoluteNodePath value object: + * + * - node://my-node-identifier + * - //my-site/main + * - some/relative/path + * + * The following legacy syntax is not implemented and handled here: + * + * - /sites/site/absolute/path + * - ~/site-relative/path + * - ~ + * - ./neos/info + * - ../foo/../../bar + * + * For handling partially legacy paths please preprocess the path using {@see LegacyNodePathNormalizer::tryResolveLegacyPathSyntaxToAbsoluteNodePath()} */ - public function resolveNodeAddressFromPath(AbsoluteNodePath|NodePath|string $path, Node $documentNode): NodeAddress + public function resolveNodeAddressFromPath(AbsoluteNodePath|NodePath|string $path, Node $baseNode): NodeAddress { if ($path === '') { throw new \RuntimeException('Empty strings can not be resolved to nodes.', 1719999872); } if (is_string($path) && str_starts_with($path, 'node://')) { - return NodeAddress::fromNode($documentNode)->withAggregateId( + return NodeAddress::fromNode($baseNode)->withAggregateId( NodeAggregateId::fromString(substr($path, strlen('node://'))) ); } - $subgraph = $this->contentRepositoryRegistry->subgraphForNode($documentNode); + $subgraph = $this->contentRepositoryRegistry->subgraphForNode($baseNode); if (is_string($path) && AbsoluteNodePath::patternIsMatchedByString($path)) { $path = AbsoluteNodePath::fromString($path); @@ -57,13 +69,13 @@ public function resolveNodeAddressFromPath(AbsoluteNodePath|NodePath|string $pat if (is_string($path)) { $path = NodePath::fromString($path); } - $targetNode = $subgraph->findNodeByPath($path, $documentNode->aggregateId); + $targetNode = $subgraph->findNodeByPath($path, $baseNode->aggregateId); if ($targetNode === null) { throw new \RuntimeException(sprintf( 'Node on path "%s" could not be found for base node "%s" in workspace "%s" and dimension %s', $path->serializeToString(), - $documentNode->aggregateId->value, + $baseNode->aggregateId->value, $subgraph->getWorkspaceName()->value, $subgraph->getDimensionSpacePoint()->toJson() ), 1719950342);