Skip to content

Commit c07f245

Browse files
committed
[!!!][TASK] Stop guessing pid in extbase BackendConfigurationManager
Stop guessing a page in extbase BackendConfigurationmanager with extbase based backend modules not in page context. Resolves: #105728 Related: #105719 Related: #105602 Related: #84861 Releases: main Change-Id: Ib5e5584bae4faa6e922810d6cce50689b6a1f36d Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/87288 Tested-by: Christian Kuhn <lolli@schwarzbu.ch> Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch> Tested-by: core-ci <typo3@b13.com> Reviewed-by: Stefan Bürk <stefan@buerk.tech> Tested-by: Anja Leichsenring <aleichsenring@ab-softlab.de> Reviewed-by: Markus Klein <markus.klein@typo3.org> Tested-by: Stefan Bürk <stefan@buerk.tech> Reviewed-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
1 parent 68824b1 commit c07f245

File tree

3 files changed

+116
-176
lines changed

3 files changed

+116
-176
lines changed
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
.. include:: /Includes.rst.txt
2+
3+
.. _breaking-105728-1732882067:
4+
5+
==============================================================================================
6+
Breaking: #105728 - Extbase backend modules not in page context rely on global TypoScript only
7+
==============================================================================================
8+
9+
See :issue:`105728`
10+
11+
Description
12+
===========
13+
14+
Configuration of extbase based backend modules can be done using frontend TypoScript.
15+
16+
The standard prefix in TypoScript to do this is :typoscript:`module.tx_myextension`, extbase backend
17+
module controllers can typically retrieve their configuration using a call like
18+
:php:`$configuration = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_SETTINGS, 'myextension');`.
19+
20+
TypoScript itself is always bound to a page: In the frontend, there must be either some rootline page with
21+
a sys_template record, or a page that has a site set. The frontend rendering will otherwise bail out with
22+
an error message.
23+
24+
Extbase based backend modules are sometimes bound to pages as well: They can have a rendered page tree
25+
configured by their module configuration, and then receive the selected page uid within the request as GET parameter :php:`id`.
26+
27+
Other extbase based backend modules however are not within page scope and do not render the page tree. Examples of such
28+
modules within the core are the backend modules delivered by the `form` and `beuser` extension.
29+
30+
Such extbase based backend without page tree modules had a hard time to calculate their relevant frontend TypoScript
31+
based configuration: Since TypoScript is bound to pages, they looked for "the first" valid page in the page
32+
tree, and the first valid sys_template record to calculate their TypoScript configuration. This dependency
33+
and guesswork made final configuration of extbase backend module configuration not in page context brittle,
34+
intransparent and clumsy.
35+
36+
TYPO3 v14 puts an end to this: Extbase backend modules without page context compile their TypoScript configuration
37+
from "global" TypoScript only and stop calculating TypoScript from guessing "the first valid" page.
38+
39+
The key call to register such "global" TypoScript is :php:`ExtensionManagementUtility::addTypoScriptSetup()` in
40+
:file:`ext_localconf.php` files.
41+
42+
43+
Impact
44+
======
45+
46+
Configuration of extbase based backend modules may change if their configuration is defined by the first
47+
given valid page in the page tree. Configuration of such backend modules can no longer be changed by including
48+
TypoScript on the "first valid" page.
49+
50+
51+
Affected installations
52+
======================
53+
54+
Instances with extbase based backend modules without page tree may be affected.
55+
56+
57+
Migration
58+
=========
59+
60+
Configuration of extbase based backend modules without page tree must be supplied programmatically
61+
and "made global" by extending :php:`$GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_setup']`
62+
using :php:`ExtensionManagementUtility::addTypoScriptSetup()` within extensions
63+
:file:`ext_localconf.php` files. The backend module of the `form` extension is a good example.
64+
Additional locations of extensions that deliver form `yaml` definitions are defined like this:
65+
66+
.. code-block:: php
67+
:caption: EXT:my_extension/ext_localconf.php
68+
69+
ExtensionManagementUtility::addTypoScriptSetup('
70+
module.tx_form {
71+
settings {
72+
yamlConfigurations {
73+
1732884807 = EXT:my_extension/Configuration/Yaml/FormSetup.yaml
74+
}
75+
}
76+
}
77+
');
78+
79+
Note it is also possible to use :php:`ExtensionManagementUtility::addTypoScriptConstants()`
80+
to declare "global" TypoScrip constants, and to use them in above TypoScript.
81+
82+
83+
.. index:: Backend, PHP-API, TypoScript, NotScanned, ext:extbase

typo3/sysext/extbase/Classes/Configuration/BackendConfigurationManager.php

Lines changed: 29 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
use TYPO3\CMS\Core\Database\ConnectionPool;
2727
use TYPO3\CMS\Core\Database\Query\QueryHelper;
2828
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
29-
use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction;
3029
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
3130
use TYPO3\CMS\Core\Site\Entity\NullSite;
3231
use TYPO3\CMS\Core\Site\Entity\Site;
@@ -54,17 +53,6 @@
5453
* pages in the first place - they may not have a page tree und thus no page id at all, like
5554
* for instance the ext:beuser module.
5655
*
57-
* Unfortunately, extbase *still* has to calculate *some* TypoScript in any case, even if there
58-
* is no page id at all: The default configuration of extbase Backend modules is the "module."
59-
* TypoScript setup top-level key. The base config of this is delivered by extbase extensions that
60-
* have Backend Modules using ext_typoscript_setup.typoscript, and/or via TYPO3_CONF_VARS TypoScript
61-
* setup defaults. Those have to be loaded in any case, even if there is no page at all in the page
62-
* tree.
63-
*
64-
* The code thus has to hop through quite some loops to "find" some relevant page id it can guess
65-
* if none is incoming from the request. It even fakes a default sys_template row to trigger
66-
* TypoScript loading of globals and ext_typoscript_setup.typoscript if it couldn't find anything.
67-
*
6856
* @internal only to be used within Extbase, not part of TYPO3 Core API.
6957
*/
7058
final readonly class BackendConfigurationManager
@@ -99,9 +87,14 @@ public function getConfiguration(ServerRequestInterface $request, array $configu
9987
$pluginNameFromConfig = $configuration['pluginName'] ?? null;
10088
$configuration = $this->typoScriptService->convertTypoScriptArrayToPlainArray($configuration);
10189

102-
$frameworkConfiguration = $this->getExtbaseConfiguration($request);
90+
$typoscriptSetup = $this->getTypoScriptSetup($request);
91+
$frameworkConfiguration = [];
92+
if (isset($typoscriptSetup['config.']['tx_extbase.'])) {
93+
$frameworkConfiguration = $this->typoScriptService->convertTypoScriptArrayToPlainArray($typoscriptSetup['config.']['tx_extbase.']);
94+
}
95+
10396
if (!isset($frameworkConfiguration['persistence']['storagePid'])) {
104-
$currentPageId = $this->getCachedCurrentPageId($request);
97+
$currentPageId = $this->getCurrentPageId($request);
10598
$frameworkConfiguration['persistence']['storagePid'] = $currentPageId;
10699
}
107100
// only merge $configuration and override controller configuration when retrieving configuration of the current plugin
@@ -145,7 +138,7 @@ public function getConfiguration(ServerRequestInterface $request, array $configu
145138
*/
146139
public function getTypoScriptSetup(ServerRequestInterface $request): array
147140
{
148-
$currentPageId = $this->getCachedCurrentPageId($request);
141+
$currentPageId = $this->getCurrentPageId($request);
149142

150143
$cacheIdentifier = 'extbase-backend-typoscript-pageId-' . $currentPageId;
151144
$setupArray = $this->runtimeCache->get($cacheIdentifier);
@@ -174,34 +167,33 @@ public function getTypoScriptSetup(ServerRequestInterface $request): array
174167

175168
$rootLine = [];
176169
$sysTemplateRows = [];
177-
$sysTemplateFakeRow = [
178-
'uid' => 0,
179-
'pid' => 0,
180-
'title' => 'Fake sys_template row to force extension statics loading',
181-
'root' => 1,
182-
'clear' => 3,
183-
'include_static_file' => '',
184-
'basedOn' => '',
185-
'includeStaticAfterBasedOn' => 0,
186-
'static_file_mode' => false,
187-
'constants' => '',
188-
'config' => '',
189-
'deleted' => 0,
190-
'hidden' => 0,
191-
'starttime' => 0,
192-
'endtime' => 0,
193-
'sorting' => 0,
194-
];
195170
if ($currentPageId > 0) {
196171
$rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $currentPageId)->get();
197172
$sysTemplateRows = $this->sysTemplateRepository->getSysTemplateRowsByRootline($rootLine, $request);
198173
ksort($rootLine);
199174
}
200-
201175
$sets = $site instanceof Site ? $this->setRegistry->getSets(...$site->getSets()) : [];
202176
if (empty($sysTemplateRows) && $sets === []) {
203-
// If there is no page (pid 0 only), or if the first 'is_siteroot' site has no sys_template record or assigned site sets,
204-
// then we "fake" a sys_template row: This triggers inclusion of 'global' and 'extension static' TypoScript.
177+
// If no page with sys_template rows or site sets could be derived, we
178+
// "fake" a row to trigger inclusion of 'global' TypoScript only.
179+
$sysTemplateFakeRow = [
180+
'uid' => 0,
181+
'pid' => 0,
182+
'title' => 'Fake sys_template row to force global TypoScript loading',
183+
'root' => 1,
184+
'clear' => 3,
185+
'include_static_file' => '',
186+
'basedOn' => '',
187+
'includeStaticAfterBasedOn' => 0,
188+
'static_file_mode' => false,
189+
'constants' => '',
190+
'config' => '',
191+
'deleted' => 0,
192+
'hidden' => 0,
193+
'starttime' => 0,
194+
'endtime' => 0,
195+
'sorting' => 0,
196+
];
205197
$sysTemplateRows[] = $sysTemplateFakeRow;
206198
}
207199

@@ -220,19 +212,6 @@ public function getTypoScriptSetup(ServerRequestInterface $request): array
220212
return $setupArray;
221213
}
222214

223-
/**
224-
* Returns the TypoScript configuration found in config.tx_extbase
225-
*/
226-
private function getExtbaseConfiguration(ServerRequestInterface $request): array
227-
{
228-
$setup = $this->getTypoScriptSetup($request);
229-
$extbaseConfiguration = [];
230-
if (isset($setup['config.']['tx_extbase.'])) {
231-
$extbaseConfiguration = $this->typoScriptService->convertTypoScriptArrayToPlainArray($setup['config.']['tx_extbase.']);
232-
}
233-
return $extbaseConfiguration;
234-
}
235-
236215
/**
237216
* Returns the TypoScript configuration found in module.tx_yourextension_yourmodule
238217
* merged with the global configuration of your extension from module.tx_yourextension
@@ -256,39 +235,10 @@ private function getPluginConfiguration(ServerRequestInterface $request, string
256235
return $pluginConfiguration;
257236
}
258237

259-
private function getCachedCurrentPageId(ServerRequestInterface $request): int
260-
{
261-
$currentPageId = $this->runtimeCache->get('extbase-backend-typoscript-currentPageId');
262-
if (!is_int($currentPageId)) {
263-
$currentPageId = $this->getCurrentPageId($request);
264-
$this->runtimeCache->set('extbase-backend-typoscript-currentPageId', $currentPageId);
265-
}
266-
return $currentPageId;
267-
}
268-
269238
/**
270-
* The full madness to guess a page id:
271-
* - First try to get one from the request, accessing POST / GET 'id'
272-
* - else, fetch the first page in page tree that has 'is_siteroot' set
273-
* - else, fetch the first sys_template record that has 'root' flag set, and use its pid
274-
* - else, 0, indicating "there are no 'is_siteroot' pages and no sys_template 'root' records"
275-
*
276-
* @return int current page id. If no page is selected current root page id is returned
239+
* Get page id from the request, accessing POST / GET 'id'
277240
*/
278241
private function getCurrentPageId(ServerRequestInterface $request): int
279-
{
280-
$currentPageId = $this->getCurrentPageIdFromRequest($request);
281-
$currentPageId = $currentPageId ?: $this->getCurrentPageIdFromCurrentSiteRoot();
282-
$currentPageId = $currentPageId ?: $this->getCurrentPageIdFromRootTemplate();
283-
return $currentPageId ?: 0;
284-
}
285-
286-
/**
287-
* Gets the current page ID from the GET/POST data.
288-
*
289-
* @return int the page UID, will be 0 if none has been set
290-
*/
291-
private function getCurrentPageIdFromRequest(ServerRequestInterface $request): int
292242
{
293243
// @todo: This misuses 'id' as a broken convention for pages-uid. The filelist module for instance
294244
// uses 'id' as "storage-uid:path", which is only mitigated here by testing the argument
@@ -302,64 +252,6 @@ private function getCurrentPageIdFromRequest(ServerRequestInterface $request): i
302252
return $id;
303253
}
304254

305-
/**
306-
* Gets the current page ID from the first site root in tree.
307-
*
308-
* @return int the page UID, will be 0 if none has been set
309-
*/
310-
private function getCurrentPageIdFromCurrentSiteRoot(): int
311-
{
312-
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages');
313-
$queryBuilder->getRestrictions()->removeAll()
314-
->add(GeneralUtility::makeInstance(DeletedRestriction::class))
315-
->add(GeneralUtility::makeInstance(HiddenRestriction::class));
316-
$rootPage = $queryBuilder
317-
->select('uid')
318-
->from('pages')
319-
->where(
320-
$queryBuilder->expr()->eq('is_siteroot', $queryBuilder->createNamedParameter(1, Connection::PARAM_INT)),
321-
$queryBuilder->expr()->eq('sys_language_uid', $queryBuilder->createNamedParameter(0, Connection::PARAM_INT)),
322-
// Only consider live root page IDs, never return a versioned root page ID
323-
$queryBuilder->expr()->eq('t3ver_oid', $queryBuilder->createNamedParameter(0, Connection::PARAM_INT)),
324-
$queryBuilder->expr()->eq('t3ver_wsid', $queryBuilder->createNamedParameter(0, Connection::PARAM_INT))
325-
)
326-
->orderBy('sorting')
327-
->setMaxResults(1)
328-
->executeQuery()
329-
->fetchAssociative();
330-
if (empty($rootPage)) {
331-
return 0;
332-
}
333-
return (int)$rootPage['uid'];
334-
}
335-
336-
/**
337-
* Gets the current page ID from the first created root template.
338-
*
339-
* @return int the page UID, will be 0 if none has been set
340-
*/
341-
private function getCurrentPageIdFromRootTemplate(): int
342-
{
343-
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_template');
344-
$queryBuilder->getRestrictions()->removeAll()
345-
->add(GeneralUtility::makeInstance(DeletedRestriction::class))
346-
->add(GeneralUtility::makeInstance(HiddenRestriction::class));
347-
$rootTemplate = $queryBuilder
348-
->select('pid')
349-
->from('sys_template')
350-
->where(
351-
$queryBuilder->expr()->eq('root', $queryBuilder->createNamedParameter(1, Connection::PARAM_INT))
352-
)
353-
->orderBy('crdate')
354-
->setMaxResults(1)
355-
->executeQuery()
356-
->fetchAssociative();
357-
if (empty($rootTemplate)) {
358-
return 0;
359-
}
360-
return (int)$rootTemplate['pid'];
361-
}
362-
363255
/**
364256
* Returns an array of storagePIDs that are below a list of storage pids.
365257
*
@@ -390,7 +282,6 @@ private function getRecursiveStoragePids(array $storagePids, int $recursionDepth
390282
/**
391283
* Recursively fetch all children of a given page
392284
*
393-
* @param int $pid uid of the page
394285
* @return int[] List of child row $uid's
395286
*/
396287
private function getPageChildrenRecursive(int $pid, int $depth, int $begin, string $permsClause): array

typo3/sysext/extbase/Tests/Functional/Configuration/BackendConfigurationManagerTest.php

Lines changed: 4 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919

2020
use PHPUnit\Framework\Attributes\Test;
2121
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
22-
use TYPO3\CMS\Core\Database\ConnectionPool;
2322
use TYPO3\CMS\Core\Http\ServerRequest;
2423
use TYPO3\CMS\Extbase\Configuration\BackendConfigurationManager;
2524
use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
@@ -54,7 +53,8 @@ public function getConfigurationRecursivelyMergesCurrentExtensionConfigurationWi
5453
'throwPageNotFoundExceptionIfActionCantBeResolved' => '0',
5554
],
5655
];
57-
self::assertEquals($expectedResult, $subject->getConfiguration(new ServerRequest(), [], 'CurrentExtensionName'));
56+
$request = (new ServerRequest())->withQueryParams(['id' => 1]);
57+
self::assertEquals($expectedResult, $subject->getConfiguration($request, [], 'CurrentExtensionName'));
5858
}
5959

