Skip to content

Commit

Permalink
FEATURE: Implement edit preview mode for Neos 9
Browse files Browse the repository at this point in the history
The main change is that edit and preview action are now separated with distinct constraints.

- NodeController: has separate edit and preview actions that take the previewMode as argument
- NodeUriBuilder and LinkingService: will use preview / edit action once the main request is from the same action
- Neos.BackendHelper: has additional methods isEditMode, isPreviewMode and editPreviewModeCacheIdentifier
  • Loading branch information
mficzel committed May 11, 2023
1 parent 7c20714 commit 20a8bd0
Show file tree
Hide file tree
Showing 13 changed files with 268 additions and 19 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

/*
* This file is part of the Neos.Neos package.
*
* (c) Contributors of the Neos Project - www.neos.io
*
* This package is Open Source Software. For the full copyright and license
* information, please view the LICENSE file which was distributed with this
* source code.
*/

declare(strict_types=1);

namespace Neos\Neos\Controller\Exception;

use Neos\Neos\Controller\Exception;

/**
* A "Node Creation" exception
*
*/
class InvalidEditPreviewModeException extends Exception
{
}
49 changes: 44 additions & 5 deletions Neos.Neos/Classes/Controller/Frontend/NodeController.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@
use Neos\Flow\Security\Context as SecurityContext;
use Neos\Flow\Session\SessionInterface;
use Neos\Flow\Utility\Now;
use Neos\Neos\Controller\Exception\InvalidEditPreviewModeException;
use Neos\Neos\Domain\Model\EditPreviewMode;
use Neos\Neos\Domain\Repository\EditPreviewModeRepository;
use Neos\Neos\Domain\Service\NodeSiteResolvingService;
use Neos\Neos\FrontendRouting\Exception\InvalidShortcutException;
use Neos\Neos\FrontendRouting\Exception\NodeNotFoundException;
Expand Down Expand Up @@ -107,8 +110,37 @@ class NodeController extends ActionController
*/
protected $nodeSiteResolvingService;

/**
* @Flow\Inject
* @var EditPreviewModeRepository
*/
protected $editPreviewModeRepository;

/**
* @param string $node Legacy name for backwards compatibility of route components
* @param string|null $editPreviewMode Rendering mode like "rawContent" defaults to defaultEditPreviewMode from settings
* @throws NodeNotFoundException
* @throws \Neos\Flow\Mvc\Exception\StopActionException
* @throws \Neos\Flow\Mvc\Exception\UnsupportedRequestTypeException
* @throws \Neos\Flow\Mvc\Routing\Exception\MissingActionNameException
* @throws \Neos\Flow\Session\Exception\SessionNotStartedException
* @throws \Neos\Neos\Exception
* @Flow\SkipCsrfProtection We need to skip CSRF protection here because this action could be called
* with unsafe requests from widgets or plugins that are rendered on the node
* - For those the CSRF token is validated on the sub-request, so it is safe to be skipped here
*/
public function previewAction(string $node, string $editPreviewMode = null): void
{
$editPreviewModeObject = $editPreviewMode ? $this->editPreviewModeRepository->findByName($editPreviewMode) : $this->editPreviewModeRepository->findDefault();
if (is_null($editPreviewModeObject) || $editPreviewModeObject->isPreviewMode === false) {
throw new InvalidEditPreviewModeException(sprintf('"%s" is not a preview mode', $editPreviewMode), 1683127314);
}
$this->renderEditPreviewMode($node, $editPreviewModeObject);
}

/**
* @param string $node Legacy name for backwards compatibility of route components
* @param string|null $editPreviewMode Rendering mode like "rawContent" defaults to defaultEditPreviewMode from settings
* @throws NodeNotFoundException
* @throws \Neos\Flow\Mvc\Exception\StopActionException
* @throws \Neos\Flow\Mvc\Exception\UnsupportedRequestTypeException
Expand All @@ -119,7 +151,16 @@ class NodeController extends ActionController
* with unsafe requests from widgets or plugins that are rendered on the node
* - For those the CSRF token is validated on the sub-request, so it is safe to be skipped here
*/
public function previewAction(string $node): void
public function editAction(string $node, ?string $editPreviewMode = null): void
{
$editPreviewModeObject = $editPreviewMode ? $this->editPreviewModeRepository->findByName($editPreviewMode) : $this->editPreviewModeRepository->findDefault();
if (is_null($editPreviewModeObject) || $editPreviewModeObject->isEditMode === false) {
throw new InvalidEditPreviewModeException(sprintf('"%s" is not an edit mode', $editPreviewMode), 1683127295);
}
$this->renderEditPreviewMode($node, $editPreviewModeObject);
}

