Skip to content

Commit

Permalink
Merge pull request #49 from neos/feature/automatic-redirects-on-publish
Browse files Browse the repository at this point in the history
FEATURE: Allow automatic redirect generation for CLI publishing
  • Loading branch information
Sebobo authored Dec 11, 2020
2 parents 419d626 + f864100 commit d04e181
Show file tree
Hide file tree
Showing 8 changed files with 261 additions and 87 deletions.
55 changes: 55 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: Tests

on:
push:
branches: [ master ]
pull_request:
branches: [ master ]

jobs:
build:
env:
FLOW_TARGET_VERSION: 6.3
FLOW_CONTEXT: Testing
FLOW_FOLDER: ../flow-base-distribution

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2

- name: Update Composer
run: |
sudo composer self-update
composer --version
# Directory permissions for .composer are wrong, so we remove the complete directory
# https://github.com/actions/virtual-environments/issues/824
- name: Delete .composer directory
run: |
sudo rm -rf ~/.composer
- name: Cache dependencies
uses: actions/cache@v1
with:
path: ~/.composer/cache
key: dependencies-composer-${{ hashFiles('composer.json') }}

- name: Prepare Flow distribution
run: |
git clone https://github.com/neos/flow-base-distribution.git -b ${FLOW_TARGET_VERSION} ${FLOW_FOLDER}
cd ${FLOW_FOLDER}
composer require --no-update --no-interaction neos/redirecthandler-databasestorage:~4.0
composer require --no-update --no-interaction neos/redirecthandler-neosadapter:~4.0
- name: Install distribution
run: |
cd ${FLOW_FOLDER}
composer install --no-interaction --no-progress
rm -rf Packages/Application/Neos.RedirectHandler.NeosAdapter
cp -r ../redirecthandler-neosadapter Packages/Application/Neos.RedirectHandler.NeosAdapter
- name: Run Functional tests
run: |
cd ${FLOW_FOLDER}
bin/phpunit --colors -c Build/BuildEssentials/PhpUnit/FunctionalTests.xml Packages/Application/Neos.RedirectHandler.NeosAdapter/Tests/Functional/*
98 changes: 78 additions & 20 deletions Classes/Service/NodeRedirectService.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,26 @@
* 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\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\Http\ServerRequestAttributes;
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\Service\ContentContext;
use Neos\RedirectHandler\Storage\RedirectStorageInterface;
use Psr\Log\LoggerInterface;

Expand All @@ -40,6 +45,8 @@
*/
class NodeRedirectService
{
use CreateContentContextTrait;

/**
* @var UriBuilder
*/
Expand Down Expand Up @@ -129,11 +136,22 @@ class NodeRedirectService
*/
protected $enableAutomaticRedirects;

/**
* @Flow\InjectConfiguration(path="http.baseUri", package="Neos.Flow")
* @var string
*/
protected $baseUri;

/**
* @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
*
Expand All @@ -155,6 +173,54 @@ public function collectPossibleRedirects(NodeInterface $node, Workspace $targetW
$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);
} 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.
*
Expand All @@ -169,9 +235,10 @@ public function createPendingRedirects(): void

$this->nodeFactory->reset();
foreach ($this->pendingRedirects as $nodeIdentifierAndWorkspace => $oldUriPerDimensionCombination) {
list($nodeIdentifier, $workspaceName) = explode('@', $nodeIdentifierAndWorkspace);
[$nodeIdentifier, $workspaceName] = explode('@', $nodeIdentifierAndWorkspace);
$this->buildRedirects($nodeIdentifier, $workspaceName, $oldUriPerDimensionCombination);
}
$this->pendingRedirects = [];

$this->persistenceManager->persistAll();
}
Expand Down Expand Up @@ -260,7 +327,7 @@ protected function hasNodeUriChanged(NodeInterface $node, Workspace $targetWorks
*/
protected function buildRedirects(string $nodeIdentifier, string $workspaceName, array $oldUriPerDimensionCombination): void
{
foreach ($oldUriPerDimensionCombination as list($oldRelativeUri, $dimensionCombination)) {
foreach ($oldUriPerDimensionCombination as [$oldRelativeUri, $dimensionCombination]) {
$this->createRedirectFrom($oldRelativeUri, $nodeIdentifier, $workspaceName, $dimensionCombination);
}
}
Expand Down Expand Up @@ -296,7 +363,7 @@ protected function createRedirectFrom(string $oldUri, string $nodeIdentifer, str
return false;
}

$hosts = $this->getHostnames($node->getContext());
$hosts = $this->getHostnames($node);
$this->flushRoutingCacheForNode($node);
$statusCode = (integer)$this->defaultStatusCode['redirect'];

Expand All @@ -318,7 +385,7 @@ protected function removeNodeRedirectIfNeeded(NodeInterface $node, string $newUr
// 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->getContext());
$hosts = $this->getHostnames($node);
$this->flushRoutingCacheForNode($node);
$statusCode = (integer)$this->defaultStatusCode['gone'];
$this->redirectStorage->addRedirect($newUri, '', $statusCode, $hosts);
Expand Down Expand Up @@ -437,11 +504,12 @@ protected function isRestrictedByOldUri(string $oldUri, NodeInterface $node): bo
/**
* Collects all hostnames from the Domain entries attached to the current site.
*
* @param ContentContext $contentContext
* @param NodeInterface $node
* @return array
*/
protected function getHostnames(ContentContext $contentContext): array
protected function getHostnames(NodeInterface $node): array
{
$contentContext = $this->createContextMatchingNodeData($node->getNodeData());
$domains = [];
$site = $contentContext->getCurrentSite();
if ($site === null) {
Expand Down Expand Up @@ -478,6 +546,7 @@ protected function flushRoutingCacheForNode(NodeInterface $node): void
* @param NodeInterface $node
* @return string the resulting (relative) URI
* @throws MissingActionNameException
* @throws HttpException
*/
protected function buildUriPathForNode(NodeInterface $node): string
{
Expand All @@ -496,22 +565,11 @@ protected function getUriBuilder(): UriBuilder
return $this->uriBuilder;
}

/** @var HttpRequestHandlerInterface $requestHandler */
$requestHandler = $this->bootstrap->getActiveRequestHandler();
if (method_exists(ActionRequest::class, 'fromHttpRequest')) {
// From Flow 6+ we have to use a static method to create an ActionRequest. Earlier versions use the constructor.
$actionRequest = ActionRequest::fromHttpRequest($requestHandler->getHttpRequest());
} else {
// This can be cleaned up when this package in a future release only support Flow 6+.
$actionRequest = new ActionRequest($requestHandler->getHttpRequest());
}

$this->uriBuilder = new UriBuilder();
$this->uriBuilder
->setRequest($actionRequest);
$this->uriBuilder
->setFormat('html')
->setCreateAbsoluteUri(false);
->setCreateAbsoluteUri(false)
->setRequest($this->getActionRequestForUriBuilder());

return $this->uriBuilder;
}
Expand Down
15 changes: 15 additions & 0 deletions Documentation/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,18 @@ If you have the `Neos.RedirectHandler.Ui` [package](https://github.com/neos/redi
you can also export the redirects via the UI in the redirect handler backend module.

The same restrictions and requirements apply as for the import via the CLI.


===========================================
Redirect generation when publishing via CLI
===========================================

When publishing nodes from CLI f.e. via `flow workspace:publish` you might need additional configuration
to make sure that redirects are properly generated.
As commands don't have a http request, a fake http request has to be generated to make the url generation work.
In this case the setting `Neos.Flow.http.baseUri` will be used.
If this is not set `http://localhost` is used.

This should not be a problem in most cases as without an active domain for a site
only relative redirects are generated.
If a site has an active domain this on will be used to set the `Origin domain` for a new redirect.
24 changes: 11 additions & 13 deletions Tests/Behavior/Features/Bootstrap/FeatureContext.php
Original file line number Diff line number Diff line change
@@ -1,45 +1,43 @@
<?php

require_once(__DIR__ . '/../../../../../Neos.Behat/Tests/Behat/FlowContext.php');
require_once(__DIR__ . '/../../../../../Neos.Behat/Tests/Behat/FlowContextTrait.php');
require_once(__DIR__ . '/../../../../../../Neos/Neos.ContentRepository/Tests/Behavior/Features/Bootstrap/NodeOperationsTrait.php');
require_once(__DIR__ . '/RedirectOperationTrait.php');
require_once(__DIR__ . '/../../../../../../Neos/Neos.ContentRepository/Tests/Behavior/Features/Bootstrap/NodeAuthorizationTrait.php');
require_once(__DIR__ . '/../../../../../../Framework/Neos.Flow/Tests/Behavior/Features/Bootstrap/IsolatedBehatStepsTrait.php');
require_once(__DIR__ . '/../../../../../../Framework/Neos.Flow/Tests/Behavior/Features/Bootstrap/SecurityOperationsTrait.php');

use Behat\MinkExtension\Context\MinkContext;
use Neos\Behat\Tests\Behat\FlowContextTrait;
use Neos\ContentRepository\Domain\Service\NodeTypeManager;
use Neos\ContentRepository\Service\AuthorizationService;
use Neos\ContentRepository\Tests\Behavior\Features\Bootstrap\NodeAuthorizationTrait;
use Neos\ContentRepository\Tests\Behavior\Features\Bootstrap\NodeOperationsTrait;
use Neos\Flow\ObjectManagement\ObjectManagerInterface;
use Neos\Flow\Tests\Behavior\Features\Bootstrap\IsolatedBehatStepsTrait;
use Neos\Flow\Tests\Behavior\Features\Bootstrap\SecurityOperationsTrait;
use Neos\RedirectHandler\NeosAdapter\Tests\Behavior\Features\Bootstrap\RedirectOperationTrait;

/**
* Features context
*/
class FeatureContext extends \Neos\Behat\Tests\Behat\FlowContext
class FeatureContext extends MinkContext
{
use NodeOperationsTrait;
use FlowContextTrait;
use NodeAuthorizationTrait;
use IsolatedBehatStepsTrait;
use SecurityOperationsTrait;
use RedirectOperationTrait;

/**
* @var ObjectManagerInterface
*/
protected $objectManager;
use NodeOperationsTrait;

/**
* Initializes the context
*
* @param array $parameters Context parameters (configured through behat.yml)
*/
public function __construct(array $parameters)
public function __construct()
{
parent::__construct($parameters);
if (self::$bootstrap === null) {
self::$bootstrap = $this->initializeFlow();
}
$this->objectManager = self::$bootstrap->getObjectManager();
$this->nodeAuthorizationService = $this->objectManager->get(AuthorizationService::class);
$this->nodeTypeManager = $this->objectManager->get(NodeTypeManager::class);
$this->setupSecurity();
Expand Down
Loading

0 comments on commit d04e181

Please sign in to comment.