From b40282fef42436c95beb45483e9f0c4c027c3229 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 28 Jan 2024 16:20:16 +0100 Subject: [PATCH 01/18] WIP: FEATURE: Fusion Runtime render directly http response --- Neos.Fusion/Classes/Core/Runtime.php | 39 +++++++++++++++++- Neos.Fusion/Classes/View/FusionView.php | 41 ++++++++++--------- .../View/Fixtures/Fusion/HttpResponse.fusion | 9 ++++ .../View/Fixtures/Fusion/Root.fusion | 4 ++ .../Tests/Functional/View/FusionViewTest.php | 36 ++++++++++++++++ 5 files changed, 108 insertions(+), 21 deletions(-) create mode 100644 Neos.Fusion/Tests/Functional/View/Fixtures/Fusion/HttpResponse.fusion diff --git a/Neos.Fusion/Classes/Core/Runtime.php b/Neos.Fusion/Classes/Core/Runtime.php index e14a792a41c..ef05a4d492d 100644 --- a/Neos.Fusion/Classes/Core/Runtime.php +++ b/Neos.Fusion/Classes/Core/Runtime.php @@ -11,6 +11,10 @@ * source code. */ +use GuzzleHttp\Psr7\Message; +use GuzzleHttp\Psr7\Response; +use GuzzleHttp\Psr7\Utils; +use Psr\Http\Message\ResponseInterface; use Neos\Eel\Utility as EelUtility; use Neos\Flow\Annotations as Flow; use Neos\Flow\Configuration\Exception\InvalidConfigurationException; @@ -298,6 +302,39 @@ public function getLastEvaluationStatus() return $this->lastEvaluationStatus; } + public function renderResponse(string $fusionPath, array $contextArray): ResponseInterface + { + foreach ($contextArray as $key => $_) { + if ($this->fusionGlobals->has($key)) { + throw new Exception(sprintf('Overriding Fusion global variable "%s" via @context is not allowed.', $key), 1706452063); + } + } + $this->pushContextArray($contextArray); + try { + $output = $this->render($fusionPath); + } finally { + $this->popContext(); + } + + /** + * parse potential raw http response possibly rendered via "Neos.Fusion:Http.Message" + * {@see \Neos\Fusion\FusionObjects\HttpResponseImplementation} + */ + $outputStringHasHttpPreamble = is_string($output) && str_starts_with($output, 'HTTP/'); + if ($outputStringHasHttpPreamble) { + return Message::parseResponse($output); + } + + $stream = match(true) { + is_string($output), + $output instanceof \Stringable => Utils::streamFor((string)$output), + $output === null, $output === false => Utils::streamFor(''), + default => throw new \RuntimeException(sprintf('Cannot render %s into http response body.', get_debug_type($output)), 1706454898) + }; + + return new Response(body: $stream); + } + /** * Render an absolute Fusion path and return the result. * @@ -625,7 +662,7 @@ protected function prepareContextForFusionObject(AbstractFusionObject $fusionObj $newContextArray ??= $this->currentContext; foreach ($fusionConfiguration['__meta']['context'] as $contextKey => $contextValue) { if ($this->fusionGlobals->has($contextKey)) { - throw new Exception(sprintf('Overriding Fusion global variable "%s" via @context is not allowed.', $contextKey), 1694247627130); + throw new Exception(sprintf('Overriding Fusion global variable "%s" via @context is not allowed.', $contextKey), 1706452069); } $newContextArray[$contextKey] = $this->evaluate($fusionPath . '/__meta/context/' . $contextKey, $fusionObject, self::BEHAVIOR_EXCEPTION); } diff --git a/Neos.Fusion/Classes/View/FusionView.php b/Neos.Fusion/Classes/View/FusionView.php index bf983346f4f..37392406a57 100644 --- a/Neos.Fusion/Classes/View/FusionView.php +++ b/Neos.Fusion/Classes/View/FusionView.php @@ -22,6 +22,7 @@ use Neos\Fusion\Core\Runtime; use Neos\Fusion\Core\RuntimeFactory; use Neos\Fusion\Exception\RuntimeException; +use Psr\Http\Message\ResponseInterface; /** * View for using Fusion for standard MVC controllers. @@ -46,7 +47,8 @@ class FusionView extends AbstractView 'fusionGlobals' => [null, 'Additional global variables; merged together with the "request". Must only be specified at creation.', FusionGlobals::class], 'packageKey' => [null, 'The package key where the Fusion should be loaded from. If not given, is automatically derived from the current request.', 'string'], 'debugMode' => [false, 'Flag to enable debug mode of the Fusion runtime explicitly (overriding the global setting).', 'boolean'], - 'enableContentCache' => [false, 'Flag to enable content caching inside Fusion (overriding the global setting).', 'boolean'] + 'enableContentCache' => [false, 'Flag to enable content caching inside Fusion (overriding the global setting).', 'boolean'], + 'renderHttpResponse' => [false, 'Flag to render fusion as http repose for advanced form support and Neos.Fusion:Http.ResponseHead support.', 'boolean'], ]; /** @@ -139,13 +141,29 @@ public function setFusionPathPatterns(array $pathPatterns) /** * Render the view * - * @return mixed The rendered view + * @return mixed|ResponseInterface The rendered view * @api */ public function render() { $this->initializeFusionRuntime(); - return $this->renderFusion(); + + if ($this->getOption('renderHttpResponse') === true) { + try { + return $this->fusionRuntime->renderResponse($this->getFusionPathForCurrentRequest(), $this->variables); + } catch (RuntimeException $exception) { + throw $exception->getPrevious(); + } + } else { + try { + $this->fusionRuntime->pushContextArray($this->variables); + return $this->fusionRuntime->render($this->getFusionPathForCurrentRequest()); + } catch (RuntimeException $exception) { + throw $exception->getPrevious(); + } finally { + $this->fusionRuntime->popContext(); + } + } } /** @@ -283,21 +301,4 @@ protected function getFusionPathForCurrentRequest() } return $this->fusionPath; } - - /** - * Render the given Fusion and return the rendered page - * @return mixed - * @throws \Exception - */ - protected function renderFusion() - { - $this->fusionRuntime->pushContextArray($this->variables); - try { - $output = $this->fusionRuntime->render($this->getFusionPathForCurrentRequest()); - } catch (RuntimeException $exception) { - throw $exception->getPrevious(); - } - $this->fusionRuntime->popContext(); - return $output; - } } diff --git a/Neos.Fusion/Tests/Functional/View/Fixtures/Fusion/HttpResponse.fusion b/Neos.Fusion/Tests/Functional/View/Fixtures/Fusion/HttpResponse.fusion new file mode 100644 index 00000000000..d75b5c47d1f --- /dev/null +++ b/Neos.Fusion/Tests/Functional/View/Fixtures/Fusion/HttpResponse.fusion @@ -0,0 +1,9 @@ + +response = Neos.Fusion:Http.Message { + httpResponseHead { + statusCode = 404 + headers.Content-Type = 'application/json' + } + + body = '{"some":"json"}' +} diff --git a/Neos.Fusion/Tests/Functional/View/Fixtures/Fusion/Root.fusion b/Neos.Fusion/Tests/Functional/View/Fixtures/Fusion/Root.fusion index b190f6bd5a0..fbcf0fc40a3 100644 --- a/Neos.Fusion/Tests/Functional/View/Fixtures/Fusion/Root.fusion +++ b/Neos.Fusion/Tests/Functional/View/Fixtures/Fusion/Root.fusion @@ -1 +1,5 @@ include: ./**/*.fusion +include: 'resource://Neos.Fusion/Private/Fusion/Prototypes/Join.fusion' +include: 'resource://Neos.Fusion/Private/Fusion/Prototypes/DataStructure.fusion' +include: 'resource://Neos.Fusion/Private/Fusion/Prototypes/Http.Message.fusion' +include: 'resource://Neos.Fusion/Private/Fusion/Prototypes/Http.ResponseHead.fusion' diff --git a/Neos.Fusion/Tests/Functional/View/FusionViewTest.php b/Neos.Fusion/Tests/Functional/View/FusionViewTest.php index dc2f06d8ba9..629508c8643 100644 --- a/Neos.Fusion/Tests/Functional/View/FusionViewTest.php +++ b/Neos.Fusion/Tests/Functional/View/FusionViewTest.php @@ -15,6 +15,7 @@ use Neos\Flow\Mvc\Controller\ControllerContext; use Neos\Flow\Tests\FunctionalTestCase; use Neos\Fusion\View\FusionView; +use Psr\Http\Message\ResponseInterface; /** * Testcase for the Fusion View @@ -64,6 +65,41 @@ public function fusionViewOutputsVariable() self::assertEquals('XHallo Welt', $view->render()); } + /** + * @test + */ + public function fusionViewCanReturnHttpResponse() + { + $view = $this->buildView('Foo\Bar\Controller\TestController', 'index'); + $view->setOption('renderHttpResponse', true); + $view->assign('test', 'Hallo Welt'); + $response = $view->render(); + self::assertInstanceOf(ResponseInterface::class, $response); + self::assertEquals('XHallo Welt', $view->render()->getBody()->getContents()); + } + + /** + * @test + */ + public function fusionViewCanReturnHttpResponseFromHttpMessagePrototype() + { + $view = $this->buildView('Foo\Bar\Controller\TestController', 'index'); + $view->setFusionPath('response'); + self::assertSame(<<render()); + + $view->setOption('renderHttpResponse', true); + $response = $view->render(); + self::assertInstanceOf(ResponseInterface::class, $response); + self::assertSame('{"some":"json"}', $response->getBody()->getContents()); + self::assertSame(404, $response->getStatusCode()); + self::assertSame("application/json", $response->getHeaderLine("Content-Type")); + } + /** * Prepare a FusionView for testing that Mocks a request with the given controller and action names. * From 856780239af6e14edc8b72b87a1bb047b85a24ff Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 28 Jan 2024 16:24:23 +0100 Subject: [PATCH 02/18] WIP: Neos Fusion View use Runtime::renderResponse --- Neos.Neos/Classes/View/FusionView.php | 48 ++++----------------------- 1 file changed, 7 insertions(+), 41 deletions(-) diff --git a/Neos.Neos/Classes/View/FusionView.php b/Neos.Neos/Classes/View/FusionView.php index 6ff365b5127..62f6dceb278 100644 --- a/Neos.Neos/Classes/View/FusionView.php +++ b/Neos.Neos/Classes/View/FusionView.php @@ -57,11 +57,11 @@ class FusionView extends AbstractView /** * Renders the view * - * @return string|ResponseInterface The rendered view + * @return ResponseInterface The rendered view * @throws \Exception if no node is given * @api */ - public function render(): string|ResponseInterface + public function render(): ResponseInterface { $currentNode = $this->getCurrentNode(); @@ -74,20 +74,15 @@ public function render(): string|ResponseInterface $fusionRuntime = $this->getFusionRuntime($currentSiteNode); - $fusionRuntime->pushContextArray([ - 'node' => $currentNode, - 'documentNode' => $this->getClosestDocumentNode($currentNode) ?: $currentNode, - 'site' => $currentSiteNode - ]); try { - $output = $fusionRuntime->render($this->fusionPath); - $output = $this->parsePotentialRawHttpResponse($output); + return $fusionRuntime->renderResponse($this->fusionPath, [ + 'node' => $currentNode, + 'documentNode' => $this->getClosestDocumentNode($currentNode) ?: $currentNode, + 'site' => $currentSiteNode + ]); } catch (RuntimeException $exception) { throw $exception->getPrevious() ?: $exception; } - $fusionRuntime->popContext(); - - return $output; } /** @@ -129,35 +124,6 @@ public function render(): string|ResponseInterface */ protected $securityContext; - /** - * @param string $output - * @return string|ResponseInterface If output is a string with a HTTP preamble a ResponseInterface - * otherwise the original output. - */ - protected function parsePotentialRawHttpResponse($output) - { - if ($this->isRawHttpResponse($output)) { - return Message::parseResponse($output); - } - - return $output; - } - - /** - * Checks if the mixed input looks like a raw HTTTP response. - * - * @param mixed $value - * @return bool - */ - protected function isRawHttpResponse($value): bool - { - if (is_string($value) && strpos($value, 'HTTP/') === 0) { - return true; - } - - return false; - } - /** * Is it possible to render $node with $his->fusionPath? * From d8d16db66a2b4e51974fb8fe5581ad2ec29da066 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 28 Jan 2024 17:00:52 +0100 Subject: [PATCH 03/18] WIP: Hacky HttpResponseConstraints --- .../Classes/Core/HttpResponseConstraints.php | 100 ++++++++++++++++++ Neos.Fusion/Classes/Core/Runtime.php | 11 +- 2 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 Neos.Fusion/Classes/Core/HttpResponseConstraints.php diff --git a/Neos.Fusion/Classes/Core/HttpResponseConstraints.php b/Neos.Fusion/Classes/Core/HttpResponseConstraints.php new file mode 100644 index 00000000000..89b58b3bdd3 --- /dev/null +++ b/Neos.Fusion/Classes/Core/HttpResponseConstraints.php @@ -0,0 +1,100 @@ +partialResponse = new Response(); + } + + /** + * Gets the response status code. + * + * @return int Status code. + */ + public function getStatusCode() + { + return $this->partialResponse->getStatusCode(); + } + + /** + * @param int $code The 3-digit integer result code to set. + */ + public function setStatus(int $code) + { + $this->partialResponse = $this->partialResponse->withStatus($code); + } + + /** + * Retrieves all message header values. + * + * While header names are not case-sensitive, getHeaders() will preserve the + * exact case in which headers were originally specified. + * + * @return string[][] Returns an associative array of the message's headers. Each + * key MUST be a header name, and each value MUST be an array of strings + * for that header. + */ + public function getPartialResponse(): ResponseInterface + { + return $this->partialResponse->getHeaders(); + } + + /** + * While header names are case-insensitive, the casing of the header will + * be preserved by this function, and returned from getHeaders(). + * + * @param string $name Case-insensitive header field name. + * @param string|string[] $value Header value(s). + */ + public function setHeader(string $name, $value) + { + $this->partialResponse = $this->partialResponse->withHeader($name, $value); + } + + /** + * Existing values for the specified header will be maintained. The new + * value(s) will be appended to the existing list. If the header did not + * exist previously, it will be added. + * + * @param string $name Case-insensitive header field name to add. + * @param string|string[] $value Header value(s). + */ + public function setAndMergeHeader(string $name, $value) + { + $this->partialResponse = $this->partialResponse->withAddedHeader($name, $value); + } + + /** + * @param string $name Case-insensitive header field name to remove. + */ + public function unsetHeader(string $name) + { + $this->partialResponse = $this->partialResponse->withoutHeader($name); + } + + public function applyToResponse(ResponseInterface $response): ResponseInterface + { + foreach ($this->partialResponse->getHeaders() as $name => $values) { + $response = $response->withAddedHeader($name, $values); + } + + // preserve non 200 status codes that would otherwise be overwritten + if ($this->partialResponse->getStatusCode() !== 200) { + $response = $response->withStatus($this->partialResponse->getStatusCode()); + } + + $this->partialResponse = new Response(); + + return $response; + } +} diff --git a/Neos.Fusion/Classes/Core/Runtime.php b/Neos.Fusion/Classes/Core/Runtime.php index ef05a4d492d..0725f2d2961 100644 --- a/Neos.Fusion/Classes/Core/Runtime.php +++ b/Neos.Fusion/Classes/Core/Runtime.php @@ -107,6 +107,8 @@ class Runtime */ public readonly FusionGlobals $fusionGlobals; + public readonly HttpResponseConstraints $unsafeHttpResponseConstrains; + /** * @var RuntimeConfiguration */ @@ -152,6 +154,7 @@ public function __construct( ); $this->runtimeContentCache = new RuntimeContentCache($this); $this->fusionGlobals = $fusionGlobals; + $this->unsafeHttpResponseConstrains = new HttpResponseConstraints(); } /** @@ -322,7 +325,9 @@ public function renderResponse(string $fusionPath, array $contextArray): Respons */ $outputStringHasHttpPreamble = is_string($output) && str_starts_with($output, 'HTTP/'); if ($outputStringHasHttpPreamble) { - return Message::parseResponse($output); + return $this->unsafeHttpResponseConstrains->applyToResponse( + Message::parseResponse($output) + ); } $stream = match(true) { @@ -332,7 +337,9 @@ public function renderResponse(string $fusionPath, array $contextArray): Respons default => throw new \RuntimeException(sprintf('Cannot render %s into http response body.', get_debug_type($output)), 1706454898) }; - return new Response(body: $stream); + return $this->unsafeHttpResponseConstrains->applyToResponse( + new Response(body: $stream) + ); } /** From c12b3c8c7c143cf25e1ed60dd13d8f0540c52de0 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 28 Jan 2024 17:21:09 +0100 Subject: [PATCH 04/18] WIP: Hurray make Runtime independent of ControllerContext + working legacy layer for manipulating getResponse and accessing getRequest --- .../Classes/Core/HttpResponseConstraints.php | 14 +++++ Neos.Fusion/Classes/Core/Runtime.php | 55 +++++++++---------- Neos.Fusion/Classes/Core/RuntimeFactory.php | 26 ++------- Neos.Fusion/Classes/View/FusionView.php | 3 - .../Classes/View/FusionExceptionView.php | 1 - Neos.Neos/Classes/View/FusionView.php | 1 - 6 files changed, 45 insertions(+), 55 deletions(-) diff --git a/Neos.Fusion/Classes/Core/HttpResponseConstraints.php b/Neos.Fusion/Classes/Core/HttpResponseConstraints.php index 89b58b3bdd3..7b774c36e73 100644 --- a/Neos.Fusion/Classes/Core/HttpResponseConstraints.php +++ b/Neos.Fusion/Classes/Core/HttpResponseConstraints.php @@ -5,6 +5,7 @@ namespace Neos\Fusion\Core; use GuzzleHttp\Psr7\Response; +use Neos\Flow\Mvc\ActionResponse; use Psr\Http\Message\ResponseInterface; final class HttpResponseConstraints @@ -16,6 +17,19 @@ public function __construct() $this->partialResponse = new Response(); } + /** + * @deprecated + */ + public static function createFromActionResponse(?ActionResponse $actionResponse) + { + $constraints = new self(); + if (!$actionResponse) { + return $constraints; + } + $constraints->partialResponse = $actionResponse->buildHttpResponse(); + return $constraints; + } + /** * Gets the response status code. * diff --git a/Neos.Fusion/Classes/Core/Runtime.php b/Neos.Fusion/Classes/Core/Runtime.php index 0725f2d2961..9b43afb6805 100644 --- a/Neos.Fusion/Classes/Core/Runtime.php +++ b/Neos.Fusion/Classes/Core/Runtime.php @@ -117,7 +117,7 @@ class Runtime /** * @deprecated */ - protected ControllerContext $controllerContext; + protected ?ControllerContext $controllerContext = null; /** * @var array @@ -157,15 +157,6 @@ public function __construct( $this->unsafeHttpResponseConstrains = new HttpResponseConstraints(); } - /** - * @deprecated {@see self::getControllerContext()} - * @internal - */ - public function setControllerContext(ControllerContext $controllerContext): void - { - $this->controllerContext = $controllerContext; - } - /** * Returns the context which has been passed by the currently active MVC Controller * @@ -176,23 +167,10 @@ public function setControllerContext(ControllerContext $controllerContext): void */ public function getControllerContext(): ControllerContext { - if (isset($this->controllerContext)) { - return $this->controllerContext; - } - - if (!($request = $this->fusionGlobals->get('request')) instanceof ActionRequest) { - throw new Exception(sprintf('Expected Fusion variable "request" to be of type ActionRequest, got value of type "%s".', get_debug_type($request)), 1693558026485); + if ($this->controllerContext === null) { + throw new Exception(sprintf('Legacy controller context in runtime is only available when fusion global "request" is a ActionRequest and during "renderResponse".'), 1706458355); } - - $uriBuilder = new UriBuilder(); - $uriBuilder->setRequest($request); - - return $this->controllerContext = new ControllerContext( - $request, - new ActionResponse(), - new Arguments([]), - $uriBuilder - ); + return $this->controllerContext; } /** @@ -307,6 +285,20 @@ public function getLastEvaluationStatus() public function renderResponse(string $fusionPath, array $contextArray): ResponseInterface { + // legacy controller context layer + $possibleRequest = $this->fusionGlobals->get('request'); + if ($possibleRequest instanceof ActionRequest) { + $uriBuilder = new UriBuilder(); + $uriBuilder->setRequest($possibleRequest); + + $this->controllerContext = new ControllerContext( + $possibleRequest, + new ActionResponse(), + new Arguments([]), + $uriBuilder + ); + } + foreach ($contextArray as $key => $_) { if ($this->fusionGlobals->has($key)) { throw new Exception(sprintf('Overriding Fusion global variable "%s" via @context is not allowed.', $key), 1706452063); @@ -319,6 +311,9 @@ public function renderResponse(string $fusionPath, array $contextArray): Respons $this->popContext(); } + $legacyControllerContextResponseConstraints = HttpResponseConstraints::createFromActionResponse($this->controllerContext?->getResponse()); + $this->controllerContext = null; + /** * parse potential raw http response possibly rendered via "Neos.Fusion:Http.Message" * {@see \Neos\Fusion\FusionObjects\HttpResponseImplementation} @@ -326,7 +321,9 @@ public function renderResponse(string $fusionPath, array $contextArray): Respons $outputStringHasHttpPreamble = is_string($output) && str_starts_with($output, 'HTTP/'); if ($outputStringHasHttpPreamble) { return $this->unsafeHttpResponseConstrains->applyToResponse( - Message::parseResponse($output) + $legacyControllerContextResponseConstraints->applyToResponse( + Message::parseResponse($output) + ) ); } @@ -338,7 +335,9 @@ public function renderResponse(string $fusionPath, array $contextArray): Respons }; return $this->unsafeHttpResponseConstrains->applyToResponse( - new Response(body: $stream) + $legacyControllerContextResponseConstraints->applyToResponse( + new Response(body: $stream) + ) ); } diff --git a/Neos.Fusion/Classes/Core/RuntimeFactory.php b/Neos.Fusion/Classes/Core/RuntimeFactory.php index e435fc0f261..702bea6cb1f 100644 --- a/Neos.Fusion/Classes/Core/RuntimeFactory.php +++ b/Neos.Fusion/Classes/Core/RuntimeFactory.php @@ -44,19 +44,18 @@ class RuntimeFactory */ public function create(array $fusionConfiguration, ControllerContext $controllerContext = null): Runtime { - if ($controllerContext === null) { - $controllerContext = self::createControllerContextFromEnvironment(); - } $defaultContextVariables = EelUtility::getDefaultContextVariables( $this->defaultContextConfiguration ?? [] ); $runtime = new Runtime( FusionConfiguration::fromArray($fusionConfiguration), FusionGlobals::fromArray( - ['request' => $controllerContext->getRequest(), ...$defaultContextVariables] + [ + 'request' => $controllerContext?->getRequest() ?? ActionRequest::fromHttpRequest(ServerRequest::fromGlobals()), + ...$defaultContextVariables + ] ) ); - $runtime->setControllerContext($controllerContext); return $runtime; } @@ -81,21 +80,4 @@ public function createFromSourceCode( $fusionGlobals ); } - - private static function createControllerContextFromEnvironment(): ControllerContext - { - $httpRequest = ServerRequest::fromGlobals(); - - $request = ActionRequest::fromHttpRequest($httpRequest); - - $uriBuilder = new UriBuilder(); - $uriBuilder->setRequest($request); - - return new ControllerContext( - $request, - new ActionResponse(), - new Arguments([]), - $uriBuilder - ); - } } diff --git a/Neos.Fusion/Classes/View/FusionView.php b/Neos.Fusion/Classes/View/FusionView.php index 37392406a57..bdc4413e113 100644 --- a/Neos.Fusion/Classes/View/FusionView.php +++ b/Neos.Fusion/Classes/View/FusionView.php @@ -194,9 +194,6 @@ public function initializeFusionRuntime() $this->parsedFusion, $fusionGlobals ); - if (isset($this->controllerContext)) { - $this->fusionRuntime->setControllerContext($this->controllerContext); - } } if (isset($this->options['debugMode'])) { $this->fusionRuntime->setDebugMode($this->options['debugMode']); diff --git a/Neos.Neos/Classes/View/FusionExceptionView.php b/Neos.Neos/Classes/View/FusionExceptionView.php index bf4807b0e65..46241f48729 100644 --- a/Neos.Neos/Classes/View/FusionExceptionView.php +++ b/Neos.Neos/Classes/View/FusionExceptionView.php @@ -216,7 +216,6 @@ protected function getFusionRuntime( $fusionConfiguration, $fusionGlobals ); - $this->fusionRuntime->setControllerContext($controllerContext); if (isset($this->options['enableContentCache']) && $this->options['enableContentCache'] !== null) { $this->fusionRuntime->setEnableContentCache($this->options['enableContentCache']); diff --git a/Neos.Neos/Classes/View/FusionView.php b/Neos.Neos/Classes/View/FusionView.php index 62f6dceb278..d3142635911 100644 --- a/Neos.Neos/Classes/View/FusionView.php +++ b/Neos.Neos/Classes/View/FusionView.php @@ -210,7 +210,6 @@ protected function getFusionRuntime(Node $currentSiteNode) $fusionConfiguration, $fusionGlobals ); - $this->fusionRuntime->setControllerContext($this->controllerContext); if (isset($this->options['enableContentCache']) && $this->options['enableContentCache'] !== null) { $this->fusionRuntime->setEnableContentCache($this->options['enableContentCache']); From 143da926247c05c678284f416d6cc0f8b0f54195 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 28 Jan 2024 17:28:15 +0100 Subject: [PATCH 05/18] WIP: Migrate PluginImplementation to use `unsafeHttpResponseConstrains` --- Neos.Fusion/Classes/Core/HttpResponseConstraints.php | 9 ++------- Neos.Fusion/Classes/Core/Runtime.php | 5 ++++- Neos.Neos/Classes/Fusion/PluginImplementation.php | 7 +------ 3 files changed, 7 insertions(+), 14 deletions(-) diff --git a/Neos.Fusion/Classes/Core/HttpResponseConstraints.php b/Neos.Fusion/Classes/Core/HttpResponseConstraints.php index 7b774c36e73..2c7a98df064 100644 --- a/Neos.Fusion/Classes/Core/HttpResponseConstraints.php +++ b/Neos.Fusion/Classes/Core/HttpResponseConstraints.php @@ -20,14 +20,9 @@ public function __construct() /** * @deprecated */ - public static function createFromActionResponse(?ActionResponse $actionResponse) + public function setAndMergeFromActionResponse(ActionResponse $actionResponse) { - $constraints = new self(); - if (!$actionResponse) { - return $constraints; - } - $constraints->partialResponse = $actionResponse->buildHttpResponse(); - return $constraints; + $this->partialResponse = $this->applyToResponse($actionResponse->buildHttpResponse()); } /** diff --git a/Neos.Fusion/Classes/Core/Runtime.php b/Neos.Fusion/Classes/Core/Runtime.php index 9b43afb6805..e406e09e32d 100644 --- a/Neos.Fusion/Classes/Core/Runtime.php +++ b/Neos.Fusion/Classes/Core/Runtime.php @@ -311,7 +311,10 @@ public function renderResponse(string $fusionPath, array $contextArray): Respons $this->popContext(); } - $legacyControllerContextResponseConstraints = HttpResponseConstraints::createFromActionResponse($this->controllerContext?->getResponse()); + $legacyControllerContextResponseConstraints = new HttpResponseConstraints(); + if ($this->controllerContext) { + $legacyControllerContextResponseConstraints->setAndMergeFromActionResponse($this->controllerContext->getResponse()); + } $this->controllerContext = null; /** diff --git a/Neos.Neos/Classes/Fusion/PluginImplementation.php b/Neos.Neos/Classes/Fusion/PluginImplementation.php index c623542afd9..02c388453f5 100644 --- a/Neos.Neos/Classes/Fusion/PluginImplementation.php +++ b/Neos.Neos/Classes/Fusion/PluginImplementation.php @@ -166,17 +166,12 @@ public function evaluate(): string $currentContext = $this->runtime->getCurrentContext(); $this->node = $currentContext['node']; $this->documentNode = $currentContext['documentNode']; - $parentResponse = $this->runtime->getControllerContext()->getResponse(); $pluginResponse = new ActionResponse(); $this->dispatcher->dispatch($this->buildPluginRequest(), $pluginResponse); - // We need to make sure to not merge content up into the parent ActionResponse - // because that would break the Fusion HttpResponse. $content = $pluginResponse->getContent(); - $pluginResponse->setContent(''); - - $pluginResponse->mergeIntoParentResponse($parentResponse); + $this->runtime->unsafeHttpResponseConstrains->setAndMergeFromActionResponse($pluginResponse); return $content; } From b24aee4cc6f2604391d380347a7c78bbd55ee902 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 28 Jan 2024 17:44:41 +0100 Subject: [PATCH 06/18] WIP: FusionObject to not depend on the controller context --- .../FusionObjects/TemplateImplementation.php | 2 +- .../FusionObjects/UriBuilderImplementation.php | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/Neos.Fusion/Classes/FusionObjects/TemplateImplementation.php b/Neos.Fusion/Classes/FusionObjects/TemplateImplementation.php index 2287410a718..4ccc72f2381 100644 --- a/Neos.Fusion/Classes/FusionObjects/TemplateImplementation.php +++ b/Neos.Fusion/Classes/FusionObjects/TemplateImplementation.php @@ -79,7 +79,7 @@ public function getPath() */ public function evaluate() { - $actionRequest = $this->runtime->getControllerContext()->getRequest(); + $actionRequest = $this->runtime->fusionGlobals->get('request'); if (!$actionRequest instanceof ActionRequest) { $actionRequest = null; } diff --git a/Neos.Fusion/Classes/FusionObjects/UriBuilderImplementation.php b/Neos.Fusion/Classes/FusionObjects/UriBuilderImplementation.php index ce3ef08ae15..032992e051a 100644 --- a/Neos.Fusion/Classes/FusionObjects/UriBuilderImplementation.php +++ b/Neos.Fusion/Classes/FusionObjects/UriBuilderImplementation.php @@ -11,6 +11,10 @@ * source code. */ +use GuzzleHttp\Psr7\ServerRequest; +use Neos\Flow\Mvc\ActionRequest; +use Neos\Flow\Mvc\Routing\UriBuilder; + /** * A Fusion UriBuilder object @@ -150,8 +154,16 @@ public function isAbsolute() */ public function evaluate() { - $controllerContext = $this->runtime->getControllerContext(); - $uriBuilder = $controllerContext->getUriBuilder()->reset(); + $uriBuilder = new UriBuilder(); + $possibleRequest = $this->runtime->fusionGlobals->get('request'); + if ($possibleRequest instanceof ActionRequest) { + $uriBuilder->setRequest($possibleRequest); + } else { + // legacy + $uriBuilder->setRequest( + ActionRequest::fromHttpRequest(ServerRequest::fromGlobals()) + ); + } $format = $this->getFormat(); if ($format !== null) { From 2d620e5f2982fe6d2195de908c8d2f74128d4290 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 28 Jan 2024 18:49:53 +0100 Subject: [PATCH 07/18] WIP: Skip non working test :( --- .../Classes/Core/HttpResponseConstraints.php | 1 + Neos.Fusion/Classes/Core/Runtime.php | 2 +- Neos.Fusion/Classes/Core/RuntimeFactory.php | 3 --- .../UriBuilderImplementation.php | 1 - .../Unit/Fusion/PluginImplementationTest.php | 21 +++++++++++++++---- 5 files changed, 19 insertions(+), 9 deletions(-) diff --git a/Neos.Fusion/Classes/Core/HttpResponseConstraints.php b/Neos.Fusion/Classes/Core/HttpResponseConstraints.php index 2c7a98df064..2f572cf6471 100644 --- a/Neos.Fusion/Classes/Core/HttpResponseConstraints.php +++ b/Neos.Fusion/Classes/Core/HttpResponseConstraints.php @@ -102,6 +102,7 @@ public function applyToResponse(ResponseInterface $response): ResponseInterface $response = $response->withStatus($this->partialResponse->getStatusCode()); } + // reset internal state $this->partialResponse = new Response(); return $response; diff --git a/Neos.Fusion/Classes/Core/Runtime.php b/Neos.Fusion/Classes/Core/Runtime.php index e406e09e32d..49febd5d9e3 100644 --- a/Neos.Fusion/Classes/Core/Runtime.php +++ b/Neos.Fusion/Classes/Core/Runtime.php @@ -330,7 +330,7 @@ public function renderResponse(string $fusionPath, array $contextArray): Respons ); } - $stream = match(true) { + $stream = match (true) { is_string($output), $output instanceof \Stringable => Utils::streamFor((string)$output), $output === null, $output === false => Utils::streamFor(''), diff --git a/Neos.Fusion/Classes/Core/RuntimeFactory.php b/Neos.Fusion/Classes/Core/RuntimeFactory.php index 702bea6cb1f..24522dbb6d8 100644 --- a/Neos.Fusion/Classes/Core/RuntimeFactory.php +++ b/Neos.Fusion/Classes/Core/RuntimeFactory.php @@ -15,10 +15,7 @@ use Neos\Eel\Utility as EelUtility; use Neos\Flow\Annotations as Flow; use Neos\Flow\Mvc\ActionRequest; -use Neos\Flow\Mvc\ActionResponse; -use Neos\Flow\Mvc\Controller\Arguments; use Neos\Flow\Mvc\Controller\ControllerContext; -use Neos\Flow\Mvc\Routing\UriBuilder; /** * @Flow\Scope("singleton") diff --git a/Neos.Fusion/Classes/FusionObjects/UriBuilderImplementation.php b/Neos.Fusion/Classes/FusionObjects/UriBuilderImplementation.php index 032992e051a..2e0eae65ff3 100644 --- a/Neos.Fusion/Classes/FusionObjects/UriBuilderImplementation.php +++ b/Neos.Fusion/Classes/FusionObjects/UriBuilderImplementation.php @@ -15,7 +15,6 @@ use Neos\Flow\Mvc\ActionRequest; use Neos\Flow\Mvc\Routing\UriBuilder; - /** * A Fusion UriBuilder object * diff --git a/Neos.Neos/Tests/Unit/Fusion/PluginImplementationTest.php b/Neos.Neos/Tests/Unit/Fusion/PluginImplementationTest.php index e8aff26d8ea..90c721757bb 100644 --- a/Neos.Neos/Tests/Unit/Fusion/PluginImplementationTest.php +++ b/Neos.Neos/Tests/Unit/Fusion/PluginImplementationTest.php @@ -11,6 +11,7 @@ * source code. */ +use GuzzleHttp\Psr7\Response; use GuzzleHttp\Psr7\Uri; use Neos\Flow\Mvc\ActionRequest; use Neos\Flow\Mvc\ActionResponse; @@ -18,6 +19,8 @@ use Neos\Flow\Mvc\Dispatcher; use Neos\Flow\Mvc\RequestInterface; use Neos\Flow\Tests\UnitTestCase; +use Neos\Fusion\Core\FusionConfiguration; +use Neos\Fusion\Core\FusionGlobals; use Neos\Fusion\Core\Runtime; use Neos\Neos\Fusion\PluginImplementation; use PHPUnit\Framework\MockObject\MockObject; @@ -79,7 +82,7 @@ public function setUp(): void $this->mockControllerContext = $this->getMockBuilder(ControllerContext::class)->disableOriginalConstructor()->getMock(); $this->mockControllerContext->method('getRequest')->willReturn($this->mockActionRequest); - $this->mockRuntime = $this->getMockBuilder(Runtime::class)->disableOriginalConstructor()->getMock(); + $this->mockRuntime = $this->getMockBuilder(Runtime::class)->setConstructorArgs([FusionConfiguration::fromArray([]), FusionGlobals::fromArray([])])->getMock(); $this->mockRuntime->method('getControllerContext')->willReturn($this->mockControllerContext); $this->pluginImplementation->_set('runtime', $this->mockRuntime); @@ -96,12 +99,12 @@ public function responseHeadersDataProvider(): array [ 'Plugin response key does already exist in parent with same value', ['parent' => ['key' => 'value'], 'plugin' => ['key' => 'value']], - ['key' => 'value'] + ['key' => 'value'] // 'value, value' ], [ 'Plugin response key does not exist in parent with different value', ['parent' => ['key' => 'value'], 'plugin' => ['key' => 'otherValue']], - ['key' => 'otherValue'] + ['key' => 'otherValue'] // 'otherValue, value' ], [ 'Plugin response key does not exist in parent', @@ -119,6 +122,8 @@ public function responseHeadersDataProvider(): array */ public function evaluateSetHeaderIntoParent(string $message, array $input, array $expected): void { + $this->markTestSkipped('DOESNT WORK.'); + $this->pluginImplementation->method('buildPluginRequest')->willReturn($this->mockActionRequest); $parentResponse = new ActionResponse(); @@ -133,8 +138,16 @@ public function evaluateSetHeaderIntoParent(string $message, array $input, array $this->pluginImplementation->evaluate(); + // in the runtime would be: + $runtimeResponse = $this->mockRuntime->unsafeHttpResponseConstrains->applyToResponse(new Response()); + + // in the action would be: + $parentResponse->replaceHttpResponse($runtimeResponse); + foreach ($expected as $expectedKey => $expectedValue) { - self::assertEquals($expectedValue, (string)$parentResponse->getHttpHeader($expectedKey), $message); + // previously tests succeeded: + // self::assertEquals($expectedValue, join(', ', \Neos\Utility\ObjectAccess::getProperty($parentResponse, 'headers', true)[$expectedKey]), $message); + self::assertEquals($expectedValue, $parentResponse->buildHttpResponse()->getHeaderLine($expectedKey), $message); } } From 56b460a92186532cb7c301bf896db319954f0457 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Tue, 30 Jan 2024 14:24:11 +0100 Subject: [PATCH 08/18] TASK: Migrate further fusion objects to `$this->runtime->fusionGlobals->get('request');` --- .../UriBuilderImplementation.php | 5 +++- .../Fusion/ConvertUrisImplementation.php | 25 ++++++++++++++++--- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/Neos.Fusion/Classes/FusionObjects/UriBuilderImplementation.php b/Neos.Fusion/Classes/FusionObjects/UriBuilderImplementation.php index 2e0eae65ff3..f59cdde7359 100644 --- a/Neos.Fusion/Classes/FusionObjects/UriBuilderImplementation.php +++ b/Neos.Fusion/Classes/FusionObjects/UriBuilderImplementation.php @@ -158,7 +158,10 @@ public function evaluate() if ($possibleRequest instanceof ActionRequest) { $uriBuilder->setRequest($possibleRequest); } else { - // legacy + // unfortunately, the uri-builder always needs a request at hand and cannot build uris without + // even, if the default param merging would not be required + // this will improve with a reformed uri building: + // https://github.com/neos/flow-development-collection/pull/2744 $uriBuilder->setRequest( ActionRequest::fromHttpRequest(ServerRequest::fromGlobals()) ); diff --git a/Neos.Neos/Classes/Fusion/ConvertUrisImplementation.php b/Neos.Neos/Classes/Fusion/ConvertUrisImplementation.php index 9cf34254f07..395378bd382 100644 --- a/Neos.Neos/Classes/Fusion/ConvertUrisImplementation.php +++ b/Neos.Neos/Classes/Fusion/ConvertUrisImplementation.php @@ -14,8 +14,10 @@ namespace Neos\Neos\Fusion; +use GuzzleHttp\Psr7\ServerRequest; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\Flow\Log\Utility\LogEnvironment; +use Neos\Flow\Mvc\ActionRequest; use Neos\Flow\Mvc\Exception\NoMatchingRouteException; use Neos\Neos\Domain\Model\RenderingMode; use Neos\Neos\FrontendRouting\NodeAddressFactory; @@ -147,12 +149,23 @@ public function evaluate() NodeAggregateId::fromString($matches[2]) ); $uriBuilder = new UriBuilder(); - $uriBuilder->setRequest($this->runtime->getControllerContext()->getRequest()); + $possibleRequest = $this->runtime->fusionGlobals->get('request'); + if ($possibleRequest instanceof ActionRequest) { + $uriBuilder->setRequest($possibleRequest); + } else { + // unfortunately, the uri-builder always needs a request at hand and cannot build uris without + // even, if the default param merging would not be required + // this will improve with a reformed uri building: + // https://github.com/neos/flow-development-collection/pull/2744 + $uriBuilder->setRequest( + ActionRequest::fromHttpRequest(ServerRequest::fromGlobals()) + ); + } $uriBuilder->setCreateAbsoluteUri($absolute); try { $resolvedUri = (string)NodeUriBuilder::fromUriBuilder($uriBuilder)->uriFor($nodeAddress); } catch (NoMatchingRouteException) { - $this->systemLogger->warning(sprintf('Could not resolve "%s" to a node uri. Arguments: %s', $matches[0], json_encode($uriBuilder->getLastArguments())), LogEnvironment::fromMethodName(__METHOD__)); + $this->systemLogger->info(sprintf('Could not resolve "%s" to a live node uri. Arguments: %s', $matches[0], json_encode($uriBuilder->getLastArguments())), LogEnvironment::fromMethodName(__METHOD__)); } $this->runtime->addCacheTag('node', $matches[2]); break; @@ -202,8 +215,12 @@ protected function replaceLinkTargets($processedContent) $setExternal = $this->fusionValue('setExternal'); $externalLinkTarget = \trim((string)$this->fusionValue('externalLinkTarget')); $resourceLinkTarget = \trim((string)$this->fusionValue('resourceLinkTarget')); - $controllerContext = $this->runtime->getControllerContext(); - $host = $controllerContext->getRequest()->getHttpRequest()->getUri()->getHost(); + $possibleRequest = $this->runtime->fusionGlobals->get('request'); + if ($possibleRequest instanceof ActionRequest) { + $host = $possibleRequest->getHttpRequest()->getUri()->getHost(); + } else { + $host = null; + } $processedContent = \preg_replace_callback( '~~i', static function ($matches) use ($externalLinkTarget, $resourceLinkTarget, $host, $setNoOpener, $setExternal) { From 520b6e439210589ba1c0d4d72ef2608db6d3e15a Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Tue, 30 Jan 2024 14:42:48 +0100 Subject: [PATCH 09/18] Revert introduction of `HttpResponseConstraints` and api in runtime to allow setting headers dynamically Instead, the legacy layer > $this->runtime->getControllerContext()->getResponse(); should be continued to be used by fusion forms and fusion plugin impl. --- .../Classes/Core/HttpResponseConstraints.php | 110 ------------------ Neos.Fusion/Classes/Core/Runtime.php | 49 ++++---- .../Classes/Fusion/PluginImplementation.php | 7 +- .../Unit/Fusion/PluginImplementationTest.php | 21 +--- 4 files changed, 39 insertions(+), 148 deletions(-) delete mode 100644 Neos.Fusion/Classes/Core/HttpResponseConstraints.php diff --git a/Neos.Fusion/Classes/Core/HttpResponseConstraints.php b/Neos.Fusion/Classes/Core/HttpResponseConstraints.php deleted file mode 100644 index 2f572cf6471..00000000000 --- a/Neos.Fusion/Classes/Core/HttpResponseConstraints.php +++ /dev/null @@ -1,110 +0,0 @@ -partialResponse = new Response(); - } - - /** - * @deprecated - */ - public function setAndMergeFromActionResponse(ActionResponse $actionResponse) - { - $this->partialResponse = $this->applyToResponse($actionResponse->buildHttpResponse()); - } - - /** - * Gets the response status code. - * - * @return int Status code. - */ - public function getStatusCode() - { - return $this->partialResponse->getStatusCode(); - } - - /** - * @param int $code The 3-digit integer result code to set. - */ - public function setStatus(int $code) - { - $this->partialResponse = $this->partialResponse->withStatus($code); - } - - /** - * Retrieves all message header values. - * - * While header names are not case-sensitive, getHeaders() will preserve the - * exact case in which headers were originally specified. - * - * @return string[][] Returns an associative array of the message's headers. Each - * key MUST be a header name, and each value MUST be an array of strings - * for that header. - */ - public function getPartialResponse(): ResponseInterface - { - return $this->partialResponse->getHeaders(); - } - - /** - * While header names are case-insensitive, the casing of the header will - * be preserved by this function, and returned from getHeaders(). - * - * @param string $name Case-insensitive header field name. - * @param string|string[] $value Header value(s). - */ - public function setHeader(string $name, $value) - { - $this->partialResponse = $this->partialResponse->withHeader($name, $value); - } - - /** - * Existing values for the specified header will be maintained. The new - * value(s) will be appended to the existing list. If the header did not - * exist previously, it will be added. - * - * @param string $name Case-insensitive header field name to add. - * @param string|string[] $value Header value(s). - */ - public function setAndMergeHeader(string $name, $value) - { - $this->partialResponse = $this->partialResponse->withAddedHeader($name, $value); - } - - /** - * @param string $name Case-insensitive header field name to remove. - */ - public function unsetHeader(string $name) - { - $this->partialResponse = $this->partialResponse->withoutHeader($name); - } - - public function applyToResponse(ResponseInterface $response): ResponseInterface - { - foreach ($this->partialResponse->getHeaders() as $name => $values) { - $response = $response->withAddedHeader($name, $values); - } - - // preserve non 200 status codes that would otherwise be overwritten - if ($this->partialResponse->getStatusCode() !== 200) { - $response = $response->withStatus($this->partialResponse->getStatusCode()); - } - - // reset internal state - $this->partialResponse = new Response(); - - return $response; - } -} diff --git a/Neos.Fusion/Classes/Core/Runtime.php b/Neos.Fusion/Classes/Core/Runtime.php index 49febd5d9e3..5986813ea93 100644 --- a/Neos.Fusion/Classes/Core/Runtime.php +++ b/Neos.Fusion/Classes/Core/Runtime.php @@ -107,8 +107,6 @@ class Runtime */ public readonly FusionGlobals $fusionGlobals; - public readonly HttpResponseConstraints $unsafeHttpResponseConstrains; - /** * @var RuntimeConfiguration */ @@ -154,7 +152,6 @@ public function __construct( ); $this->runtimeContentCache = new RuntimeContentCache($this); $this->fusionGlobals = $fusionGlobals; - $this->unsafeHttpResponseConstrains = new HttpResponseConstraints(); } /** @@ -287,13 +284,16 @@ public function renderResponse(string $fusionPath, array $contextArray): Respons { // legacy controller context layer $possibleRequest = $this->fusionGlobals->get('request'); + $legacyActionResponse = null; if ($possibleRequest instanceof ActionRequest) { $uriBuilder = new UriBuilder(); $uriBuilder->setRequest($possibleRequest); $this->controllerContext = new ControllerContext( $possibleRequest, - new ActionResponse(), + // expose action response to be possibly mutated in neos forms or fusion plugins. + // this behaviour is highly internal and deprecated! + $legacyActionResponse = new ActionResponse(), new Arguments([]), $uriBuilder ); @@ -311,23 +311,18 @@ public function renderResponse(string $fusionPath, array $contextArray): Respons $this->popContext(); } - $legacyControllerContextResponseConstraints = new HttpResponseConstraints(); - if ($this->controllerContext) { - $legacyControllerContextResponseConstraints->setAndMergeFromActionResponse($this->controllerContext->getResponse()); - } - $this->controllerContext = null; - /** * parse potential raw http response possibly rendered via "Neos.Fusion:Http.Message" * {@see \Neos\Fusion\FusionObjects\HttpResponseImplementation} */ $outputStringHasHttpPreamble = is_string($output) && str_starts_with($output, 'HTTP/'); if ($outputStringHasHttpPreamble) { - return $this->unsafeHttpResponseConstrains->applyToResponse( - $legacyControllerContextResponseConstraints->applyToResponse( - Message::parseResponse($output) - ) - ); + $response = Message::parseResponse($output); + if ($legacyActionResponse) { + $response = self::applyActionResponseToPsrResponse($legacyActionResponse, $response); + $this->controllerContext = null; + } + return $response; } $stream = match (true) { @@ -337,11 +332,25 @@ public function renderResponse(string $fusionPath, array $contextArray): Respons default => throw new \RuntimeException(sprintf('Cannot render %s into http response body.', get_debug_type($output)), 1706454898) }; - return $this->unsafeHttpResponseConstrains->applyToResponse( - $legacyControllerContextResponseConstraints->applyToResponse( - new Response(body: $stream) - ) - ); + $response = new Response(body: $stream); + if ($legacyActionResponse) { + $response = self::applyActionResponseToPsrResponse($legacyActionResponse, $response); + $this->controllerContext = null; + } + return $response; + } + + private static function applyActionResponseToPsrResponse(ActionResponse $actionResponse, ResponseInterface $response): ResponseInterface + { + $actionResponseAsHttp = $actionResponse->buildHttpResponse(); + foreach ($actionResponseAsHttp->getHeaders() as $name => $values) { + $response = $response->withAddedHeader($name, $values); + } + // preserve non 200 status codes that would otherwise be overwritten + if ($actionResponseAsHttp->getStatusCode() !== 200) { + $response = $response->withStatus($actionResponseAsHttp->getStatusCode()); + } + return $response; } /** diff --git a/Neos.Neos/Classes/Fusion/PluginImplementation.php b/Neos.Neos/Classes/Fusion/PluginImplementation.php index 02c388453f5..c623542afd9 100644 --- a/Neos.Neos/Classes/Fusion/PluginImplementation.php +++ b/Neos.Neos/Classes/Fusion/PluginImplementation.php @@ -166,12 +166,17 @@ public function evaluate(): string $currentContext = $this->runtime->getCurrentContext(); $this->node = $currentContext['node']; $this->documentNode = $currentContext['documentNode']; + $parentResponse = $this->runtime->getControllerContext()->getResponse(); $pluginResponse = new ActionResponse(); $this->dispatcher->dispatch($this->buildPluginRequest(), $pluginResponse); + // We need to make sure to not merge content up into the parent ActionResponse + // because that would break the Fusion HttpResponse. $content = $pluginResponse->getContent(); + $pluginResponse->setContent(''); + + $pluginResponse->mergeIntoParentResponse($parentResponse); - $this->runtime->unsafeHttpResponseConstrains->setAndMergeFromActionResponse($pluginResponse); return $content; } diff --git a/Neos.Neos/Tests/Unit/Fusion/PluginImplementationTest.php b/Neos.Neos/Tests/Unit/Fusion/PluginImplementationTest.php index 90c721757bb..e8aff26d8ea 100644 --- a/Neos.Neos/Tests/Unit/Fusion/PluginImplementationTest.php +++ b/Neos.Neos/Tests/Unit/Fusion/PluginImplementationTest.php @@ -11,7 +11,6 @@ * source code. */ -use GuzzleHttp\Psr7\Response; use GuzzleHttp\Psr7\Uri; use Neos\Flow\Mvc\ActionRequest; use Neos\Flow\Mvc\ActionResponse; @@ -19,8 +18,6 @@ use Neos\Flow\Mvc\Dispatcher; use Neos\Flow\Mvc\RequestInterface; use Neos\Flow\Tests\UnitTestCase; -use Neos\Fusion\Core\FusionConfiguration; -use Neos\Fusion\Core\FusionGlobals; use Neos\Fusion\Core\Runtime; use Neos\Neos\Fusion\PluginImplementation; use PHPUnit\Framework\MockObject\MockObject; @@ -82,7 +79,7 @@ public function setUp(): void $this->mockControllerContext = $this->getMockBuilder(ControllerContext::class)->disableOriginalConstructor()->getMock(); $this->mockControllerContext->method('getRequest')->willReturn($this->mockActionRequest); - $this->mockRuntime = $this->getMockBuilder(Runtime::class)->setConstructorArgs([FusionConfiguration::fromArray([]), FusionGlobals::fromArray([])])->getMock(); + $this->mockRuntime = $this->getMockBuilder(Runtime::class)->disableOriginalConstructor()->getMock(); $this->mockRuntime->method('getControllerContext')->willReturn($this->mockControllerContext); $this->pluginImplementation->_set('runtime', $this->mockRuntime); @@ -99,12 +96,12 @@ public function responseHeadersDataProvider(): array [ 'Plugin response key does already exist in parent with same value', ['parent' => ['key' => 'value'], 'plugin' => ['key' => 'value']], - ['key' => 'value'] // 'value, value' + ['key' => 'value'] ], [ 'Plugin response key does not exist in parent with different value', ['parent' => ['key' => 'value'], 'plugin' => ['key' => 'otherValue']], - ['key' => 'otherValue'] // 'otherValue, value' + ['key' => 'otherValue'] ], [ 'Plugin response key does not exist in parent', @@ -122,8 +119,6 @@ public function responseHeadersDataProvider(): array */ public function evaluateSetHeaderIntoParent(string $message, array $input, array $expected): void { - $this->markTestSkipped('DOESNT WORK.'); - $this->pluginImplementation->method('buildPluginRequest')->willReturn($this->mockActionRequest); $parentResponse = new ActionResponse(); @@ -138,16 +133,8 @@ public function evaluateSetHeaderIntoParent(string $message, array $input, array $this->pluginImplementation->evaluate(); - // in the runtime would be: - $runtimeResponse = $this->mockRuntime->unsafeHttpResponseConstrains->applyToResponse(new Response()); - - // in the action would be: - $parentResponse->replaceHttpResponse($runtimeResponse); - foreach ($expected as $expectedKey => $expectedValue) { - // previously tests succeeded: - // self::assertEquals($expectedValue, join(', ', \Neos\Utility\ObjectAccess::getProperty($parentResponse, 'headers', true)[$expectedKey]), $message); - self::assertEquals($expectedValue, $parentResponse->buildHttpResponse()->getHeaderLine($expectedKey), $message); + self::assertEquals($expectedValue, (string)$parentResponse->getHttpHeader($expectedKey), $message); } } From 0b8c64968cf7b8c2ba0c60c6fc2b3d4931e468a1 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Tue, 30 Jan 2024 15:26:04 +0100 Subject: [PATCH 10/18] TASK: Migrate further fusion objects to `$this->runtime->fusionGlobals->get('request');` --- Neos.Fusion/Classes/Core/Runtime.php | 20 +++++++++---------- .../ResourceUriImplementation.php | 10 ++++++---- .../ResourceUriImplementationTest.php | 18 ++++++----------- .../Classes/Fusion/ImageUriImplementation.php | 5 ++++- .../Classes/Fusion/NodeUriImplementation.php | 17 ++++++++++++++-- .../Classes/Fusion/PluginImplementation.php | 5 ++++- 6 files changed, 45 insertions(+), 30 deletions(-) diff --git a/Neos.Fusion/Classes/Core/Runtime.php b/Neos.Fusion/Classes/Core/Runtime.php index 5986813ea93..85e9c445764 100644 --- a/Neos.Fusion/Classes/Core/Runtime.php +++ b/Neos.Fusion/Classes/Core/Runtime.php @@ -299,6 +299,7 @@ public function renderResponse(string $fusionPath, array $contextArray): Respons ); } + /** unlike pushContextArray, we will only allow "legal" fusion global variables. {@see self::pushContext} */ foreach ($contextArray as $key => $_) { if ($this->fusionGlobals->has($key)) { throw new Exception(sprintf('Overriding Fusion global variable "%s" via @context is not allowed.', $key), 1706452063); @@ -319,7 +320,7 @@ public function renderResponse(string $fusionPath, array $contextArray): Respons if ($outputStringHasHttpPreamble) { $response = Message::parseResponse($output); if ($legacyActionResponse) { - $response = self::applyActionResponseToPsrResponse($legacyActionResponse, $response); + $response = self::applyActionResponseToHttpResponse($legacyActionResponse, $response); $this->controllerContext = null; } return $response; @@ -334,23 +335,22 @@ public function renderResponse(string $fusionPath, array $contextArray): Respons $response = new Response(body: $stream); if ($legacyActionResponse) { - $response = self::applyActionResponseToPsrResponse($legacyActionResponse, $response); + $response = self::applyActionResponseToHttpResponse($legacyActionResponse, $response); $this->controllerContext = null; } return $response; } - private static function applyActionResponseToPsrResponse(ActionResponse $actionResponse, ResponseInterface $response): ResponseInterface + private static function applyActionResponseToHttpResponse(ActionResponse $actionResponse, ResponseInterface $httpResponse): ResponseInterface { - $actionResponseAsHttp = $actionResponse->buildHttpResponse(); - foreach ($actionResponseAsHttp->getHeaders() as $name => $values) { - $response = $response->withAddedHeader($name, $values); + foreach ($actionResponse->buildHttpResponse()->getHeaders() as $name => $values) { + $httpResponse = $httpResponse->withAddedHeader($name, $values); } - // preserve non 200 status codes that would otherwise be overwritten - if ($actionResponseAsHttp->getStatusCode() !== 200) { - $response = $response->withStatus($actionResponseAsHttp->getStatusCode()); + // if the status code is 200 we assume it's the default and will not overrule it + if ($actionResponse->getStatusCode() !== 200) { + $httpResponse = $httpResponse->withStatus($actionResponse->getStatusCode()); } - return $response; + return $httpResponse; } /** diff --git a/Neos.Fusion/Classes/FusionObjects/ResourceUriImplementation.php b/Neos.Fusion/Classes/FusionObjects/ResourceUriImplementation.php index 06804001fa0..ce4603163db 100644 --- a/Neos.Fusion/Classes/FusionObjects/ResourceUriImplementation.php +++ b/Neos.Fusion/Classes/FusionObjects/ResourceUriImplementation.php @@ -116,10 +116,12 @@ public function evaluate() } else { $package = $this->getPackage(); if ($package === null) { - $controllerContext = $this->runtime->getControllerContext(); - /** @var $actionRequest ActionRequest */ - $actionRequest = $controllerContext->getRequest(); - $package = $actionRequest->getControllerPackageKey(); + $possibleRequest = $this->runtime->fusionGlobals->get('request'); + if ($possibleRequest instanceof ActionRequest) { + $package = $possibleRequest->getControllerPackageKey(); + } else { + throw new \RuntimeException('Could not infer package-key from action request. Please render Fusion with request or specify a package-key.', 1706624314); + } } } $localize = $this->isLocalize(); diff --git a/Neos.Fusion/Tests/Unit/FusionObjects/ResourceUriImplementationTest.php b/Neos.Fusion/Tests/Unit/FusionObjects/ResourceUriImplementationTest.php index 742f53d1cc9..a0cc8983cb1 100644 --- a/Neos.Fusion/Tests/Unit/FusionObjects/ResourceUriImplementationTest.php +++ b/Neos.Fusion/Tests/Unit/FusionObjects/ResourceUriImplementationTest.php @@ -13,10 +13,11 @@ use Neos\Flow\I18n\Service; use Neos\Flow\Mvc\ActionRequest; -use Neos\Flow\Mvc\Controller\ControllerContext; use Neos\Flow\ResourceManagement\PersistentResource; use Neos\Flow\ResourceManagement\ResourceManager; use Neos\Flow\Tests\UnitTestCase; +use Neos\Fusion\Core\FusionConfiguration; +use Neos\Fusion\Core\FusionGlobals; use Neos\Fusion\Core\Runtime; use Neos\Fusion\Exception; use Neos\Fusion\FusionObjects\ResourceUriImplementation; @@ -46,11 +47,6 @@ class ResourceUriImplementationTest extends UnitTestCase */ protected $mockI18nService; - /** - * @var ControllerContext - */ - protected $mockControllerContext; - /** * @var ActionRequest */ @@ -58,14 +54,12 @@ class ResourceUriImplementationTest extends UnitTestCase public function setUp(): void { - $this->mockRuntime = $this->getMockBuilder(Runtime::class)->disableOriginalConstructor()->getMock(); - - $this->mockControllerContext = $this->getMockBuilder(ControllerContext::class)->disableOriginalConstructor()->getMock(); - $this->mockActionRequest = $this->getMockBuilder(ActionRequest::class)->disableOriginalConstructor()->getMock(); - $this->mockControllerContext->expects(self::any())->method('getRequest')->will(self::returnValue($this->mockActionRequest)); - $this->mockRuntime->expects(self::any())->method('getControllerContext')->will(self::returnValue($this->mockControllerContext)); + $this->mockRuntime = $this->getMockBuilder(Runtime::class)->setConstructorArgs([ + FusionConfiguration::fromArray([]), + FusionGlobals::fromArray(['request' => $this->mockActionRequest]) + ])->getMock(); $this->resourceUriImplementation = new ResourceUriImplementation($this->mockRuntime, 'resourceUri/test', 'Neos.Fusion:ResourceUri'); diff --git a/Neos.Neos/Classes/Fusion/ImageUriImplementation.php b/Neos.Neos/Classes/Fusion/ImageUriImplementation.php index 506fb8296f4..263861658a5 100644 --- a/Neos.Neos/Classes/Fusion/ImageUriImplementation.php +++ b/Neos.Neos/Classes/Fusion/ImageUriImplementation.php @@ -15,6 +15,7 @@ namespace Neos\Neos\Fusion; use Neos\Flow\Annotations as Flow; +use Neos\Flow\Mvc\ActionRequest; use Neos\Media\Domain\Model\AssetInterface; use Neos\Media\Domain\Model\ThumbnailConfiguration; use Neos\Media\Domain\Service\AssetService; @@ -181,7 +182,9 @@ public function evaluate() $this->getFormat() ); } - $request = $this->getRuntime()->getControllerContext()->getRequest(); + + $possibleRequest = $this->runtime->fusionGlobals->get('request'); + $request = $possibleRequest instanceof ActionRequest ? $possibleRequest : null; $thumbnailData = $this->assetService->getThumbnailUriAndSizeForAsset($asset, $thumbnailConfiguration, $request); if ($thumbnailData === null) { return ''; diff --git a/Neos.Neos/Classes/Fusion/NodeUriImplementation.php b/Neos.Neos/Classes/Fusion/NodeUriImplementation.php index 7845fdbcaed..9e1da3c7d83 100644 --- a/Neos.Neos/Classes/Fusion/NodeUriImplementation.php +++ b/Neos.Neos/Classes/Fusion/NodeUriImplementation.php @@ -14,14 +14,16 @@ namespace Neos\Neos\Fusion; +use GuzzleHttp\Psr7\ServerRequest; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; +use Neos\Flow\Mvc\ActionRequest; +use Neos\Neos\FrontendRouting\NodeAddressFactory; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Annotations as Flow; use Neos\Flow\Log\Utility\LogEnvironment; use Neos\Flow\Mvc\Exception\NoMatchingRouteException; use Neos\Flow\Mvc\Routing\UriBuilder; use Neos\Fusion\FusionObjects\AbstractFusionObject; -use Neos\Neos\FrontendRouting\NodeAddressFactory; use Neos\Neos\FrontendRouting\NodeUriBuilder; use Psr\Log\LoggerInterface; @@ -159,7 +161,18 @@ public function evaluate() );*/ $uriBuilder = new UriBuilder(); - $uriBuilder->setRequest($this->runtime->getControllerContext()->getRequest()); + $possibleRequest = $this->runtime->fusionGlobals->get('request'); + if ($possibleRequest instanceof ActionRequest) { + $uriBuilder->setRequest($possibleRequest); + } else { + // unfortunately, the uri-builder always needs a request at hand and cannot build uris without + // even, if the default param merging would not be required + // this will improve with a reformed uri building: + // https://github.com/neos/flow-development-collection/pull/2744 + $uriBuilder->setRequest( + ActionRequest::fromHttpRequest(ServerRequest::fromGlobals()) + ); + } $uriBuilder ->setFormat($this->getFormat()) ->setCreateAbsoluteUri($this->isAbsolute()) diff --git a/Neos.Neos/Classes/Fusion/PluginImplementation.php b/Neos.Neos/Classes/Fusion/PluginImplementation.php index c623542afd9..36ba957cc32 100644 --- a/Neos.Neos/Classes/Fusion/PluginImplementation.php +++ b/Neos.Neos/Classes/Fusion/PluginImplementation.php @@ -90,7 +90,10 @@ public function getArgumentNamespace() */ protected function buildPluginRequest(): ActionRequest { - $parentRequest = $this->runtime->getControllerContext()->getRequest(); + $parentRequest = $this->runtime->fusionGlobals->get('request'); + if (!$parentRequest instanceof ActionRequest) { + throw new \RuntimeException('Fusion Plugins must be rendered with an ActionRequest set as fusion-global.', 1706624581); + } $pluginRequest = $parentRequest->createSubRequest(); $pluginRequest->setArgumentNamespace('--' . $this->getPluginNamespace()); $this->passArgumentsToPluginRequest($pluginRequest); From dcfcbb542c85fe749be91e2cb848366bd9b761d2 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 3 Feb 2024 22:29:25 +0100 Subject: [PATCH 11/18] TASK: Document legacy Runtime::getControllerContext --- Neos.Fusion/Classes/Core/Runtime.php | 104 ++++++++++++++++----------- 1 file changed, 63 insertions(+), 41 deletions(-) diff --git a/Neos.Fusion/Classes/Core/Runtime.php b/Neos.Fusion/Classes/Core/Runtime.php index 85e9c445764..59d1c1a8c27 100644 --- a/Neos.Fusion/Classes/Core/Runtime.php +++ b/Neos.Fusion/Classes/Core/Runtime.php @@ -113,9 +113,9 @@ class Runtime protected $runtimeConfiguration; /** - * @deprecated + * @deprecated legacy layer {@see self::getControllerContext()} */ - protected ?ControllerContext $controllerContext = null; + private ?ActionResponse $legacyActionResponseForCurrentRendering = null; /** * @var array @@ -154,22 +154,6 @@ public function __construct( $this->fusionGlobals = $fusionGlobals; } - /** - * Returns the context which has been passed by the currently active MVC Controller - * - * DEPRECATED CONCEPT. We only implement this as backwards-compatible layer. - * - * @deprecated use `Runtime::fusionGlobals->get('request')` instead to get the request. {@see FusionGlobals::get()} - * @internal - */ - public function getControllerContext(): ControllerContext - { - if ($this->controllerContext === null) { - throw new Exception(sprintf('Legacy controller context in runtime is only available when fusion global "request" is a ActionRequest and during "renderResponse".'), 1706458355); - } - return $this->controllerContext; - } - /** * Inject settings of this package * @@ -282,22 +266,11 @@ public function getLastEvaluationStatus() public function renderResponse(string $fusionPath, array $contextArray): ResponseInterface { - // legacy controller context layer - $possibleRequest = $this->fusionGlobals->get('request'); - $legacyActionResponse = null; - if ($possibleRequest instanceof ActionRequest) { - $uriBuilder = new UriBuilder(); - $uriBuilder->setRequest($possibleRequest); - - $this->controllerContext = new ControllerContext( - $possibleRequest, - // expose action response to be possibly mutated in neos forms or fusion plugins. - // this behaviour is highly internal and deprecated! - $legacyActionResponse = new ActionResponse(), - new Arguments([]), - $uriBuilder - ); + if ($this->legacyActionResponseForCurrentRendering !== null) { + throw new Exception('Recursion detected in `Runtime::renderResponse`. This entry point is only allowed to be invoked once per rendering.', 1706993940); } + /** Legacy layer {@see self::getControllerContext()} */ + $this->legacyActionResponseForCurrentRendering = new ActionResponse(); /** unlike pushContextArray, we will only allow "legal" fusion global variables. {@see self::pushContext} */ foreach ($contextArray as $key => $_) { @@ -319,10 +292,8 @@ public function renderResponse(string $fusionPath, array $contextArray): Respons $outputStringHasHttpPreamble = is_string($output) && str_starts_with($output, 'HTTP/'); if ($outputStringHasHttpPreamble) { $response = Message::parseResponse($output); - if ($legacyActionResponse) { - $response = self::applyActionResponseToHttpResponse($legacyActionResponse, $response); - $this->controllerContext = null; - } + $response = self::applyActionResponseToHttpResponse($this->legacyActionResponseForCurrentRendering, $response); + $this->legacyActionResponseForCurrentRendering = null; return $response; } @@ -334,10 +305,8 @@ public function renderResponse(string $fusionPath, array $contextArray): Respons }; $response = new Response(body: $stream); - if ($legacyActionResponse) { - $response = self::applyActionResponseToHttpResponse($legacyActionResponse, $response); - $this->controllerContext = null; - } + $response = self::applyActionResponseToHttpResponse($this->legacyActionResponseForCurrentRendering, $response); + $this->legacyActionResponseForCurrentRendering = null; return $response; } @@ -984,6 +953,59 @@ protected function throwExceptionForUnrenderablePathIfNeeded($fusionPath, $fusio } } + /** + * The concept of the controller context inside Fusion has been deprecated. + * + * To migrate the use case of fetching the active request, please look into {@see FusionGlobals::get()} instead. + * By convention, an {@see ActionRequest} will be available as `request`: + * + * ```php + * $actionRequest = $this->runtime->fusionGlobals->get('request'); + * if (!$actionRequest instanceof ActionRequest) { + * // fallback or error + * } + * ``` + * + * To get an {@see UriBuilder} proceed with: + * + * ```php + * $uriBuilder = new UriBuilder(); + * $uriBuilder->setRequest($actionRequest); + * ``` + * + * WARNING: + * Invoking this backwards-compatible layer is possibly unsafe, if the rendering was not started + * in {@see self::renderResponse()} or no `request` global is available. This will raise an exception. + * + * MAINTAINER NOTE: + * Initially it was possible to mutate the current response of the active MVC controller though $response. + * While HIGHLY internal behaviour and ONLY to be used by Neos.Fusion.Form or Neos.Neos:Plugin + * a legacy layer in place still allows this functionality. + * + * @deprecated with Neos 9.0 + * @internal + */ + public function getControllerContext(): ControllerContext + { + // legacy controller context layer + $actionRequest = $this->fusionGlobals->get('request'); + if ($this->legacyActionResponseForCurrentRendering === null || !$actionRequest instanceof ActionRequest) { + throw new Exception(sprintf('Fusions simulated legacy controller context is only available inside `Runtime::renderResponse` and when the Fusion global "request" is an ActionRequest.'), 1706458355); + } + + $uriBuilder = new UriBuilder(); + $uriBuilder->setRequest($actionRequest); + + return new ControllerContext( + $actionRequest, + // expose action response to be possibly mutated in neos forms or fusion plugins. + // this behaviour is highly internal and deprecated! + $this->legacyActionResponseForCurrentRendering, + new Arguments([]), + $uriBuilder + ); + } + /** * Configures this runtime to override the default exception handler configured in the settings * or via Fusion's \@exceptionHandler {@see AbstractRenderingExceptionHandler}. From cad48237e2c61c458cc350f7a9c3911741aa8908 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 3 Feb 2024 22:41:59 +0100 Subject: [PATCH 12/18] TASK: Extract controller context legacy layer into withSimulatedLegacyControllerContext --- Neos.Fusion/Classes/Core/Runtime.php | 105 ++++++++++++++------------- 1 file changed, 56 insertions(+), 49 deletions(-) diff --git a/Neos.Fusion/Classes/Core/Runtime.php b/Neos.Fusion/Classes/Core/Runtime.php index 59d1c1a8c27..2b380348e37 100644 --- a/Neos.Fusion/Classes/Core/Runtime.php +++ b/Neos.Fusion/Classes/Core/Runtime.php @@ -266,60 +266,39 @@ public function getLastEvaluationStatus() public function renderResponse(string $fusionPath, array $contextArray): ResponseInterface { - if ($this->legacyActionResponseForCurrentRendering !== null) { - throw new Exception('Recursion detected in `Runtime::renderResponse`. This entry point is only allowed to be invoked once per rendering.', 1706993940); - } - /** Legacy layer {@see self::getControllerContext()} */ - $this->legacyActionResponseForCurrentRendering = new ActionResponse(); - - /** unlike pushContextArray, we will only allow "legal" fusion global variables. {@see self::pushContext} */ + /** Unlike pushContextArray, we don't allow to overrule fusion globals {@see self::pushContext} */ foreach ($contextArray as $key => $_) { if ($this->fusionGlobals->has($key)) { throw new Exception(sprintf('Overriding Fusion global variable "%s" via @context is not allowed.', $key), 1706452063); } } $this->pushContextArray($contextArray); - try { - $output = $this->render($fusionPath); - } finally { - $this->popContext(); - } - /** - * parse potential raw http response possibly rendered via "Neos.Fusion:Http.Message" - * {@see \Neos\Fusion\FusionObjects\HttpResponseImplementation} - */ - $outputStringHasHttpPreamble = is_string($output) && str_starts_with($output, 'HTTP/'); - if ($outputStringHasHttpPreamble) { - $response = Message::parseResponse($output); - $response = self::applyActionResponseToHttpResponse($this->legacyActionResponseForCurrentRendering, $response); - $this->legacyActionResponseForCurrentRendering = null; - return $response; - } + return $this->withSimulatedLegacyControllerContext(function () use ($fusionPath) { + try { + $output = $this->render($fusionPath); + } finally { + $this->popContext(); + } - $stream = match (true) { - is_string($output), - $output instanceof \Stringable => Utils::streamFor((string)$output), - $output === null, $output === false => Utils::streamFor(''), - default => throw new \RuntimeException(sprintf('Cannot render %s into http response body.', get_debug_type($output)), 1706454898) - }; + /** + * parse potential raw http response possibly rendered via "Neos.Fusion:Http.Message" + * {@see \Neos\Fusion\FusionObjects\HttpResponseImplementation} + */ + $outputStringHasHttpPreamble = is_string($output) && str_starts_with($output, 'HTTP/'); + if ($outputStringHasHttpPreamble) { + return Message::parseResponse($output); + } - $response = new Response(body: $stream); - $response = self::applyActionResponseToHttpResponse($this->legacyActionResponseForCurrentRendering, $response); - $this->legacyActionResponseForCurrentRendering = null; - return $response; - } + $stream = match (true) { + is_string($output), + $output instanceof \Stringable => Utils::streamFor((string)$output), + $output === null, $output === false => Utils::streamFor(''), + default => throw new \RuntimeException(sprintf('Cannot render %s into http response body.', get_debug_type($output)), 1706454898) + }; - private static function applyActionResponseToHttpResponse(ActionResponse $actionResponse, ResponseInterface $httpResponse): ResponseInterface - { - foreach ($actionResponse->buildHttpResponse()->getHeaders() as $name => $values) { - $httpResponse = $httpResponse->withAddedHeader($name, $values); - } - // if the status code is 200 we assume it's the default and will not overrule it - if ($actionResponse->getStatusCode() !== 200) { - $httpResponse = $httpResponse->withStatus($actionResponse->getStatusCode()); - } - return $httpResponse; + return new Response(body: $stream); + }); } /** @@ -953,6 +932,39 @@ protected function throwExceptionForUnrenderablePathIfNeeded($fusionPath, $fusio } } + /** + * Implements the legacy controller context simulation {@see self::getControllerContext()} + * + * Initially it was possible to mutate the current response of the active MVC controller though $response. + * While HIGHLY internal behaviour and ONLY to be used by Neos.Fusion.Form or Neos.Neos:Plugin + * this legacy layer is in place still allows this functionality. + * + * @param \Closure(): ResponseInterface $renderer + */ + private function withSimulatedLegacyControllerContext(\Closure $renderer): ResponseInterface + { + if ($this->legacyActionResponseForCurrentRendering !== null) { + throw new Exception('Recursion detected in `Runtime::renderResponse`. This entry point is only allowed to be invoked once per rendering.', 1706993940); + } + $this->legacyActionResponseForCurrentRendering = new ActionResponse(); + + // actual rendering + $httpResponse = $renderer(); + + // transfer possible headers that have been set dynamically + foreach ($this->legacyActionResponseForCurrentRendering->buildHttpResponse()->getHeaders() as $name => $values) { + $httpResponse = $httpResponse->withAddedHeader($name, $values); + } + // if the status code is 200 we assume it's the default and will not overrule it + if ($this->legacyActionResponseForCurrentRendering->getStatusCode() !== 200) { + $httpResponse = $httpResponse->withStatus($this->legacyActionResponseForCurrentRendering->getStatusCode()); + } + + // reset for next render + $this->legacyActionResponseForCurrentRendering = null; + return $httpResponse; + } + /** * The concept of the controller context inside Fusion has been deprecated. * @@ -977,11 +989,6 @@ protected function throwExceptionForUnrenderablePathIfNeeded($fusionPath, $fusio * Invoking this backwards-compatible layer is possibly unsafe, if the rendering was not started * in {@see self::renderResponse()} or no `request` global is available. This will raise an exception. * - * MAINTAINER NOTE: - * Initially it was possible to mutate the current response of the active MVC controller though $response. - * While HIGHLY internal behaviour and ONLY to be used by Neos.Fusion.Form or Neos.Neos:Plugin - * a legacy layer in place still allows this functionality. - * * @deprecated with Neos 9.0 * @internal */ From 1dd307c702918f31ed820ec2d44daab59fdde0a3 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Fri, 16 Feb 2024 22:41:56 +0100 Subject: [PATCH 13/18] TASK: Runtime fix `renderResponse` lock not being released --- Neos.Fusion/Classes/Core/Runtime.php | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/Neos.Fusion/Classes/Core/Runtime.php b/Neos.Fusion/Classes/Core/Runtime.php index 2b380348e37..8f188e0b83b 100644 --- a/Neos.Fusion/Classes/Core/Runtime.php +++ b/Neos.Fusion/Classes/Core/Runtime.php @@ -949,19 +949,23 @@ private function withSimulatedLegacyControllerContext(\Closure $renderer): Respo $this->legacyActionResponseForCurrentRendering = new ActionResponse(); // actual rendering - $httpResponse = $renderer(); + try { + $httpResponse = $renderer(); + } finally { + $toBeMergedLegacyActionResponse = $this->legacyActionResponseForCurrentRendering; + // reset for next render + $this->legacyActionResponseForCurrentRendering = null; + } // transfer possible headers that have been set dynamically - foreach ($this->legacyActionResponseForCurrentRendering->buildHttpResponse()->getHeaders() as $name => $values) { + foreach ($toBeMergedLegacyActionResponse->buildHttpResponse()->getHeaders() as $name => $values) { $httpResponse = $httpResponse->withAddedHeader($name, $values); } // if the status code is 200 we assume it's the default and will not overrule it - if ($this->legacyActionResponseForCurrentRendering->getStatusCode() !== 200) { - $httpResponse = $httpResponse->withStatus($this->legacyActionResponseForCurrentRendering->getStatusCode()); + if ($toBeMergedLegacyActionResponse->getStatusCode() !== 200) { + $httpResponse = $httpResponse->withStatus($toBeMergedLegacyActionResponse->getStatusCode()); } - // reset for next render - $this->legacyActionResponseForCurrentRendering = null; return $httpResponse; } From b737c01916022cb8ff231230d274974633e7d48a Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Fri, 16 Feb 2024 22:47:21 +0100 Subject: [PATCH 14/18] TASK: Runtime `renderResponse` unwrap `RuntimeException` itself Previously the pattern was that the utmost caller of the runtime would unwrap the exception. To simplify this, as the runtime now has a single entry point, we add this behaviour into the runtime. --- Neos.Fusion/Classes/Core/Runtime.php | 3 +++ Neos.Fusion/Classes/View/FusionView.php | 6 +----- Neos.Neos/Classes/View/FusionView.php | 16 +++++----------- 3 files changed, 9 insertions(+), 16 deletions(-) diff --git a/Neos.Fusion/Classes/Core/Runtime.php b/Neos.Fusion/Classes/Core/Runtime.php index 8f188e0b83b..c2f8ae38741 100644 --- a/Neos.Fusion/Classes/Core/Runtime.php +++ b/Neos.Fusion/Classes/Core/Runtime.php @@ -277,6 +277,9 @@ public function renderResponse(string $fusionPath, array $contextArray): Respons return $this->withSimulatedLegacyControllerContext(function () use ($fusionPath) { try { $output = $this->render($fusionPath); + } catch (RuntimeException $exception) { + // unwrap the FusionRuntimeException + throw $exception->getPrevious(); } finally { $this->popContext(); } diff --git a/Neos.Fusion/Classes/View/FusionView.php b/Neos.Fusion/Classes/View/FusionView.php index bdc4413e113..36d4fe6ae5c 100644 --- a/Neos.Fusion/Classes/View/FusionView.php +++ b/Neos.Fusion/Classes/View/FusionView.php @@ -149,11 +149,7 @@ public function render() $this->initializeFusionRuntime(); if ($this->getOption('renderHttpResponse') === true) { - try { - return $this->fusionRuntime->renderResponse($this->getFusionPathForCurrentRequest(), $this->variables); - } catch (RuntimeException $exception) { - throw $exception->getPrevious(); - } + return $this->fusionRuntime->renderResponse($this->getFusionPathForCurrentRequest(), $this->variables); } else { try { $this->fusionRuntime->pushContextArray($this->variables); diff --git a/Neos.Neos/Classes/View/FusionView.php b/Neos.Neos/Classes/View/FusionView.php index d3142635911..d86ef54c240 100644 --- a/Neos.Neos/Classes/View/FusionView.php +++ b/Neos.Neos/Classes/View/FusionView.php @@ -14,7 +14,6 @@ namespace Neos\Neos\View; -use GuzzleHttp\Psr7\Message; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindClosestNodeFilter; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; @@ -24,7 +23,6 @@ use Neos\Fusion\Core\FusionGlobals; use Neos\Fusion\Core\Runtime; use Neos\Fusion\Core\RuntimeFactory; -use Neos\Fusion\Exception\RuntimeException; use Neos\Neos\Domain\Model\RenderingMode; use Neos\Neos\Domain\Repository\SiteRepository; use Neos\Neos\Domain\Service\FusionService; @@ -74,15 +72,11 @@ public function render(): ResponseInterface $fusionRuntime = $this->getFusionRuntime($currentSiteNode); - try { - return $fusionRuntime->renderResponse($this->fusionPath, [ - 'node' => $currentNode, - 'documentNode' => $this->getClosestDocumentNode($currentNode) ?: $currentNode, - 'site' => $currentSiteNode - ]); - } catch (RuntimeException $exception) { - throw $exception->getPrevious() ?: $exception; - } + return $fusionRuntime->renderResponse($this->fusionPath, [ + 'node' => $currentNode, + 'documentNode' => $this->getClosestDocumentNode($currentNode) ?: $currentNode, + 'site' => $currentSiteNode + ]); } /** From 310cb7d94747dfdefae498c5ef36e25f06849988 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 18 Feb 2024 12:15:58 +0100 Subject: [PATCH 15/18] TASK: Add Fusion RuntimeException::getWrappedException ... to centralise tricking phpstan --- Neos.Fusion/Classes/Core/Runtime.php | 3 +-- Neos.Fusion/Classes/Exception/RuntimeException.php | 9 +++++++++ Neos.Fusion/Classes/View/FusionView.php | 2 +- Neos.Neos/Classes/View/FusionExceptionView.php | 2 +- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/Neos.Fusion/Classes/Core/Runtime.php b/Neos.Fusion/Classes/Core/Runtime.php index c2f8ae38741..839187779b9 100644 --- a/Neos.Fusion/Classes/Core/Runtime.php +++ b/Neos.Fusion/Classes/Core/Runtime.php @@ -278,8 +278,7 @@ public function renderResponse(string $fusionPath, array $contextArray): Respons try { $output = $this->render($fusionPath); } catch (RuntimeException $exception) { - // unwrap the FusionRuntimeException - throw $exception->getPrevious(); + throw $exception->getWrappedException(); } finally { $this->popContext(); } diff --git a/Neos.Fusion/Classes/Exception/RuntimeException.php b/Neos.Fusion/Classes/Exception/RuntimeException.php index 8ee31394250..b827edf4b74 100644 --- a/Neos.Fusion/Classes/Exception/RuntimeException.php +++ b/Neos.Fusion/Classes/Exception/RuntimeException.php @@ -32,4 +32,13 @@ public function getFusionPath() { return $this->fusionPath; } + + /** + * Unwrap this Fusion RuntimeException + */ + public function getWrappedException(): \Exception + { + /** @phpstan-ignore-next-line due to overridden construction, we are sure that the previous exists. */ + return $this->getPrevious(); + } } diff --git a/Neos.Fusion/Classes/View/FusionView.php b/Neos.Fusion/Classes/View/FusionView.php index 36d4fe6ae5c..e44ffb6f664 100644 --- a/Neos.Fusion/Classes/View/FusionView.php +++ b/Neos.Fusion/Classes/View/FusionView.php @@ -155,7 +155,7 @@ public function render() $this->fusionRuntime->pushContextArray($this->variables); return $this->fusionRuntime->render($this->getFusionPathForCurrentRequest()); } catch (RuntimeException $exception) { - throw $exception->getPrevious(); + throw $exception->getWrappedException(); } finally { $this->fusionRuntime->popContext(); } diff --git a/Neos.Neos/Classes/View/FusionExceptionView.php b/Neos.Neos/Classes/View/FusionExceptionView.php index 46241f48729..1d4a85119d9 100644 --- a/Neos.Neos/Classes/View/FusionExceptionView.php +++ b/Neos.Neos/Classes/View/FusionExceptionView.php @@ -171,7 +171,7 @@ public function render() $output = $fusionRuntime->render('error'); return $this->extractBodyFromOutput($output); } catch (RuntimeException $exception) { - throw $exception->getPrevious() ?: $exception; + throw $exception->getWrappedException(); } finally { $fusionRuntime->popContext(); } From 96bcea1480dcf427a2498795ec894198bb891fb9 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 18 Feb 2024 12:26:26 +0100 Subject: [PATCH 16/18] TASK: Remove manual http response parsing from FusionExceptionView --- .../Classes/View/FusionExceptionView.php | 33 +++++-------------- 1 file changed, 8 insertions(+), 25 deletions(-) diff --git a/Neos.Neos/Classes/View/FusionExceptionView.php b/Neos.Neos/Classes/View/FusionExceptionView.php index 1d4a85119d9..4feb70ea7c5 100644 --- a/Neos.Neos/Classes/View/FusionExceptionView.php +++ b/Neos.Neos/Classes/View/FusionExceptionView.php @@ -32,7 +32,6 @@ use Neos\Fusion\Core\FusionGlobals; use Neos\Fusion\Core\Runtime as FusionRuntime; use Neos\Fusion\Core\RuntimeFactory; -use Neos\Fusion\Exception\RuntimeException; use Neos\Neos\Domain\Model\RenderingMode; use Neos\Neos\Domain\Repository\DomainRepository; use Neos\Neos\Domain\Repository\SiteRepository; @@ -158,7 +157,7 @@ public function render() $this->setFallbackRuleFromDimension($dimensionSpacePoint); - $fusionRuntime->pushContextArray(array_merge( + $httpResponse = $fusionRuntime->renderResponse('error', array_merge( $this->variables, [ 'node' => $currentSiteNode, @@ -167,29 +166,13 @@ public function render() ] )); - try { - $output = $fusionRuntime->render('error'); - return $this->extractBodyFromOutput($output); - } catch (RuntimeException $exception) { - throw $exception->getWrappedException(); - } finally { - $fusionRuntime->popContext(); - } - } - - /** - * @param string $output - * @return string The message body without the message head - */ - protected function extractBodyFromOutput(string $output): string - { - if (substr($output, 0, 5) === 'HTTP/') { - $endOfHeader = strpos($output, "\r\n\r\n"); - if ($endOfHeader !== false) { - $output = substr($output, $endOfHeader + 4); - } - } - return $output; + /** + * Workaround: The http status code will already be sent and + * Flow's {@see \Neos\Flow\Error\DebugExceptionHandler::echoExceptionWeb()} + * expects a view to return a string to be echo'd. + * Thus, we unwrap the repose here: + */ + return $httpResponse->getBody()->getContents(); } /** From 3862d0f886899f0ebf35ed9551ab3c549c8e9d2e Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 18 Feb 2024 15:15:28 +0100 Subject: [PATCH 17/18] TASK: Revert Fusion `FusionView` HttpResponse support This will be discussed separately and not part of the change of the Neos Node `FusionView` --- Neos.Fusion/Classes/View/FusionView.php | 40 +++++++++++-------- .../View/Fixtures/Fusion/Root.fusion | 4 -- .../Tests/Functional/View/FusionViewTest.php | 36 ----------------- 3 files changed, 23 insertions(+), 57 deletions(-) diff --git a/Neos.Fusion/Classes/View/FusionView.php b/Neos.Fusion/Classes/View/FusionView.php index e44ffb6f664..bf983346f4f 100644 --- a/Neos.Fusion/Classes/View/FusionView.php +++ b/Neos.Fusion/Classes/View/FusionView.php @@ -22,7 +22,6 @@ use Neos\Fusion\Core\Runtime; use Neos\Fusion\Core\RuntimeFactory; use Neos\Fusion\Exception\RuntimeException; -use Psr\Http\Message\ResponseInterface; /** * View for using Fusion for standard MVC controllers. @@ -47,8 +46,7 @@ class FusionView extends AbstractView 'fusionGlobals' => [null, 'Additional global variables; merged together with the "request". Must only be specified at creation.', FusionGlobals::class], 'packageKey' => [null, 'The package key where the Fusion should be loaded from. If not given, is automatically derived from the current request.', 'string'], 'debugMode' => [false, 'Flag to enable debug mode of the Fusion runtime explicitly (overriding the global setting).', 'boolean'], - 'enableContentCache' => [false, 'Flag to enable content caching inside Fusion (overriding the global setting).', 'boolean'], - 'renderHttpResponse' => [false, 'Flag to render fusion as http repose for advanced form support and Neos.Fusion:Http.ResponseHead support.', 'boolean'], + 'enableContentCache' => [false, 'Flag to enable content caching inside Fusion (overriding the global setting).', 'boolean'] ]; /** @@ -141,25 +139,13 @@ public function setFusionPathPatterns(array $pathPatterns) /** * Render the view * - * @return mixed|ResponseInterface The rendered view + * @return mixed The rendered view * @api */ public function render() { $this->initializeFusionRuntime(); - - if ($this->getOption('renderHttpResponse') === true) { - return $this->fusionRuntime->renderResponse($this->getFusionPathForCurrentRequest(), $this->variables); - } else { - try { - $this->fusionRuntime->pushContextArray($this->variables); - return $this->fusionRuntime->render($this->getFusionPathForCurrentRequest()); - } catch (RuntimeException $exception) { - throw $exception->getWrappedException(); - } finally { - $this->fusionRuntime->popContext(); - } - } + return $this->renderFusion(); } /** @@ -190,6 +176,9 @@ public function initializeFusionRuntime() $this->parsedFusion, $fusionGlobals ); + if (isset($this->controllerContext)) { + $this->fusionRuntime->setControllerContext($this->controllerContext); + } } if (isset($this->options['debugMode'])) { $this->fusionRuntime->setDebugMode($this->options['debugMode']); @@ -294,4 +283,21 @@ protected function getFusionPathForCurrentRequest() } return $this->fusionPath; } + + /** + * Render the given Fusion and return the rendered page + * @return mixed + * @throws \Exception + */ + protected function renderFusion() + { + $this->fusionRuntime->pushContextArray($this->variables); + try { + $output = $this->fusionRuntime->render($this->getFusionPathForCurrentRequest()); + } catch (RuntimeException $exception) { + throw $exception->getPrevious(); + } + $this->fusionRuntime->popContext(); + return $output; + } } diff --git a/Neos.Fusion/Tests/Functional/View/Fixtures/Fusion/Root.fusion b/Neos.Fusion/Tests/Functional/View/Fixtures/Fusion/Root.fusion index fbcf0fc40a3..b190f6bd5a0 100644 --- a/Neos.Fusion/Tests/Functional/View/Fixtures/Fusion/Root.fusion +++ b/Neos.Fusion/Tests/Functional/View/Fixtures/Fusion/Root.fusion @@ -1,5 +1 @@ include: ./**/*.fusion -include: 'resource://Neos.Fusion/Private/Fusion/Prototypes/Join.fusion' -include: 'resource://Neos.Fusion/Private/Fusion/Prototypes/DataStructure.fusion' -include: 'resource://Neos.Fusion/Private/Fusion/Prototypes/Http.Message.fusion' -include: 'resource://Neos.Fusion/Private/Fusion/Prototypes/Http.ResponseHead.fusion' diff --git a/Neos.Fusion/Tests/Functional/View/FusionViewTest.php b/Neos.Fusion/Tests/Functional/View/FusionViewTest.php index 629508c8643..dc2f06d8ba9 100644 --- a/Neos.Fusion/Tests/Functional/View/FusionViewTest.php +++ b/Neos.Fusion/Tests/Functional/View/FusionViewTest.php @@ -15,7 +15,6 @@ use Neos\Flow\Mvc\Controller\ControllerContext; use Neos\Flow\Tests\FunctionalTestCase; use Neos\Fusion\View\FusionView; -use Psr\Http\Message\ResponseInterface; /** * Testcase for the Fusion View @@ -65,41 +64,6 @@ public function fusionViewOutputsVariable() self::assertEquals('XHallo Welt', $view->render()); } - /** - * @test - */ - public function fusionViewCanReturnHttpResponse() - { - $view = $this->buildView('Foo\Bar\Controller\TestController', 'index'); - $view->setOption('renderHttpResponse', true); - $view->assign('test', 'Hallo Welt'); - $response = $view->render(); - self::assertInstanceOf(ResponseInterface::class, $response); - self::assertEquals('XHallo Welt', $view->render()->getBody()->getContents()); - } - - /** - * @test - */ - public function fusionViewCanReturnHttpResponseFromHttpMessagePrototype() - { - $view = $this->buildView('Foo\Bar\Controller\TestController', 'index'); - $view->setFusionPath('response'); - self::assertSame(<<render()); - - $view->setOption('renderHttpResponse', true); - $response = $view->render(); - self::assertInstanceOf(ResponseInterface::class, $response); - self::assertSame('{"some":"json"}', $response->getBody()->getContents()); - self::assertSame(404, $response->getStatusCode()); - self::assertSame("application/json", $response->getHeaderLine("Content-Type")); - } - /** * Prepare a FusionView for testing that Mocks a request with the given controller and action names. * From df8cf3cdff28b82342e02acc2506b863aee9e26a Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 18 Feb 2024 15:16:48 +0100 Subject: [PATCH 18/18] WIP: Add Fusion `FusionView` HttpResponse support --- Neos.Fusion/Classes/View/FusionView.php | 40 ++++++++----------- .../View/Fixtures/Fusion/Root.fusion | 4 ++ .../Tests/Functional/View/FusionViewTest.php | 36 +++++++++++++++++ .../Classes/View/FusionExceptionView.php | 7 +++- 4 files changed, 63 insertions(+), 24 deletions(-) diff --git a/Neos.Fusion/Classes/View/FusionView.php b/Neos.Fusion/Classes/View/FusionView.php index bf983346f4f..e44ffb6f664 100644 --- a/Neos.Fusion/Classes/View/FusionView.php +++ b/Neos.Fusion/Classes/View/FusionView.php @@ -22,6 +22,7 @@ use Neos\Fusion\Core\Runtime; use Neos\Fusion\Core\RuntimeFactory; use Neos\Fusion\Exception\RuntimeException; +use Psr\Http\Message\ResponseInterface; /** * View for using Fusion for standard MVC controllers. @@ -46,7 +47,8 @@ class FusionView extends AbstractView 'fusionGlobals' => [null, 'Additional global variables; merged together with the "request". Must only be specified at creation.', FusionGlobals::class], 'packageKey' => [null, 'The package key where the Fusion should be loaded from. If not given, is automatically derived from the current request.', 'string'], 'debugMode' => [false, 'Flag to enable debug mode of the Fusion runtime explicitly (overriding the global setting).', 'boolean'], - 'enableContentCache' => [false, 'Flag to enable content caching inside Fusion (overriding the global setting).', 'boolean'] + 'enableContentCache' => [false, 'Flag to enable content caching inside Fusion (overriding the global setting).', 'boolean'], + 'renderHttpResponse' => [false, 'Flag to render fusion as http repose for advanced form support and Neos.Fusion:Http.ResponseHead support.', 'boolean'], ]; /** @@ -139,13 +141,25 @@ public function setFusionPathPatterns(array $pathPatterns) /** * Render the view * - * @return mixed The rendered view + * @return mixed|ResponseInterface The rendered view * @api */ public function render() { $this->initializeFusionRuntime(); - return $this->renderFusion(); + + if ($this->getOption('renderHttpResponse') === true) { + return $this->fusionRuntime->renderResponse($this->getFusionPathForCurrentRequest(), $this->variables); + } else { + try { + $this->fusionRuntime->pushContextArray($this->variables); + return $this->fusionRuntime->render($this->getFusionPathForCurrentRequest()); + } catch (RuntimeException $exception) { + throw $exception->getWrappedException(); + } finally { + $this->fusionRuntime->popContext(); + } + } } /** @@ -176,9 +190,6 @@ public function initializeFusionRuntime() $this->parsedFusion, $fusionGlobals ); - if (isset($this->controllerContext)) { - $this->fusionRuntime->setControllerContext($this->controllerContext); - } } if (isset($this->options['debugMode'])) { $this->fusionRuntime->setDebugMode($this->options['debugMode']); @@ -283,21 +294,4 @@ protected function getFusionPathForCurrentRequest() } return $this->fusionPath; } - - /** - * Render the given Fusion and return the rendered page - * @return mixed - * @throws \Exception - */ - protected function renderFusion() - { - $this->fusionRuntime->pushContextArray($this->variables); - try { - $output = $this->fusionRuntime->render($this->getFusionPathForCurrentRequest()); - } catch (RuntimeException $exception) { - throw $exception->getPrevious(); - } - $this->fusionRuntime->popContext(); - return $output; - } } diff --git a/Neos.Fusion/Tests/Functional/View/Fixtures/Fusion/Root.fusion b/Neos.Fusion/Tests/Functional/View/Fixtures/Fusion/Root.fusion index b190f6bd5a0..fbcf0fc40a3 100644 --- a/Neos.Fusion/Tests/Functional/View/Fixtures/Fusion/Root.fusion +++ b/Neos.Fusion/Tests/Functional/View/Fixtures/Fusion/Root.fusion @@ -1 +1,5 @@ include: ./**/*.fusion +include: 'resource://Neos.Fusion/Private/Fusion/Prototypes/Join.fusion' +include: 'resource://Neos.Fusion/Private/Fusion/Prototypes/DataStructure.fusion' +include: 'resource://Neos.Fusion/Private/Fusion/Prototypes/Http.Message.fusion' +include: 'resource://Neos.Fusion/Private/Fusion/Prototypes/Http.ResponseHead.fusion' diff --git a/Neos.Fusion/Tests/Functional/View/FusionViewTest.php b/Neos.Fusion/Tests/Functional/View/FusionViewTest.php index dc2f06d8ba9..629508c8643 100644 --- a/Neos.Fusion/Tests/Functional/View/FusionViewTest.php +++ b/Neos.Fusion/Tests/Functional/View/FusionViewTest.php @@ -15,6 +15,7 @@ use Neos\Flow\Mvc\Controller\ControllerContext; use Neos\Flow\Tests\FunctionalTestCase; use Neos\Fusion\View\FusionView; +use Psr\Http\Message\ResponseInterface; /** * Testcase for the Fusion View @@ -64,6 +65,41 @@ public function fusionViewOutputsVariable() self::assertEquals('XHallo Welt', $view->render()); } + /** + * @test + */ + public function fusionViewCanReturnHttpResponse() + { + $view = $this->buildView('Foo\Bar\Controller\TestController', 'index'); + $view->setOption('renderHttpResponse', true); + $view->assign('test', 'Hallo Welt'); + $response = $view->render(); + self::assertInstanceOf(ResponseInterface::class, $response); + self::assertEquals('XHallo Welt', $view->render()->getBody()->getContents()); + } + + /** + * @test + */ + public function fusionViewCanReturnHttpResponseFromHttpMessagePrototype() + { + $view = $this->buildView('Foo\Bar\Controller\TestController', 'index'); + $view->setFusionPath('response'); + self::assertSame(<<render()); + + $view->setOption('renderHttpResponse', true); + $response = $view->render(); + self::assertInstanceOf(ResponseInterface::class, $response); + self::assertSame('{"some":"json"}', $response->getBody()->getContents()); + self::assertSame(404, $response->getStatusCode()); + self::assertSame("application/json", $response->getHeaderLine("Content-Type")); + } + /** * Prepare a FusionView for testing that Mocks a request with the given controller and action names. * diff --git a/Neos.Neos/Classes/View/FusionExceptionView.php b/Neos.Neos/Classes/View/FusionExceptionView.php index 4feb70ea7c5..877c840b8b6 100644 --- a/Neos.Neos/Classes/View/FusionExceptionView.php +++ b/Neos.Neos/Classes/View/FusionExceptionView.php @@ -39,6 +39,7 @@ use Neos\Neos\Domain\Service\SiteNodeUtility; use Neos\Neos\FrontendRouting\SiteDetection\SiteDetectionFailedException; use Neos\Neos\FrontendRouting\SiteDetection\SiteDetectionResult; +use Psr\Http\Message\ResponseInterface; class FusionExceptionView extends AbstractView { @@ -218,6 +219,10 @@ private function renderErrorWelcomeScreen(): mixed 'enableContentCache' => false, ]); $view->assignMultiple($this->variables); - return $view->render(); + $output = $view->render(); + if ($output instanceof ResponseInterface) { + return $output->getBody()->getContents(); + } + return $output; } }