Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed support for URI suffixes #4

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
183 changes: 129 additions & 54 deletions Classes/Routing/FrontendNodeRoutePartHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,16 @@
* source code.
*/

use GuzzleHttp\Psr7\Uri;
use Flowpack\Neos\DimensionResolver\Http;
use Neos\Flow\Annotations as Flow;
use Psr\Log\LoggerInterface;
use Neos\Flow\Persistence\Exception\IllegalObjectTypeException;
use Neos\Neos\Routing\Exception\InvalidDimensionPresetCombinationException;
use Neos\Neos\Routing\Exception\InvalidRequestPathException;
use Neos\Neos\Routing\Exception\NoSuchDimensionValueException;
use Neos\Flow\Mvc\Routing\Dto\MatchResult;
use Neos\Flow\Mvc\Routing\Dto\ResolveResult;
use Neos\Flow\Mvc\Routing\Dto\UriConstraints;
use Neos\Flow\Mvc\Routing\Dto\RouteTags;
use Neos\Flow\Mvc\Routing\DynamicRoutePart;
use Neos\Flow\Security\Context;
Expand All @@ -25,19 +30,23 @@
use Neos\Neos\Domain\Service\ContentContext;
use Neos\Neos\Domain\Service\ContentContextFactory;
use Neos\Neos\Domain\Service\ContentDimensionPresetSourceInterface;
use Neos\Neos\Domain\Service\NodeShortcutResolver;
use Neos\Neos\Domain\Service\SiteService;
use Neos\ContentRepository\Domain\Model\NodeInterface;
use Neos\ContentRepository\Domain\Utility\NodePaths;
use Neos\Neos\Routing\FrontendNodeRoutePartHandlerInterface;
use Neos\Neos\Routing\Exception;
use Psr\Http\Message\UriInterface;
use Psr\Log\LoggerInterface;

