getAttribute('route'); $selectedSubModule = null; $inaccessibleSubModule = null; $ensureToPersistUserSettings = false; $backendUser = $GLOBALS['BE_USER'] ?? null; if (!$backendUser) { return $handler->handle($request); } // Exit if access to module was denied using module access inheritance check $inheritAccessFromModule = $route->getOption('inheritAccessFromModule'); if ($inheritAccessFromModule !== null && !$this->moduleProvider->accessGranted($inheritAccessFromModule, $backendUser)) { return new Response(null, 403); } $module = $route->getOption('module'); if (!$module instanceof ModuleInterface) { return $handler->handle($request); } // If on a second level module with further sub modules, jump to the third-level modules // (either the last used or the first in the list) and store this selection for the user. // Skip this automatic redirection if the module should show a submodule overview instead. if ($module->getParentModule() && $module->hasSubModules() && !$module->hasSubmoduleOverview()) { // Note: "action" is a special setting, which is evaluated here individually $subModuleIdentifier = (string)($backendUser->getModuleData($module->getIdentifier())['action'] ?? ''); if ($module->hasSubModule($subModuleIdentifier)) { if ($this->moduleProvider->accessGranted($subModuleIdentifier, $backendUser)) { // Use the selected sub module if user has access to it. By checking access here, // we prevent that the user can no longer access the parent module, since it would // always run into the ModuleAccessDeniedException. $selectedSubModule = $module->getSubModule($subModuleIdentifier); } else { // Stored sub module exists but is currently not accessible. Store the // requested module to later inform the user about the forced redirect. $inaccessibleSubModule = $module->getSubModule($subModuleIdentifier); } } if ($selectedSubModule === null) { // Try to fetch the first accessible sub module. We check access here to prevent // that the user can no longer access the parent module, since it would always run // into the ModuleAccessDeniedException. foreach ($module->getSubModules() as $subModule) { if ($this->moduleProvider->accessGranted($subModule->getIdentifier(), $backendUser)) { $selectedSubModule = $subModule; break; } } } if ($selectedSubModule !== null) { // Overwrite the requested module and the route target if an accessible sub module has been found $module = $selectedSubModule; $route->setOptions(array_replace_recursive($route->getOptions(), $module->getDefaultRouteOptions()['_default'])); } } elseif (($routeIdentifier = $route->getOption('_identifier')) !== null && $routeIdentifier === $module->getParentModule()?->getIdentifier() ) { // In case the actually requested module is the parent of the actually resolved module, // the parent module does not define a route itself and uses the current third-level module // as fallback. Therefore, we have to check the special "action" key on the "inaccessible" // parent module to still allow rerouting to another (last used) third-level module. $inaccessibleParentModule = $module->getParentModule(); $subModuleIdentifier = (string)($backendUser->getModuleData($inaccessibleParentModule->getIdentifier())['action'] ?? ''); if ($inaccessibleParentModule->hasSubModule($subModuleIdentifier)) { $module = $inaccessibleParentModule->getSubModule($subModuleIdentifier); $route->setOptions(array_replace_recursive($route->getOptions(), $module->getDefaultRouteOptions()['_default'])); } } // Validate the requested module try { $this->validateModuleAccess($request, $module); if ($selectedSubModule !== null && $inaccessibleSubModule !== null) { $this->enqueueRedirectMessage($inaccessibleSubModule, $selectedSubModule); } } catch (ModuleAccessDeniedException $e) { // Since the user might request a module which is just temporarily blocked, e.g. due to workspace // restrictions, do not throw an exception but redirect to the first accessible module - if any. if (($firstAccessibleModule = $this->moduleProvider->getFirstAccessibleModule($backendUser)) !== null) { $this->enqueueRedirectMessage($module, $firstAccessibleModule); return new RedirectResponse($this->uriBuilder->buildUriFromRoute($firstAccessibleModule->getIdentifier())); } // User does not have access to any module.. ¯\_(ツ)_/¯ throw new NoAccessibleModuleException('You don\'t have access to any module.', 1702480600); } // This module request (which is usually opened inside the list_frame) // has been issued from a toplevel browser window (e.g. a link was opened in a new tab). // Redirect to open the module as frame inside the TYPO3 backend layout. // HEADS UP: This header will only be available in secure connections (https:// or .localhost TLD) if ($request->getHeaderLine('Sec-Fetch-Dest') === 'document') { return new RedirectResponse( $this->uriBuilder->buildUriWithRedirect( 'main', [], RouteRedirect::createFromRoute($route, $request->getQueryParams()) ) ); } // Third-level module, make sure to remember the previously selected module in the parent module if ($module->getParentModule()?->getParentModule()) { $parentModuleData = $backendUser->getModuleData($module->getParentIdentifier()); if (($parentModuleData['action'] ?? '') !== $module->getIdentifier()) { $parentModuleData['action'] = $module->getIdentifier(); $backendUser->pushModuleData($module->getParentIdentifier(), $parentModuleData, true); $ensureToPersistUserSettings = true; } } // Check for module data, send via GET/POST parameters. // Only consider the configured keys from the module configuration. $requestModuleData = []; foreach (array_keys($module->getDefaultModuleData()) as $name) { $newValue = $request->getParsedBody()[$name] ?? $request->getQueryParams()[$name] ?? null; if ($newValue !== null) { $requestModuleData[$name] = $newValue; } } // Get stored module data if (!is_array(($persistedModuleData = $backendUser->getModuleData($module->getIdentifier())))) { $persistedModuleData = []; } // Settings were changed from the request, so they need to get persisted if ($requestModuleData !== []) { $moduleData = ModuleData::createFromModule($module, array_replace_recursive($persistedModuleData, $requestModuleData)); $backendUser->pushModuleData($module->getIdentifier(), $moduleData->toArray(), true); $ensureToPersistUserSettings = true; } else { $moduleData = ModuleData::createFromModule($module, $persistedModuleData); } // Add validated module and its data to the current request $request = $request ->withAttribute('module', $module) ->withAttribute('moduleData', $moduleData); $response = $handler->handle($request); if ($ensureToPersistUserSettings) { $backendUser->writeUC(); } return $response; } /** * Checks whether the current user is allowed to access the requested module. Does * also evaluate page access permissions, in case an "id" is given in the request. * * @throws ModuleAccessDeniedException * @throws \RuntimeException */ protected function validateModuleAccess(ServerRequestInterface $request, ModuleInterface $module): void { $backendUserAuthentication = $GLOBALS['BE_USER']; if (!$this->moduleProvider->accessGranted($module->getIdentifier(), $backendUserAuthentication)) { throw new ModuleAccessDeniedException('You don\'t have access to this module.', 1642450334); } // @todo: This misuses 'id' as a broken convention for pages-uid. The filelist module for instance // uses 'id' as "storage-uid:path", which is only mitigated here by testing the argument // with MU:canBeInterpretedAsInteger(). // Also see a similar misuse in extbase BackendConfigurationManager, which does this as well // to guess a pages-uid for TypoScript retrieval. $id = $request->getQueryParams()['id'] ?? $request->getParsedBody()['id'] ?? 0; if (MathUtility::canBeInterpretedAsInteger($id) && $id > 0) { $id = (int)$id; $permClause = $backendUserAuthentication->getPagePermsClause(Permission::PAGE_SHOW); // Check page access if (!is_array(BackendUtility::readPageAccess($id, $permClause))) { // Check if page has been deleted if (!$this->tcaSchemaFactory->has('pages')) { throw new \RuntimeException('You don\'t have access to this page', 1289918924); } $schema = $this->tcaSchemaFactory->get('pages'); if (!$schema->hasCapability(TcaSchemaCapability::SoftDelete)) { throw new \RuntimeException('You don\'t have access to this page', 1289919924); } $deleteField = $schema->getCapability(TcaSchemaCapability::SoftDelete)->getFieldName(); $pageInfo = BackendUtility::getRecord('pages', $id, $deleteField, $permClause ? ' AND ' . $permClause : '', false); if (!($pageInfo[$deleteField] ?? false)) { throw new \RuntimeException('You don\'t have access to this page', 1289917924); } } } } protected function enqueueRedirectMessage(ModuleInterface $requestedModule, ModuleInterface $redirectedModule): void { $languageService = $this->getLanguageService(); $this->flashMessageService ->getMessageQueueByIdentifier(FlashMessageQueue::NOTIFICATION_QUEUE) ->enqueue( new FlashMessage( sprintf( $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang.xlf:module.noAccess.message'), $languageService->sL($redirectedModule->getTitle()), $languageService->sL($requestedModule->getTitle()) ), $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang.xlf:module.noAccess.title'), ContextualFeedbackSeverity::INFO, true ) ); } protected function getLanguageService(): LanguageService { return $GLOBALS['LANG']; } }