diff --git a/Neos.Fusion/Classes/Core/Runtime.php b/Neos.Fusion/Classes/Core/Runtime.php index e14a792a41c..839187779b9 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; @@ -109,9 +113,9 @@ class Runtime protected $runtimeConfiguration; /** - * @deprecated + * @deprecated legacy layer {@see self::getControllerContext()} */ - protected ControllerContext $controllerContext; + private ?ActionResponse $legacyActionResponseForCurrentRendering = null; /** * @var array @@ -150,44 +154,6 @@ public function __construct( $this->fusionGlobals = $fusionGlobals; } - /** - * @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 - * - * 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 (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); - } - - $uriBuilder = new UriBuilder(); - $uriBuilder->setRequest($request); - - return $this->controllerContext = new ControllerContext( - $request, - new ActionResponse(), - new Arguments([]), - $uriBuilder - ); - } - /** * Inject settings of this package * @@ -298,6 +264,45 @@ public function getLastEvaluationStatus() return $this->lastEvaluationStatus; } + public function renderResponse(string $fusionPath, array $contextArray): ResponseInterface + { + /** 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); + + return $this->withSimulatedLegacyControllerContext(function () use ($fusionPath) { + try { + $output = $this->render($fusionPath); + } catch (RuntimeException $exception) { + throw $exception->getWrappedException(); + } 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 +630,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); } @@ -929,6 +934,91 @@ 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 + try { + $httpResponse = $renderer(); + } finally { + $toBeMergedLegacyActionResponse = $this->legacyActionResponseForCurrentRendering; + // reset for next render + $this->legacyActionResponseForCurrentRendering = null; + } + + // transfer possible headers that have been set dynamically + 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 ($toBeMergedLegacyActionResponse->getStatusCode() !== 200) { + $httpResponse = $httpResponse->withStatus($toBeMergedLegacyActionResponse->getStatusCode()); + } + + return $httpResponse; + } + + /** + * 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. + * + * @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}. diff --git a/Neos.Fusion/Classes/Core/RuntimeFactory.php b/Neos.Fusion/Classes/Core/RuntimeFactory.php index e435fc0f261..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") @@ -44,19 +41,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 +77,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/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/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/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..f59cdde7359 100644 --- a/Neos.Fusion/Classes/FusionObjects/UriBuilderImplementation.php +++ b/Neos.Fusion/Classes/FusionObjects/UriBuilderImplementation.php @@ -11,6 +11,9 @@ * source code. */ +use GuzzleHttp\Psr7\ServerRequest; +use Neos\Flow\Mvc\ActionRequest; +use Neos\Flow\Mvc\Routing\UriBuilder; /** * A Fusion UriBuilder object @@ -150,8 +153,19 @@ 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 { + // 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()) + ); + } $format = $this->getFormat(); if ($format !== null) { 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/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. * 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/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) { 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); diff --git a/Neos.Neos/Classes/View/FusionExceptionView.php b/Neos.Neos/Classes/View/FusionExceptionView.php index bf4807b0e65..877c840b8b6 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; @@ -40,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 { @@ -158,7 +158,7 @@ public function render() $this->setFallbackRuleFromDimension($dimensionSpacePoint); - $fusionRuntime->pushContextArray(array_merge( + $httpResponse = $fusionRuntime->renderResponse('error', array_merge( $this->variables, [ 'node' => $currentSiteNode, @@ -167,29 +167,13 @@ public function render() ] )); - try { - $output = $fusionRuntime->render('error'); - return $this->extractBodyFromOutput($output); - } catch (RuntimeException $exception) { - throw $exception->getPrevious() ?: $exception; - } 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(); } /** @@ -216,7 +200,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']); @@ -236,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; } } diff --git a/Neos.Neos/Classes/View/FusionView.php b/Neos.Neos/Classes/View/FusionView.php index 6ff365b5127..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; @@ -57,11 +55,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 +72,11 @@ public function render(): string|ResponseInterface $fusionRuntime = $this->getFusionRuntime($currentSiteNode); - $fusionRuntime->pushContextArray([ + return $fusionRuntime->renderResponse($this->fusionPath, [ 'node' => $currentNode, 'documentNode' => $this->getClosestDocumentNode($currentNode) ?: $currentNode, 'site' => $currentSiteNode ]); - try { - $output = $fusionRuntime->render($this->fusionPath); - $output = $this->parsePotentialRawHttpResponse($output); - } catch (RuntimeException $exception) { - throw $exception->getPrevious() ?: $exception; - } - $fusionRuntime->popContext(); - - return $output; } /** @@ -129,35 +118,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? * @@ -244,7 +204,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']);