From 500725cec51c6b487aaf84a9e5f07a4100f07288 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20B=C3=BCrk?= Date: Sat, 25 Jul 2026 17:10:49 +0200 Subject: [PATCH] [BUGFIX] Restore Context and DI container state FrameworkState::push()/pop() back up and restore framework state around frontend sub-requests fired with executeFrontendSubRequest(). Two pieces of global state were not covered so far: the Context singleton and the globally registered dependency injection container. executeFrontendSubRequest() boots the frontend application through Bootstrap::init(), which registers a fresh container and fills the Context with the aspects of the frontend request, most notably the language aspect. As neither was restored afterwards, code running after a sub-request within the test scope silently continued with frontend request state, for instance Extbase repository calls resolving records in the language of the sub-request instead of the default language. push() now stashes a clone of the current Context along with the container instance, reset() applies a pristine Context and drops the container, and pop() puts both back in place. Context is a singleton whose instance reference is held in many places, so its aspects are copied over using reflection instead of exchanging the object itself. The core change identifying the leak is: https://review.typo3.org/c/Packages/TYPO3.CMS/+/94931 It works around the leak in the affected test case by resetting the language aspect by hand. That workaround is verified to be obsolete with this patch by the core change: https://review.typo3.org/c/Packages/TYPO3.CMS/+/94945 which requires this branch, runs the core test suites against it and reverts the workaround again. The class is further hardened along the way: a State array shape is declared and applied to the state stack payload, giving proper type information for the pushed and popped values, and pop() now throws a dedicated exception instead of running into undefined array access when the stack is empty. Releases: main, 10 --- .../Functional/Framework/FrameworkState.php | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/Classes/Core/Functional/Framework/FrameworkState.php b/Classes/Core/Functional/Framework/FrameworkState.php index 5c2728b0..1a91ed6d 100644 --- a/Classes/Core/Functional/Framework/FrameworkState.php +++ b/Classes/Core/Functional/Framework/FrameworkState.php @@ -17,6 +17,11 @@ * The TYPO3 project - inspiring people to share! */ +use Psr\Container\ContainerInterface; +use Psr\Http\Message\ServerRequestInterface; +use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; +use TYPO3\CMS\Core\Context\Context; +use TYPO3\CMS\Core\SingletonInterface; use TYPO3\CMS\Core\Utility\GeneralUtility; /** @@ -29,6 +34,18 @@ * * This class should not be needed. It is a manifest of technical core debt. * It should shrink over time and vanish altogether in the end. + * + * @phpstan-type State array{ + * globals-server: array, + * globals-beUser: BackendUserAuthentication|null, + * globals-typo3-conf-vars: array|null, + * globals-tca: array, + * request: ServerRequestInterface|null, + * generalUtilityIndpEnvCache?: array|null>, + * generalUtilitySingletonInstances: array, + * contextData: Context, + * container: ContainerInterface, + * } */ class FrameworkState { @@ -39,6 +56,7 @@ class FrameworkState */ public static function push(): void { + /** @var State $state */ $state = []; $state['globals-server'] = $GLOBALS['_SERVER']; $state['globals-beUser'] = $GLOBALS['BE_USER'] ?? null; @@ -61,6 +79,8 @@ public static function push(): void } $state['generalUtilitySingletonInstances'] = GeneralUtility::getSingletonInstances(); + $state['contextData'] = clone GeneralUtility::makeInstance(Context::class); + $state['container'] = GeneralUtility::getContainer(); self::$state[] = $state; } @@ -73,14 +93,16 @@ public static function reset(): void unset($GLOBALS['BE_USER']); unset($GLOBALS['TYPO3_REQUEST']); + $generalUtilityReflection = new \ReflectionClass(GeneralUtility::class); // @todo: Remove when v14 compat is dropped, the property no longer exists in v15. if (property_exists(GeneralUtility::class, 'indpEnvCache')) { - $generalUtilityReflection = new \ReflectionClass(GeneralUtility::class); $generalUtilityIndpEnvCache = $generalUtilityReflection->getProperty('indpEnvCache'); $generalUtilityIndpEnvCache->setValue(null, []); } GeneralUtility::resetSingletonInstances([]); + self::overrideContextData(GeneralUtility::makeInstance(Context::class), new Context()); + $generalUtilityReflection->getProperty('container')->setValue(null, null); } /** @@ -88,7 +110,11 @@ public static function reset(): void */ public static function pop(): void { + /** @var ?State $state */ $state = array_pop(self::$state); + if ($state === null) { + throw new \RuntimeException('No state item found in stack to pop.', 1784990840); + } $GLOBALS['_SERVER'] = $state['globals-server']; if ($state['globals-beUser'] !== null) { @@ -109,5 +135,13 @@ public static function pop(): void } GeneralUtility::resetSingletonInstances($state['generalUtilitySingletonInstances']); + self::overrideContextData(GeneralUtility::makeInstance(Context::class), $state['contextData']); + GeneralUtility::setContainer($state['container']); + } + + private static function overrideContextData(Context $context, Context $overrideContext): void + { + $propertyAccessor = new \ReflectionProperty(Context::class, 'aspects'); + $propertyAccessor->setValue($context, $propertyAccessor->getValue($overrideContext)); } }