diff --git a/.composer.json b/.composer.json index 521bc7e5acb..1cfda9628d7 100644 --- a/.composer.json +++ b/.composer.json @@ -38,7 +38,7 @@ "@test:behat-cli -c Neos.ContentRepository.Export/Tests/Behavior/behat.yml.dist", "@test:behat-cli -c Neos.TimeableNodeVisibility/Tests/Behavior/behat.yml.dist", "../../flow doctrine:migrate --quiet; ../../flow cr:setup", - "@test:behat-cli -c Neos.Neos/Tests/Behavior/behat.yml" + "@test:behat-cli -c Neos.Neos/Tests/Behavior/behat.yml.dist" ], "test:behavioral:stop-on-failure": [ "@test:behat-cli -vvv --stop-on-failure -c Neos.ContentRepository.BehavioralTests/Tests/Behavior/behat.yml.dist", @@ -47,7 +47,7 @@ "@test:behat-cli -vvv --stop-on-failure -c Neos.ContentRepository.Export/Tests/Behavior/behat.yml.dist", "@test:behat-cli -vvv --stop-on-failure -c Neos.TimeableNodeVisibility/Tests/Behavior/behat.yml.dist", "../../flow doctrine:migrate --quiet; ../../flow cr:setup", - "@test:behat-cli -vvv --stop-on-failure -c Neos.Neos/Tests/Behavior/behat.yml" + "@test:behat-cli -vvv --stop-on-failure -c Neos.Neos/Tests/Behavior/behat.yml.dist" ], "test": [ "@test:unit", diff --git a/.github/workflows/postgresql-versions.yml b/.github/workflows/postgresql-versions.yml index 77b90e0c980..09ea54c913b 100644 --- a/.github/workflows/postgresql-versions.yml +++ b/.github/workflows/postgresql-versions.yml @@ -154,5 +154,5 @@ jobs: FLOW_CONTEXT=Testing/Behat ./flow behat:setup FLOW_CONTEXT=Testing/Behat ./flow doctrine:create FLOW_CONTEXT=Testing/Behat ./flow doctrine:migrationversion --add --version all - bin/behat --stop-on-failure -f progress -c Packages/Neos/Neos.Neos/Tests/Behavior/behat.yml + bin/behat --stop-on-failure -f progress -c Packages/Neos/Neos.Neos/Tests/Behavior/behat.yml.dist bin/behat --stop-on-failure -f progress -c Packages/Neos/Neos.ContentRepository/Tests/Behavior/behat.yml.dist diff --git a/Neos.Neos/Classes/FrontendRouting/NodeUriBuilder.php b/Neos.Neos/Classes/FrontendRouting/NodeUriBuilder.php index 71fbd46e4d0..8e9f7e3b4de 100644 --- a/Neos.Neos/Classes/FrontendRouting/NodeUriBuilder.php +++ b/Neos.Neos/Classes/FrontendRouting/NodeUriBuilder.php @@ -115,6 +115,7 @@ public function __construct( * @api * @throws NoMatchingRouteException * The exception is thrown for various unlike cases in which uri building fails: + * - the node is disabled in the live workspace (a preview url should be built instead) * - the default route definitions are misconfigured * - the custom uri building options don't macht a route * - the shortcut points to an invalid target 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 diff --git a/Neos.Neos/Classes/Fusion/Helper/LinkHelper.php b/Neos.Neos/Classes/Fusion/Helper/LinkHelper.php index d6c92994f74..3c44b5a613a 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 @@ -66,77 +65,55 @@ class LinkHelper implements ProtectedContextAwareInterface protected $nodeUriBuilderFactory; /** - * @param string|Uri $uri - * @return boolean + * @Flow\Inject + * @var LinkingService */ - public function hasSupportedScheme($uri): bool - { - return in_array($this->getScheme($uri), ['node', 'asset'], true); - } + protected $linkingService; - /** - * @param string|UriInterface $uri - * @return string - */ - public function getScheme($uri): string + public function hasSupportedScheme(string|UriInterface|null $uri): bool { - if ($uri instanceof UriInterface) { - return $uri->getScheme(); - } - - if (is_string($uri) && preg_match(ConvertUrisImplementation::PATTERN_SUPPORTED_URIS, $uri, $matches) === 1) { - return $matches[1]; - } - - return ''; + $scheme = $this->getScheme($uri); + return $scheme === self::NODE_SCHEME || $scheme === self::ASSET_SCHEME; } - 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__) - ); + public function getScheme(string|UriInterface|null $uri): ?string + { + 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|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): ?string + { + 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); } + 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); @@ -148,33 +125,26 @@ public function resolveAssetUri(string|Uri $uri): string } public function convertUriToObject( - string|Uri $uri, + string|UriInterface $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; } diff --git a/Neos.Neos/Classes/Fusion/Helper/NodeHelper.php b/Neos.Neos/Classes/Fusion/Helper/NodeHelper.php index 5317ca9a715..49266fc0b70 100644 --- a/Neos.Neos/Classes/Fusion/Helper/NodeHelper.php +++ b/Neos.Neos/Classes/Fusion/Helper/NodeHelper.php @@ -62,14 +62,10 @@ public function nearestContentCollection(Node $node, string $nodePath): Node $contentCollectionType ), 1409300545); } - $nodePath = AbsoluteNodePath::patternIsMatchedByString($nodePath) - ? AbsoluteNodePath::fromString($nodePath) - : NodePath::fromString($nodePath); + $nodePath = NodePath::fromString($nodePath); $subgraph = $this->contentRepositoryRegistry->subgraphForNode($node); - $subNode = $nodePath instanceof AbsoluteNodePath - ? $subgraph->findNodeByAbsolutePath($nodePath) - : $subgraph->findNodeByPath($nodePath, $node->aggregateId); + $subNode = $subgraph->findNodeByPath($nodePath, $node->aggregateId); if ($subNode !== null && $this->isOfType($subNode, $contentCollectionType)) { return $subNode; diff --git a/Neos.Neos/Classes/Fusion/NodeUriImplementation.php b/Neos.Neos/Classes/Fusion/NodeUriImplementation.php index 69ad10218f8..254da86f05f 100644 --- a/Neos.Neos/Classes/Fusion/NodeUriImplementation.php +++ b/Neos.Neos/Classes/Fusion/NodeUriImplementation.php @@ -25,10 +25,30 @@ use Neos\Fusion\FusionObjects\AbstractFusionObject; use Neos\Neos\FrontendRouting\NodeUriBuilderFactory; use Neos\Neos\FrontendRouting\Options; +use Neos\Neos\Utility\LegacyNodePathNormalizer; +use Neos\Neos\Utility\NodePathResolver; use Psr\Log\LoggerInterface; /** * Create a link to a node + * + * If the node is passed as string the base node is required. + * Following string syntax is allowed: + * + * - //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 { @@ -51,12 +71,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 NodePathResolver */ - 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 +127,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->resolveNodeAddressByPath( + $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,10 +185,10 @@ 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__)); + $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/Service/LinkingService.php b/Neos.Neos/Classes/Service/LinkingService.php index 6354c69b937..aeb7cb30ee4 100644 --- a/Neos.Neos/Classes/Service/LinkingService.php +++ b/Neos.Neos/Classes/Service/LinkingService.php @@ -17,10 +17,7 @@ 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; @@ -28,15 +25,14 @@ 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\SiteDetection\SiteDetectionResult; +use Neos\Neos\FrontendRouting\NodeUriBuilder; +use Neos\Neos\Fusion\Helper\LinkHelper; +use Neos\Neos\Utility\LegacyNodePathNormalizer; +use Neos\Neos\Utility\NodePathResolver; use Neos\Utility\Arrays; use Psr\Http\Message\UriInterface; use Psr\Log\LoggerInterface; @@ -57,8 +53,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,55 +60,48 @@ * ``~/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 NodePathResolver::resolveNodeAddressByPath()} 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})/'; + protected ?Node $lastLinkedNode; /** * @Flow\Inject - * @var AssetRepository + * @var LoggerInterface */ - protected $assetRepository; + protected $systemLogger; /** * @Flow\Inject - * @var ResourceManager + * @var SiteRepository */ - protected $resourceManager; + protected $siteRepository; /** * @Flow\Inject - * @var PropertyMapper + * @var BaseUriProvider */ - protected $propertyMapper; - - protected ?Node $lastLinkedNode; + protected $baseUriProvider; /** * @Flow\Inject - * @var LoggerInterface + * @var NodePathResolver */ - protected $systemLogger; + protected $nodePathResolver; /** * @Flow\Inject - * @var SiteRepository + * @var LegacyNodePathNormalizer */ - protected $siteRepository; + protected $legacyNodePathNormalizer; /** * @Flow\Inject - * @var BaseUriProvider + * @var LinkHelper */ - protected $baseUriProvider; + protected $newLinkHelper; #[Flow\Inject] protected ContentRepositoryRegistry $contentRepositoryRegistry; @@ -122,31 +109,21 @@ class LinkingService /** * @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) ?? ''; } /** @@ -161,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, @@ -168,17 +146,21 @@ public function resolveNodeUri( ControllerContext $controllerContext, bool $absolute = false ): ?string { - $targetObject = $this->convertUriToObject($uri, $contextNode); - if ($targetObject === null) { + 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; The node was probably deleted.', $uri), + sprintf('Could not resolve "%s" to an existing node; %s', $uri, $e->getMessage()), LogEnvironment::fromMethodName(__METHOD__) ); - return null; } - - return $this->createNodeUri($controllerContext, $targetObject, null, null, $absolute); } /** @@ -186,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; } /** @@ -212,45 +189,20 @@ 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); } /** * 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 @@ -268,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, @@ -289,72 +242,55 @@ 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->nodePathResolver->resolveNodeAddressByPath( + $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()); + $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 +313,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 +323,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'); } /** @@ -396,6 +332,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 { @@ -426,6 +363,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 { diff --git a/Neos.Neos/Classes/Utility/LegacyNodePathNormalizer.php b/Neos.Neos/Classes/Utility/LegacyNodePathNormalizer.php new file mode 100644 index 00000000000..8e75c788513 --- /dev/null +++ b/Neos.Neos/Classes/Utility/LegacyNodePathNormalizer.php @@ -0,0 +1,108 @@ +/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 tryResolveLegacyPathSyntaxToAbsoluteNodePath( + string $path, + Node $baseNode + ): ?AbsoluteNodePath { + if (str_contains($path, '..') || str_starts_with($path, './') || str_contains($path, '/.')) { + throw new \InvalidArgumentException(sprintf('NodePath traversal via /../ is not allowed. Got: "%s"', $path), 1707732065); + } + + if (AbsoluteNodePath::patternIsMatchedByString($path)) { + // not a legacy absolute node path + return null; + } + + $isSiteRelative = str_starts_with($path, '~'); + $isLegacyAbsolute = str_starts_with($path, '/'); + + 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 ($isLegacyAbsolute) { + $pathWithoutSitesRoot = substr($path, strlen('/sites/')); + return AbsoluteNodePath::fromRootNodeTypeNameAndRelativePath( + NodeTypeNameFactory::forSites(), + NodePath::fromString($pathWithoutSitesRoot) + ); + } + + 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); + } + + if ($path === '~') { + $pathSegments = []; + } elseif (str_starts_with($path, '~/')) { + $pathSegments = explode('/', substr($path, 2)); + } else { + throw new \RuntimeException(sprintf( + 'Malformed site relative path "%s"', + $path, + ), 1728571610); + } + return AbsoluteNodePath::fromRootNodeTypeNameAndRelativePath( + NodeTypeNameFactory::forSites(), + NodePath::fromPathSegments( + [$siteNode->name->value, ...$pathSegments] + ) + ); + } + + // not a legacy absolute node path + return null; + } +} diff --git a/Neos.Neos/Classes/Utility/NodePathResolver.php b/Neos.Neos/Classes/Utility/NodePathResolver.php new file mode 100644 index 00000000000..11bc4469c75 --- /dev/null +++ b/Neos.Neos/Classes/Utility/NodePathResolver.php @@ -0,0 +1,82 @@ +/my-site/main + * - some/relative/path + * + * The node protocol node://my-node-identifier is not handled here. + * + * 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 resolveNodeAddressByPath(AbsoluteNodePath|NodePath|string $path, Node $baseNode): NodeAddress + { + if ($path === '') { + throw new \RuntimeException('Empty strings can not be resolved to nodes.', 1719999872); + } + + $subgraph = $this->contentRepositoryRegistry->subgraphForNode($baseNode); + + 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, $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(), + $baseNode->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..877269b3c39 100644 --- a/Neos.Neos/Classes/ViewHelpers/Link/NodeViewHelper.php +++ b/Neos.Neos/Classes/ViewHelpers/Link/NodeViewHelper.php @@ -14,10 +14,7 @@ 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; @@ -28,12 +25,10 @@ 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; +use Neos\Neos\Utility\NodePathResolver; use Neos\Neos\Utility\NodeTypeWithFallbackProvider; /** @@ -134,12 +129,6 @@ class NodeViewHelper extends AbstractTagBasedViewHelper */ protected $tagName = 'a'; - /** - * @Flow\Inject - * @var NodeShortcutResolver - */ - protected $nodeShortcutResolver; - /** * @Flow\Inject * @var ThrowableStorageInterface @@ -152,6 +141,18 @@ class NodeViewHelper extends AbstractTagBasedViewHelper */ protected $nodeUriBuilderFactory; + /** + * @Flow\Inject + * @var NodePathResolver + */ + protected $nodePathResolver; + + /** + * @Flow\Inject + * @var LegacyNodePathNormalizer + */ + protected $legacyNodePathNormalizer; + /** * @Flow\Inject * @var NodeLabelGeneratorInterface @@ -244,59 +245,49 @@ public function initializeArguments() */ public function render(): string { + $resolvedNode = null; $node = $this->arguments['node']; - if (!$node instanceof Node) { - $node = $this->getContextVariable($this->arguments['baseNodeName']); - } - - if ($node instanceof Node) { - $nodeAddress = NodeAddress::fromNode($node); - } elseif (is_string($node)) { - $documentNode = $this->getContextVariable('documentNode'); - assert($documentNode instanceof Node); - $nodeAddress = $this->resolveNodeAddressFromString($node, $documentNode); - $node = $documentNode; - } 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) - ), 1601372376); - } - - $contentRepository = $this->contentRepositoryRegistry->get($nodeAddress->contentRepositoryId); - $subgraph = $contentRepository->getContentGraph($nodeAddress->workspaceName) - ->getSubgraph( - $nodeAddress->dimensionSpacePoint, - $node->visibilityConstraints - ); + 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); + } - $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)); - } - if ($resolvedNode && $this->getNodeType($resolvedNode)->isOfType(NodeTypeNameFactory::NAME_SHORTCUT)) { - try { - $shortcutNodeAddress = $this->nodeShortcutResolver->resolveShortcutTarget( - $nodeAddress + if (str_starts_with($node, 'node://')) { + $nodeAddress = NodeAddress::fromNode($baseNode)->withAggregateId( + NodeAggregateId::fromString(substr($node, strlen('node://'))) ); - if ($shortcutNodeAddress instanceof NodeAddress) { - $resolvedNode = $subgraph - ->findNodeById($shortcutNodeAddress->aggregateId); - } - } catch (NodeNotFoundException | InvalidShortcutException $e) { + } else { + $possibleAbsoluteNodePath = $this->legacyNodePathNormalizer->tryResolveLegacyPathSyntaxToAbsoluteNodePath($node, $baseNode); + $nodeAddress = $this->nodePathResolver->resolveNodeAddressByPath( + $possibleAbsoluteNodePath ?? $node, + $baseNode + ); + } + + $subgraph = $this->contentRepositoryRegistry->subgraphForNode($baseNode); + $resolvedNode = $subgraph->findNodeById($nodeAddress->aggregateId); + if ($resolvedNode === null) { $this->throwableStorage->logThrowable(new ViewHelperException(sprintf( - 'Failed to resolve shortcut node "%s" in workspace "%s" and dimension %s', - $resolvedNode->aggregateId->value, + 'Failed to resolve node "%s" (path %s) in workspace "%s" and dimension %s', + $nodeAddress->aggregateId->value, + $node, $subgraph->getWorkspaceName()->value, $subgraph->getDimensionSpacePoint()->toJson() - ), 1601370239, $e)); + ), 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 `Node`. Given: %s', + get_debug_type($node) + ), 1601372376); } $nodeUriBuilder = $this->nodeUriBuilderFactory->forActionRequest($this->controllerContext->getRequest()); @@ -338,62 +329,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..585844d240a 100644 --- a/Neos.Neos/Classes/ViewHelpers/Uri/NodeViewHelper.php +++ b/Neos.Neos/Classes/ViewHelpers/Uri/NodeViewHelper.php @@ -14,10 +14,7 @@ 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; @@ -27,9 +24,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\NodePathResolver; /** * A view helper for creating URIs pointing to nodes. @@ -47,8 +45,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 +117,17 @@ class NodeViewHelper extends AbstractViewHelper */ protected $nodeUriBuilderFactory; + /** + * @Flow\Inject + * @var NodePathResolver + */ + protected $nodePathResolver; + + /** + * @Flow\Inject + * @var LegacyNodePathNormalizer + */ + protected $legacyNodePathNormalizer; /** * Initialize arguments @@ -176,14 +183,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 - ); } /** @@ -192,22 +191,33 @@ public function initializeArguments() public function render(): string { $node = $this->arguments['node']; - if (!$node instanceof Node) { - $node = $this->getContextVariable($this->arguments['baseNodeName']); - } - - /* @var Node $documentNode */ - $documentNode = $this->getContextVariable('documentNode'); + 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) { + if (str_starts_with($node, 'node://')) { + $nodeAddress = NodeAddress::fromNode($baseNode)->withAggregateId( + NodeAggregateId::fromString(substr($node, strlen('node://'))) + ); + } else { + $possibleAbsoluteNodePath = $this->legacyNodePathNormalizer->tryResolveLegacyPathSyntaxToAbsoluteNodePath($node, $baseNode); + $nodeAddress = $this->nodePathResolver->resolveNodeAddressByPath( + $possibleAbsoluteNodePath ?? $node, + $baseNode + ); + } + } elseif ($node instanceof Node) { $nodeAddress = NodeAddress::fromNode($node); - } elseif (is_string($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', - 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); } @@ -238,62 +248,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); - } } diff --git a/Neos.Neos/Tests/Behavior/Features/Fusion/NodeUri.feature b/Neos.Neos/Tests/Behavior/Features/Fusion/NodeUri.feature new file mode 100644 index 00000000000..2f6aab70ae4 --- /dev/null +++ b/Neos.Neos/Tests/Behavior/Features/Fusion/NodeUri.feature @@ -0,0 +1,150 @@ +@flowEntities +Feature: Tests for the "Neos.Neos:NodeUri" Fusion prototype + + Background: + Given using no content dimensions + And using the following node types: + """yaml + 'Neos.ContentRepository:Root': {} + 'Neos.Neos:Sites': + superTypes: + 'Neos.ContentRepository:Root': true + 'Neos.Neos:Document': + properties: + title: + type: string + uriPathSegment: + type: string + 'Neos.Neos:Site': + superTypes: + 'Neos.Neos:Document': true + 'Neos.Neos:Test.DocumentType': + superTypes: + 'Neos.Neos:Document': true + """ + And using identifier "default", I define a content repository + And I am in content repository "default" + + When the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | newContentStreamId | "cs-identifier" | + And I am in workspace "live" and dimension space point {} + And the command CreateRootNodeAggregateWithNode is executed with payload: + | Key | Value | + | nodeAggregateId | "root" | + | nodeTypeName | "Neos.Neos:Sites" | + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | parentNodeAggregateId | nodeTypeName | initialPropertyValues | nodeName | + | a | root | Neos.Neos:Site | {"title": "Node a"} | a | + | a1 | a | Neos.Neos:Test.DocumentType | {"uriPathSegment": "a1", "title": "Node a1"} | a1 | + And A site exists for node name "a" and domain "http://localhost" + And the sites configuration is: + """yaml + Neos: + Neos: + sites: + 'a': + preset: default + uriPathSuffix: '' + contentDimensions: + resolver: + factoryClassName: Neos\Neos\FrontendRouting\DimensionResolution\Resolver\NoopResolverFactory + """ + And the Fusion context request URI is "http://localhost" + And the Fusion renderingMode is "frontend" + + Scenario: Node uris + And the Fusion context node is "a1" + When I execute the following Fusion code: + """fusion + include: resource://Neos.Fusion/Private/Fusion/Root.fusion + include: resource://Neos.Neos/Private/Fusion/Root.fusion + + test = Neos.Fusion:DataStructure { + @process.toString = ${Array.join(Array.map(value, (v, k) => k + ': ' + v), String.chr(10))} + uri = Neos.Neos:NodeUri { + node = ${node} + } + link = Neos.Neos:NodeLink { + node = ${node} + } + uriWithSection = Neos.Neos:NodeUri { + node = ${node} + section = 'foo' + } + uriWithAppendExceedingArguments = Neos.Neos:NodeUri { + node = ${node} + arguments = ${{q: 'abc'}} + } + absoluteUri = Neos.Neos:NodeUri { + node = ${node} + absolute = true + } + mixed = Neos.Neos:NodeUri { + node = ${node} + section = 'foo' + arguments = ${{q: 'abc'}} + absolute = true + } + } + """ + Then I expect the following Fusion rendering result: + """ + uri: /a1 + link: Neos.Neos:Test.DocumentType (a1) + uriWithSection: /a1#foo + uriWithAppendExceedingArguments: /a1?q=abc + absoluteUri: http://localhost/a1 + mixed: http://localhost/a1?q=abc#foo + """ + + Scenario: Node as string node path syntax + And the Fusion context node is "a" + When I execute the following Fusion code: + """fusion + include: resource://Neos.Fusion/Private/Fusion/Root.fusion + include: resource://Neos.Neos/Private/Fusion/Root.fusion + + test = Neos.Fusion:DataStructure { + @process.toString = ${Array.join(Array.map(value, (v, k) => k + ': ' + v), String.chr(10))} + sitesRootPath = Neos.Neos:NodeUri { + node = '//a/a1' + } + relativePath = Neos.Neos:NodeUri { + node = 'a1' + } + } + """ + Then I expect the following Fusion rendering result: + """ + sitesRootPath: /a1 + relativePath: /a1 + """ + + Scenario: Node as legacy string node path syntax + And the Fusion context node is "a" + When I execute the following Fusion code: + """fusion + include: resource://Neos.Fusion/Private/Fusion/Root.fusion + include: resource://Neos.Neos/Private/Fusion/Root.fusion + + test = Neos.Fusion:DataStructure { + @process.toString = ${Array.join(Array.map(value, (v, k) => k + ': ' + v), String.chr(10))} + sitesRootPath = Neos.Neos:NodeUri { + node = '/sites/a/a1' + } + siteRelativePath = Neos.Neos:NodeUri { + node = '~/a1' + } + site = Neos.Neos:NodeUri { + node = '~' + } + } + """ + Then I expect the following Fusion rendering result: + """ + sitesRootPath: /a1 + siteRelativePath: /a1 + site: / + """ diff --git a/Neos.Neos/Tests/Behavior/Features/Fusion/NodeUri.feature.wip b/Neos.Neos/Tests/Behavior/Features/Fusion/NodeUri.feature.wip deleted file mode 100644 index 91bc96c962b..00000000000 --- a/Neos.Neos/Tests/Behavior/Features/Fusion/NodeUri.feature.wip +++ /dev/null @@ -1,111 +0,0 @@ -@fixtures -Feature: Tests for the "Neos.Neos:NodeUri" Fusion prototype - - Background: - Given I have the site "a" - And I have the following NodeTypes configuration: - """yaml - 'unstructured': {} - 'Neos.Neos:FallbackNode': {} - 'Neos.Neos:Document': {} - 'Neos.Neos:ContentCollection': {} - 'Neos.Neos:Test.DocumentType': - superTypes: - 'Neos.Neos:Document': true - """ - And I have the following nodes: - | Identifier | Path | Node Type | Properties | - | root | /sites | unstructured | | - | a | /sites/a | Neos.Neos:Test.DocumentType | {"uriPathSegment": "a", "title": "Node a"} | - | a1 | /sites/a/a1 | Neos.Neos:Test.DocumentType | {"uriPathSegment": "a1", "title": "Node a1"} | - And the Fusion context request URI is "http://localhost" - - Scenario: Node uris - And the Fusion context node is "a1" - When I execute the following Fusion code: - """fusion - include: resource://Neos.Fusion/Private/Fusion/Root.fusion - include: resource://Neos.Neos/Private/Fusion/Root.fusion - - test = Neos.Fusion:DataStructure { - @process.toString = ${Array.join(Array.map(value, (v, k) => k + ': ' + v), String.chr(10))} - uri = Neos.Neos:NodeUri { - node = ${node} - } - link = Neos.Neos:NodeLink { - node = ${node} - } - uriWithSection = Neos.Neos:NodeUri { - node = ${node} - section = 'foo' - } - uriWithAppendExceedingArguments = Neos.Neos:NodeUri { - node = ${node} - arguments = ${{q: 'abc'}} - } - absoluteUri = Neos.Neos:NodeUri { - node = ${node} - absolute = true - } - mixed = Neos.Neos:NodeUri { - node = ${node} - section = 'foo' - arguments = ${{q: 'abc'}} - absolute = true - } - } - """ - Then I expect the following Fusion rendering result: - """ - uri: /en/a1 - link: Neos.Neos:Test.DocumentType (a1) - uriWithSection: /en/a1#foo - uriWithAppendExceedingArguments: /en/a1?q=abc - absoluteUri: http://localhost/en/a1 - mixed: http://localhost/en/a1?q=abc#foo - """ - - Scenario: Node as string node path syntax - And the Fusion context node is "a" - When I execute the following Fusion code: - """fusion - include: resource://Neos.Fusion/Private/Fusion/Root.fusion - include: resource://Neos.Neos/Private/Fusion/Root.fusion - - test = Neos.Fusion:DataStructure { - @process.toString = ${Array.join(Array.map(value, (v, k) => k + ': ' + v), String.chr(10))} - sitesRootPath = Neos.Neos:NodeUri { - node = '/sites/a/a1' - } - siteRelativePath = Neos.Neos:NodeUri { - node = '~/a1' - } - site = Neos.Neos:NodeUri { - node = '~' - } - relativePath = Neos.Neos:NodeUri { - node = 'a1' - } - dotRelativePath = Neos.Neos:NodeUri { - node = './a1' - } - dotTraversalRelativePath = Neos.Neos:NodeUri { - @context.childNode = ${q(node).find('#a1').get(0)} - node = '..' - baseNodeName = 'childNode' - } - contextPath = Neos.Neos:NodeUri { - node = '/sites/a/a1@live' - } - } - """ - Then I expect the following Fusion rendering result: - """ - sitesRootPath: /en/a1 - siteRelativePath: /en/a1 - site: / - relativePath: /en/a1 - dotRelativePath: /en/a1 - dotTraversalRelativePath: / - contextPath: /en/a1 - """ diff --git a/Neos.Neos/Tests/Behavior/behat.yml b/Neos.Neos/Tests/Behavior/behat.yml.dist similarity index 100% rename from Neos.Neos/Tests/Behavior/behat.yml rename to Neos.Neos/Tests/Behavior/behat.yml.dist diff --git a/composer.json b/composer.json index 4008398d3b9..4425532f575 100644 --- a/composer.json +++ b/composer.json @@ -121,7 +121,7 @@ "@test:behat-cli -c Neos.ContentRepository.Export/Tests/Behavior/behat.yml.dist", "@test:behat-cli -c Neos.TimeableNodeVisibility/Tests/Behavior/behat.yml.dist", "../../flow doctrine:migrate --quiet; ../../flow cr:setup", - "@test:behat-cli -c Neos.Neos/Tests/Behavior/behat.yml" + "@test:behat-cli -c Neos.Neos/Tests/Behavior/behat.yml.dist" ], "test:behavioral:stop-on-failure": [ "@test:behat-cli -vvv --stop-on-failure -c Neos.ContentRepository.BehavioralTests/Tests/Behavior/behat.yml.dist", @@ -130,7 +130,7 @@ "@test:behat-cli -vvv --stop-on-failure -c Neos.ContentRepository.Export/Tests/Behavior/behat.yml.dist", "@test:behat-cli -vvv --stop-on-failure -c Neos.TimeableNodeVisibility/Tests/Behavior/behat.yml.dist", "../../flow doctrine:migrate --quiet; ../../flow cr:setup", - "@test:behat-cli -vvv --stop-on-failure -c Neos.Neos/Tests/Behavior/behat.yml" + "@test:behat-cli -vvv --stop-on-failure -c Neos.Neos/Tests/Behavior/behat.yml.dist" ], "test": [ "@test:unit",