/**
* A route part handler for finding nodes specifically in the website's frontend.
*/
class FrontendNodeRoutePartHandler extends DynamicRoutePart implements FrontendNodeRoutePartHandlerInterface
{

/**
* @Flow\Inject(name="Neos.Flow:SystemLogger")
* @Flow\Inject
* @var LoggerInterface
*/
protected $systemLogger;
Expand Down Expand Up @@ -73,7 +82,7 @@ class FrontendNodeRoutePartHandler extends DynamicRoutePart implements FrontendN
protected $contentSubgraphUriProcessor;

/**
* @Flow\InjectConfiguration("routing.supportEmptySegmentForDimensions")
* @Flow\InjectConfiguration("routing.supportEmptySegmentForDimensions", package="Neos.Neos")
* @var boolean
*/
protected $supportEmptySegmentForDimensions;
Expand All @@ -84,6 +93,12 @@ class FrontendNodeRoutePartHandler extends DynamicRoutePart implements FrontendN
*/
protected $contentDimensionPresetSource;

/**
* @Flow\Inject
* @var NodeShortcutResolver
*/
protected $nodeShortcutResolver;

const DIMENSION_REQUEST_PATH_MATCHER = '|^
(?<firstUriPart>[^/@]+) # the first part of the URI, before the first slash, may contain the encoded dimension preset
(?: # start of non-capturing submatch for the remaining URL
Expand Down Expand Up @@ -121,7 +136,7 @@ protected function findValueToMatch($requestPath)
* in time the route part handler is invoked, the security framework is not yet fully initialized.
*
* @param string $requestPath The request path (without leading "/", relative to the current Site Node)
* @return bool|MatchResult An instance of MatchResult if value could be matched successfully, otherwise false.
* @return bool|MatchResult An instance of MatchResult if the route matches the $requestPath, otherwise FALSE. @see DynamicRoutePart::matchValue()
* @throws \Exception
* @throws Exception\NoHomepageException if no node could be found on the homepage (empty $requestPath)
*/
Expand All @@ -145,6 +160,9 @@ protected function matchValue($requestPath)

return false;
}
if (!$this->nodeTypeIsAllowed($node)) {
return false;
}
if ($this->onlyMatchSiteNodes() && $node !== $node->getContext()->getCurrentSiteNode()) {
return false;
}
Expand Down Expand Up @@ -172,11 +190,12 @@ protected function matchValue($requestPath)
* @throws Exception\NoSiteNodeException
* @throws Exception\NoSuchNodeException
* @throws Exception\NoWorkspaceException
* @throws \Neos\ContentRepository\Exception\NodeException
* @throws IllegalObjectTypeException
* @throws InvalidRequestPathException
*/
protected function convertRequestPathToNode($requestPath)
{
$contentContext = $this->buildContentContextFromParameters();
$contentContext = $this->buildContextFromRequestPath($requestPath);
$requestPathWithoutContext = $this->removeContextFromPath($requestPath);

$workspace = $contentContext->getWorkspace();
Expand All @@ -198,7 +217,9 @@ protected function convertRequestPathToNode($requestPath)
if ($requestPathWithoutContext === '') {
$node = $siteNode;
} else {
$node = $this->getNodeFromRequestPath($siteNode, $requestPathWithoutContext) ?? null;
$requestPathWithoutContext = $this->truncateUriPathSuffix((string)$requestPathWithoutContext);
$relativeNodePath = $this->getRelativeNodePathByUriPathSegmentProperties($siteNode, $requestPathWithoutContext);
$node = ($relativeNodePath !== false) ? $siteNode->getNode($relativeNodePath) : null;
}

if (!$node instanceof NodeInterface) {
Expand All @@ -220,14 +241,16 @@ protected function convertRequestPathToNode($requestPath)
* absolute node path: /sites/neostypo3org/homepage/about@user-admin
* $this->value: homepage/about@user-admin
*
* @param mixed $node Either a Node object or an absolute context node path
* @return ResolveResult|false ResolveResult if value could be resolved successfully, otherwise false.
* @throws Exception\MissingNodePropertyException
* @throws \Flowpack\Neos\DimensionResolver\Http\Exception\InvalidDimensionPresetLinkProcessorException
* @throws \Neos\ContentRepository\Exception\NodeException
* @param NodeInterface|string|string[] $node Either a Node object or an absolute context node path (potentially wrapped in an array as ['__contextNodePath' => '<value>'])
* @return bool|ResolveResult An instance of ResolveResult if the route coulr resolve the $node, otherwise FALSE. @see DynamicRoutePart::resolveValue()
* @throws IllegalObjectTypeException
* @see NodeIdentityConverterAspect
*/
protected function resolveValue($node)
{
if (is_array($node) && isset($node['__contextNodePath'])) {
$node = $node['__contextNodePath'];
}
if (!$node instanceof NodeInterface && !is_string($node)) {
return false;
}
Expand All @@ -248,19 +271,93 @@ protected function resolveValue($node)
$contentContext = $node->getContext();
}

if (!$node->getNodeType()->isOfType('Neos.Neos:Document')) {
if (!$this->nodeTypeIsAllowed($node)) {
return false;
}

$siteNode = $contentContext->getCurrentSiteNode();
if ($this->onlyMatchSiteNodes() && $node !== $siteNode) {
return false;
}

$routePath = $this->resolveRoutePathForNode($node);
try {
$nodeOrUri = $this->resolveShortcutNode($node);
} catch (Exception\InvalidShortcutException $exception) {
$this->systemLogger->debug('FrontendNodeRoutePartHandler resolveValue(): ' . $exception->getMessage());
return false;
}
if ($nodeOrUri instanceof UriInterface) {
return new ResolveResult('', UriConstraints::fromUri($nodeOrUri), null);
}
$uriConstraints = $this->contentSubgraphUriProcessor->resolveDimensionUriConstraints($node);
if (!empty($this->options['uriPathSuffix']) && $node->getParentPath() !== SiteService::SITES_ROOT_PATH) {
$uriConstraints = $uriConstraints->withPathSuffix($this->options['uriPathSuffix']);
}
$uriPath = $this->resolveRoutePathForNode($nodeOrUri);
return new ResolveResult($uriPath, $uriConstraints);
}

/**
* Removes the configured suffix from the given $uriPath
* If the "uriPathSuffix" option is not set (or set to an empty string) the unaltered $uriPath is returned
*
* @param string $uriPath
* @return false|string|null
* @throws Exception\InvalidRequestPathException
*/
protected function truncateUriPathSuffix(string $uriPath)
{
if (empty($this->options['uriPathSuffix'])) {
return $uriPath;
}
$suffixLength = strlen($this->options['uriPathSuffix']);
if (substr($uriPath, -$suffixLength) !== $this->options['uriPathSuffix']) {
throw new Exception\InvalidRequestPathException(sprintf('The request path "%s" doesn\'t contain the configured uriPathSuffix "%s"', $uriPath, $this->options['uriPathSuffix']), 1604912439);
}
return substr($uriPath, 0, -$suffixLength);
}

/**
* @param NodeInterface $node
* @return NodeInterface|Uri The original, unaltered $node if it's not a shortcut node. Otherwise the nodes shortcut target (a node or an URI for external & asset shortcuts)
* @throws Exception\InvalidShortcutException
*/
protected function resolveShortcutNode(NodeInterface $node)
{
$resolvedNode = $this->nodeShortcutResolver->resolveShortcutTarget($node);
if (is_string($resolvedNode)) {
return new Uri($resolvedNode);
}
if (!$resolvedNode instanceof NodeInterface) {
throw new Exception\InvalidShortcutException(sprintf('Could not resolve shortcut target for node "%s"', $node->getPath()), 1414771137);
}
return $resolvedNode;
}
/**
* Creates a content context from the given request path, considering possibly mentioned content dimension values.
*
* @param string &$requestPath The request path. If at least one content dimension is configured, the first path segment will identify the content dimension values
* @return ContentContext The built content context
*/
protected function buildContextFromRequestPath(&$requestPath)
{
$workspaceName = $this->parameters->getValue('workspaceName') ?? 'live';
$dimensionsAndDimensionValues = $this->parameters->getValue('dimensionValues') ? json_decode($this->parameters->getValue('dimensionValues'), true) : [];

// This is a workaround as NodePaths::explodeContextPath() (correctly)
// expects a context path to have something before the '@', but the requestPath
// could potentially contain only the context information.
if (strpos($requestPath, '@') === 0) {
$requestPath = '/' . $requestPath;
}

return new ResolveResult($routePath, $uriConstraints);
if ($requestPath !== '' && NodePaths::isContextPath($requestPath)) {
try {
$nodePathAndContext = NodePaths::explodeContextPath($requestPath);
$workspaceName = $nodePathAndContext['workspaceName'];
} catch (\InvalidArgumentException $exception) {
}
}
return $this->buildContextFromWorkspaceNameAndDimensions($workspaceName, $dimensionsAndDimensionValues);
}

/**
Expand All @@ -286,7 +383,7 @@ protected function buildContextFromPath($path, $convertLiveDimensions)

/**
* @param string $workspaceName
* @param array $dimensions
* @param array|null $dimensions
* @return ContentContext
*/
protected function buildContextFromWorkspaceName($workspaceName, array $dimensions = null)
Expand Down Expand Up @@ -389,6 +486,18 @@ protected function onlyMatchSiteNodes()
return isset($this->options['onlyMatchSiteNodes']) && $this->options['onlyMatchSiteNodes'] === true;
}

/**
* Whether the given $node is allowed according to the "nodeType" option
*
* @param NodeInterface $node
* @return bool
*/
protected function nodeTypeIsAllowed(NodeInterface $node): bool
{
$allowedNodeType = !empty($this->options['nodeType']) ? $this->options['nodeType'] : 'Neos.Neos:Document';
return $node->getNodeType()->isOfType($allowedNodeType);
}

/**
* Resolves the request path, also known as route path, identifying the given node.
*
Expand Down Expand Up @@ -421,7 +530,6 @@ protected function resolveRoutePathForNode(NodeInterface $node)
*
* @param NodeInterface $siteNode The site node, used as a starting point while traversing the tree
* @param string $relativeRequestPath The request path, relative to the site's root path
* @deprecated Use getNodeFromRequestPath() - return only the node
* @return string
* @throws \Neos\ContentRepository\Exception\NodeException
*/
Expand All @@ -448,44 +556,12 @@ protected function getRelativeNodePathByUriPathSegmentProperties(NodeInterface $
return implode('/', $relativeNodePathSegments);
}

/**
* Return Node from RequestPath
*
* @param NodeInterface $siteNode The site node, used as a starting point while traversing the tree
* @param string $relativeRequestPath The request path, relative to the site's root path
* @return NodeInterface
* @throws \Neos\ContentRepository\Exception\NodeException
*/
protected function getNodeFromRequestPath(NodeInterface $siteNode, $relativeRequestPath)
{
$matchedNode = false;
$node = $siteNode;

foreach (explode('/', $relativeRequestPath) as $pathSegment) {
$foundNodeInThisSegment = false;
foreach ($node->getChildNodes('Neos.Neos:Document') as $node) {
/** @var NodeInterface $node */
if ($node->getProperty('uriPathSegment') === $pathSegment) {
$matchedNode = $node;
$foundNodeInThisSegment = true;
break;
}
}
if (!$foundNodeInThisSegment) {
return false;
}
}

return $matchedNode;
}

/**
* Renders a request path based on the "uriPathSegment" properties of the nodes leading to the given node.
*
* @param NodeInterface $node The node where the generated path should lead to
* @return string A relative request path
* @throws Exception\MissingNodePropertyException if the given node doesn't have a "uriPathSegment" property set
* @throws \Neos\ContentRepository\Exception\NodeException
*/
protected function getRequestPathByNode(NodeInterface $node)
{
Expand All @@ -504,15 +580,14 @@ protected function getRequestPathByNode(NodeInterface $node)
$requestPathSegments = [];
while ($currentNode instanceof NodeInterface && $currentNode->getParentPath() !== SiteService::SITES_ROOT_PATH) {
if (!$currentNode->hasProperty('uriPathSegment')) {
throw new Exception\MissingNodePropertyException(sprintf('Missing "uriPathSegment" property for node "%s". Nodes can be migrated with the "flow node:repair" command.',
$node->getPath()), 1415020326);
throw new Exception\MissingNodePropertyException(sprintf('Missing "uriPathSegment" property for node "%s". Nodes can be migrated with the "flow node:repair" command.', $node->getPath()), 1415020326);
}

$pathSegment = $currentNode->getProperty('uriPathSegment');
$requestPathSegments[] = $pathSegment;
$currentNode = $currentNode->getParent();
}

return '/' . implode('/', array_reverse($requestPathSegments));
return implode('/', array_reverse($requestPathSegments));
}
}
}
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
"license": "GPL-3.0+",
"description": "A support package for Neos CMS that allows for arbitrary content dimension resolution.",
"require": {
"neos/neos": "~4.0||~5.0||~7.0||dev-master",
"neos/flow": "~5.0||~6.0||~7.0||dev-master"
"neos/neos": "~4.0||~5.0||~7.0||~8.0||dev-master",
"neos/flow": "~5.0||~6.0||~7.0||~8.0||dev-master"
},
"autoload": {
"psr-4": {
Expand Down