Skip to content

Commit f743bc5

Browse files
committed
[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 clones the current Context and container, 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 https://review.typo3.org/c/Packages/TYPO3.CMS/+/94931 describes the very same leak and works around it in the affected test case by resetting the language aspect by hand. That part of the workaround becomes obsolete with this patch. 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
1 parent 9724b52 commit f743bc5

1 file changed

Lines changed: 35 additions & 1 deletion

File tree

Classes/Core/Functional/Framework/FrameworkState.php

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@
1717
* The TYPO3 project - inspiring people to share!
1818
*/
1919

20+
use Psr\Container\ContainerInterface;
21+
use Psr\Http\Message\ServerRequestInterface;
22+
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
23+
use TYPO3\CMS\Core\Context\Context;
24+
use TYPO3\CMS\Core\SingletonInterface;
2025
use TYPO3\CMS\Core\Utility\GeneralUtility;
2126

2227
/**
@@ -29,6 +34,18 @@
2934
*
3035
* This class should not be needed. It is a manifest of technical core debt.
3136
* It should shrink over time and vanish altogether in the end.
37+
*
38+
* @phpstan-type State array{
39+
* globals-server: array<string, mixed>,
40+
* globals-beUser: BackendUserAuthentication|null,
41+
* globals-typo3-conf-vars: array<string, mixed>|null,
42+
* globals-tca: array<string, mixed>,
43+
* request: ServerRequestInterface|null,
44+
* generalUtilityIndpEnvCache?: array<string, string|bool|array<string, string|bool|null>|null>,
45+
* generalUtilitySingletonInstances: array<class-string, SingletonInterface>,
46+
* contextData: Context,
47+
* container: ContainerInterface,
48+
* }
3249
*/
3350
class FrameworkState
3451
{
@@ -39,6 +56,7 @@ class FrameworkState
3956
*/
4057
public static function push(): void
4158
{
59+
/** @var State $state */
4260
$state = [];
4361
$state['globals-server'] = $GLOBALS['_SERVER'];
4462
$state['globals-beUser'] = $GLOBALS['BE_USER'] ?? null;
@@ -61,6 +79,8 @@ public static function push(): void
6179
}
6280

6381
$state['generalUtilitySingletonInstances'] = GeneralUtility::getSingletonInstances();
82+
$state['contextData'] = clone GeneralUtility::makeInstance(Context::class);
83+
$state['container'] = GeneralUtility::getContainer();
6484

6585
self::$state[] = $state;
6686
}
@@ -73,22 +93,28 @@ public static function reset(): void
7393
unset($GLOBALS['BE_USER']);
7494
unset($GLOBALS['TYPO3_REQUEST']);
7595

96+
$generalUtilityReflection = new \ReflectionClass(GeneralUtility::class);
7697
// @todo: Remove when v14 compat is dropped, the property no longer exists in v15.
7798
if (property_exists(GeneralUtility::class, 'indpEnvCache')) {
78-
$generalUtilityReflection = new \ReflectionClass(GeneralUtility::class);
7999
$generalUtilityIndpEnvCache = $generalUtilityReflection->getProperty('indpEnvCache');
80100
$generalUtilityIndpEnvCache->setValue(null, []);
81101
}
82102

83103
GeneralUtility::resetSingletonInstances([]);
104+
self::overrideContextData(GeneralUtility::makeInstance(Context::class), new Context());
105+
$generalUtilityReflection->getProperty('container')->setValue(null, null);
84106
}
85107

86108
/**
87109
* Pop state from stash and apply again to set state back to 'before frontend call'
88110
*/
89111
public static function pop(): void
90112
{
113+
/** @var ?State $state */
91114
$state = array_pop(self::$state);
115+
if ($state === null) {
116+
throw new \RuntimeException('No state item found in stack to pop.', 1784990840);
117+
}
92118

93119
$GLOBALS['_SERVER'] = $state['globals-server'];
94120
if ($state['globals-beUser'] !== null) {
@@ -109,5 +135,13 @@ public static function pop(): void
109135
}
110136

111137
GeneralUtility::resetSingletonInstances($state['generalUtilitySingletonInstances']);
138+
self::overrideContextData(GeneralUtility::makeInstance(Context::class), $state['contextData']);
139+
GeneralUtility::setContainer($state['container']);
140+
}
141+
142+
private static function overrideContextData(Context $context, Context $overrideContext): void
143+
{
144+
$propertyAccessor = new \ReflectionProperty(Context::class, 'aspects');
145+
$propertyAccessor->setValue($context, $propertyAccessor->getValue($overrideContext));
112146
}
113147
}

0 commit comments

Comments
 (0)