diff --git a/Classes/Package.php b/Classes/Package.php index 3d0b686..e0d56d3 100644 --- a/Classes/Package.php +++ b/Classes/Package.php @@ -13,11 +13,12 @@ * source code. */ -use Neos\Flow\Persistence\Doctrine\PersistenceManager; -use Neos\RedirectHandler\NeosAdapter\Service\NodeRedirectService; +use Neos\EventStore\Model\EventEnvelope; use Neos\Flow\Core\Bootstrap; use Neos\Flow\Package\Package as BasePackage; -use Neos\ContentRepository\Domain\Model\Workspace; +use Neos\Neos\Domain\Model\SiteNodeName; +use Neos\Neos\FrontendRouting\Projection\DocumentUriPathProjection; +use Neos\RedirectHandler\NeosAdapter\Service\NodeRedirectService; /** * The Neos RedirectHandler NeosAdapter Package @@ -32,7 +33,26 @@ public function boot(Bootstrap $bootstrap): void { $dispatcher = $bootstrap->getSignalSlotDispatcher(); - $dispatcher->connect(Workspace::class, 'beforeNodePublishing', NodeRedirectService::class, 'collectPossibleRedirects'); - $dispatcher->connect(PersistenceManager::class, 'allObjectsPersisted', NodeRedirectService::class, 'createPendingRedirects'); + $dispatcher->connect(DocumentUriPathProjection::class, 'afterNodeAggregateWasMoved', function ( + string $oldUriPath, string $newUriPath, SiteNodeName $siteNodeName, $_, + ) use ($bootstrap) { + $nodeRedirectService = $bootstrap->getObjectManager()->get(NodeRedirectService::class); + $nodeRedirectService->createRedirect($oldUriPath, $newUriPath, $siteNodeName); + }); + + $dispatcher->connect(DocumentUriPathProjection::class, 'afterNodeAggregateWasRemoved', function ( + string $oldUriPath, SiteNodeName $siteNodeName, $_, + ) use ($bootstrap) { + $nodeRedirectService = $bootstrap->getObjectManager()->get(NodeRedirectService::class); + $nodeRedirectService->createRedirect($oldUriPath, null, $siteNodeName); + }); + + $dispatcher->connect(DocumentUriPathProjection::class, 'afterDocumentUriPathChanged', function ( + string $oldUriPath, string $newUriPath, SiteNodeName $siteNodeName, $_, EventEnvelope $eventEnvelope, + ) use ($bootstrap) { + $nodeRedirectService = $bootstrap->getObjectManager()->get(NodeRedirectService::class); + $nodeRedirectService->createRedirect($oldUriPath, $newUriPath, $siteNodeName); + }); + } } diff --git a/Classes/Service/NodeRedirectService.php b/Classes/Service/NodeRedirectService.php index e7ab051..f49eaf5 100644 --- a/Classes/Service/NodeRedirectService.php +++ b/Classes/Service/NodeRedirectService.php @@ -13,25 +13,14 @@ * source code. */ -use GuzzleHttp\Psr7\ServerRequest; -use Neos\ContentRepository\Domain\Factory\NodeFactory; -use Neos\ContentRepository\Domain\Model\NodeInterface; -use Neos\ContentRepository\Domain\Model\Workspace; -use Neos\ContentRepository\Domain\Service\ContentDimensionCombinator; -use Neos\ContentRepository\Domain\Service\ContextFactoryInterface; +use Neos\ContentRepository\Core\Projection\ContentGraph\Node; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; +use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Annotations as Flow; -use Neos\Flow\Cli\CommandRequestHandler; -use Neos\Flow\Core\Bootstrap; -use Neos\Flow\Http\Exception as HttpException; -use Neos\Flow\Http\HttpRequestHandlerInterface; -use Neos\Flow\Mvc\ActionRequest; -use Neos\Flow\Mvc\Routing\Dto\RouteParameters; -use Neos\Flow\Mvc\Routing\Exception\MissingActionNameException; -use Neos\Flow\Mvc\Routing\RouterCachingService; -use Neos\Flow\Mvc\Routing\UriBuilder; use Neos\Flow\Persistence\PersistenceManagerInterface; -use Neos\Neos\Controller\CreateContentContextTrait; use Neos\Neos\Domain\Model\Domain; +use Neos\Neos\Domain\Model\SiteNodeName; +use Neos\Neos\Domain\Repository\SiteRepository; use Neos\RedirectHandler\Storage\RedirectStorageInterface; use Psr\Log\LoggerInterface; @@ -44,12 +33,8 @@ */ class NodeRedirectService { - use CreateContentContextTrait; - - /** - * @var UriBuilder - */ - protected $uriBuilder; + const STATUS_CODE_TYPE_REDIRECT = 'redirect'; + const STATUS_CODE_TYPE_GONE = 'gone'; /** * @Flow\Inject @@ -57,42 +42,18 @@ class NodeRedirectService */ protected $redirectStorage; - /** - * @Flow\Inject - * @var RouterCachingService - */ - protected $routerCachingService; - /** * @Flow\Inject * @var PersistenceManagerInterface */ protected $persistenceManager; - /** - * @Flow\Inject - * @var ContextFactoryInterface - */ - protected $contextFactory; - - /** - * @Flow\Inject - * @var NodeFactory - */ - protected $nodeFactory; - /** * @Flow\Inject * @var LoggerInterface */ protected $logger; - /** - * @var Bootstrap - * @Flow\Inject - */ - protected $bootstrap; - /** * @Flow\InjectConfiguration(path="statusCode", package="Neos.RedirectHandler") * @var array @@ -100,10 +61,10 @@ class NodeRedirectService protected $defaultStatusCode; /** - * @Flow\Inject - * @var ContentDimensionCombinator + * @Flow\InjectConfiguration(path="enableAutomaticRedirects", package="Neos.RedirectHandler.NeosAdapter") + * @var array */ - protected $contentDimensionCombinator; + protected $enableAutomaticRedirects; /** * @Flow\InjectConfiguration(path="enableRemovedNodeRedirect", package="Neos.RedirectHandler.NeosAdapter") @@ -111,12 +72,6 @@ class NodeRedirectService */ protected $enableRemovedNodeRedirect; - /** - * @Flow\InjectConfiguration(path="restrictByPathPrefix", package="Neos.RedirectHandler.NeosAdapter") - * @var array - */ - protected $restrictByPathPrefix; - /** * @Flow\InjectConfiguration(path="restrictByOldUriPrefix", package="Neos.RedirectHandler.NeosAdapter") * @var array @@ -130,261 +85,64 @@ class NodeRedirectService protected $restrictByNodeType; /** - * @Flow\InjectConfiguration(path="enableAutomaticRedirects", package="Neos.RedirectHandler.NeosAdapter") - * @var array + * @Flow\Inject + * @var SiteRepository */ - protected $enableAutomaticRedirects; + protected $siteRepository; - /** - * @Flow\InjectConfiguration(path="http.baseUri", package="Neos.Flow") - * @var string - */ - protected $baseUri; + #[\Neos\Flow\Annotations\Inject] + protected ContentRepositoryRegistry $contentRepositoryRegistry; - /** - * @var array - */ - protected $pendingRedirects = []; /** - * @var ActionRequest - */ - protected $actionRequestForUriBuilder; - - /** - * Collects the node for redirection if it is a 'Neos.Neos:Document' node and its URI has changed - * - * @param NodeInterface $node The node that is about to be published - * @param Workspace $targetWorkspace + * @param string $oldUriPath + * @param string|null $newUriPath + * @param SiteNodeName $siteNodeName * @return void - * @throws MissingActionNameException */ - public function collectPossibleRedirects(NodeInterface $node, Workspace $targetWorkspace): void - { + public function createRedirect( + string $oldUriPath, + ?string $newUriPath, + SiteNodeName $siteNodeName, + ): void { + if (!$this->enableAutomaticRedirects) { return; } - $nodeType = $node->getNodeType(); - if ($targetWorkspace->isPublicWorkspace() === false || $nodeType->isOfType('Neos.Neos:Document') === false) { + // TODO: Restrict by NodeType + if (/*$this->isRestrictedByNodeType($targetNodeInfo->node)|| */ $this->isRestrictedByOldUri($oldUriPath)) { return; } - $this->appendNodeAndChildrenDocumentsToPendingRedirects($node, $targetWorkspace); - } - - /** - * Returns the current http request or a generated http request - * based on a configured baseUri to allow redirect generation - * for CLI requests. - * - * @return ActionRequest - */ - protected function getActionRequestForUriBuilder(): ?ActionRequest - { - if ($this->actionRequestForUriBuilder) { - return $this->actionRequestForUriBuilder; - } - - /** @var HttpRequestHandlerInterface $requestHandler */ - $requestHandler = $this->bootstrap->getActiveRequestHandler(); - - if ($requestHandler instanceof CommandRequestHandler) { - // Generate a custom request when the current request was triggered from CLI - $baseUri = $this->baseUri ?? 'http://localhost'; - - // Prevent `index.php` appearing in generated redirects - putenv('FLOW_REWRITEURLS=1'); - - $httpRequest = new ServerRequest('POST', $baseUri); + $oldUriPath = $this->buildUri($oldUriPath); + if ($newUriPath !== null) { + $newUriPath = $this->buildUri($newUriPath); + $this->createRedirectWithNewTarget($oldUriPath, $newUriPath, $siteNodeName); } else { - $httpRequest = $requestHandler->getHttpRequest(); - } - - if (method_exists(ActionRequest::class, 'fromHttpRequest')) { - $routeParameters = $httpRequest->getAttribute('routingParameters') ?? RouteParameters::createEmpty(); - $httpRequest = $httpRequest->withAttribute('routingParameters', $routeParameters->withParameter('requestUriHost', $httpRequest->getUri()->getHost())); - // From Flow 6+ we have to use a static method to create an ActionRequest. Earlier versions use the constructor. - $this->actionRequestForUriBuilder = ActionRequest::fromHttpRequest($httpRequest); - } else { - /* @deprecated This case can be removed up when this package only supports Flow 6+. */ - if ($httpRequest instanceof ServerRequest) { - $httpRequest = new \Neos\Flow\Http\Request([], [], [], [ - 'HTTP_HOST' => $httpRequest->getHeaderLine('host'), - 'HTTPS' => $httpRequest->getHeaderLine('scheme') === 'https', - 'REQUEST_URI' => $httpRequest->getHeaderLine('path'), - ]); - } - $this->actionRequestForUriBuilder = new ActionRequest($httpRequest); - } - - return $this->actionRequestForUriBuilder; - } - - /** - * Creates the queued redirects provided we can find the node. - * - * @return void - * @throws MissingActionNameException - */ - public function createPendingRedirects(): void - { - if (!$this->enableAutomaticRedirects) { - return; - } - - $this->nodeFactory->reset(); - foreach ($this->pendingRedirects as $nodeIdentifierAndWorkspace => $oldUriPerDimensionCombination) { - [$nodeIdentifier, $workspaceName] = explode('@', $nodeIdentifierAndWorkspace); - $this->buildRedirects($nodeIdentifier, $workspaceName, $oldUriPerDimensionCombination); + $this->createRedirectForRemovedTarget($oldUriPath, $siteNodeName); } - $this->pendingRedirects = []; $this->persistenceManager->persistAll(); } /** - * @param NodeInterface $node - * @param Workspace $targetWorkspace - * @return void - * @throws MissingActionNameException - */ - protected function appendNodeAndChildrenDocumentsToPendingRedirects(NodeInterface $node, Workspace $targetWorkspace): void - { - $identifierAndWorkspaceKey = $node->getIdentifier() . '@' . $targetWorkspace->getName(); - if (isset($this->pendingRedirects[$identifierAndWorkspaceKey])) { - return; - } - - if (!$this->hasNodeUriChanged($node, $targetWorkspace)) { - return; - } - - $this->pendingRedirects[$identifierAndWorkspaceKey] = $this->createUriPathsAcrossDimensionsForNode($node->getIdentifier(), $targetWorkspace); - - foreach ($node->getChildNodes('Neos.Neos:Document') as $childNode) { - $this->appendNodeAndChildrenDocumentsToPendingRedirects($childNode, $targetWorkspace); - } - } - - /** - * @param string $nodeIdentifier - * @param Workspace $targetWorkspace - * @return array - * @throws MissingActionNameException - */ - protected function createUriPathsAcrossDimensionsForNode(string $nodeIdentifier, Workspace $targetWorkspace): array - { - $result = []; - foreach ($this->contentDimensionCombinator->getAllAllowedCombinations() as $allowedCombination) { - $nodeInDimensions = $this->getNodeInWorkspaceAndDimensions($nodeIdentifier, $targetWorkspace->getName(), $allowedCombination); - if ($nodeInDimensions === null) { - continue; - } - - try { - $nodeUriPath = $this->buildUriPathForNode($nodeInDimensions); - } catch (\Exception $_) { - continue; - } - $nodeUriPath = $this->removeContextInformationFromRelativeNodeUri($nodeUriPath); - $result[] = [ - $nodeUriPath, - $allowedCombination - ]; - } - - return $result; - } - - /** - * Has the Uri changed at all. + * Adds a redirect for given $oldUriPath to $newUriPath for all domains set up for $siteNode * - * @param NodeInterface $node - * @param Workspace $targetWorkspace + * @param NodeAggregateId $nodeAggregateId + * @param string $oldUriPath + * @param string $newUriPath * @return bool - * @throws MissingActionNameException */ - protected function hasNodeUriChanged(NodeInterface $node, Workspace $targetWorkspace): bool + protected function createRedirectWithNewTarget(string $oldUriPath, string $newUriPath, SiteNodeName $siteNodeName): bool { - $nodeInTargetWorkspace = $this->getNodeInWorkspace($node, $targetWorkspace); - if (!$nodeInTargetWorkspace) { - return false; - } - try { - $newUriPath = $this->buildUriPathForNode($node); - } catch (\Exception $exception) { - $this->logger->info(sprintf('Failed to build new URI for updated node "%s": %s', $node->getContextPath(), $exception->getMessage())); + if ($oldUriPath === $newUriPath) { return false; } - $newUriPath = $this->removeContextInformationFromRelativeNodeUri($newUriPath); - try { - $oldUriPath = $this->buildUriPathForNode($nodeInTargetWorkspace); - } catch (\Exception $exception) { - $this->logger->info(sprintf('Failed to build previous URI for updated node "%s": %s', $node->getContextPath(), $exception->getMessage())); - return false; - } - $oldUriPath = $this->removeContextInformationFromRelativeNodeUri($oldUriPath); - - return ($newUriPath !== $oldUriPath); - } - /** - * Build redirects in all dimensions for a given node. - * - * @param string $nodeIdentifier - * @param string $workspaceName - * @param $oldUriPerDimensionCombination - * @return void - * @throws MissingActionNameException - */ - protected function buildRedirects(string $nodeIdentifier, string $workspaceName, array $oldUriPerDimensionCombination): void - { - foreach ($oldUriPerDimensionCombination as [$oldRelativeUri, $dimensionCombination]) { - $this->createRedirectFrom($oldRelativeUri, $nodeIdentifier, $workspaceName, $dimensionCombination); - } - } + $hosts = $this->getHostnames($siteNodeName); + $statusCode = (integer)$this->defaultStatusCode[self::STATUS_CODE_TYPE_REDIRECT]; - /** - * Gets the node in the given dimensions and workspace and redirects the oldUri to the new one. - * - * @param string $oldUri - * @param string $nodeIdentifer - * @param string $workspaceName - * @param array $dimensionCombination - * @return bool - * @throws MissingActionNameException - */ - protected function createRedirectFrom(string $oldUri, string $nodeIdentifer, string $workspaceName, array $dimensionCombination): bool - { - $node = $this->getNodeInWorkspaceAndDimensions($nodeIdentifer, $workspaceName, $dimensionCombination); - if ($node === null) { - return false; - } - - if ($this->isRestrictedByNodeType($node) || $this->isRestrictedByPath($node) || $this->isRestrictedByOldUri($oldUri, $node)) { - return false; - } - - try { - $newUri = $this->buildUriPathForNode($node); - } catch (\Exception $exception) { - $this->logger->info(sprintf('Redirect creation skipped since URL for node "%s" could not be created and led to an exception: %s', $node->getContextPath(), $exception->getMessage())); - return false; - } - - if ($node->isRemoved()) { - return $this->removeNodeRedirectIfNeeded($node, $newUri); - } - - if ($oldUri === $newUri) { - return false; - } - - $hosts = $this->getHostnames($node); - $this->flushRoutingCacheForNode($node); - $statusCode = (integer)$this->defaultStatusCode['redirect']; - - $this->redirectStorage->addRedirect($oldUri, $newUri, $statusCode, $hosts); + $this->redirectStorage->addRedirect($oldUriPath, $newUriPath, $statusCode, $hosts); return true; } @@ -392,20 +150,19 @@ protected function createRedirectFrom(string $oldUri, string $nodeIdentifer, str /** * Removes a redirect * - * @param NodeInterface $node - * @param string $newUri + * @param string $oldUriPath + * @param SiteNodeName $siteNodeName * @return bool */ - protected function removeNodeRedirectIfNeeded(NodeInterface $node, string $newUri): bool + protected function createRedirectForRemovedTarget(string $oldUriPath, SiteNodeName $siteNodeName): bool { // By default the redirect handling for removed nodes is activated. // If it is deactivated in your settings you will be able to handle the redirects on your own. // For example redirect to dedicated landing pages for deleted campaign NodeTypes if ($this->enableRemovedNodeRedirect) { - $hosts = $this->getHostnames($node); - $this->flushRoutingCacheForNode($node); - $statusCode = (integer)$this->defaultStatusCode['gone']; - $this->redirectStorage->addRedirect($newUri, '', $statusCode, $hosts); + $hosts = $this->getHostnames($siteNodeName); + $statusCode = (integer)$this->defaultStatusCode[self::STATUS_CODE_TYPE_GONE]; + $this->redirectStorage->addRedirect($oldUriPath, '', $statusCode, $hosts); return true; } @@ -413,25 +170,13 @@ protected function removeNodeRedirectIfNeeded(NodeInterface $node, string $newUr return false; } - /** - * Removes any context information appended to a node Uri. - * - * @param string $relativeNodeUri - * @return string - */ - protected function removeContextInformationFromRelativeNodeUri(string $relativeNodeUri): string - { - // FIXME: Uses the same regexp than the ContentContextBar Ember View, but we can probably find something better. - return (string)preg_replace('/@[A-Za-z0-9;&,\-_=]+/', '', $relativeNodeUri); - } - /** * Check if the current node type is restricted by Settings * - * @param NodeInterface $node + * @param Node $node * @return bool */ - protected function isRestrictedByNodeType(NodeInterface $node): bool + protected function isRestrictedByNodeType(Node $node): bool { if (!isset($this->restrictByNodeType)) { return false; @@ -441,10 +186,10 @@ protected function isRestrictedByNodeType(NodeInterface $node): bool if ($status !== true) { continue; } - if ($node->getNodeType()->isOfType($disabledNodeType)) { + if ($node->nodeType->isOfType($disabledNodeType)) { $this->logger->debug(vsprintf('Redirect skipped based on the current node type (%s) for node %s because is of type %s', [ - $node->getNodeType()->getName(), - $node->getContextPath(), + $node->nodeType->name->value, + $node->nodeAggregateId->value, $disabledNodeType ])); @@ -455,45 +200,13 @@ protected function isRestrictedByNodeType(NodeInterface $node): bool return false; } - /** - * Check if the current node path is restricted by Settings - * - * @param NodeInterface $node - * @return bool - */ - protected function isRestrictedByPath(NodeInterface $node): bool - { - if (!isset($this->restrictByPathPrefix)) { - return false; - } - - foreach ($this->restrictByPathPrefix as $pathPrefix => $status) { - if ($status !== true) { - continue; - } - $pathPrefix = rtrim($pathPrefix, '/') . '/'; - if (mb_strpos($node->getPath(), $pathPrefix) === 0) { - $this->logger->debug(vsprintf('Redirect skipped based on the current node path (%s) for node %s because prefix matches %s', [ - $node->getPath(), - $node->getContextPath(), - $pathPrefix - ])); - - return true; - } - } - - return false; - } - /** * Check if the old URI is restricted by Settings * - * @param string $oldUri - * @param NodeInterface $node + * @param string $oldUriPath * @return bool */ - protected function isRestrictedByOldUri(string $oldUri, NodeInterface $node): bool + protected function isRestrictedByOldUri(string $oldUriPath): bool { if (!isset($this->restrictByOldUriPrefix)) { return false; @@ -504,10 +217,10 @@ protected function isRestrictedByOldUri(string $oldUri, NodeInterface $node): bo continue; } $uriPrefix = rtrim($uriPrefix, '/') . '/'; - if (mb_strpos($oldUri, $uriPrefix) === 0) { - $this->logger->debug(vsprintf('Redirect skipped based on the old URI (%s) for node %s because prefix matches %s', [ - $oldUri, - $node->getContextPath(), + $oldUriPath = rtrim($oldUriPath, '/') . '/'; + if (mb_strpos($oldUriPath, $uriPrefix) === 0) { + $this->logger->debug(vsprintf('Redirect skipped based on the old URI (%s) because prefix matches %s', [ + $oldUriPath, $uriPrefix ])); @@ -521,14 +234,15 @@ protected function isRestrictedByOldUri(string $oldUri, NodeInterface $node): bo /** * Collects all hostnames from the Domain entries attached to the current site. * - * @param NodeInterface $node + * @param SiteNodeName $siteNodeName * @return array */ - protected function getHostnames(NodeInterface $node): array + protected function getHostnames(SiteNodeName $siteNodeName): array { - $contentContext = $this->createContextMatchingNodeData($node->getNodeData()); + // TODO: Caching + $site = $this->siteRepository->findOneByNodeName($siteNodeName); + $domains = []; - $site = $contentContext->getCurrentSite(); if ($site === null) { return $domains; } @@ -542,79 +256,15 @@ protected function getHostnames(NodeInterface $node): array } /** - * Removes all routing cache entries for the given $nodeData - * - * @param NodeInterface $node - * @return void - */ - protected function flushRoutingCacheForNode(NodeInterface $node): void - { - $nodeData = $node->getNodeData(); - $nodeDataIdentifier = $this->persistenceManager->getIdentifierByObject($nodeData); - if ($nodeDataIdentifier === null) { - return; - } - $this->routerCachingService->flushCachesByTag($nodeDataIdentifier); - } - - /** - * Creates a (relative) URI for the given $nodeContextPath removing the "@workspace-name" from the result + * Creates a (relative) URI for the given $nodeInfo * - * @param NodeInterface $node + * @param string $uriPath * @return string the resulting (relative) URI - * @throws MissingActionNameException - * @throws HttpException - */ - protected function buildUriPathForNode(NodeInterface $node): string - { - return $this->getUriBuilder() - ->uriFor('show', ['node' => $node], 'Frontend\\Node', 'Neos.Neos'); - } - - /** - * Creates an UriBuilder instance for the current request - * - * @return UriBuilder */ - protected function getUriBuilder(): UriBuilder + protected function buildUri(string $uriPath): string { - if ($this->uriBuilder !== null) { - return $this->uriBuilder; - } - - $this->uriBuilder = new UriBuilder(); - $this->uriBuilder - ->setFormat('html') - ->setCreateAbsoluteUri(false) - ->setRequest($this->getActionRequestForUriBuilder()); - - return $this->uriBuilder; - } - - /** - * @param NodeInterface $node - * @param Workspace $targetWorkspace - * @return NodeInterface|null - */ - protected function getNodeInWorkspace(NodeInterface $node, Workspace $targetWorkspace): ?NodeInterface - { - return $this->getNodeInWorkspaceAndDimensions($node->getIdentifier(), $targetWorkspace->getName(), $node->getContext()->getDimensions()); - } - - /** - * @param string $nodeIdentifier - * @param string $workspaceName - * @param array $dimensionCombination - * @return NodeInterface|null - */ - protected function getNodeInWorkspaceAndDimensions(string $nodeIdentifier, string $workspaceName, array $dimensionCombination): ?NodeInterface - { - $context = $this->contextFactory->create([ - 'workspaceName' => $workspaceName, - 'dimensions' => $dimensionCombination, - 'invisibleContentShown' => true, - ]); - - return $context->getNodeByIdentifier($nodeIdentifier); + // TODO: Add dimension prefix + // TODO: Add uriSuffix + return $uriPath; } } diff --git a/Configuration/Settings.yaml b/Configuration/Settings.yaml index bb9f2fb..42d0436 100755 --- a/Configuration/Settings.yaml +++ b/Configuration/Settings.yaml @@ -6,9 +6,6 @@ Neos: # For example redirect to dedicated landing pages for deleted campaign NodeTypes enableRemovedNodeRedirect: true - pathPrefixConfiguration: [] - # '/sites/neosdemo/': false - restrictByNodeType: [] # Neos.Neos:Document: true diff --git a/Configuration/Testing/Behat/NodeTypes.Test.Redirect.yaml b/Configuration/Testing/Behat/NodeTypes.Test.Redirect.yaml new file mode 100644 index 0000000..682fd71 --- /dev/null +++ b/Configuration/Testing/Behat/NodeTypes.Test.Redirect.yaml @@ -0,0 +1,17 @@ +# Those node type definitions are required for the Redirect Behat tests + +'Neos.Neos:Test.Redirect.Page': + superTypes: + 'Neos.Neos:Document': true + constraints: + nodeTypes: + '*': true + 'Neos.Neos:Test.Redirect.Page': true + +'Neos.Neos:Test.Redirect.RestrictedPage': + superTypes: + 'Neos.Neos:Document': true + constraints: + nodeTypes: + '*': true + 'Neos.Neos:Test.Redirect.Page': true \ No newline at end of file diff --git a/Configuration/Testing/Behat/Settings.Restictions.yaml b/Configuration/Testing/Behat/Settings.Restictions.yaml new file mode 100644 index 0000000..742ea15 --- /dev/null +++ b/Configuration/Testing/Behat/Settings.Restictions.yaml @@ -0,0 +1,10 @@ +Neos: + RedirectHandler: + NeosAdapter: + enableRemovedNodeRedirect: true + enableAutomaticRedirects: true + restrictByNodeType: + 'Neos.Neos:Test.Redirect.RestrictedPage': true + restrictByOldUriPrefix: + 'restricted-by-path': true + diff --git a/Documentation/index.rst b/Documentation/index.rst index a0c8095..71e84d6 100644 --- a/Documentation/index.rst +++ b/Documentation/index.rst @@ -58,23 +58,6 @@ Restrict redirect generation by node type. restrictByNodeType: Neos.Neos:Document: true -restrictByPathPrefix -^^^^^^^^^^^^^^^^^^^^ - -Restrict redirect generation by node path prefix. - -**Note**: No redirect will be created if you move a node within the restricted path or if you move it away from the -restricted path. But if you move a node into the restricted path the restriction rule will not apply, because the -restriction is based on the source node path. - -.. code-block:: yaml - - Neos: - RedirectHandler: - NeosAdapter: - restrictByPathPrefix: - - '/sites/neosdemo': true - restrictByOldUriPrefix ^^^^^^^^^^^^^^^^^^^^^^ diff --git a/Tests/Behavior/Features/Bootstrap/FeatureContext.php b/Tests/Behavior/Features/Bootstrap/FeatureContext.php index e12e29f..3af7b4c 100644 --- a/Tests/Behavior/Features/Bootstrap/FeatureContext.php +++ b/Tests/Behavior/Features/Bootstrap/FeatureContext.php @@ -1,45 +1,157 @@ initializeFlow(); } $this->objectManager = self::$bootstrap->getObjectManager(); + $this->environment = $this->objectManager->get(Environment::class); + $this->nodeAuthorizationService = $this->objectManager->get(AuthorizationService::class); - $this->nodeTypeManager = $this->objectManager->get(NodeTypeManager::class); $this->setupSecurity(); + $this->setupEventSourcedTrait(true); + } + + protected function getContentRepositoryRegistry(): ContentRepositoryRegistry + { + /** @var ContentRepositoryRegistry $contentRepositoryRegistry */ + $contentRepositoryRegistry = $this->objectManager->get(ContentRepositoryRegistry::class); + + return $contentRepositoryRegistry; + } + + protected function getContentRepositoryService(ContentRepositoryId $contentRepositoryId, ContentRepositoryServiceFactoryInterface $factory): ContentRepositoryServiceInterface + { + return $this->getContentRepositoryRegistry()->getService($contentRepositoryId, $factory); + } + + /** + * @param array $adapterKeys "DoctrineDBAL" if + * @return void + */ + protected function initCleanContentRepository(array $adapterKeys): void + { + $this->logToRaceConditionTracker(['msg' => 'initCleanContentRepository']); + + $configurationManager = $this->getObjectManager()->get(ConfigurationManager::class); + $registrySettings = $configurationManager->getConfiguration( + ConfigurationManager::CONFIGURATION_TYPE_SETTINGS, + 'Neos.ContentRepositoryRegistry' + ); + + if (!in_array('Postgres', $adapterKeys)) { + // in case we do not have tests annotated with @adapters=Postgres, we + // REMOVE the Postgres projection from the Registry settings. This way, we won't trigger + // Postgres projection catchup for tests which are not yet postgres-aware. + // + // This is to make the testcases more stable and deterministic. We can remove this workaround + // once the Postgres adapter is fully ready. + unset($registrySettings['presets'][$this->contentRepositoryId->value]['projections']['Neos.ContentGraph.PostgreSQLAdapter:Hypergraph']); + } + $registrySettings['presets'][$this->contentRepositoryId->value]['userIdProvider']['factoryObjectName'] = FakeUserIdProviderFactory::class; + $registrySettings['presets'][$this->contentRepositoryId->value]['clock']['factoryObjectName'] = FakeClockFactory::class; + + $this->contentRepositoryRegistry = new ContentRepositoryRegistry( + $registrySettings, + $this->getObjectManager() + ); + + + $this->contentRepository = $this->contentRepositoryRegistry->get($this->contentRepositoryId); + // Big performance optimization: only run the setup once - DRAMATICALLY reduces test time + if ($this->alwaysRunContentRepositorySetup || !self::$wasContentRepositorySetupCalled) { + $this->contentRepository->setUp(); + self::$wasContentRepositorySetupCalled = true; + } + $this->contentRepositoryInternals = $this->contentRepositoryRegistry->getService($this->contentRepositoryId, new ContentRepositoryInternalsFactory()); + + $availableContentGraphs = []; + $availableContentGraphs['DoctrineDBAL'] = $this->contentRepository->getContentGraph(); + // NOTE: to disable a content graph (do not run the tests for it), you can use "null" as value. + if (in_array('Postgres', $adapterKeys)) { + $availableContentGraphs['Postgres'] = $this->contentRepository->projectionState(ContentHypergraph::class); + } + + if (count($availableContentGraphs) === 0) { + throw new \RuntimeException('No content graph active during testing. Please set one in settings in activeContentGraphs'); + } + $this->availableContentGraphs = new ContentGraphs($availableContentGraphs); } } diff --git a/Tests/Behavior/Features/Bootstrap/RedirectOperationTrait.php b/Tests/Behavior/Features/Bootstrap/RedirectOperationTrait.php index abb3b78..f28410e 100755 --- a/Tests/Behavior/Features/Bootstrap/RedirectOperationTrait.php +++ b/Tests/Behavior/Features/Bootstrap/RedirectOperationTrait.php @@ -39,23 +39,29 @@ public function iHaveTheFollowingRedirects($table): void } /** - * @Then /^A redirect should be created for the node with path "([^"]*)" and with the following context:$/ + * @Given /^I should have a redirect with sourceUri "([^"]*)" and targetUri "([^"]*)"$/ */ - public function aRedirectShouldBeCreatedForTheNodeWithPathAndWithTheFollowingContext($path, $table): void + public function iShouldHaveARedirectWithSourceUriAndTargetUri($sourceUri, $targetUri): void { - $rows = $table->getHash(); - $context = $this->getContextForProperties($rows[0]); - $workspace = $context->getWorkspace(); - $redirectNode = $context->getNode($path); - $redirectService = $this->objectManager->get(NodeRedirectService::class); + $nodeRedirectStorage = $this->objectManager->get(RedirectStorage::class); + + $redirect = $nodeRedirectStorage->getOneBySourceUriPathAndHost($sourceUri); - $redirectService->createRedirectsForPublishedNode($redirectNode, $workspace); + if ($redirect !== null) { + Assert::assertEquals( + $targetUri, + $redirect->getTargetUriPath(), + 'A redirect was created, but the target URI does not match' + ); + } else { + Assert::assertNotNull($redirect, 'No redirect was created for asserted sourceUri'); + } } /** - * @Given /^I should have a redirect with sourceUri "([^"]*)" and targetUri "([^"]*)"$/ + * @Given /^I should have a redirect with sourceUri "([^"]*)" and statusCode "([^"]*)"$/ */ - public function iShouldHaveARedirectWithSourceUriAndTargetUri($sourceUri, $targetUri): void + public function iShouldHaveARedirectWithSourceUriAndStatus($sourceUri, $statusCode): void { $nodeRedirectStorage = $this->objectManager->get(RedirectStorage::class); @@ -63,9 +69,9 @@ public function iShouldHaveARedirectWithSourceUriAndTargetUri($sourceUri, $targe if ($redirect !== null) { Assert::assertEquals( - $targetUri, - $redirect->getTargetUriPath(), - 'A redirect was created, but the target URI does not match' + $statusCode, + $redirect->getStatusCode(), + 'A redirect was created, but the status code does not match' ); } else { Assert::assertNotNull($redirect, 'No redirect was created for asserted sourceUri'); @@ -84,11 +90,11 @@ public function iShouldHaveNoRedirectWithSourceUriAndTargetUri($sourceUri, $targ Assert::assertNotEquals( $targetUri, $redirect->getTargetUriPath(), - 'An untwanted redirect was created for given source and target URI' + 'An unwanted redirect was created for given source and target URI' ); + } else { + Assert::assertNull($redirect); } - - Assert::assertNull($redirect); } /** diff --git a/Tests/Behavior/Features/Redirect.feature b/Tests/Behavior/Features/Redirect.feature index d14265f..ca3f44d 100755 --- a/Tests/Behavior/Features/Redirect.feature +++ b/Tests/Behavior/Features/Redirect.feature @@ -1,129 +1,254 @@ -Feature: Redirects are created automatically when the URI of an existing node is changed +@fixtures @contentrepository +Feature: Basic redirect handling with document nodes in one dimension + Background: - Given I am authenticated with role "Neos.Neos:Editor" - And I have the following content dimensions: - | Identifier | Default | Presets | - | language | en | en=en; de=de,en; fr=fr | - And I have the following nodes: - | Identifier | Path | Node Type | Properties | Workspace | Hidden | Language | - | ecf40ad1-3119-0a43-d02e-55f8b5aa3c70 | /sites | unstructured | | live | | | - | fd5ba6e1-4313-b145-1004-dad2f1173a35 | /sites/behat | Neos.Neos:Document | {"uriPathSegment": "home"} | live | | en | - | 68ca0dcd-2afb-ef0e-1106-a5301e65b8a0 | /sites/behat/company | Neos.Neos:Document | {"uriPathSegment": "company"} | live | | en | - | 52540602-b417-11e3-9358-14109fd7a2dd | /sites/behat/service | Neos.Neos:Document | {"uriPathSegment": "service"} | live | | en | - | dc48851c-f653-ebd5-4d35-3feac69a3e09 | /sites/behat/about | Neos.Neos:Document | {"uriPathSegment": "about"} | live | | en | - | 511e9e4b-2193-4100-9a91-6fde2586ae95 | /sites/behat/imprint | Neos.Neos:Document | {"uriPathSegment": "impressum"} | live | | de | - | 511e9e4b-2193-4100-9a91-6fde2586ae95 | /sites/behat/imprint | Neos.Neos:Document | {"uriPathSegment": "imprint"} | live | | en | - | 511e9e4b-2193-4100-9a91-6fde2586ae95 | /sites/behat/imprint | Neos.Neos:Document | {"uriPathSegment": "empreinte"} | live | | fr | - | 4bba27c8-5029-4ae6-8371-0f2b3e1700a9 | /sites/behat/buy | Neos.Neos:Document | {"uriPathSegment": "buy", "title": "Buy"} | live | | en | - | 4bba27c8-5029-4ae6-8371-0f2b3e1700a9 | /sites/behat/buy | Neos.Neos:Document | {"uriPathSegment": "acheter"} | live | | fr | - | 4bba27c8-5029-4ae6-8371-0f2b3e1700a9 | /sites/behat/buy | Neos.Neos:Document | {"uriPathSegment": "kaufen"} | live | true | de | - | 81dc6c8c-f478-434c-9ac9-bd5d1781cd95 | /sites/behat/mail | Neos.Neos:Document | {"uriPathSegment": "mail"} | live | | en | - | 81dc6c8c-f478-434c-9ac9-bd5d1781cd95 | /sites/behat/mail | Neos.Neos:Document | {"uriPathSegment": "mail"} | live | true | de | + Given I have no content dimensions + And I am user identified by "initiating-user-identifier" + And the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | newContentStreamId | "cs-identifier" | + And the event RootNodeAggregateWithNodeWasCreated was published with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "site-root" | + | nodeTypeName | "Neos.Neos:Sites" | + | coveredDimensionSpacePoints | [{}] | + | nodeAggregateClassification | "root" | + And the graph projection is fully up to date + + # site-root + # behat + # company + # service + # about + # imprint + # buy + # mail + And I am in content stream "cs-identifier" and dimension space point {} + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | parentNodeAggregateId | nodeTypeName | initialPropertyValues | nodeName | + | behat | site-root | Neos.Neos:Test.Redirect.Page | {"uriPathSegment": "home"} | node1 | + | company | behat | Neos.Neos:Test.Redirect.Page | {"uriPathSegment": "company"} | node2 | + | service | company | Neos.Neos:Test.Redirect.Page | {"uriPathSegment": "service"} | node3 | + | about | company | Neos.Neos:Test.Redirect.Page | {"uriPathSegment": "about"} | node4 | + | imprint | behat | Neos.Neos:Test.Redirect.Page | {"uriPathSegment": "imprint"} | node5 | + | buy | behat | Neos.Neos:Test.Redirect.Page | {"uriPathSegment": "buy", "title": "Buy"} | node6 | + | mail | behat | Neos.Neos:Test.Redirect.Page | {"uriPathSegment": "mail"} | node7 | + | restricted-by-nodetype | behat | Neos.Neos:Test.Redirect.RestrictedPage | {"uriPathSegment": "restricted-by-nodetype"} | node8 | + | restricted-by-path | behat | Neos.Neos:Test.Redirect.Page | {"uriPathSegment": "restricted-by-path"} | node9 | + And A site exists for node name "node1" + And the sites configuration is: + """ + Neos: + Neos: + sites: + '*': + contentRepository: default + contentDimensions: + resolver: + factoryClassName: Neos\Neos\FrontendRouting\DimensionResolution\Resolver\NoopResolverFactory + """ + And The documenturipath projection is up to date @fixtures - Scenario: Move a node into different node and a redirect will be created - When I get a node by path "/sites/behat/service" with the following context: - | Workspace | - | user-testaccount | - And I move the node into the node with path "/sites/behat/company" - And I publish the node - Then I should have a redirect with sourceUri "en/service.html" and targetUri "en/company/service.html" + Scenario: Move a node down into different node and a redirect will be created + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "imprint" | + | dimensionSpacePoint | {} | + | newParentNodeAggregateId | "company" | + | newSucceedingSiblingNodeAggregateId | null | + And The documenturipath projection is up to date + Then I should have a redirect with sourceUri "imprint" and targetUri "company/imprint" + + Scenario: Move a node up into different node and a redirect will be created + When the command MoveNodeAggregate is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "service" | + | dimensionSpacePoint | {} | + | newParentNodeAggregateId | "behat" | + | newSucceedingSiblingNodeAggregateId | null | + And The documenturipath projection is up to date + Then I should have a redirect with sourceUri "company/service" and targetUri "service" @fixtures Scenario: Change the the `uriPathSegment` and a redirect will be created - When I get a node by path "/sites/behat/company" with the following context: - | Workspace | - | user-testaccount | - And I set the node property "uriPathSegment" to "evil-corp" - And I publish the node - Then I should have a redirect with sourceUri "en/company.html" and targetUri "en/evil-corp.html" - - #fixed in 1.0.2 - @fixtures - Scenario: Retarget an existing redirect when the target URI matches the source URI of the new redirect - When I get a node by path "/sites/behat/about" with the following context: - | Workspace | - | user-testaccount | - And I have the following redirects: - | sourceuripath | targeturipath | - | en/about.html | en/about-you.html | - And I set the node property "uriPathSegment" to "about-me" - And I publish the node - And I should have a redirect with sourceUri "en/about.html" and targetUri "en/about-me.html" + When the command SetNodeProperties is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "company" | + | originDimensionSpacePoint | {} | + | propertyValues | {"uriPathSegment": "evil-corp"} | + And The documenturipath projection is up to date + Then I should have a redirect with sourceUri "company" and targetUri "evil-corp" + And I should have a redirect with sourceUri "company/service" and targetUri "evil-corp/service" @fixtures - Scenario: Redirects should aways be created in the same dimension the node is in - When I get a node by path "/sites/behat/imprint" with the following context: - | Workspace | Language | - | user-testaccount | fr | - And I set the node property "uriPathSegment" to "empreinte-nouveau" - And I publish the node - Then I should have a redirect with sourceUri "fr/empreinte.html" and targetUri "fr/empreinte-nouveau.html" - - #fixed in 1.0.3 - @fixtures - Scenario: Redirects should aways be created in the same dimension the node is in and not the fallback dimension - When I get a node by path "/sites/behat/imprint" with the following context: - | Workspace | Language | - | user-testaccount | de,en | - And I set the node property "uriPathSegment" to "impressum-neu" - And I publish the node - Then I should have a redirect with sourceUri "de/impressum.html" and targetUri "de/impressum-neu.html" - And I should have no redirect with sourceUri "en/impressum.html" and targetUri "de/impressum-neu.html" - - #fixed in 1.0.3 + Scenario: Change the the `uriPathSegment` mutiple times and mutliple redirects will be created + When the command SetNodeProperties is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "company" | + | originDimensionSpacePoint | {} | + | propertyValues | {"uriPathSegment": "evil-corp"} | + And the command SetNodeProperties is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "company" | + | originDimensionSpacePoint | {} | + | propertyValues | {"uriPathSegment": "more-evil-corp"} | + And The documenturipath projection is up to date + + Then I should have a redirect with sourceUri "company" and targetUri "more-evil-corp" + And I should have a redirect with sourceUri "company/service" and targetUri "more-evil-corp/service" + And I should have a redirect with sourceUri "evil-corp" and targetUri "more-evil-corp" + And I should have a redirect with sourceUri "evil-corp/service" and targetUri "more-evil-corp/service" + + @fixtures - Scenario: I have an existing redirect and it should never be overwritten for a node variant from a different dimension + Scenario: Retarget an existing redirect when the source URI matches the source URI of the new redirect When I have the following redirects: - | sourceuripath | targeturipath | - | important-page-from-the-old-site | en/mail.html | - When I get a node by path "/sites/behat/mail" with the following context: - | Workspace | Language | - | user-testaccount | de,en | - And I unhide the node - And I publish the node - Then I should have a redirect with sourceUri "important-page-from-the-old-site" and targetUri "en/mail.html" - And I should have no redirect with sourceUri "en/mail.html" and targetUri "de/mail.html" + | sourceuripath | targeturipath | + | company | company-old | + And the command SetNodeProperties is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "company" | + | originDimensionSpacePoint | {} | + | propertyValues | {"uriPathSegment": "my-company"} | + And The documenturipath projection is up to date + Then I should have a redirect with sourceUri "company" and targetUri "my-company" + And I should have no redirect with sourceUri "company" and targetUri "company-old" + And I should have a redirect with sourceUri "company/service" and targetUri "my-company/service" @fixtures Scenario: No redirect should be created for an existing node if any non URI related property changes - When I get a node by path "/sites/behat/buy" with the following context: - | Workspace | - | user-testaccount | - And I set the node property "title" to "Buy later" - And I publish the node - Then I should have no redirect with sourceUri "en/buy.html" + When the command SetNodeProperties is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "buy" | + | originDimensionSpacePoint | {} | + | propertyValues | {"title": "my-buy"} | + And The documenturipath projection is up to date + Then I should have no redirect with sourceUri "buy" @fixtures - Scenario: Redirects should be created for a hidden node - When I get a node by path "/sites/behat/buy" with the following context: - | Workspace | Language | - | user-testaccount | de,en | - And I set the node property "uriPathSegment" to "nicht-kaufen" - And I publish the node - Then I should have a redirect with sourceUri "de/kaufen.html" and targetUri "de/nicht-kaufen.html" + Scenario: No redirect should be created for an restricted node by nodetype + When the command SetNodeProperties is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "restricted-by-nodetype" | + | originDimensionSpacePoint | {} | + | propertyValues | {"uriPathSegment": "restricted-by-nodetype-new"} | + And The documenturipath projection is up to date + Then I should have no redirect with sourceUri "restricted" + @fixtures - Scenario: Create redirects for nodes published in different dimensions - When I get a node by path "/sites/behat/buy" with the following context: - | Workspace | - | user-testaccount | - And I move the node into the node with path "/sites/behat/company" - And I publish the node - When I get a node by path "/sites/behat/company/buy" with the following context: - | Workspace | Language | - | user-testaccount | de,en | - And I publish the node - Then I should have a redirect with sourceUri "en/buy.html" and targetUri "en/company/buy.html" - And I should have a redirect with sourceUri "de/kaufen.html" and targetUri "de/company/kaufen.html" - - #fixed in 1.0.4 + Scenario: No redirect should be created for an restricted node by path + When the command SetNodeProperties is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "restricted-by-path" | + | originDimensionSpacePoint | {} | + | propertyValues | {"uriPathSegment": "restricted-by-path-new"} | + And The documenturipath projection is up to date + Then I should have no redirect with sourceUri "restricted-by-path" + +# @fixtures +# Scenario: Redirects should always be created in the same dimension the node is in +# When I get a node by path "/sites/behat/imprint" with the following context: +# | Workspace | Language | +# | user-testaccount | fr | +# And I set the node property "uriPathSegment" to "empreinte-nouveau" +# And I publish the node +# Then I should have a redirect with sourceUri "fr/empreinte" and targetUri "fr/empreinte-nouveau" +# +# #fixed in 1.0.3 +# @fixtures +# Scenario: Redirects should aways be created in the same dimension the node is in and not the fallback dimension +# When I get a node by path "/sites/behat/imprint" with the following context: +# | Workspace | Language | +# | user-testaccount | de,en | +# And I set the node property "uriPathSegment" to "impressum-neu" +# And I publish the node +# Then I should have a redirect with sourceUri "de/impressum" and targetUri "de/impressum-neu" +# And I should have no redirect with sourceUri "en/impressum" and targetUri "de/impressum-neu" +# +# #fixed in 1.0.3 +# @fixtures +# Scenario: I have an existing redirect and it should never be overwritten for a node variant from a different dimension +# When I have the following redirects: +# | sourceuripath | targeturipath | +# | important-page-from-the-old-site | en/mail | +# When I get a node by path "/sites/behat/mail" with the following context: +# | Workspace | Language | +# | user-testaccount | de,en | +# And I unhide the node +# And I publish the node +# Then I should have a redirect with sourceUri "important-page-from-the-old-site" and targetUri "en/mail" +# And I should have no redirect with sourceUri "en/mail" and targetUri "de/mail" +# + @fixtures - Scenario: Create redirects for nodes that use the current dimension as fallback - When I get a node by path "/sites/behat/company" with the following context: - | Workspace | Language | - | user-testaccount | en | - And I move the node into the node with path "/sites/behat/service" - And I publish the node - Then I should have a redirect with sourceUri "en/company.html" and targetUri "en/service/company.html" - And I should have a redirect with sourceUri "de/company.html" and targetUri "de/service/company.html" + Scenario: Redirects should be created for a hidden node + When the command DisableNodeAggregate is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "mail" | + | originDimensionSpacePoint | {} | + | nodeVariantSelectionStrategy | "allVariants" | + And the graph projection is fully up to date + When the command SetNodeProperties is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "mail" | + | originDimensionSpacePoint | {} | + | propertyValues | {"uriPathSegment": "not-mail"} | + And The documenturipath projection is up to date + Then I should have a redirect with sourceUri "mail" and targetUri "not-mail" + +# @fixtures +# Scenario: Create redirects for nodes published in different dimensions +# When I get a node by path "/sites/behat/buy" with the following context: +# | Workspace | +# | user-testaccount | +# And I move the node into the node with path "/sites/behat/company" +# And I publish the node +# When I get a node by path "/sites/behat/company/buy" with the following context: +# | Workspace | Language | +# | user-testaccount | de,en | +# And I publish the node +# Then I should have a redirect with sourceUri "en/buy" and targetUri "en/company/buy" +# And I should have a redirect with sourceUri "de/kaufen" and targetUri "de/company/kaufen" +# +# #fixed in 1.0.4 +# @fixtures +# Scenario: Create redirects for nodes that use the current dimension as fallback +# When I get a node by path "/sites/behat/company" with the following context: +# | Workspace | Language | +# | user-testaccount | en | +# And I move the node into the node with path "/sites/behat/service" +# And I publish the node +# Then I should have a redirect with sourceUri "en/company" and targetUri "en/service/company" +# And I should have a redirect with sourceUri "de/company" and targetUri "de/service/company" + + + @fixtures + Scenario: A removed node should lead to a GONE response with empty target uri + Given the event NodeAggregateWasRemoved was published with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "company" | + | affectedOccupiedDimensionSpacePoints | [] | + | affectedCoveredDimensionSpacePoints | [[]] | + And the graph projection is fully up to date + And The documenturipath projection is up to date + + Then I should have a redirect with sourceUri "company" and statusCode "410" + And I should have a redirect with sourceUri "company" and targetUri "" + And I should have a redirect with sourceUri "company/service" and statusCode "410" + And I should have a redirect with sourceUri "company/service" and targetUri "" \ No newline at end of file diff --git a/Tests/Behavior/behat.yml b/Tests/Behavior/behat.yml deleted file mode 100644 index c950ae8..0000000 --- a/Tests/Behavior/behat.yml +++ /dev/null @@ -1,25 +0,0 @@ -# Behat distribution configuration -# -# Override with behat.yml for local configuration. -# -default: - autoload: - '': "%paths.base%/Features/Bootstrap" - suites: - content: - paths: - - "%paths.base%/Features" - contexts: - - FeatureContext - extensions: - Behat\MinkExtension: - files_path: features/Resources - show_cmd: 'open %s' - goutte: ~ - selenium2: ~ - - # Project base URL - # - # Use BEHAT_PARAMS="extensions[Behat\MinkExtension\Extension][base_url]=http://example.local/" for configuration during runtime. - # - base_url: http://neos5.behat.test/ diff --git a/Tests/Behavior/behat.yml.dist b/Tests/Behavior/behat.yml.dist index d27ab4b..a97facc 100644 --- a/Tests/Behavior/behat.yml.dist +++ b/Tests/Behavior/behat.yml.dist @@ -2,6 +2,7 @@ # # Override with behat.yml for local configuration. # + default: autoload: '': "%paths.base%/Features/Bootstrap" @@ -10,16 +11,25 @@ default: paths: - "%paths.base%/Features" contexts: - - FeatureContext - extensions: - Behat\MinkExtension: - files_path: features/Resources - show_cmd: 'open %s' - goutte: ~ - selenium2: ~ + - FeatureContext # Project base URL # - # Use BEHAT_PARAMS="extensions[Behat\MinkExtension\Extension][base_url]=http://example.local/" for configuration during runtime. + # Use BEHAT_PARAMS="extensions[Behat\MinkExtension\Extension][base_url]=http://neos.local/" for configuration during + # runtime. + # + # base_url: http://localhost/ + + # Saucelabs configuration + # + # Use this configuration, if you want to use saucelabs for your @javascript-tests # - base_url: http://localhost/ + #javascript_session: saucelabs + #saucelabs: + #username: + #access_key: + +# Import a bunch of browser configurations for saucelab tests +# +#imports: + #- saucelabsBrowsers.yml diff --git a/Tests/Functional/Service/NodeRedirectServiceTest.php b/Tests/Functional/Service/NodeRedirectServiceTest.php index 360e5bc..12433d3 100644 --- a/Tests/Functional/Service/NodeRedirectServiceTest.php +++ b/Tests/Functional/Service/NodeRedirectServiceTest.php @@ -62,27 +62,27 @@ class NodeRedirectServiceTest extends FunctionalTestCase protected $nodeDataRepository; /** - * @var NodeTypeManager + * @var \Neos\ContentRepository\Core\NodeType\NodeTypeManager */ protected $nodeTypeManager; /** - * @var Workspace + * @var \Neos\ContentRepository\Core\Projection\Workspace\Workspace */ protected $liveWorkspace; /** - * @var Workspace + * @var \Neos\ContentRepository\Core\Projection\Workspace\Workspace */ protected $userWorkspace; /** - * @var ContentContext + * @var \Neos\Rector\ContentRepository90\Legacy\LegacyContextStub */ protected $userContext; /** - * @var NodeInterface + * @var \Neos\ContentRepository\Core\Projection\ContentGraph\Node */ protected $site; @@ -111,10 +111,10 @@ public function setUp(): void $this->mockRedirectStorage = $this->getMockBuilder(RedirectStorageInterface::class)->getMock(); $this->inject($this->nodeRedirectService, 'redirectStorage', $this->mockRedirectStorage); $this->contentContextFactory = $this->objectManager->get(ContentContextFactory::class); - $this->nodeTypeManager = $this->objectManager->get(NodeTypeManager::class); + $this->nodeTypeManager = $this->objectManager->get(\Neos\ContentRepository\Core\NodeType\NodeTypeManager::class); $this->workspaceRepository = $this->objectManager->get(WorkspaceRepository::class); - $this->liveWorkspace = new Workspace('live'); - $this->userWorkspace = new Workspace('user-me', $this->liveWorkspace); + $this->liveWorkspace = new \Neos\ContentRepository\Core\Projection\Workspace\Workspace('live'); + $this->userWorkspace = new \Neos\ContentRepository\Core\Projection\Workspace\Workspace('user-me', $this->liveWorkspace); $this->workspaceRepository->add($this->liveWorkspace); $this->workspaceRepository->add($this->userWorkspace); $liveContext = $this->contentContextFactory->create([ diff --git a/composer.json b/composer.json index 41ba162..2972f80 100644 --- a/composer.json +++ b/composer.json @@ -5,8 +5,8 @@ "license": "GPL-3.0-or-later", "require": { "neos/redirecthandler": "~3.0 || ~4.0 || ~5.0 || dev-master", - "neos/neos": "~9.0 || dev-master", - "neos/flow": "~9.0 || dev-master" + "neos/neos": "^9.0", + "neos/flow": "^9.0" }, "autoload": { "psr-4": {