diff --git a/Neos.Fusion/Classes/Core/FusionGlobals.php b/Neos.Fusion/Classes/Core/FusionGlobals.php new file mode 100644 index 00000000000..d9077a1809b --- /dev/null +++ b/Neos.Fusion/Classes/Core/FusionGlobals.php @@ -0,0 +1,63 @@ +fusionGlobals->get('request')` + */ + public function get(string $name): mixed + { + return $this->value[$name] ?? null; + } + + public function has(string $name): bool + { + return array_key_exists($name, $this->value); + } + + public function merge(FusionGlobals $other): self + { + return new self( + Arrays::arrayMergeRecursiveOverrule($this->value, $other->value) + ); + } +} diff --git a/Neos.Fusion/Classes/Core/Runtime.php b/Neos.Fusion/Classes/Core/Runtime.php index 0480101e208..320315f4f12 100644 --- a/Neos.Fusion/Classes/Core/Runtime.php +++ b/Neos.Fusion/Classes/Core/Runtime.php @@ -11,23 +11,27 @@ * source code. */ +use Neos\Eel\Utility as EelUtility; use Neos\Flow\Annotations as Flow; use Neos\Flow\Configuration\Exception\InvalidConfigurationException; +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\Exception\StopActionException; +use Neos\Flow\Mvc\Routing\UriBuilder; use Neos\Flow\ObjectManagement\ObjectManagerInterface; -use Neos\Utility\Arrays; -use Neos\Utility\ObjectAccess; -use Neos\Utility\PositionalArraySorter; +use Neos\Flow\Security\Exception as SecurityException; use Neos\Fusion\Core\Cache\RuntimeContentCache; use Neos\Fusion\Core\ExceptionHandlers\AbstractRenderingExceptionHandler; -use Neos\Fusion\Exception as Exceptions; use Neos\Fusion\Exception; -use Neos\Flow\Security\Exception as SecurityException; +use Neos\Fusion\Exception as Exceptions; use Neos\Fusion\Exception\RuntimeException; use Neos\Fusion\FusionObjects\AbstractArrayFusionObject; use Neos\Fusion\FusionObjects\AbstractFusionObject; -use Neos\Eel\Utility as EelUtility; +use Neos\Utility\Arrays; +use Neos\Utility\ObjectAccess; +use Neos\Utility\PositionalArraySorter; /** * Fusion Runtime @@ -94,11 +98,9 @@ class Runtime protected $currentApplyValues = []; /** - * Default context with helper definitions - * - * @var array + * Fusion global variables like EEL helper definitions {@see FusionGlobals} */ - protected $defaultContextVariables; + public readonly FusionGlobals $fusionGlobals; /** * @var array @@ -106,9 +108,9 @@ class Runtime protected $runtimeConfiguration; /** - * @var ControllerContext + * @deprecated */ - protected $controllerContext; + protected ControllerContext $controllerContext; /** * @var array @@ -130,15 +132,56 @@ class Runtime */ protected $lastEvaluationStatus; - public function __construct(FusionConfiguration|array $fusionConfiguration, ControllerContext $controllerContext) - { + /** + * @internal use {@see RuntimeFactory} for instantiating. + */ + public function __construct( + FusionConfiguration $fusionConfiguration, + FusionGlobals $fusionGlobals + ) { $this->runtimeConfiguration = new RuntimeConfiguration( - $fusionConfiguration instanceof FusionConfiguration - ? $fusionConfiguration->toArray() - : $fusionConfiguration + $fusionConfiguration->toArray() ); - $this->controllerContext = $controllerContext; $this->runtimeContentCache = new RuntimeContentCache($this); + $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 \RuntimeException(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 + ); } /** @@ -179,8 +222,11 @@ public function addCacheTag($key, $value) /** * Completely replace the context array with the new $contextArray. * - * Purely internal method, should not be called outside of Neos.Fusion. + * Warning unlike in Fusion's \@context or {@see Runtime::pushContext()}, + * no checks are imposed to prevent overriding Fusion globals like "request". + * Relying on this behaviour is highly discouraged but leveraged by Neos.Fusion.Form {@see FusionGlobals}. * + * @internal purely internal method, should not be called outside Neos.Fusion. * @param array $contextArray * @return void */ @@ -191,7 +237,8 @@ public function pushContextArray(array $contextArray) } /** - * Push a new context object to the rendering stack + * Push a new context object to the rendering stack. + * It is disallowed to replace global variables {@see FusionGlobals}. * * @param string $key the key inside the context * @param mixed $context @@ -199,6 +246,9 @@ public function pushContextArray(array $contextArray) */ public function pushContext($key, $context) { + if ($this->fusionGlobals->has($key)) { + throw new RuntimeException(sprintf('Overriding Fusion global variable "%s" via @context is not allowed.', $key), 1694284229044); + } $newContext = $this->currentContext; $newContext[$key] = $context; $this->contextStack[] = $newContext; @@ -218,7 +268,9 @@ public function popContext() } /** - * Get the current context array + * Get the current context array. + * This PHP context api unlike Fusion, doesn't include the Fusion globals {@see FusionGlobals}. + * The globals can be accessed via {@see Runtime::$fusionGlobals}. * * @return array the array of current context objects */ @@ -563,6 +615,9 @@ protected function prepareContextForFusionObject(AbstractFusionObject $fusionObj if (isset($fusionConfiguration['__meta']['context'])) { $newContextArray ??= $this->currentContext; foreach ($fusionConfiguration['__meta']['context'] as $contextKey => $contextValue) { + if ($this->fusionGlobals->has($contextKey)) { + throw new RuntimeException(sprintf('Overriding Fusion global variable "%s" via @context is not allowed.', $contextKey), 1694247627130); + } $newContextArray[$contextKey] = $this->evaluate($fusionPath . '/__meta/context/' . $contextKey, $fusionObject, self::BEHAVIOR_EXCEPTION); } } @@ -684,7 +739,7 @@ protected function evaluateEelExpression($expression, AbstractFusionObject $cont $expression = '${' . $expression . '}'; } - $contextVariables = array_merge($this->getDefaultContextVariables(), $this->currentContext); + $contextVariables = array_merge($this->fusionGlobals->value, $this->currentContext); if (isset($contextVariables['this'])) { throw new Exception('Context variable "this" not allowed, as it is already reserved for a pointer to the current Fusion object.', 1344325044); @@ -831,34 +886,6 @@ protected function evaluateIfCondition($configurationWithEventualIf, $configurat return true; } - /** - * Returns the context which has been passed by the currently active MVC Controller - * - * @return ControllerContext - */ - public function getControllerContext() - { - return $this->controllerContext; - } - - /** - * Get variables from configuration that should be set in the context by default. - * For example Eel helpers are made available by this. - * - * @return array Array with default context variable objects. - */ - protected function getDefaultContextVariables() - { - if ($this->defaultContextVariables === null) { - $this->defaultContextVariables = []; - if (isset($this->settings['defaultContext']) && is_array($this->settings['defaultContext'])) { - $this->defaultContextVariables = EelUtility::getDefaultContextVariables($this->settings['defaultContext']); - } - $this->defaultContextVariables['request'] = $this->controllerContext->getRequest(); - } - return $this->defaultContextVariables; - } - /** * Checks and throws an exception for an unrenderable path. * diff --git a/Neos.Fusion/Classes/Core/RuntimeFactory.php b/Neos.Fusion/Classes/Core/RuntimeFactory.php index fc159cb9071..d76ec2fbf7a 100644 --- a/Neos.Fusion/Classes/Core/RuntimeFactory.php +++ b/Neos.Fusion/Classes/Core/RuntimeFactory.php @@ -12,6 +12,7 @@ */ use GuzzleHttp\Psr7\ServerRequest; +use Neos\Eel\Utility as EelUtility; use Neos\Flow\Annotations as Flow; use Neos\Flow\Mvc\ActionRequest; use Neos\Flow\Mvc\ActionResponse; @@ -31,6 +32,11 @@ class RuntimeFactory */ protected $fusionParser; + /** + * @Flow\InjectConfiguration(path="defaultContext", package="Neos.Fusion") + */ + protected ?array $defaultContextConfiguration; + /** * @deprecated with Neos 8.3 might be removed with Neos 9.0 use {@link createFromConfiguration} instead. */ @@ -39,24 +45,38 @@ public function create(array $fusionConfiguration, ControllerContext $controller if ($controllerContext === null) { $controllerContext = self::createControllerContextFromEnvironment(); } - return new Runtime( + $defaultContextVariables = EelUtility::getDefaultContextVariables( + $this->defaultContextConfiguration ?? [] + ); + $runtime = new Runtime( FusionConfiguration::fromArray($fusionConfiguration), - $controllerContext + FusionGlobals::fromArray( + ['request' => $controllerContext->getRequest(), ...$defaultContextVariables] + ) ); + $runtime->setControllerContext($controllerContext); + return $runtime; } - public function createFromConfiguration(FusionConfiguration $fusionConfiguration, ControllerContext $controllerContext): Runtime - { - return new Runtime($fusionConfiguration, $controllerContext); + public function createFromConfiguration( + FusionConfiguration $fusionConfiguration, + FusionGlobals $fusionGlobals + ): Runtime { + $fusionGlobalHelpers = FusionGlobals::fromArray( + EelUtility::getDefaultContextVariables( + $this->defaultContextConfiguration ?? [] + ) + ); + return new Runtime($fusionConfiguration, $fusionGlobalHelpers->merge($fusionGlobals)); } public function createFromSourceCode( FusionSourceCodeCollection $sourceCode, - ControllerContext $controllerContext + FusionGlobals $fusionGlobals ): Runtime { - return new Runtime( + return $this->createFromConfiguration( $this->fusionParser->parseFromSource($sourceCode), - $controllerContext + $fusionGlobals ); } diff --git a/Neos.Fusion/Classes/View/FusionView.php b/Neos.Fusion/Classes/View/FusionView.php index 7f1e4f91b02..c1ba4337296 100644 --- a/Neos.Fusion/Classes/View/FusionView.php +++ b/Neos.Fusion/Classes/View/FusionView.php @@ -14,6 +14,8 @@ use Neos\Flow\Annotations as Flow; use Neos\Flow\Mvc\ActionRequest; use Neos\Flow\Mvc\View\AbstractView; +use Neos\Fusion\Core\FusionConfiguration; +use Neos\Fusion\Core\FusionGlobals; use Neos\Fusion\Core\FusionSourceCode; use Neos\Fusion\Core\FusionSourceCodeCollection; use Neos\Fusion\Core\Parser; @@ -41,6 +43,7 @@ class FusionView extends AbstractView protected $supportedOptions = [ 'fusionPathPatterns' => [['resource://@package/Private/Fusion'], 'Fusion files that will be loaded if directories are given the Root.fusion is used.', 'array'], 'fusionPath' => [null, 'The Fusion path which should be rendered; derived from the controller and action names or set by the user.', 'string'], + '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'] @@ -54,10 +57,8 @@ class FusionView extends AbstractView /** * The parsed Fusion array in its internal representation - * - * @var array */ - protected $parsedFusion; + protected FusionConfiguration $parsedFusion; /** * Runtime cache of the Fusion path which should be rendered; derived from the controller @@ -158,7 +159,26 @@ public function initializeFusionRuntime() { if ($this->fusionRuntime === null) { $this->loadFusion(); - $this->fusionRuntime = $this->runtimeFactory->create($this->parsedFusion, $this->controllerContext); + + $fusionGlobals = $this->options['fusionGlobals'] ?? FusionGlobals::empty(); + if (!$fusionGlobals instanceof FusionGlobals) { + throw new \InvalidArgumentException('View option "fusionGlobals" must be of type FusionGlobals', 1694252923947); + } + $fusionGlobals = $fusionGlobals->merge( + FusionGlobals::fromArray( + array_filter([ + 'request' => $this->controllerContext?->getRequest() + ]) + ) + ); + + $this->fusionRuntime = $this->runtimeFactory->createFromConfiguration( + $this->parsedFusion, + $fusionGlobals + ); + if (isset($this->controllerContext)) { + $this->fusionRuntime->setControllerContext($this->controllerContext); + } } if (isset($this->options['debugMode'])) { $this->fusionRuntime->setDebugMode($this->options['debugMode']); @@ -179,11 +199,9 @@ protected function loadFusion() } /** - * Parse all the fusion files the are in the current fusionPathPatterns - * - * @return array + * Parse all the fusion files that are in the current fusionPathPatterns */ - protected function getMergedFusionObjectTree(): array + protected function getMergedFusionObjectTree(): FusionConfiguration { $fusionCodeCollection = []; $fusionPathPatterns = $this->getFusionPathPatterns(); @@ -195,7 +213,7 @@ protected function getMergedFusionObjectTree(): array $fusionCodeCollection[] = FusionSourceCode::fromFilePath($fusionPathPattern); } } - return $this->fusionParser->parseFromSource(new FusionSourceCodeCollection(...$fusionCodeCollection))->toArray(); + return $this->fusionParser->parseFromSource(new FusionSourceCodeCollection(...$fusionCodeCollection)); } /** diff --git a/Neos.Fusion/Tests/Benchmark/RuntimeBench.php b/Neos.Fusion/Tests/Benchmark/RuntimeBench.php index 025e592d87b..a4f20081531 100644 --- a/Neos.Fusion/Tests/Benchmark/RuntimeBench.php +++ b/Neos.Fusion/Tests/Benchmark/RuntimeBench.php @@ -12,6 +12,9 @@ */ use Neos\Eel\CompilingEvaluator; +use Neos\Fusion\Core\FusionConfiguration; +use Neos\Fusion\Core\FusionGlobals; +use Neos\Fusion\Core\RuntimeFactory; /** * A benchmark to test the Fusion runtime @@ -92,8 +95,7 @@ public function init() ] ] ]; - $runtimeFactory = new \Neos\Fusion\Core\RuntimeFactory(); - $this->runtime = $runtimeFactory->create($fusionConfiguration); + $this->runtime = (new RuntimeFactory())->createFromConfiguration(FusionConfiguration::fromArray($fusionConfiguration), FusionGlobals::empty()); // Build an EEL evaluator suitable for benchmarking $evaluator = $this->buildEelEvaluator(); diff --git a/Neos.Fusion/Tests/Functional/FusionObjects/ExpressionsTest.php b/Neos.Fusion/Tests/Functional/FusionObjects/ExpressionsTest.php index d24db9d4adf..faed6f7178c 100644 --- a/Neos.Fusion/Tests/Functional/FusionObjects/ExpressionsTest.php +++ b/Neos.Fusion/Tests/Functional/FusionObjects/ExpressionsTest.php @@ -11,10 +11,10 @@ * source code. */ -use Neos\Flow\Mvc\Controller\ControllerContext; +use Neos\Fusion\Core\FusionGlobals; use Neos\Fusion\Core\FusionSourceCodeCollection; use Neos\Fusion\Core\Parser; -use Neos\Fusion\Core\Runtime; +use Neos\Fusion\Core\RuntimeFactory; /** * Testcase for Eel expressions in Fusion @@ -53,10 +53,9 @@ public function expressionsWork($path, $expected) */ public function usingEelWorksWithoutSetCurrentContextInRuntime() { - $fusionAst = (new Parser())->parseFromSource(FusionSourceCodeCollection::fromString('root = ${"foo"}'))->toArray(); + $fusionAst = (new Parser())->parseFromSource(FusionSourceCodeCollection::fromString('root = ${"foo"}')); - $controllerContext = $this->getMockBuilder(ControllerContext::class)->disableOriginalConstructor()->getMock(); - $runtime = new Runtime($fusionAst, $controllerContext); + $runtime = (new RuntimeFactory())->createFromConfiguration($fusionAst, FusionGlobals::empty()); $renderedFusion = $runtime->render('root'); diff --git a/Neos.Fusion/Tests/Unit/Core/RuntimeTest.php b/Neos.Fusion/Tests/Unit/Core/RuntimeTest.php index ad0ea6e56fc..e96a1fa8137 100644 --- a/Neos.Fusion/Tests/Unit/Core/RuntimeTest.php +++ b/Neos.Fusion/Tests/Unit/Core/RuntimeTest.php @@ -14,12 +14,14 @@ use Neos\Eel\EelEvaluatorInterface; use Neos\Eel\ProtectedContext; use Neos\Flow\Exception; -use Neos\Flow\Mvc\Controller\ControllerContext; use Neos\Flow\ObjectManagement\ObjectManager; use Neos\Flow\Tests\UnitTestCase; use Neos\Fusion\Core\ExceptionHandlers\ThrowingHandler; +use Neos\Fusion\Core\FusionConfiguration; +use Neos\Fusion\Core\FusionGlobals; use Neos\Fusion\Core\Runtime; use Neos\Fusion\Exception\RuntimeException; +use Neos\Fusion\FusionObjects\ValueImplementation; class RuntimeTest extends UnitTestCase { @@ -31,10 +33,8 @@ class RuntimeTest extends UnitTestCase */ public function renderHandlesExceptionDuringRendering() { - $controllerContext = $this->getMockBuilder(ControllerContext::class)->disableOriginalConstructor()->getMock(); $runtimeException = new RuntimeException('I am a parent exception', 123, new Exception('I am a previous exception')); - $runtime = $this->getMockBuilder(Runtime::class)->setMethods(['evaluate', 'handleRenderingException'])->setConstructorArgs([[], $controllerContext])->getMock(); - $runtime->injectSettings(['rendering' => ['exceptionHandler' => ThrowingHandler::class]]); + $runtime = $this->getMockBuilder(Runtime::class)->onlyMethods(['evaluate', 'handleRenderingException'])->disableOriginalConstructor()->getMock(); $runtime->expects(self::any())->method('evaluate')->will(self::throwException($runtimeException)); $runtime->expects(self::once())->method('handleRenderingException')->with('/foo/bar', $runtimeException)->will(self::returnValue('Exception Message')); @@ -54,9 +54,8 @@ public function handleRenderingExceptionThrowsException() { $this->expectException(Exception::class); $objectManager = $this->getMockBuilder(ObjectManager::class)->disableOriginalConstructor()->setMethods(['isRegistered', 'get'])->getMock(); - $controllerContext = $this->getMockBuilder(ControllerContext::class)->disableOriginalConstructor()->getMock(); $runtimeException = new RuntimeException('I am a parent exception', 123, new Exception('I am a previous exception')); - $runtime = new Runtime([], $controllerContext); + $runtime = new Runtime(FusionConfiguration::fromArray([]), FusionGlobals::empty()); $this->inject($runtime, 'objectManager', $objectManager); $exceptionHandlerSetting = 'settings'; $runtime->injectSettings(['rendering' => ['exceptionHandler' => $exceptionHandlerSetting]]); @@ -72,21 +71,20 @@ public function handleRenderingExceptionThrowsException() */ public function evaluateProcessorForEelExpressionUsesProtectedContext() { - $controllerContext = $this->getMockBuilder(ControllerContext::class)->disableOriginalConstructor()->getMock(); - $eelEvaluator = $this->createMock(EelEvaluatorInterface::class); - $runtime = $this->getAccessibleMock(Runtime::class, ['dummy'], [[], $controllerContext]); + $eelEvaluator->expects(self::once())->method('evaluate')->with( + 'foo + "89"', + self::callback(fn (ProtectedContext $actualContext) => $actualContext->get('foo') === '19') + ); + $runtime = new Runtime(FusionConfiguration::fromArray([]), FusionGlobals::empty()); $this->inject($runtime, 'eelEvaluator', $eelEvaluator); + $runtime->pushContextArray(['foo' => '19']); - $eelEvaluator->expects(self::once())->method('evaluate')->with('q(node).property("title")', $this->isInstanceOf(ProtectedContext::class)); - - $runtime->pushContextArray([ - 'node' => 'Foo' - ]); + $ref = (new \ReflectionClass($runtime))->getMethod('evaluateEelExpression'); - $runtime->_call('evaluateEelExpression', 'q(node).property("title")'); + $ref->invoke($runtime, 'foo + "89"'); } /** @@ -96,8 +94,7 @@ public function evaluateWithCacheModeUncachedAndUnspecifiedContextThrowsExceptio { $this->expectException(\Neos\Fusion\Exception::class); $this->expectExceptionCode(1395922119); - $mockControllerContext = $this->getMockBuilder(ControllerContext::class)->disableOriginalConstructor()->getMock(); - $runtime = new Runtime([ + $runtime = new Runtime(FusionConfiguration::fromArray([ 'foo' => [ 'bar' => [ '__meta' => [ @@ -107,7 +104,7 @@ public function evaluateWithCacheModeUncachedAndUnspecifiedContextThrowsExceptio ] ] ] - ], $mockControllerContext); + ]), FusionGlobals::empty()); $runtime->evaluate('foo/bar'); } @@ -118,9 +115,8 @@ public function evaluateWithCacheModeUncachedAndUnspecifiedContextThrowsExceptio public function renderRethrowsSecurityExceptions() { $this->expectException(\Neos\Flow\Security\Exception::class); - $controllerContext = $this->getMockBuilder(ControllerContext::class)->disableOriginalConstructor()->getMock(); $securityException = new \Neos\Flow\Security\Exception(); - $runtime = $this->getMockBuilder(Runtime::class)->setMethods(['evaluate', 'handleRenderingException'])->setConstructorArgs([[], $controllerContext])->getMock(); + $runtime = $this->getMockBuilder(Runtime::class)->onlyMethods(['evaluate', 'handleRenderingException'])->disableOriginalConstructor()->getMock(); $runtime->expects(self::any())->method('evaluate')->will(self::throwException($securityException)); $runtime->render('/foo/bar'); @@ -131,8 +127,7 @@ public function renderRethrowsSecurityExceptions() */ public function runtimeCurrentContextStackWorksSimplePushPop() { - $controllerContext = $this->getMockBuilder(ControllerContext::class)->disableOriginalConstructor()->getMock(); - $runtime = new Runtime([], $controllerContext); + $runtime = new Runtime(FusionConfiguration::fromArray([]), FusionGlobals::empty()); self::assertSame([], $runtime->getCurrentContext(), 'context should be empty at start.'); @@ -150,8 +145,7 @@ public function runtimeCurrentContextStackWorksSimplePushPop() */ public function runtimeCurrentContextStack3PushesAndPops() { - $controllerContext = $this->getMockBuilder(ControllerContext::class)->disableOriginalConstructor()->getMock(); - $runtime = new Runtime([], $controllerContext); + $runtime = new Runtime(FusionConfiguration::fromArray([]), FusionGlobals::empty()); self::assertSame([], $runtime->getCurrentContext(), 'empty at start'); @@ -177,4 +171,54 @@ public function runtimeCurrentContextStack3PushesAndPops() self::assertSame([], $runtime->getCurrentContext(), 'empty at end'); } + + /** + * @test + */ + public function fusionContextIsNotAllowedToOverrideFusionGlobals() + { + $this->expectException(\Neos\Fusion\Exception\RuntimeException::class); + $this->expectExceptionMessage('Overriding Fusion global variable "request" via @context is not allowed.'); + $runtime = new Runtime(FusionConfiguration::fromArray([ + 'foo' => [ + '__objectType' => 'Neos.Fusion:Value', + '__meta' => [ + 'class' => ValueImplementation::class, + 'context' => [ + 'request' => 'anything' + ] + ] + ] + ]), FusionGlobals::fromArray(['request' => 'fixed'])); + + $runtime->evaluate('foo'); + } + + /** + * @test + */ + public function pushContextIsNotAllowedToOverrideFusionGlobals() + { + $this->expectException(\Neos\Fusion\Exception\RuntimeException::class); + $this->expectExceptionMessage('Overriding Fusion global variable "request" via @context is not allowed.'); + $runtime = new Runtime(FusionConfiguration::fromArray([]), FusionGlobals::fromArray(['request' => 'fixed'])); + + $runtime->pushContext('request', 'anything'); + } + + /** + * Legacy compatible layer to possibly override fusion globals like "request". + * This functionality is only allowed for internal packages. + * Currently Neos.Fusion.Form overrides the request, and we need to keep this behaviour. + * + * {@link https://github.com/neos/fusion-form/blob/224a26afe11f182e6fc5d4bb27ce3f8d0f981ba2/Classes/Runtime/FusionObjects/RuntimeFormImplementation.php#L103} + * + * @test + */ + public function pushContextArrayIsAllowedToOverrideFusionGlobals() + { + $runtime = new Runtime(FusionConfiguration::fromArray([]), FusionGlobals::fromArray(['request' => 'fixed'])); + $runtime->pushContextArray(['bing' => 'beer', 'request' => 'anything']); + self::assertTrue(true); + } } diff --git a/Neos.Neos/Classes/Domain/Repository/SiteRepository.php b/Neos.Neos/Classes/Domain/Repository/SiteRepository.php index bda735773e9..815d9057028 100644 --- a/Neos.Neos/Classes/Domain/Repository/SiteRepository.php +++ b/Neos.Neos/Classes/Domain/Repository/SiteRepository.php @@ -14,6 +14,7 @@ namespace Neos\Neos\Domain\Repository; +use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\Flow\Annotations as Flow; use Neos\Flow\Persistence\QueryInterface; use Neos\Flow\Persistence\QueryResultInterface; @@ -94,6 +95,15 @@ public function findOneByNodeName(string|SiteNodeName $nodeName): ?Site return $site; } + public function findSiteBySiteNode(Node $siteNode): Site + { + if ($siteNode->nodeName === null) { + throw new \Neos\Neos\Domain\Exception(sprintf('Site node "%s" is unnamed', $siteNode->nodeAggregateId->value), 1681286146); + } + return $this->findOneByNodeName(SiteNodeName::fromNodeName($siteNode->nodeName)) + ?? throw new \Neos\Neos\Domain\Exception(sprintf('No site found for nodeNodeName "%s"', $siteNode->nodeName->value), 1677245517); + } + /** * Find the site that was specified in the configuration ``defaultSiteNodeName`` * diff --git a/Neos.Neos/Classes/Domain/Service/FusionService.php b/Neos.Neos/Classes/Domain/Service/FusionService.php index ad3863f6452..05db82195f5 100644 --- a/Neos.Neos/Classes/Domain/Service/FusionService.php +++ b/Neos.Neos/Classes/Domain/Service/FusionService.php @@ -18,6 +18,7 @@ use Neos\Flow\Mvc\Controller\ControllerContext; use Neos\Fusion\Core\FusionConfiguration; use Neos\Flow\Annotations as Flow; +use Neos\Fusion\Core\FusionGlobals; use Neos\Fusion\Core\FusionSourceCode; use Neos\Fusion\Core\FusionSourceCodeCollection; use Neos\Fusion\Core\Parser; @@ -152,10 +153,15 @@ public function createRuntime( Node $currentSiteNode, ControllerContext $controllerContext ) { - return $this->runtimeFactory->createFromConfiguration( + $fusionGlobals = FusionGlobals::fromArray( + ['request' => $controllerContext->getRequest()] + ); + $runtime = $this->runtimeFactory->createFromConfiguration( $this->createFusionConfigurationFromSite($this->findSiteBySiteNode($currentSiteNode)), - $controllerContext + $fusionGlobals ); + $runtime->setControllerContext($controllerContext); + return $runtime; } /** diff --git a/Neos.Neos/Classes/View/FusionExceptionView.php b/Neos.Neos/Classes/View/FusionExceptionView.php index 07dfb1e7a70..c9b3c4262fb 100644 --- a/Neos.Neos/Classes/View/FusionExceptionView.php +++ b/Neos.Neos/Classes/View/FusionExceptionView.php @@ -23,17 +23,20 @@ use Neos\Flow\Annotations as Flow; use Neos\Flow\Core\Bootstrap; use Neos\Flow\Http\RequestHandler as HttpRequestHandler; +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; use Neos\Flow\Mvc\View\AbstractView; +use Neos\Flow\ObjectManagement\ObjectManagerInterface; +use Neos\Flow\Security\Context as SecurityContext; +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\Repository\SiteRepository; use Neos\Neos\Domain\Service\FusionService; -use Neos\Fusion\Core\Runtime as FusionRuntime; -use Neos\Flow\Security\Context as SecurityContext; -use Neos\Flow\ObjectManagement\ObjectManagerInterface; -use Neos\Flow\Mvc\ActionRequest; -use Neos\Flow\Mvc\Routing\UriBuilder; -use Neos\Flow\Mvc\Controller\ControllerContext; -use Neos\Flow\Mvc\Controller\Arguments; use Neos\Neos\Domain\Service\SiteNodeUtility; use Neos\Neos\FrontendRouting\SiteDetection\SiteDetectionResult; @@ -72,6 +75,12 @@ class FusionExceptionView extends AbstractView */ protected $fusionRuntime; + #[Flow\Inject] + protected RuntimeFactory $runtimeFactory; + + #[Flow\Inject] + protected SiteRepository $siteRepository; + #[Flow\Inject] protected SiteNodeUtility $siteNodeUtility; @@ -185,7 +194,18 @@ protected function getFusionRuntime( ControllerContext $controllerContext ): FusionRuntime { if ($this->fusionRuntime === null) { - $this->fusionRuntime = $this->fusionService->createRuntime($currentSiteNode, $controllerContext); + $site = $this->siteRepository->findSiteBySiteNode($currentSiteNode); + + $fusionConfiguration = $this->fusionService->createFusionConfigurationFromSite($site); + + $fusionGlobals = FusionGlobals::fromArray([ + 'request' => $this->controllerContext->getRequest() + ]); + $this->fusionRuntime = $this->runtimeFactory->createFromConfiguration( + $fusionConfiguration, + $fusionGlobals + ); + $this->fusionRuntime->setControllerContext($this->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 17c666f6f02..8264acdd06e 100644 --- a/Neos.Neos/Classes/View/FusionView.php +++ b/Neos.Neos/Classes/View/FusionView.php @@ -20,8 +20,11 @@ use Neos\Flow\Annotations as Flow; use Neos\Flow\Mvc\View\AbstractView; use Neos\Flow\Security\Context; +use Neos\Fusion\Core\FusionGlobals; use Neos\Fusion\Core\Runtime; +use Neos\Fusion\Core\RuntimeFactory; use Neos\Fusion\Exception\RuntimeException; +use Neos\Neos\Domain\Repository\SiteRepository; use Neos\Neos\Domain\Service\FusionService; use Neos\Neos\Domain\Service\SiteNodeUtility; use Neos\Neos\Exception; @@ -40,6 +43,12 @@ class FusionView extends AbstractView */ protected $siteNodeUtility; + #[Flow\Inject] + protected RuntimeFactory $runtimeFactory; + + #[Flow\Inject] + protected SiteRepository $siteRepository; + /** * @Flow\Inject * @var ContentRepositoryRegistry @@ -217,7 +226,17 @@ protected function getCurrentNode(): Node protected function getFusionRuntime(Node $currentSiteNode) { if ($this->fusionRuntime === null) { - $this->fusionRuntime = $this->fusionService->createRuntime($currentSiteNode, $this->controllerContext); + $site = $this->siteRepository->findSiteBySiteNode($currentSiteNode); + $fusionConfiguration = $this->fusionService->createFusionConfigurationFromSite($site); + + $fusionGlobals = FusionGlobals::fromArray([ + 'request' => $this->controllerContext->getRequest() + ]); + $this->fusionRuntime = $this->runtimeFactory->createFromConfiguration( + $fusionConfiguration, + $fusionGlobals + ); + $this->fusionRuntime->setControllerContext($this->controllerContext); if (isset($this->options['enableContentCache']) && $this->options['enableContentCache'] !== null) { $this->fusionRuntime->setEnableContentCache($this->options['enableContentCache']); diff --git a/Neos.Neos/Tests/Unit/View/FusionViewTest.php b/Neos.Neos/Tests/Unit/View/FusionViewTest.php index 51b3a5ab510..ef65be71cc6 100644 --- a/Neos.Neos/Tests/Unit/View/FusionViewTest.php +++ b/Neos.Neos/Tests/Unit/View/FusionViewTest.php @@ -82,7 +82,7 @@ public function setUpMockView() $this->mockSecurityContext = $this->getMockBuilder(Context::class)->disableOriginalConstructor()->getMock(); $mockFusionService = $this->createMock(FusionService::class); - $mockFusionService->expects(self::any())->method('createRuntime')->will(self::returnValue($this->mockRuntime)); + // $mockFusionService->expects(self::any())->method('createRuntime')->will(self::returnValue($this->mockRuntime)); $this->mockView = $this->getAccessibleMock(FusionView::class, ['getClosestDocumentNode']); $this->mockView->expects(self::any())->method('getClosestDocumentNode')->will(self::returnValue($this->mockContextualizedNode)); @@ -152,7 +152,7 @@ public function renderMergesHttpResponseIfOutputIsHttpMessage() $mockRuntime->expects(self::any())->method('getControllerContext')->will(self::returnValue($mockControllerContext)); $mockFusionService = $this->createMock(FusionService::class); - $mockFusionService->expects(self::any())->method('createRuntime')->will(self::returnValue($mockRuntime)); + // $mockFusionService->expects(self::any())->method('createRuntime')->will(self::returnValue($mockRuntime)); $mockSecurityContext = $this->getMockBuilder(Context::class)->disableOriginalConstructor()->getMock();