protected function renderEditPreviewMode(string $node, EditPreviewMode $editPreviewMode): void
{
$visibilityConstraints = VisibilityConstraints::frontend();
if ($this->privilegeManager->isPrivilegeTargetGranted('Neos.Neos:Backend.GeneralAccess')) {
Expand Down Expand Up @@ -163,6 +204,7 @@ public function previewAction(string $node): void
$this->view->assignMultiple([
'value' => $nodeInstance,
'site' => $site,
'editPreviewMode' => $editPreviewMode
]);

if (!$nodeAddress->isInLiveWorkspace()) {
Expand Down Expand Up @@ -192,7 +234,7 @@ public function previewAction(string $node): void
* with unsafe requests from widgets or plugins that are rendered on the node
* - For those the CSRF token is validated on the sub-request, so it is safe to be skipped here
*/
public function showAction(string $node, bool $showInvisible = false): void
public function showAction(string $node): void
{
$siteDetectionResult = SiteDetectionResult::fromRequest($this->request->getHttpRequest());
$contentRepository = $this->contentRepositoryRegistry->get($siteDetectionResult->contentRepositoryId);
Expand All @@ -203,9 +245,6 @@ public function showAction(string $node, bool $showInvisible = false): void
}

$visibilityConstraints = VisibilityConstraints::frontend();
if ($showInvisible && $this->privilegeManager->isPrivilegeTargetGranted('Neos.Neos:Backend.GeneralAccess')) {
$visibilityConstraints = VisibilityConstraints::withoutRestrictions();
}

$subgraph = $contentRepository->getContentGraph()->getSubgraph(
$nodeAddress->contentStreamId,
Expand Down
43 changes: 43 additions & 0 deletions Neos.Neos/Classes/Domain/Model/EditPreviewMode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

/*
* This file is part of the Neos.Neos package.
*
* (c) Contributors of the Neos Project - www.neos.io
*
* This package is Open Source Software. For the full copyright and license
* information, please view the LICENSE file which was distributed with this
* source code.
*/

declare(strict_types=1);

namespace Neos\Neos\Domain\Model;

final class EditPreviewMode
{
protected function __construct(
public readonly string $name,
public readonly string $title,
public readonly ?string $fusionPath,
public readonly bool $isEditMode,
public readonly bool $isPreviewMode
) {
}

/**
* @param string $name
* @param array{'title'?:string, 'fusionRenderingPath'?:string, 'isEditingMode'?:bool, 'isPreviewMode'?:bool} $configuration
* @return self
*/
public static function fromNameAndConfiguration(string $name, array $configuration): self
{
return new static(
$name,
$configuration['title'] ?? $name,
$configuration['fusionRenderingPath'] ?? null,
$configuration['isEditingMode'] ?? false,
$configuration['isPreviewMode'] ?? false
);
}
}
43 changes: 43 additions & 0 deletions Neos.Neos/Classes/Domain/Repository/EditPreviewModeRepository.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

/*
* This file is part of the Neos.Neos package.
*
* (c) Contributors of the Neos Project - www.neos.io
*
* This package is Open Source Software. For the full copyright and license
* information, please view the LICENSE file which was distributed with this
* source code.
*/

declare(strict_types=1);

namespace Neos\Neos\Domain\Repository;

use Neos\Neos\Domain\Model\EditPreviewMode;
use Neos\Flow\Annotations as Flow;

class EditPreviewModeRepository
{
#[Flow\InjectConfiguration(path:"userInterface.defaultEditPreviewMode")]
protected string $defaultEditPreviewMode;

/**
* @var array<string, array{'title'?:string, 'fusionRenderingPath'?:string, 'isEditingMode'?:bool, 'isPreviewMode'?:bool}>
*/
#[Flow\InjectConfiguration(path:"userInterface.editPreviewModes")]
protected array $editPreviewModeConfigurations;

public function findDefault(): EditPreviewMode
{
return EditPreviewMode::fromNameAndConfiguration($this->defaultEditPreviewMode, $this->editPreviewModeConfigurations[$this->defaultEditPreviewMode]);
}

public function findByName(string $name): ?EditPreviewMode
{
if (array_key_exists($name,$this->editPreviewModeConfigurations)) {
return EditPreviewMode::fromNameAndConfiguration($name, $this->editPreviewModeConfigurations[$name]);
}
return null;
}
}
51 changes: 48 additions & 3 deletions Neos.Neos/Classes/FrontendRouting/NodeUriBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,17 @@ public static function fromUriBuilder(UriBuilder $uriBuilder): self
public function uriFor(NodeAddress $nodeAddress): UriInterface
{
if (!$nodeAddress->isInLiveWorkspace()) {
$request = $this->uriBuilder->getRequest();
if ($request->getControllerPackageKey() === 'Neos.Neos'
&& $request->getControllerName() === "Frontend\Node"
) {
if ($request->getControllerActionName() == 'edit') {
return $this->editUriFor($nodeAddress, $request->hasArgument('editPreviewMode') ? $request->getArgument('editPreviewMode') : null);
} elseif ($request->getControllerActionName() == 'preview') {
return $this->previewUriFor($nodeAddress, $request->hasArgument('editPreviewMode') ? $request->getArgument('editPreviewMode') : null);
}
}

return $this->previewUriFor($nodeAddress);
}
return new Uri($this->uriBuilder->uriFor('show', ['node' => $nodeAddress], 'Frontend\Node', 'Neos.Neos'));
Expand All @@ -75,16 +86,50 @@ public function uriFor(NodeAddress $nodeAddress): UriInterface
* A preview URI is used to display a node that is not public yet (i.e. not in a live workspace).
*
* @param NodeAddress $nodeAddress
* @param string|null $editPreviewMode
* @return UriInterface
* @throws NoMatchingRouteException | MissingActionNameException | HttpException
*/
public function previewUriFor(NodeAddress $nodeAddress): UriInterface
public function previewUriFor(NodeAddress $nodeAddress, ?string $editPreviewMode = null): UriInterface
{
return new Uri($this->uriBuilder->uriFor(
$uri = new Uri($this->uriBuilder->uriFor(
'preview',
['node' => $nodeAddress->serializeForUri()],
[],
'Frontend\Node',
'Neos.Neos'
));

$queryParameters = ['node' => $nodeAddress->serializeForUri()];
if ($editPreviewMode) {
$queryParameters['editPreviewMode'] = $editPreviewMode;
}

return $uri->withQuery(http_build_query($queryParameters));
}

/**
* Renders a stable "edit" URI for the given $nodeAddress
* A edit URI is used to render a node for inline editing that is not public yet (i.e. not in a live workspace).
*
* @param NodeAddress $nodeAddress
* @param string|null $editPreviewMode
* @return UriInterface
* @throws NoMatchingRouteException | MissingActionNameException | HttpException
*/
public function editUriFor(NodeAddress $nodeAddress, ?string $editPreviewMode = null): UriInterface
{
$uri = new Uri($this->uriBuilder->uriFor(
'edit',
[],
'Frontend\Node',
'Neos.Neos'
));

$queryParameters = ['node' => $nodeAddress->serializeForUri()];
if ($editPreviewMode) {
$queryParameters['editPreviewMode'] = $editPreviewMode;
}

return $uri->withQuery(http_build_query($queryParameters));
}
}
29 changes: 29 additions & 0 deletions Neos.Neos/Classes/Fusion/Helper/BackendHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

use Neos\Eel\ProtectedContextAwareInterface;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\Mvc\ActionRequest;
use Neos\Neos\Service\UserService;

/**
Expand All @@ -40,6 +41,34 @@ public function interfaceLanguage(): string
return $this->userService->getInterfaceLanguage();
}

public function isEditMode(ActionRequest $request): bool
{
return ($request->getControllerPackageKey() === 'Neos.Neos'
&& $request->getControllerName() === "Frontend\Node"
&& $request->getControllerActionName() === 'edit'
);
}

public function isPreviewMode(ActionRequest $request): bool
{
return ($request->getControllerPackageKey() === 'Neos.Neos'
&& $request->getControllerName() === "Frontend\Node"
&& $request->getControllerActionName() === 'preview'
);
}

public function editPreviewModeCacheIdentifier(ActionRequest $request): string
{
if ($request->getControllerPackageKey() === 'Neos.Neos'
&& $request->getControllerName() === "Frontend\Node"
&& ($request->getControllerActionName() === 'edit' || $request->getControllerActionName() === 'preview')
) {
return $request->getControllerActionName() . ($request->hasArgument('editPreviewMode') ? ':' . $request->getArgument('editPreviewMode') : '');
} else {
return "";
}
}

public function allowsCallOfMethod($methodName)
{
return true;
Expand Down
19 changes: 16 additions & 3 deletions Neos.Neos/Classes/Service/LinkingService.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
use Neos\ContentRepository\Core\Projection\NodeHiddenState\NodeHiddenStateProjection;
use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId;
use Neos\ContentRepository\Core\Projection\ContentGraph\NodePath;
use Neos\Flow\Mvc\ActionRequest;
use Neos\Flow\Persistence\Exception\IllegalObjectTypeException;
use Neos\Neos\FrontendRouting\NodeAddressFactory;
use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints;
use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry;
Expand Down Expand Up @@ -272,7 +274,7 @@ public function convertUriToObject($uri, Node $contextNode = null)
* @throws \Neos\Flow\Property\Exception
* @throws \Neos\Flow\Security\Exception
* @throws HttpException
* @throws \Neos\Flow\Persistence\Exception\IllegalObjectTypeException
* @throws IllegalObjectTypeException
*/
public function createNodeUri(
ControllerContext $controllerContext,
Expand Down Expand Up @@ -367,7 +369,18 @@ public function createNodeUri(
$request = $controllerContext->getRequest()->getMainRequest();
$uriBuilder = clone $controllerContext->getUriBuilder();
$uriBuilder->setRequest($request);
$action = $workspace && $workspace->isPublicWorkspace() && !$hiddenState->isHidden ? 'show' : 'preview';

if ($request->getControllerPackageKey() === 'Neos.Neos'
&& $request->getControllerName() === "Frontend\Node"
&& in_array($request->getControllerActionName(), ['edit', 'preview'])
) {
$action = $request->getControllerActionName();
if ( $request->hasArgument('editPreviewMode')) {
$arguments['editPreviewMode'] = $request->getArgument('editPreviewMode');
}
} else {
$action = $workspace && $workspace->isPublicWorkspace() && !$hiddenState->isHidden ? 'show' : 'preview';
}

return $uriBuilder
->reset()
Expand All @@ -377,7 +390,7 @@ public function createNodeUri(
->setArgumentsToBeExcludedFromQueryString($argumentsToBeExcludedFromQueryString)
->setFormat($format ?: $request->getFormat())
->setCreateAbsoluteUri($absolute)
->uriFor($action, ['node' => $node], 'Frontend\Node', 'Neos.Neos');
->uriFor($action, ['node' => $node], 'Frontend\Node', 'Neos.Neos') . '&grrr';
}

/**
Expand Down
8 changes: 8 additions & 0 deletions Neos.Neos/Configuration/Policy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ privilegeTargets:
label: Access to the backend content preview
matcher: 'method(Neos\Neos\Controller\Frontend\NodeController->previewAction())'

'Neos.Neos:ContentEdit':
label: Access to the backend edit mode
matcher: 'method(Neos\Neos\Controller\Frontend\NodeController->editAction())'

'Neos.Neos:BackendLogin':
label: General access to the backend login
matcher: 'method(Neos\Neos\Controller\LoginController->(index|tokenLogin|authenticate)Action()) || method(Neos\Flow\Security\Authentication\Controller\AbstractAuthenticationController->authenticateAction())'
Expand Down Expand Up @@ -225,6 +229,10 @@ roles:
privilegeTarget: 'Neos.Neos:ContentPreview'
permission: GRANT

-
privilegeTarget: 'Neos.Neos:ContentEdit'
permission: GRANT

-
privilegeTarget: 'Neos.Neos:Backend.PersonalWorkspaceReadAccess.NodeConverter'
permission: GRANT
Expand Down
Loading

0 comments on commit 20a8bd0

Please sign in to comment.