6060
#[Test]
@@ -85,7 +85,8 @@ public function getConfigurationRecursivelyMergesCurrentPluginConfigurationWithF
8585
'throwPageNotFoundExceptionIfActionCantBeResolved' => '0',
8686
],
8787
];
88-
self::assertEquals($expectedResult, $subject->getConfiguration(new ServerRequest(), [], 'CurrentExtensionName', 'CurrentPluginName'));
88+
$request = (new ServerRequest())->withQueryParams(['id' => 1]);
89+
self::assertEquals($expectedResult, $subject->getConfiguration($request, [], 'CurrentExtensionName', 'CurrentPluginName'));
8990
}
9091

9192
#[Test]
@@ -108,41 +109,6 @@ public function getCurrentPageIdReturnsPageIdFromPost(): void
108109
self::assertEquals(321, $actualResult);
109110
}
110111

111-
#[Test]
112-
public function getCurrentPageIdReturnsPidFromFirstRootTemplateIfIdIsNotSetAndNoRootPageWasFound(): void
113-
{
114-
(new ConnectionPool())->getConnectionForTable('sys_template')->insert(
115-
'sys_template',
116-
[
117-
'pid' => 123,
118-
'deleted' => 0,
119-
'hidden' => 0,
120-
'root' => 1,
121-
]
122-
);
123-
$subject = $this->get(BackendConfigurationManager::class);
124-
$getCurrentPageIdReflectionMethod = (new \ReflectionMethod($subject, 'getCurrentPageId'));
125-
$actualResult = $getCurrentPageIdReflectionMethod->invoke($subject, new ServerRequest());
126-
self::assertEquals(123, $actualResult);
127-
}
128-
129-
#[Test]
130-
public function getCurrentPageIdReturnsUidFromFirstRootPageIfIdIsNotSet(): void
131-
{
132-
(new ConnectionPool())->getConnectionForTable('pages')->insert(
133-
'pages',
134-
[
135-
'deleted' => 0,
136-
'hidden' => 0,
137-
'is_siteroot' => 1,
138-
]
139-
);
140-
$subject = $this->get(BackendConfigurationManager::class);
141-
$getCurrentPageIdReflectionMethod = (new \ReflectionMethod($subject, 'getCurrentPageId'));
142-
$actualResult = $getCurrentPageIdReflectionMethod->invoke($subject, new ServerRequest());
143-
self::assertEquals(1, $actualResult);
144-
}
145-
146112
#[Test]
147113
public function getCurrentPageIdReturnsDefaultStoragePidIfIdIsNotSetNoRootTemplateAndRootPageWasFound(): void
148114
{

0 commit comments

Comments
 (0)