-
-
Notifications
You must be signed in to change notification settings - Fork 223
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
FEATURE: Edit preview mode support for Neos 9 #4067
Changes from all commits
323e8ad
7a84dae
da5a085
c4fa795
885eff7
dcc9d3d
ff71109
d234ca3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
{ | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
|
@@ -107,8 +110,15 @@ 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 | ||
|
@@ -119,7 +129,38 @@ 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 previewAction(string $node, string $editPreviewMode = null): void | ||
{ | ||
$editPreviewModeObject = $editPreviewMode ? $this->editPreviewModeRepository->findByName($editPreviewMode) : $this->editPreviewModeRepository->findDefault(); | ||
if ($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 | ||
* @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 editAction(string $node, ?string $editPreviewMode = null): void | ||
{ | ||
$editPreviewModeObject = $editPreviewMode ? $this->editPreviewModeRepository->findByName($editPreviewMode) : $this->editPreviewModeRepository->findDefault(); | ||
if ($editPreviewModeObject->isEditMode === false) { | ||
throw new InvalidEditPreviewModeException(sprintf('"%s" is not an edit mode', $editPreviewModeObject->name), 1683127295); | ||
Comment on lines
+156
to
+158
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Likewise |
||
} | ||
$this->renderEditPreviewMode($node, $editPreviewModeObject); | ||
} | ||
|
||
protected function renderEditPreviewMode(string $node, EditPreviewMode $editPreviewMode): void | ||
{ | ||
$visibilityConstraints = VisibilityConstraints::frontend(); | ||
if ($this->privilegeManager->isPrivilegeTargetGranted('Neos.Neos:Backend.GeneralAccess')) { | ||
|
@@ -160,9 +201,13 @@ public function previewAction(string $node): void | |
$this->handleShortcutNode($nodeAddress, $contentRepository); | ||
} | ||
|
||
if ($editPreviewMode->fusionPath) { | ||
$this->view->setFusionPath($editPreviewMode->fusionPath); | ||
} | ||
|
||
$this->view->assignMultiple([ | ||
'value' => $nodeInstance, | ||
'site' => $site, | ||
'site' => $site | ||
]); | ||
|
||
if (!$nodeAddress->isInLiveWorkspace()) { | ||
|
@@ -192,7 +237,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); | ||
|
@@ -203,9 +248,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, | ||
|
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 | ||
) { | ||
} | ||
Comment on lines
+25
to
+26
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We were talking about this model. It should contain a check, so that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. dont say enum ;) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I didn't :D But yeah, that would probably be better. |
||
|
||
/** | ||
* @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 | ||
); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,44 @@ | ||||||
<?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\Controller\Exception\InvalidEditPreviewModeException; | ||||||
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]); | ||||||
} | ||||||
throw new InvalidEditPreviewModeException(sprintf('"%s" is not a valid editPreviewMode', $name), 1683790077); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
} | ||||||
} |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm very unsure whether it's wise to check for the current edit/preview mode via this EEL-Helper and by analyzing the request. As a consequence of this strategy, this is what needs to be done for out-of-band rendering in the UI: https://github.com/neos/neos-ui/blob/0a858bb1cd5a31e4143f9b06144b0704c3acaad7/Classes/Domain/Model/Feedback/Operations/ReloadContentOutOfBand.php#L136-L146 Intuitively, I'd say it'd be better to just throw the current
On the other hand, that fake controller context may not be the worst idea after all, because usually integrators wouldn't expect (and shouldn't notice) their code to go through such different execution paths. However, semantically it seems very arbitrary to me to bind the current edit preview mode to a specific controller action... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes i rather have this additional context too. I remember that we discussed this at the Dresden sprint, but the discussion came up that in fusion we can't currently add new context variables because users would have to account for them in caching. There is a way though: We need to pass the context variable behind fusions back like I will prepare a draft pr to show you what i mean ;) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @grebaldi i prepared a concept over here: #4425 this way we open the api of the runtime to allow other variables like This would allow us to add a global There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. new Runtime(
defaultContextVariables: [
'request' => $request,
'Neos' => [
'UiMode' => new UiModeHelper($isBackend)
]
]
) |
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
|
@@ -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; | ||||||||
|
@@ -37,6 +39,7 @@ | |||||||
use Neos\Neos\Exception as NeosException; | ||||||||
use Neos\Neos\FrontendRouting\NodeShortcutResolver; | ||||||||
use Neos\Neos\FrontendRouting\SiteDetection\SiteDetectionResult; | ||||||||
use Neos\Neos\Fusion\Helper\BackendHelper; | ||||||||
use Psr\Http\Message\UriInterface; | ||||||||
use Psr\Log\LoggerInterface; | ||||||||
|
||||||||
|
@@ -272,7 +275,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, | ||||||||
|
@@ -367,7 +370,15 @@ public function createNodeUri( | |||||||
$request = $controllerContext->getRequest()->getMainRequest(); | ||||||||
$uriBuilder = clone $controllerContext->getUriBuilder(); | ||||||||
$uriBuilder->setRequest($request); | ||||||||
$action = $workspace && $workspace->isPublicWorkspace() && !$hiddenState->isHidden ? 'show' : 'preview'; | ||||||||
|
||||||||
if (BackendHelper::isEditMode($request) || BackendHelper::isPreviewMode($request)) { | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd prefer to move this closer to the domain. Looks like a job for the final class EditPreviewModeRepository
{
public function findOneByActionRequest(ActionRequest $request): ?EditPreviewMode;
} So that this check translates to
Suggested change
Depending on how we move forward with the |
||||||||
$action = $request->getControllerActionName(); | ||||||||
if ($request->hasArgument('editPreviewMode')) { | ||||||||
$arguments['editPreviewMode'] = $request->getArgument('editPreviewMode'); | ||||||||
} | ||||||||
} else { | ||||||||
$action = $workspace && $workspace->isPublicWorkspace() && !$hiddenState->isHidden ? 'show' : 'preview'; | ||||||||
} | ||||||||
|
||||||||
return $uriBuilder | ||||||||
->reset() | ||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would also prefer to move this logic closer to the domain, by letting the
EditPreviewModeRepository
provide more specific retrieval methods: