‪TYPO3CMS  ‪main
BackendModuleValidator.php
Go to the documentation of this file.
1 <?php
2 
3 declare(strict_types=1);
4 
5 /*
6  * This file is part of the TYPO3 CMS project.
7  *
8  * It is free software; you can redistribute it and/or modify it under
9  * the terms of the GNU General Public License, either version 2
10  * of the License, or any later version.
11  *
12  * For the full copyright and license information, please read the
13  * LICENSE.txt file that was distributed with this source code.
14  *
15  * The TYPO3 project - inspiring people to share!
16  */
17 
19 
20 use Psr\Http\Message\ResponseInterface;
21 use Psr\Http\Message\ServerRequestInterface;
22 use Psr\Http\Server\MiddlewareInterface;
23 use Psr\Http\Server\RequestHandlerInterface;
32 use TYPO3\CMS\Backend\Utility\BackendUtility;
41 
48 class ‪BackendModuleValidator implements MiddlewareInterface
49 {
50  public function ‪__construct(
51  protected readonly ‪UriBuilder $uriBuilder,
52  protected readonly ‪ModuleProvider $moduleProvider,
53  protected readonly ‪FlashMessageService $flashMessageService,
54  ) {}
55 
60  public function ‪process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
61  {
63  $route = $request->getAttribute('route');
64  $selectedSubModule = null;
65  $inaccessibleSubModule = null;
66  $ensureToPersistUserSettings = false;
67  $backendUser = ‪$GLOBALS['BE_USER'] ?? null;
68  if (!$backendUser
69  || !$route->hasOption('module')
70  || !(($module = $route->getOption('module')) instanceof ‪ModuleInterface)
71  ) {
72  return $handler->handle($request);
73  }
74 
75  // If on a second level module with further sub modules, jump to the third-level modules
76  // (either the last used or the first in the list) and store this selection for the user.
78  if ($module->getParentModule() && $module->hasSubModules()) {
79  // Note: "action" is a special setting, which is evaluated here individually
80  $subModuleIdentifier = (string)($backendUser->getModuleData($module->getIdentifier())['action'] ?? '');
81  if ($module->hasSubModule($subModuleIdentifier)) {
82  if ($this->moduleProvider->accessGranted($subModuleIdentifier, $backendUser)) {
83  // Use the selected sub module if user has access to it. By checking access here,
84  // we prevent that the user can no longer access the parent module, since it would
85  // always run into the ModuleAccessDeniedException.
86  $selectedSubModule = $module->getSubModule($subModuleIdentifier);
87  } else {
88  // Stored sub module exists but is currently not accessible. Store the
89  // requested module to later inform the user about the forced redirect.
90  $inaccessibleSubModule = $module->getSubModule($subModuleIdentifier);
91  }
92  }
93  if ($selectedSubModule === null) {
94  // Try to fetch the first accessible sub module. We check access here to prevent
95  // that the user can no longer access the parent module, since it would always run
96  // into the ModuleAccessDeniedException.
97  foreach ($module->getSubModules() as $subModule) {
98  if ($this->moduleProvider->accessGranted($subModule->getIdentifier(), $backendUser)) {
99  $selectedSubModule = $subModule;
100  break;
101  }
102  }
103  }
104  if ($selectedSubModule !== null) {
105  // Overwrite the requested module and the route target if an accessible sub module has been found
106  $module = $selectedSubModule;
107  $route->setOptions(array_replace_recursive($route->getOptions(), $module->getDefaultRouteOptions()['_default']));
108  }
109  } elseif (($routeIdentifier = $route->getOption('_identifier')) !== null
110  && $routeIdentifier === $module->getParentModule()?->getIdentifier()
111  ) {
112  // In case the actually requested module is the parent of the actually resolved module,
113  // the parent module does not define a route itself and uses the current third-level module
114  // as fallback. Therefore, we have to check the special "action" key on the "inaccessible"
115  // parent module to still allow rerouting to another (last used) third-level module.
116  $inaccessibleParentModule = $module->getParentModule();
117  $subModuleIdentifier = (string)($backendUser->getModuleData($inaccessibleParentModule->getIdentifier())['action'] ?? '');
118  if ($inaccessibleParentModule->hasSubModule($subModuleIdentifier)) {
119  $module = $inaccessibleParentModule->getSubModule($subModuleIdentifier);
120  $route->setOptions(array_replace_recursive($route->getOptions(), $module->getDefaultRouteOptions()['_default']));
121  }
122  }
123 
124  // Validate the requested module
125  try {
126  $this->‪validateModuleAccess($request, $module);
127  if ($selectedSubModule !== null && $inaccessibleSubModule !== null) {
128  $this->‪enqueueRedirectMessage($inaccessibleSubModule, $selectedSubModule);
129  }
130  } catch (‪ModuleAccessDeniedException $e) {
131  // Since the user might request a module which is just temporarily blocked, e.g. due to workspace
132  // restrictions, do not throw an exception but redirect to the first accessible module - if any.
133  if (($firstAccessibleModule = $this->moduleProvider->getFirstAccessibleModule($backendUser)) !== null) {
134  $this->‪enqueueRedirectMessage($module, $firstAccessibleModule);
135  return new ‪RedirectResponse($this->uriBuilder->buildUriFromRoute($firstAccessibleModule->getIdentifier()));
136  }
137  // User does not have access to any module.. ¯\_(ツ)_/¯
138  throw new ‪NoAccessibleModuleException('You don\'t have access to any module.', 1702480600);
139  }
140 
141  // This module request (which is usually opened inside the list_frame)
142  // has been issued from a toplevel browser window (e.g. a link was opened in a new tab).
143  // Redirect to open the module as frame inside the TYPO3 backend layout.
144  // HEADS UP: This header will only be available in secure connections (https:// or .localhost TLD)
145  if ($request->getHeaderLine('Sec-Fetch-Dest') === 'document') {
146  return new ‪RedirectResponse(
147  $this->uriBuilder->buildUriWithRedirect(
148  'main',
149  [],
150  ‪RouteRedirect::createFromRoute($route, $request->getQueryParams())
151  )
152  );
153  }
154 
155  // Third-level module, make sure to remember the previously selected module in the parent module
156  if ($module->getParentModule()?->getParentModule()) {
157  $parentModuleData = $backendUser->getModuleData($module->getParentIdentifier());
158  if (($parentModuleData['action'] ?? '') !== $module->getIdentifier()) {
159  $parentModuleData['action'] = $module->getIdentifier();
160  $backendUser->pushModuleData($module->getParentIdentifier(), $parentModuleData, true);
161  $ensureToPersistUserSettings = true;
162  }
163  }
164 
165  // Check for module data, send via GET/POST parameters.
166  // Only consider the configured keys from the module configuration.
167  $requestModuleData = [];
168  foreach (array_keys($module->getDefaultModuleData()) as ‪$name) {
169  $newValue = $request->getParsedBody()[‪$name] ?? $request->getQueryParams()[‪$name] ?? null;
170  if ($newValue !== null) {
171  $requestModuleData[‪$name] = $newValue;
172  }
173  }
174 
175  // Get stored module data
176  if (!is_array(($persistedModuleData = $backendUser->getModuleData($module->getIdentifier())))) {
177  $persistedModuleData = [];
178  }
179 
180  // Settings were changed from the request, so they need to get persisted
181  if ($requestModuleData !== []) {
182  $moduleData = ‪ModuleData::createFromModule($module, array_replace_recursive($persistedModuleData, $requestModuleData));
183  $backendUser->pushModuleData($module->getIdentifier(), $moduleData->toArray(), true);
184  $ensureToPersistUserSettings = true;
185  } else {
186  $moduleData = ‪ModuleData::createFromModule($module, $persistedModuleData);
187  }
188 
189  // Add validated module and its data to the current request
190  $request = $request
191  ->withAttribute('module', $module)
192  ->withAttribute('moduleData', $moduleData);
193 
194  $response = $handler->handle($request);
195 
196  if ($ensureToPersistUserSettings) {
197  $backendUser->writeUC();
198  }
199 
200  return $response;
201  }
202 
210  protected function ‪validateModuleAccess(ServerRequestInterface $request, ‪ModuleInterface $module): void
211  {
212  $backendUserAuthentication = ‪$GLOBALS['BE_USER'];
213  if (!$this->moduleProvider->accessGranted($module->‪getIdentifier(), $backendUserAuthentication)) {
214  throw new ‪ModuleAccessDeniedException('You don\'t have access to this module.', 1642450334);
215  }
216 
217  // @todo: This misuses 'id' as a broken convention for pages-uid. The filelist module for instance
218  // uses 'id' as "storage-uid:path", which is only mitigated here by testing the argument
219  // with MU:canBeInterpretedAsInteger().
220  // Also see a similar misuse in extbase BackendConfigurationManager, which does this as well
221  // to guess a pages-uid for TypoScript retrieval.
222  $id = $request->getQueryParams()['id'] ?? $request->getParsedBody()['id'] ?? 0;
223  if (‪MathUtility::canBeInterpretedAsInteger($id) && $id > 0) {
224  $id = (int)$id;
225  $permClause = $backendUserAuthentication->getPagePermsClause(‪Permission::PAGE_SHOW);
226  // Check page access
227  if (!is_array(BackendUtility::readPageAccess($id, $permClause))) {
228  // Check if page has been deleted
229  $deleteField = ‪$GLOBALS['TCA']['pages']['ctrl']['delete'];
230  $pageInfo = BackendUtility::getRecord('pages', $id, $deleteField, $permClause ? ' AND ' . $permClause : '', false);
231  if (!($pageInfo[$deleteField] ?? false)) {
232  throw new \RuntimeException('You don\'t have access to this page', 1289917924);
233  }
234  }
235  }
236  }
237 
238  protected function ‪enqueueRedirectMessage(‪ModuleInterface $requestedModule, ‪ModuleInterface $redirectedModule): void
239  {
240  $languageService = $this->‪getLanguageService();
241  $this->flashMessageService
242  ->getMessageQueueByIdentifier(‪FlashMessageQueue::NOTIFICATION_QUEUE)
243  ->enqueue(
244  new ‪FlashMessage(
245  sprintf(
246  $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang.xlf:module.noAccess.message'),
247  $languageService->sL($redirectedModule->‪getTitle()),
248  $languageService->sL($requestedModule->‪getTitle())
249  ),
250  $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang.xlf:module.noAccess.title'),
251  ContextualFeedbackSeverity::INFO,
252  true
253  )
254  );
255  }
256 
258  {
259  return ‪$GLOBALS['LANG'];
260  }
261 }
‪TYPO3\CMS\Backend\Middleware
Definition: AdditionalResponseHeaders.php:18
‪TYPO3\CMS\Backend\Middleware\BackendModuleValidator\validateModuleAccess
‪validateModuleAccess(ServerRequestInterface $request, ModuleInterface $module)
Definition: BackendModuleValidator.php:210
‪TYPO3\CMS\Backend\Exception\NoAccessibleModuleException
Definition: NoAccessibleModuleException.php:25
‪TYPO3\CMS\Backend\Module\ModuleData
Definition: ModuleData.php:30
‪TYPO3\CMS\Backend\Module\ModuleData\createFromModule
‪static createFromModule(ModuleInterface $module, array $data)
Definition: ModuleData.php:42
‪TYPO3\CMS\Backend\Routing\Route
Definition: Route.php:24
‪TYPO3\CMS\Backend\Module\ModuleProvider
Definition: ModuleProvider.php:29
‪TYPO3\CMS\Backend\Middleware\BackendModuleValidator\getLanguageService
‪getLanguageService()
Definition: BackendModuleValidator.php:257
‪TYPO3\CMS\Core\Type\Bitmask\Permission
Definition: Permission.php:26
‪TYPO3\CMS\Backend\Module\ModuleInterface\getTitle
‪getTitle()
‪TYPO3\CMS\Core\Type\ContextualFeedbackSeverity
‪ContextualFeedbackSeverity
Definition: ContextualFeedbackSeverity.php:25
‪TYPO3\CMS\Core\Utility\MathUtility\canBeInterpretedAsInteger
‪static bool canBeInterpretedAsInteger(mixed $var)
Definition: MathUtility.php:74
‪TYPO3\CMS\Backend\Module\ModuleInterface\getIdentifier
‪getIdentifier()
‪TYPO3\CMS\Backend\Exception\ModuleAccessDeniedException
Definition: ModuleAccessDeniedException.php:25
‪TYPO3\CMS\Core\Messaging\FlashMessageQueue\NOTIFICATION_QUEUE
‪const NOTIFICATION_QUEUE
Definition: FlashMessageQueue.php:31
‪TYPO3\CMS\Backend\Middleware\BackendModuleValidator\enqueueRedirectMessage
‪enqueueRedirectMessage(ModuleInterface $requestedModule, ModuleInterface $redirectedModule)
Definition: BackendModuleValidator.php:238
‪TYPO3\CMS\Backend\Middleware\BackendModuleValidator\__construct
‪__construct(protected readonly UriBuilder $uriBuilder, protected readonly ModuleProvider $moduleProvider, protected readonly FlashMessageService $flashMessageService,)
Definition: BackendModuleValidator.php:50
‪TYPO3\CMS\Backend\Middleware\BackendModuleValidator\process
‪process(ServerRequestInterface $request, RequestHandlerInterface $handler)
Definition: BackendModuleValidator.php:60
‪TYPO3\CMS\Backend\Routing\RouteRedirect
Definition: RouteRedirect.php:30
‪TYPO3\CMS\Backend\Routing\UriBuilder
Definition: UriBuilder.php:44
‪$name
‪$name
Definition: phpIntegrityChecker.php:235
‪TYPO3\CMS\Backend\Module\ModuleInterface
Definition: ModuleInterface.php:24
‪TYPO3\CMS\Core\Type\Bitmask\Permission\PAGE_SHOW
‪const PAGE_SHOW
Definition: Permission.php:35
‪TYPO3\CMS\Core\Http\RedirectResponse
Definition: RedirectResponse.php:30
‪TYPO3\CMS\Core\Messaging\FlashMessage
Definition: FlashMessage.php:27
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Core\Utility\MathUtility
Definition: MathUtility.php:24
‪TYPO3\CMS\Backend\Middleware\BackendModuleValidator
Definition: BackendModuleValidator.php:49
‪TYPO3\CMS\Backend\Routing\RouteRedirect\createFromRoute
‪static createFromRoute(Route $route, array $parameters)
Definition: RouteRedirect.php:53
‪TYPO3\CMS\Core\Localization\LanguageService
Definition: LanguageService.php:46
‪TYPO3\CMS\Core\Messaging\FlashMessageQueue
Definition: FlashMessageQueue.php:29
‪TYPO3\CMS\Core\Messaging\FlashMessageService
Definition: FlashMessageService.php:27