From ae18caad69fee5920adeb82d13ba98ba58cadf24 Mon Sep 17 00:00:00 2001
From: Matthias Vogel <typo3@kanti.de>
Date: Sat, 9 Sep 2017 13:04:20 +0200
Subject: [PATCH] [!!!][TASK] Migrate modules to regular backend routing

This patch removes the separate request handler for backend modules,
which was accessed via "&M=moduleName" GET parameter. This is now
migrated into the RouteDispatcher which can dispatch modules as well.

Now, modules are called via the "&route" parameter like all other routes.

Additionally, the requested URLs for modules were requested with the additional
"moduleToken" which is now called "token".

This way, special treatment for modules when dispatching is removed,
however the security checks are still in place so this is kept as is.

All places where URLs are generated can now still be accessed via
`BackendUtility::getModuleUrl()` which can deal with routes, module names
and routePaths (from the URL) to keep backwards-compatibility.

Next Steps:
- Migration wizard for bookmarks + Streamline bookmarks code (see todos)
- Check what needs to be added in ExtensionManagementUtility
- Introduce slugs in routes for BE, e.g. /file-edit/{fileId}/ and /module/page/view/{id}
- Document reserved GET parameters "id", "route" and "token"
- Cleanup usage of determineScriptId and getModuleUrl to use new API

Resolves: #82406
Releases: master
Change-Id: If11c3d5289e14bc9ea766468b8e94cce95c23c71
Reviewed-on: https://review.typo3.org/53881
Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch>
Tested-by: Christian Kuhn <lolli@schwarzbu.ch>
Reviewed-by: Stefan Neufeind <typo3.neufeind@speedpartner.de>
Tested-by: Stefan Neufeind <typo3.neufeind@speedpartner.de>
Reviewed-by: Matthias Vogel <typo3@kanti.de>
Tested-by: Matthias Vogel <typo3@kanti.de>
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Benni Mack <benni@typo3.org>
Tested-by: Benni Mack <benni@typo3.org>
---
 .../ToolbarItems/ShortcutToolbarItem.php      |  13 +-
 .../Controller/EditDocumentController.php     |   4 +-
 .../Controller/PageLayoutController.php       |   2 +-
 .../backend/Classes/Http/Application.php      |  13 +-
 .../Http/BackendModuleRequestHandler.php      | 222 ------------------
 .../backend/Classes/Http/RequestHandler.php   |  10 +-
 .../backend/Classes/Http/RouteDispatcher.php  |  87 +++++++
 .../Classes/RecordList/AbstractRecordList.php |   8 +-
 .../backend/Classes/Routing/UriBuilder.php    |  23 +-
 .../Buttons/Action/ShortcutButton.php         |   2 +-
 .../Classes/Template/DocumentTemplate.php     |   2 +-
 .../Classes/Template/ModuleTemplate.php       |   5 +-
 .../Classes/Tree/View/AbstractTreeView.php    |   7 +-
 .../Classes/Utility/BackendUtility.php        |  11 +-
 .../BackendUserActionController.php           |   2 +-
 .../Controller/PermissionController.php       |   2 +-
 .../Utility/ExtensionManagementUtility.php    |  29 ++-
 ...kendModulesRunThroughRegularDispatcher.rst |  49 ++++
 .../Classes/Controller/HelpController.php     |   2 +-
 .../Classes/Mvc/Web/Routing/UriBuilder.php    |  22 +-
 .../Unit/Mvc/Web/Routing/UriBuilderTest.php   |  59 ++---
 typo3/sysext/filelist/Classes/FileList.php    |   7 +-
 .../Be/Buttons/ShortcutViewHelper.php         |   4 +-
 .../Controller/FormManagerController.php      |   2 +-
 .../Controller/InfoModuleController.php       |   2 +-
 .../ExtensionScanner/Php/ClassNameMatcher.php |   5 +
 .../lowlevel/Classes/Utility/ArrayBrowser.php |   4 +-
 .../Browser/AbstractElementBrowser.php        |   8 +-
 .../AbstractLinkBrowserController.php         |   7 +-
 .../RecordList/AbstractDatabaseRecordList.php |   7 +-
 .../Classes/RecordList/DatabaseRecordList.php |  15 +-
 .../Controller/RecyclerModuleController.php   |   2 +-
 .../Classes/Controller/ReportController.php   |   2 +-
 .../TypoScriptTemplateModuleController.php    |   2 +-
 .../Controller/ViewModuleController.php       |   2 +-
 .../Classes/Controller/PreviewController.php  |   2 +-
 .../Classes/Controller/ReviewController.php   |   2 +-
 .../Classes/Service/WorkspaceService.php      |   6 +-
 38 files changed, 293 insertions(+), 360 deletions(-)
 delete mode 100644 typo3/sysext/backend/Classes/Http/BackendModuleRequestHandler.php
 create mode 100644 typo3/sysext/core/Documentation/Changelog/master/Breaking-82406-RoutingBackendModulesRunThroughRegularDispatcher.rst

diff --git a/typo3/sysext/backend/Classes/Backend/ToolbarItems/ShortcutToolbarItem.php b/typo3/sysext/backend/Classes/Backend/ToolbarItems/ShortcutToolbarItem.php
index c7f8b60062cf..8a1d39057406 100644
--- a/typo3/sysext/backend/Classes/Backend/ToolbarItems/ShortcutToolbarItem.php
+++ b/typo3/sysext/backend/Classes/Backend/ToolbarItems/ShortcutToolbarItem.php
@@ -307,6 +307,7 @@ class ShortcutToolbarItem implements ToolbarItemInterface
 
     /**
      * Adds the correct token, if the url is an index.php script
+     * @todo: this needs love
      *
      * @param string $url
      * @return string
@@ -320,16 +321,20 @@ class ShortcutToolbarItem implements ToolbarItemInterface
         if (isset($parameters['returnUrl'])) {
             $parsedReturnUrl = parse_url($parameters['returnUrl']);
             parse_str($parsedReturnUrl['query'], $returnUrlParameters);
-            if (strpos($parsedReturnUrl['path'], 'index.php') !== false && isset($returnUrlParameters['M'])) {
-                $module = $returnUrlParameters['M'];
+            if (strpos($parsedReturnUrl['path'], 'index.php') !== false && !empty($returnUrlParameters['route'])) {
+                $module = $returnUrlParameters['route'];
                 $returnUrl = BackendUtility::getModuleUrl($module, $returnUrlParameters);
                 $parameters['returnUrl'] = $returnUrl;
                 $url = $parsedUrl['path'] . '?' . http_build_query($parameters, '', '&', PHP_QUERY_RFC3986);
             }
         }
+        if (isset($parameters['M']) && empty($parameters['route'])) {
+            $parameters['route'] = $parameters['M'];
+            unset($parameters['M']);
+        }
 
-        if (strpos($parsedUrl['path'], 'index.php') !== false && isset($parameters['M'])) {
-            $module = $parameters['M'];
+        if (strpos($parsedUrl['path'], 'index.php') !== false && isset($parameters['route'])) {
+            $module = $parameters['route'];
             $url = BackendUtility::getModuleUrl($module, $parameters);
         } elseif (strpos($parsedUrl['path'], 'index.php') !== false && isset($parameters['route'])) {
             $routePath = $parameters['route'];
diff --git a/typo3/sysext/backend/Classes/Controller/EditDocumentController.php b/typo3/sysext/backend/Classes/Controller/EditDocumentController.php
index 952a8f5bfab9..7b2502c12ea5 100644
--- a/typo3/sysext/backend/Classes/Controller/EditDocumentController.php
+++ b/typo3/sysext/backend/Classes/Controller/EditDocumentController.php
@@ -1287,13 +1287,13 @@ class EditDocumentController extends AbstractModule
                     $returnUrl = $this->retUrl;
                     if ($this->firstEl['table'] === 'pages') {
                         parse_str((string)parse_url($returnUrl, PHP_URL_QUERY), $queryParams);
-                        if (isset($queryParams['M'])
+                        if (isset($queryParams['route'])
                             && isset($queryParams['id'])
                             && (string)$this->firstEl['uid'] === (string)$queryParams['id']
                         ) {
                             // TODO: Use the page's pid instead of 0, this requires a clean API to manipulate the page
                             // tree from the outside to be able to mark the pid as active
-                            $returnUrl = BackendUtility::getModuleUrl($queryParams['M'], ['id' => 0]);
+                            $returnUrl = BackendUtility::getModuleUrl($queryParams['route'], ['id' => 0]);
                         }
                     }
                     $deleteButton = $buttonBar->makeLinkButton()
diff --git a/typo3/sysext/backend/Classes/Controller/PageLayoutController.php b/typo3/sysext/backend/Classes/Controller/PageLayoutController.php
index fdf5f593be83..ddfff72aabc9 100644
--- a/typo3/sysext/backend/Classes/Controller/PageLayoutController.php
+++ b/typo3/sysext/backend/Classes/Controller/PageLayoutController.php
@@ -975,7 +975,7 @@ class PageLayoutController
             ->setModuleName($this->moduleName)
             ->setGetVariables([
                 'id',
-                'M',
+                'route',
                 'edit_record',
                 'pointer',
                 'new_unique_uid',
diff --git a/typo3/sysext/backend/Classes/Http/Application.php b/typo3/sysext/backend/Classes/Http/Application.php
index 683a1b35f0dd..fe59180c9f23 100644
--- a/typo3/sysext/backend/Classes/Http/Application.php
+++ b/typo3/sysext/backend/Classes/Http/Application.php
@@ -33,18 +33,12 @@ class Application implements ApplicationInterface
      */
     protected $entryPointLevel = 1;
 
-    /**
-     * @var \Psr\Http\Message\ServerRequestInterface
-     */
-    protected $request;
-
     /**
      * All available request handlers that can handle backend requests (non-CLI)
      * @var array
      */
     protected $availableRequestHandlers = [
         \TYPO3\CMS\Backend\Http\RequestHandler::class,
-        \TYPO3\CMS\Backend\Http\BackendModuleRequestHandler::class,
         \TYPO3\CMS\Backend\Http\AjaxRequestHandler::class
     ];
 
@@ -81,12 +75,7 @@ class Application implements ApplicationInterface
      */
     public function run(callable $execute = null)
     {
-        $this->request = \TYPO3\CMS\Core\Http\ServerRequestFactory::fromGlobals();
-        if (isset($this->request->getQueryParams()['M'])) {
-            $this->request = $this->request->withAttribute('isModuleRequest', true);
-        }
-
-        $this->bootstrap->handleRequest($this->request);
+        $this->bootstrap->handleRequest(\TYPO3\CMS\Core\Http\ServerRequestFactory::fromGlobals());
 
         if ($execute !== null) {
             call_user_func($execute);
diff --git a/typo3/sysext/backend/Classes/Http/BackendModuleRequestHandler.php b/typo3/sysext/backend/Classes/Http/BackendModuleRequestHandler.php
deleted file mode 100644
index 38c5d073db07..000000000000
--- a/typo3/sysext/backend/Classes/Http/BackendModuleRequestHandler.php
+++ /dev/null
@@ -1,222 +0,0 @@
-<?php
-namespace TYPO3\CMS\Backend\Http;
-
-/*
- * This file is part of the TYPO3 CMS project.
- *
- * It is free software; you can redistribute it and/or modify it under
- * the terms of the GNU General Public License, either version 2
- * of the License, or any later version.
- *
- * For the full copyright and license information, please read the
- * LICENSE.txt file that was distributed with this source code.
- *
- * The TYPO3 project - inspiring people to share!
- */
-
-use Psr\Http\Message\ServerRequestInterface;
-use TYPO3\CMS\Backend\Utility\BackendUtility;
-use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
-use TYPO3\CMS\Core\Core\Bootstrap;
-use TYPO3\CMS\Core\Exception;
-use TYPO3\CMS\Core\FormProtection\BackendFormProtection;
-use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
-use TYPO3\CMS\Core\Http\Dispatcher;
-use TYPO3\CMS\Core\Http\RequestHandlerInterface;
-use TYPO3\CMS\Core\Http\Response;
-use TYPO3\CMS\Core\Type\Bitmask\Permission;
-use TYPO3\CMS\Core\Utility\GeneralUtility;
-use TYPO3\CMS\Core\Utility\MathUtility;
-
-/**
- * Handles the request for backend modules and wizards
- * Juggles with $GLOBALS['TBE_MODULES']
- */
-class BackendModuleRequestHandler implements RequestHandlerInterface
-{
-    /**
-     * @var Bootstrap
-     */
-    protected $bootstrap;
-
-    /**
-     * @var array
-     */
-    protected $moduleRegistry = [];
-
-    /**
-     * @var BackendUserAuthentication
-     */
-    protected $backendUserAuthentication;
-
-    /**
-     * Instance of the current Http Request
-     * @var ServerRequestInterface
-     */
-    protected $request;
-
-    /**
-     * Constructor handing over the bootstrap and the original request
-     *
-     * @param Bootstrap $bootstrap
-     */
-    public function __construct(Bootstrap $bootstrap)
-    {
-        $this->bootstrap = $bootstrap;
-    }
-
-    /**
-     * Handles the request, evaluating the configuration and executes the module accordingly
-     *
-     * @param ServerRequestInterface $request
-     * @return NULL|\Psr\Http\Message\ResponseInterface
-     * @throws Exception
-     */
-    public function handleRequest(ServerRequestInterface $request)
-    {
-        $this->request = $request;
-        $this->boot();
-
-        $this->moduleRegistry = $GLOBALS['TBE_MODULES'];
-
-        if (!$this->isValidModuleRequest()) {
-            throw new Exception('The CSRF protection token for the requested module is missing or invalid', 1417988921);
-        }
-
-        $this->backendUserAuthentication = $GLOBALS['BE_USER'];
-
-        $moduleName = (string)$this->request->getQueryParams()['M'];
-        return $this->dispatchModule($moduleName);
-    }
-
-    /**
-     * Execute TYPO3 bootstrap
-     */
-    protected function boot()
-    {
-        $this->bootstrap->checkLockedBackendAndRedirectOrDie()
-            ->checkBackendIpOrDie()
-            ->checkSslBackendAndRedirectIfNeeded()
-            ->initializeBackendRouter()
-            ->loadExtTables()
-            ->initializeBackendUser()
-            ->initializeBackendAuthentication()
-            ->initializeLanguageObject()
-            ->initializeBackendTemplate()
-            ->endOutputBufferingAndCleanPreviousOutput()
-            ->initializeOutputCompression()
-            ->sendHttpHeaders();
-    }
-
-    /**
-     * This request handler can handle any backend request coming from index.php
-     *
-     * @param ServerRequestInterface $request
-     * @return bool
-     */
-    public function canHandleRequest(ServerRequestInterface $request)
-    {
-        return $request->getAttribute('isModuleRequest', false);
-    }
-
-    /**
-     * Checks if all parameters are met.
-     *
-     * @return bool
-     */
-    protected function isValidModuleRequest()
-    {
-        return $this->getFormProtection() instanceof BackendFormProtection
-            && $this->getFormProtection()->validateToken((string)$this->request->getQueryParams()['moduleToken'], 'moduleCall', (string)$this->request->getQueryParams()['M']);
-    }
-
-    /**
-     * Executes the modules configured via Extbase
-     *
-     * @param string $moduleName
-     * @return Response A PSR-7 response object
-     * @throws \RuntimeException
-     */
-    protected function dispatchModule($moduleName)
-    {
-        $moduleConfiguration = $this->getModuleConfiguration($moduleName);
-
-        $response = GeneralUtility::makeInstance(Response::class);
-
-        // Check permissions and exit if the user has no permission for entry
-        $this->backendUserAuthentication->modAccess($moduleConfiguration, true);
-        $id = isset($this->request->getQueryParams()['id']) ? $this->request->getQueryParams()['id'] : $this->request->getParsedBody()['id'];
-        if ($id && MathUtility::canBeInterpretedAsInteger($id)) {
-            $permClause = $this->backendUserAuthentication->getPagePermsClause(Permission::PAGE_SHOW);
-            // Check page access
-            $access = is_array(BackendUtility::readPageAccess((int)$id, $permClause));
-            if (!$access) {
-                // Check if page has been deleted
-                $deleteField = $GLOBALS['TCA']['pages']['ctrl']['delete'];
-                $pageInfo = BackendUtility::getRecord('pages', (int)$id, $deleteField, $permClause ? ' AND ' . $permClause : '', false);
-                if (!$pageInfo[$deleteField]) {
-                    throw new \RuntimeException('You don\'t have access to this page', 1289917924);
-                }
-            }
-        }
-
-        // Use Core Dispatching
-        if (isset($moduleConfiguration['routeTarget'])) {
-            $dispatcher = GeneralUtility::makeInstance(Dispatcher::class);
-            $this->request = $this->request->withAttribute('target', $moduleConfiguration['routeTarget']);
-            $response = $dispatcher->dispatch($this->request, $response);
-        } else {
-            // extbase module
-            $configuration = [
-                'extensionName' => $moduleConfiguration['extensionName'],
-                'pluginName' => $moduleName
-            ];
-            if (isset($moduleConfiguration['vendorName'])) {
-                $configuration['vendorName'] = $moduleConfiguration['vendorName'];
-            }
-
-            // Run Extbase
-            $bootstrap = GeneralUtility::makeInstance(\TYPO3\CMS\Extbase\Core\Bootstrap::class);
-            $content = $bootstrap->run('', $configuration);
-
-            $response->getBody()->write($content);
-        }
-
-        return $response;
-    }
-
-    /**
-     * Returns the module configuration which is provided during module registration
-     *
-     * @param string $moduleName
-     * @return array
-     * @throws \RuntimeException
-     */
-    protected function getModuleConfiguration($moduleName)
-    {
-        if (!isset($this->moduleRegistry['_configuration'][$moduleName])) {
-            throw new \RuntimeException('Module ' . $moduleName . ' is not configured.', 1289918325);
-        }
-        return $this->moduleRegistry['_configuration'][$moduleName];
-    }
-
-    /**
-     * Returns the priority - how eager the handler is to actually handle the request.
-     *
-     * @return int The priority of the request handler.
-     */
-    public function getPriority()
-    {
-        return 90;
-    }
-
-    /**
-     * Wrapper method for static form protection utility
-     *
-     * @return \TYPO3\CMS\Core\FormProtection\AbstractFormProtection
-     */
-    protected function getFormProtection()
-    {
-        return FormProtectionFactory::get();
-    }
-}
diff --git a/typo3/sysext/backend/Classes/Http/RequestHandler.php b/typo3/sysext/backend/Classes/Http/RequestHandler.php
index 084e891a6e20..a24905f77b2f 100644
--- a/typo3/sysext/backend/Classes/Http/RequestHandler.php
+++ b/typo3/sysext/backend/Classes/Http/RequestHandler.php
@@ -59,14 +59,22 @@ class RequestHandler implements RequestHandlerInterface
      */
     public function handleRequest(ServerRequestInterface $request)
     {
+        // Check if a module URL is requested and deprecate this call
+        $moduleName = $request->getQueryParams()['M'] ?? $request->getParsedBody()['M'] ?? null;
         // Allow the login page to be displayed if routing is not used and on index.php
-        $pathToRoute = (string)$request->getQueryParams()['route'] ?: '/login';
+        $pathToRoute = $request->getQueryParams()['route'] ?? $request->getParsedBody()['route'] ?? $moduleName ?? '/login';
         $request = $request->withAttribute('routePath', $pathToRoute);
 
         // skip the BE user check on the login page
         // should be handled differently in the future by checking the Bootstrap directly
         $this->boot($pathToRoute === '/login');
 
+        if ($moduleName !== null) {
+            trigger_error('Calling the TYPO3 Backend with "M" GET parameter will be removed in TYPO3 v10,'
+                . ' the calling code calls this script with "&M=' . $moduleName . '" and needs to be adapted'
+                . ' to use the TYPO3 API.', E_USER_DEPRECATED);
+        }
+
         // Check if the router has the available route and dispatch.
         try {
             return $this->dispatch($request);
diff --git a/typo3/sysext/backend/Classes/Http/RouteDispatcher.php b/typo3/sysext/backend/Classes/Http/RouteDispatcher.php
index 957f85c785c7..bfd9bc633904 100644
--- a/typo3/sysext/backend/Classes/Http/RouteDispatcher.php
+++ b/typo3/sysext/backend/Classes/Http/RouteDispatcher.php
@@ -19,9 +19,12 @@ use Psr\Http\Message\ServerRequestInterface;
 use TYPO3\CMS\Backend\Routing\Exception\InvalidRequestTokenException;
 use TYPO3\CMS\Backend\Routing\Route;
 use TYPO3\CMS\Backend\Routing\Router;
+use TYPO3\CMS\Backend\Utility\BackendUtility;
 use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
 use TYPO3\CMS\Core\Http\Dispatcher;
 use TYPO3\CMS\Core\Http\DispatcherInterface;
+use TYPO3\CMS\Core\Http\Response;
+use TYPO3\CMS\Core\Type\Bitmask\Permission;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 
 /**
@@ -45,10 +48,14 @@ class RouteDispatcher extends Dispatcher implements DispatcherInterface
         /** @var Route $route */
         $route = $router->matchRequest($request);
         $request = $request->withAttribute('route', $route);
+        $request = $request->withAttribute('target', $route->getOption('target'));
         if (!$this->isValidRequest($request)) {
             throw new InvalidRequestTokenException('Invalid request for route "' . $route->getPath() . '"', 1425389455);
         }
 
+        if ($route->getOption('module')) {
+            return $this->dispatchModule($request, $response);
+        }
         $targetIdentifier = $route->getOption('target');
         $target = $this->getCallableFromTarget($targetIdentifier);
         return call_user_func_array($target, [$request, $response]);
@@ -82,4 +89,84 @@ class RouteDispatcher extends Dispatcher implements DispatcherInterface
         $token = (string)(isset($request->getParsedBody()['token']) ? $request->getParsedBody()['token'] : $request->getQueryParams()['token']);
         return $this->getFormProtection()->validateToken($token, 'route', $route->getOption('_identifier'));
     }
+
+    /**
+     * Executes the modules configured via Extbase
+     *
+     * @param ServerRequestInterface $request
+     * @param ResponseInterface $response
+     * @return ResponseInterface A PSR-7 response object
+     * @throws \RuntimeException
+     */
+    protected function dispatchModule(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
+    {
+        $route = $request->getAttribute('route');
+        $moduleName = $route->getOption('moduleName');
+        $moduleConfiguration = $this->getModuleConfiguration($moduleName);
+
+        $backendUserAuthentication = $GLOBALS['BE_USER'];
+
+        // Check permissions and exit if the user has no permission for entry
+        // @todo please do not use "true" here, what a bad coding paradigm
+        $backendUserAuthentication->modAccess($moduleConfiguration, true);
+        $id = (int)$request->getQueryParams()['id'] ?? $request->getParsedBody()['id'];
+        if ($id) {
+            $permClause = $backendUserAuthentication->getPagePermsClause(Permission::PAGE_SHOW);
+            // Check page access
+            if (!is_array(BackendUtility::readPageAccess($id, $permClause))) {
+                // Check if page has been deleted
+                $deleteField = $GLOBALS['TCA']['pages']['ctrl']['delete'];
+                $pageInfo = BackendUtility::getRecord('pages', $id, $deleteField, $permClause ? ' AND ' . $permClause : '', false);
+                if (!$pageInfo[$deleteField]) {
+                    throw new \RuntimeException('You don\'t have access to this page', 1289917924);
+                }
+            }
+        }
+
+        // Use regular Dispatching
+        // @todo: unify with the code above
+        $targetIdentifier = $route->getOption('target');
+        if (!empty($targetIdentifier)) {
+            // @internal routeParameters are a helper construct for the install tool only.
+            // @todo: remove this, after sub-actions in install tool can be addressed directly
+            if (!empty($moduleConfiguration['routeParameters'])) {
+                $request = $request->withQueryParams(array_merge_recursive(
+                    $request->getQueryParams(),
+                    $moduleConfiguration['routeParameters']
+                ));
+            }
+            return parent::dispatch($request, $response);
+        }
+        // extbase module
+        $configuration = [
+                'extensionName' => $moduleConfiguration['extensionName'],
+                'pluginName' => $moduleName
+            ];
+        if (isset($moduleConfiguration['vendorName'])) {
+            $configuration['vendorName'] = $moduleConfiguration['vendorName'];
+        }
+
+        // Run Extbase
+        $bootstrap = GeneralUtility::makeInstance(\TYPO3\CMS\Extbase\Core\Bootstrap::class);
+        $content = $bootstrap->run('', $configuration);
+
+        $response->getBody()->write($content);
+
+        return $response;
+    }
+
+    /**
+     * Returns the module configuration which is provided during module registration
+     *
+     * @param string $moduleName
+     * @return array
+     * @throws \RuntimeException
+     */
+    protected function getModuleConfiguration($moduleName)
+    {
+        if (!isset($GLOBALS['TBE_MODULES']['_configuration'][$moduleName])) {
+            throw new \RuntimeException('Module ' . $moduleName . ' is not configured.', 1289918325);
+        }
+        return $GLOBALS['TBE_MODULES']['_configuration'][$moduleName];
+    }
 }
diff --git a/typo3/sysext/backend/Classes/RecordList/AbstractRecordList.php b/typo3/sysext/backend/Classes/RecordList/AbstractRecordList.php
index 21d9a61b7919..2bb2b17f33d0 100644
--- a/typo3/sysext/backend/Classes/RecordList/AbstractRecordList.php
+++ b/typo3/sysext/backend/Classes/RecordList/AbstractRecordList.php
@@ -15,9 +15,7 @@ namespace TYPO3\CMS\Backend\RecordList;
  */
 
 use TYPO3\CMS\Backend\Configuration\TranslationConfigurationProvider;
-use TYPO3\CMS\Backend\Routing\Router;
 use TYPO3\CMS\Backend\Routing\UriBuilder;
-use TYPO3\CMS\Backend\Utility\BackendUtility;
 use TYPO3\CMS\Core\Database\ConnectionPool;
 use TYPO3\CMS\Core\Database\Query\Restriction\BackendWorkspaceRestriction;
 use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
@@ -215,12 +213,8 @@ abstract class AbstractRecordList
     protected function determineScriptUrl()
     {
         if ($routePath = GeneralUtility::_GP('route')) {
-            $router = GeneralUtility::makeInstance(Router::class);
-            $route = $router->match($routePath);
             $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
-            $this->thisScript = (string)$uriBuilder->buildUriFromRoute($route->getOption('_identifier'));
-        } elseif ($moduleName = GeneralUtility::_GP('M')) {
-            $this->thisScript = BackendUtility::getModuleUrl($moduleName);
+            $this->thisScript = (string)$uriBuilder->buildUriFromRoutePath($routePath);
         } else {
             $this->thisScript = GeneralUtility::getIndpEnv('SCRIPT_NAME');
         }
diff --git a/typo3/sysext/backend/Classes/Routing/UriBuilder.php b/typo3/sysext/backend/Classes/Routing/UriBuilder.php
index 93e5fadabb38..f2a9d313b8c1 100644
--- a/typo3/sysext/backend/Classes/Routing/UriBuilder.php
+++ b/typo3/sysext/backend/Classes/Routing/UriBuilder.php
@@ -54,6 +54,25 @@ class UriBuilder
         $this->routes = $router->getRoutes();
     }
 
+    /**
+     * Generates a URL or path for a specific route based on the given rout.
+     * Currently used to link to the current script, it is encouraged to use "buildUriFromRoute" if possible.
+     *
+     * If there is no route with the given name, the generator throws the RouteNotFoundException.
+     *
+     * @param string $pathInfo The path to the route
+     * @param array $parameters An array of parameters
+     * @param string $referenceType The type of reference to be generated (one of the constants)
+     * @return Uri The generated Uri
+     * @throws RouteNotFoundException If the named route doesn't exist
+     */
+    public function buildUriFromRoutePath($pathInfo, $parameters = [], $referenceType = self::ABSOLUTE_PATH)
+    {
+        $router = GeneralUtility::makeInstance(Router::class);
+        $route = $router->match($pathInfo);
+        return $this->buildUriFromRoute($route->getOption('_identifier'), $parameters, $referenceType);
+    }
+
     /**
      * Generates a URL or path for a specific route based on the given parameters.
      * When the route is configured with "access=public" then the token generation is left out.
@@ -106,8 +125,8 @@ class UriBuilder
     public function buildUriFromModule($moduleName, $parameters = [], $referenceType = self::ABSOLUTE_PATH)
     {
         $parameters = [
-            'M' => $moduleName,
-            'moduleToken' => FormProtectionFactory::get('backend')->generateToken('moduleCall', $moduleName)
+            'route' => $moduleName,
+            'token' => FormProtectionFactory::get('backend')->generateToken('route', $moduleName)
         ] + $parameters;
         return $this->buildUri($parameters, $referenceType);
     }
diff --git a/typo3/sysext/backend/Classes/Template/Components/Buttons/Action/ShortcutButton.php b/typo3/sysext/backend/Classes/Template/Components/Buttons/Action/ShortcutButton.php
index 14db2a1d2942..b3b815041ad5 100644
--- a/typo3/sysext/backend/Classes/Template/Components/Buttons/Action/ShortcutButton.php
+++ b/typo3/sysext/backend/Classes/Template/Components/Buttons/Action/ShortcutButton.php
@@ -239,7 +239,7 @@ class ShortcutButton implements ButtonInterface, PositionInterface
 
         // Set default GET parameters
         if ($emptyGetVariables) {
-            $this->getVariables = ['id', 'M'];
+            $this->getVariables = ['id', 'route'];
         }
 
         // Automatically determine module name in Extbase context
diff --git a/typo3/sysext/backend/Classes/Template/DocumentTemplate.php b/typo3/sysext/backend/Classes/Template/DocumentTemplate.php
index 02365ea0deb4..446bf200151d 100644
--- a/typo3/sysext/backend/Classes/Template/DocumentTemplate.php
+++ b/typo3/sysext/backend/Classes/Template/DocumentTemplate.php
@@ -251,7 +251,7 @@ function jumpToUrl(URL) {
         $this->templateService = GeneralUtility::makeInstance(MarkerBasedTemplateService::class);
 
         // Setting default scriptID, trim forward slash from route
-        $this->scriptID = GeneralUtility::_GET('M') !== null ? GeneralUtility::_GET('M') : ltrim(GeneralUtility::_GET('route'), '/');
+        $this->scriptID = ltrim(GeneralUtility::_GET('route'), '/');
         $this->bodyTagId = preg_replace('/[^A-Za-z0-9-]/', '-', $this->scriptID);
         // Individual configuration per script? If so, make a recursive merge of the arrays:
         if (is_array($GLOBALS['TBE_STYLES']['scriptIDindex'][$this->scriptID])) {
diff --git a/typo3/sysext/backend/Classes/Template/ModuleTemplate.php b/typo3/sysext/backend/Classes/Template/ModuleTemplate.php
index f8b6622b1268..aa55258e0fa6 100644
--- a/typo3/sysext/backend/Classes/Template/ModuleTemplate.php
+++ b/typo3/sysext/backend/Classes/Template/ModuleTemplate.php
@@ -510,8 +510,9 @@ class ModuleTemplate
         // since this is used for icons.
         $moduleName = $modName === 'xMOD_alt_doc.php' ? 'record_edit' : $modName;
         // Add the module identifier automatically if typo3/index.php is used:
-        if (GeneralUtility::_GET('M') !== null) {
-            $storeUrl = '&M=' . $moduleName . $storeUrl;
+        // @todo: routing
+        if (GeneralUtility::_GET('route') !== null) {
+            $storeUrl = '&route=' . $moduleName . $storeUrl;
         }
         if ((int)$motherModName === 1) {
             $motherModule = 'top.currentModuleLoaded';
diff --git a/typo3/sysext/backend/Classes/Tree/View/AbstractTreeView.php b/typo3/sysext/backend/Classes/Tree/View/AbstractTreeView.php
index 2564e31bf185..13dab282fae1 100644
--- a/typo3/sysext/backend/Classes/Tree/View/AbstractTreeView.php
+++ b/typo3/sysext/backend/Classes/Tree/View/AbstractTreeView.php
@@ -14,7 +14,6 @@ namespace TYPO3\CMS\Backend\Tree\View;
  * The TYPO3 project - inspiring people to share!
  */
 
-use TYPO3\CMS\Backend\Routing\Router;
 use TYPO3\CMS\Backend\Routing\UriBuilder;
 use TYPO3\CMS\Backend\Tree\Pagetree\Commands;
 use TYPO3\CMS\Backend\Utility\BackendUtility;
@@ -289,12 +288,8 @@ abstract class AbstractTreeView
     protected function determineScriptUrl()
     {
         if ($routePath = GeneralUtility::_GP('route')) {
-            $router = GeneralUtility::makeInstance(Router::class);
-            $route = $router->match($routePath);
             $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
-            $this->thisScript = (string)$uriBuilder->buildUriFromRoute($route->getOption('_identifier'));
-        } elseif ($moduleName = GeneralUtility::_GP('M')) {
-            $this->thisScript = BackendUtility::getModuleUrl($moduleName);
+            $this->thisScript = (string)$uriBuilder->buildUriFromRoutePath($routePath);
         } else {
             $this->thisScript = GeneralUtility::getIndpEnv('SCRIPT_NAME');
         }
diff --git a/typo3/sysext/backend/Classes/Utility/BackendUtility.php b/typo3/sysext/backend/Classes/Utility/BackendUtility.php
index 103a2feb361b..46af96a288d9 100644
--- a/typo3/sysext/backend/Classes/Utility/BackendUtility.php
+++ b/typo3/sysext/backend/Classes/Utility/BackendUtility.php
@@ -2950,14 +2950,10 @@ class BackendUtility
             $script = basename(PATH_thisScript);
         }
 
-        if (GeneralUtility::_GP('route')) {
-            $router = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\Router::class);
-            $route = $router->match(GeneralUtility::_GP('route'));
+        if ($routePath = GeneralUtility::_GP('route')) {
             $uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
-            $scriptUrl = (string)$uriBuilder->buildUriFromRoute($route->getOption('_identifier'));
+            $scriptUrl = (string)$uriBuilder->buildUriFromRoutePath($routePath, $mainParams);
             $scriptUrl .= $addParams;
-        } elseif ($script === 'index.php' && GeneralUtility::_GET('M')) {
-            $scriptUrl = self::getModuleUrl(GeneralUtility::_GET('M'), $mainParams) . $addParams;
         } else {
             $scriptUrl = $script . '?' . GeneralUtility::implodeArrayForUrl('', $mainParams) . $addParams;
         }
@@ -3169,8 +3165,7 @@ class BackendUtility
         try {
             $uri = $uriBuilder->buildUriFromRoute($moduleName, $urlParameters);
         } catch (\TYPO3\CMS\Backend\Routing\Exception\RouteNotFoundException $e) {
-            // no route registered, use the fallback logic to check for a module
-            $uri = $uriBuilder->buildUriFromModule($moduleName, $urlParameters);
+            $uri = $uriBuilder->buildUriFromRoutePath($moduleName, $urlParameters);
         }
         return (string)$uri;
     }
diff --git a/typo3/sysext/beuser/Classes/Controller/BackendUserActionController.php b/typo3/sysext/beuser/Classes/Controller/BackendUserActionController.php
index fe9bce3f6a9a..61a1a70eb975 100644
--- a/typo3/sysext/beuser/Classes/Controller/BackendUserActionController.php
+++ b/typo3/sysext/beuser/Classes/Controller/BackendUserActionController.php
@@ -125,7 +125,7 @@ class BackendUserActionController extends ActionController
         $extensionName = $currentRequest->getControllerExtensionName();
         if (count($getVars) === 0) {
             $modulePrefix = strtolower('tx_' . $extensionName . '_' . $moduleName);
-            $getVars = ['id', 'M', $modulePrefix];
+            $getVars = ['id', 'route', $modulePrefix];
         }
         $shortcutName = $this->getLanguageService()->sL('LLL:EXT:beuser/Resources/Private/Language/locallang.xml:backendUsers');
         if ($this->request->getControllerName() === 'BackendUser') {
diff --git a/typo3/sysext/beuser/Classes/Controller/PermissionController.php b/typo3/sysext/beuser/Classes/Controller/PermissionController.php
index 81155f031aca..072dd3bf481f 100644
--- a/typo3/sysext/beuser/Classes/Controller/PermissionController.php
+++ b/typo3/sysext/beuser/Classes/Controller/PermissionController.php
@@ -158,7 +158,7 @@ class PermissionController extends ActionController
         $extensionName = $currentRequest->getControllerExtensionName();
         if (empty($getVars)) {
             $modulePrefix = strtolower('tx_' . $extensionName . '_' . $moduleName);
-            $getVars = ['id', 'M', $modulePrefix];
+            $getVars = ['id', 'route', $modulePrefix];
         }
 
         if ($currentRequest->getControllerActionName() === 'edit') {
diff --git a/typo3/sysext/core/Classes/Utility/ExtensionManagementUtility.php b/typo3/sysext/core/Classes/Utility/ExtensionManagementUtility.php
index c0f3b752e46a..292524f2a550 100644
--- a/typo3/sysext/core/Classes/Utility/ExtensionManagementUtility.php
+++ b/typo3/sysext/core/Classes/Utility/ExtensionManagementUtility.php
@@ -858,9 +858,8 @@ class ExtensionManagementUtility
         }
 
         // add additional configuration
+        $fullModuleSignature = $main . ($sub ? '_' . $sub : '');
         if (is_array($moduleConfiguration) && !empty($moduleConfiguration)) {
-            $fullModuleSignature = $main . ($sub ? '_' . $sub : '');
-
             if (!empty($moduleConfiguration['icon'])) {
                 $iconRegistry = GeneralUtility::makeInstance(IconRegistry::class);
                 $iconIdentifier = 'module-' . $fullModuleSignature;
@@ -876,6 +875,32 @@ class ExtensionManagementUtility
 
             $GLOBALS['TBE_MODULES']['_configuration'][$fullModuleSignature] = $moduleConfiguration;
         }
+
+        // Also register the module as regular route
+        // Build Route objects from the data
+        $name = $fullModuleSignature;
+        if (isset($moduleConfiguration['path'])) {
+            $path = $moduleConfiguration['path'];
+        } else {
+            $path = str_replace('_', '/', $name);
+        }
+        $path = '/' . trim($path, '/') . '/';
+
+        $options = [
+            'module' => true,
+            'moduleName' => $fullModuleSignature,
+            'access' => $moduleConfiguration['access'] ?: 'user,group'
+        ];
+        if ($moduleConfiguration['routeTarget']) {
+            $options['target'] = $moduleConfiguration['routeTarget'];
+        }
+
+        $router = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\Router::class);
+        $router->addRoute(
+            $name,
+            // @todo: see if we should do a "module route"
+            GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\Route::class, $path, $options)
+        );
     }
 
     /**
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Breaking-82406-RoutingBackendModulesRunThroughRegularDispatcher.rst b/typo3/sysext/core/Documentation/Changelog/master/Breaking-82406-RoutingBackendModulesRunThroughRegularDispatcher.rst
new file mode 100644
index 000000000000..fd79b8e5a708
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/master/Breaking-82406-RoutingBackendModulesRunThroughRegularDispatcher.rst
@@ -0,0 +1,49 @@
+.. include:: ../../Includes.txt
+
+==========================================================================
+Breaking: #82406 - Routing: Backend Modules run through regular dispatcher
+==========================================================================
+
+See :issue:`82406`
+
+Description
+===========
+
+Calling Backend modules was previously handled via a special `BackendModuleRequestHandler` which has
+been removed.
+
+When registering a Backend module, a route with the name of the module is automatically added to the
+Backend Router.
+
+When generating URLs for modules, the module is not added via the GET Parameter `&M=moduleName`
+anymore, but built like any other Backend Route (currently with the "route" and "token" parameters)
+
+All request handling functionality is now done by the regular Backend RequestHandler,
+which checks if the Route to be targeted is a module, and does extra module permission checks.
+
+
+Impact
+======
+
+Handling with the "&M" GET parameter in backend modules won't deliver the correct result anymore.
+
+Instantiating `BackendModuleRequestHandler` will result in a fatal PHP error.
+
+
+Affected Installations
+======================
+
+Installations with custom extensions including backend modules which work directly with the GET
+parameter "M".
+
+
+Migration
+=========
+
+If extensions use API methods like ``BackendUtility::getModuleUrl()`` are used, nothing needs to be
+modified.
+
+If a backend module is using the GET parameter "M" currently, the code needs to be adjusted to the
+GET "route" or use the UriBuilder directly.
+
+.. index:: Backend, PartiallyScanned
diff --git a/typo3/sysext/cshmanual/Classes/Controller/HelpController.php b/typo3/sysext/cshmanual/Classes/Controller/HelpController.php
index e623c99fde9d..06e257775608 100644
--- a/typo3/sysext/cshmanual/Classes/Controller/HelpController.php
+++ b/typo3/sysext/cshmanual/Classes/Controller/HelpController.php
@@ -151,7 +151,7 @@ class HelpController extends ActionController
             $extensionName = $currentRequest->getControllerExtensionName();
             if (count($getVars) === 0) {
                 $modulePrefix = strtolower('tx_' . $extensionName . '_' . $moduleName);
-                $getVars = ['id', 'M', $modulePrefix];
+                $getVars = ['id', 'route', $modulePrefix];
             }
             $shortcutButton = $buttonBar->makeShortcutButton()
                 ->setModuleName($moduleName)
diff --git a/typo3/sysext/extbase/Classes/Mvc/Web/Routing/UriBuilder.php b/typo3/sysext/extbase/Classes/Mvc/Web/Routing/UriBuilder.php
index f27a50666ee2..169644191b2e 100644
--- a/typo3/sysext/extbase/Classes/Mvc/Web/Routing/UriBuilder.php
+++ b/typo3/sysext/extbase/Classes/Mvc/Web/Routing/UriBuilder.php
@@ -664,24 +664,32 @@ class UriBuilder
             }
         } else {
             $id = GeneralUtility::_GP('id');
-            $module = GeneralUtility::_GP('M');
+            $module = GeneralUtility::_GP('route');
             if ($id !== null) {
                 $arguments['id'] = $id;
             }
             if ($module !== null) {
-                $arguments['M'] = $module;
+                $arguments['route'] = $module;
             }
         }
         ArrayUtility::mergeRecursiveWithOverrule($arguments, $this->arguments);
         $arguments = $this->convertDomainObjectsToIdentityArrays($arguments);
         $this->lastArguments = $arguments;
-        $moduleName = $arguments['M'];
-        unset($arguments['M'], $arguments['moduleToken']);
+        $moduleName = $arguments['route'] ?? null;
+        unset($arguments['route'], $arguments['token']);
         $backendUriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
-        if ($this->request instanceof WebRequest && $this->createAbsoluteUri) {
-            $uri = (string)$backendUriBuilder->buildUriFromModule($moduleName, $arguments, \TYPO3\CMS\Backend\Routing\UriBuilder::ABSOLUTE_URL);
+        if (!empty($moduleName)) {
+            if ($this->request instanceof WebRequest && $this->createAbsoluteUri) {
+                $uri = (string)$backendUriBuilder->buildUriFromRoutePath($moduleName, $arguments, \TYPO3\CMS\Backend\Routing\UriBuilder::ABSOLUTE_URL);
+            } else {
+                $uri = (string)$backendUriBuilder->buildUriFromRoutePath($moduleName, $arguments, \TYPO3\CMS\Backend\Routing\UriBuilder::ABSOLUTE_PATH);
+            }
         } else {
-            $uri = (string)$backendUriBuilder->buildUriFromModule($moduleName, $arguments);
+            if ($this->request instanceof WebRequest && $this->createAbsoluteUri) {
+                $uri = (string)$backendUriBuilder->buildUriFromModule($moduleName, $arguments, \TYPO3\CMS\Backend\Routing\UriBuilder::ABSOLUTE_URL);
+            } else {
+                $uri = (string)$backendUriBuilder->buildUriFromModule($moduleName, $arguments, \TYPO3\CMS\Backend\Routing\UriBuilder::ABSOLUTE_PATH);
+            }
         }
         if ($this->section !== '') {
             $uri .= '#' . $this->section;
diff --git a/typo3/sysext/extbase/Tests/Unit/Mvc/Web/Routing/UriBuilderTest.php b/typo3/sysext/extbase/Tests/Unit/Mvc/Web/Routing/UriBuilderTest.php
index a86b73b6d3cc..063f27200e48 100644
--- a/typo3/sysext/extbase/Tests/Unit/Mvc/Web/Routing/UriBuilderTest.php
+++ b/typo3/sysext/extbase/Tests/Unit/Mvc/Web/Routing/UriBuilderTest.php
@@ -13,6 +13,8 @@ namespace TYPO3\CMS\Extbase\Tests\Unit\Mvc\Web\Routing;
  *
  * The TYPO3 project - inspiring people to share!
  */
+use TYPO3\CMS\Backend\Routing\Route;
+use TYPO3\CMS\Backend\Routing\Router;
 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Extbase\Configuration\ConfigurationManager;
@@ -76,6 +78,9 @@ class UriBuilderTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
         $this->uriBuilder->_set('configurationManager', $this->mockConfigurationManager);
         $this->uriBuilder->_set('extensionService', $this->mockExtensionService);
         $this->uriBuilder->_set('environmentService', $this->createMock(EnvironmentService::class));
+        $router = GeneralUtility::makeInstance(Router::class);
+        $router->addRoute('module_key', new Route('/test/Path', []));
+        $router->addRoute('module_key2', new Route('/test/Path2', []));
         // Mocking backend user is required for backend URI generation as BackendUtility::getModuleUrl() is called
         $backendUserMock = $this->createMock(BackendUserAuthentication::class);
         $backendUserMock->expects($this->any())->method('check')->will($this->returnValue(true));
@@ -207,12 +212,12 @@ class UriBuilderTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
      */
     public function buildBackendUriKeepsQueryParametersIfAddQueryStringIsSet()
     {
-        GeneralUtility::_GETset(['M' => 'moduleKey', 'id' => 'pageId', 'foo' => 'bar']);
+        GeneralUtility::_GETset(['route' => '/test/Path', 'id' => 'pageId', 'foo' => 'bar']);
         $_POST = [];
         $_POST['foo2'] = 'bar2';
         $this->uriBuilder->setAddQueryString(true);
         $this->uriBuilder->setAddQueryStringMethod('GET,POST');
-        $expectedResult = '/typo3/index.php?M=moduleKey&moduleToken=dummyToken&id=pageId&foo=bar&foo2=bar2';
+        $expectedResult = '/typo3/index.php?route=%2Ftest%2FPath&token=dummyToken&id=pageId&foo=bar&foo2=bar2';
         $actualResult = $this->uriBuilder->buildBackendUri();
         $this->assertEquals($expectedResult, $actualResult);
     }
@@ -222,12 +227,12 @@ class UriBuilderTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
      */
     public function buildBackendUriKeepsQueryParametersIfAddQueryStringMethodIsNotSet()
     {
-        GeneralUtility::_GETset(['M' => 'moduleKey', 'id' => 'pageId', 'foo' => 'bar']);
+        GeneralUtility::_GETset(['route' => '/test/Path', 'id' => 'pageId', 'foo' => 'bar']);
         $_POST = [];
         $_POST['foo2'] = 'bar2';
         $this->uriBuilder->setAddQueryString(true);
         $this->uriBuilder->setAddQueryStringMethod(null);
-        $expectedResult = '/typo3/index.php?M=moduleKey&moduleToken=dummyToken&id=pageId&foo=bar';
+        $expectedResult = '/typo3/index.php?route=%2Ftest%2FPath&token=dummyToken&id=pageId&foo=bar';
         $actualResult = $this->uriBuilder->buildBackendUri();
         $this->assertEquals($expectedResult, $actualResult);
     }
@@ -240,7 +245,7 @@ class UriBuilderTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
         return [
             'Arguments to be excluded in the beginning' => [
                 [
-                    'M' => 'moduleKey',
+                    'route' => '/test/Path',
                     'id' => 'pageId',
                     'foo' => 'bar'
                 ],
@@ -248,25 +253,25 @@ class UriBuilderTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
                     'foo2' => 'bar2'
                 ],
                 [
-                    'M',
+                    'route',
                     'id'
                 ],
-                '/typo3/index.php?M=&moduleToken=dummyToken&foo=bar&foo2=bar2'
+                '/typo3/index.php?route=&token=dummyToken&foo=bar&foo2=bar2'
             ],
             'Arguments to be excluded in the end' => [
                 [
                     'foo' => 'bar',
                     'id' => 'pageId',
-                    'M' => 'moduleKey'
+                    'route' => '/test/Path'
                 ],
                 [
                     'foo2' => 'bar2'
                 ],
                 [
-                    'M',
+                    'route',
                     'id'
                 ],
-                '/typo3/index.php?M=&moduleToken=dummyToken&foo=bar&foo2=bar2'
+                '/typo3/index.php?route=&token=dummyToken&foo=bar&foo2=bar2'
             ],
             'Arguments in nested array to be excluded' => [
                 [
@@ -274,7 +279,7 @@ class UriBuilderTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
                         'bar' => 'baz'
                     ],
                     'id' => 'pageId',
-                    'M' => 'moduleKey'
+                    'route' => '/test/Path'
                 ],
                 [
                     'foo2' => 'bar2'
@@ -283,7 +288,7 @@ class UriBuilderTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
                     'id',
                     'tx_foo[bar]'
                 ],
-                '/typo3/index.php?M=moduleKey&moduleToken=dummyToken&foo2=bar2'
+                '/typo3/index.php?route=%2Ftest%2FPath&token=dummyToken&foo2=bar2'
             ],
             'Arguments in multidimensional array to be excluded' => [
                 [
@@ -293,7 +298,7 @@ class UriBuilderTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
                         ]
                     ],
                     'id' => 'pageId',
-                    'M' => 'moduleKey'
+                    'route' => '/test/Path'
                 ],
                 [
                     'foo2' => 'bar2'
@@ -302,7 +307,7 @@ class UriBuilderTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
                     'id',
                     'tx_foo[bar][baz]'
                 ],
-                '/typo3/index.php?M=moduleKey&moduleToken=dummyToken&foo2=bar2'
+                '/typo3/index.php?route=%2Ftest%2FPath&token=dummyToken&foo2=bar2'
             ],
         ];
     }
@@ -331,8 +336,8 @@ class UriBuilderTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
      */
     public function buildBackendUriKeepsModuleQueryParametersIfAddQueryStringIsNotSet()
     {
-        GeneralUtility::_GETset(['M' => 'moduleKey', 'id' => 'pageId', 'foo' => 'bar']);
-        $expectedResult = '/typo3/index.php?M=moduleKey&moduleToken=dummyToken&id=pageId';
+        GeneralUtility::_GETset(['route' => '/test/Path', 'id' => 'pageId', 'foo' => 'bar']);
+        $expectedResult = '/typo3/index.php?route=%2Ftest%2FPath&token=dummyToken&id=pageId';
         $actualResult = $this->uriBuilder->buildBackendUri();
         $this->assertEquals($expectedResult, $actualResult);
     }
@@ -342,9 +347,9 @@ class UriBuilderTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
      */
     public function buildBackendUriMergesAndOverrulesQueryParametersWithArguments()
     {
-        GeneralUtility::_GETset(['M' => 'moduleKey', 'id' => 'pageId', 'foo' => 'bar']);
-        $this->uriBuilder->setArguments(['M' => 'overwrittenModuleKey', 'somePrefix' => ['bar' => 'baz']]);
-        $expectedResult = '/typo3/index.php?M=overwrittenModuleKey&moduleToken=dummyToken&id=pageId&somePrefix%5Bbar%5D=baz';
+        GeneralUtility::_GETset(['route' => '/test/Path', 'id' => 'pageId', 'foo' => 'bar']);
+        $this->uriBuilder->setArguments(['route' => '/test/Path2', 'somePrefix' => ['bar' => 'baz']]);
+        $expectedResult = '/typo3/index.php?route=%2Ftest%2FPath2&token=dummyToken&id=pageId&somePrefix%5Bbar%5D=baz';
         $actualResult = $this->uriBuilder->buildBackendUri();
         $this->assertEquals($expectedResult, $actualResult);
     }
@@ -354,11 +359,11 @@ class UriBuilderTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
      */
     public function buildBackendUriConvertsDomainObjectsAfterArgumentsHaveBeenMerged()
     {
-        GeneralUtility::_GETset(['M' => 'moduleKey']);
+        GeneralUtility::_GETset(['route' => '/test/Path']);
         $mockDomainObject = $this->getAccessibleMock(AbstractEntity::class, ['dummy']);
         $mockDomainObject->_set('uid', '123');
         $this->uriBuilder->setArguments(['somePrefix' => ['someDomainObject' => $mockDomainObject]]);
-        $expectedResult = '/typo3/index.php?M=moduleKey&moduleToken=dummyToken&somePrefix%5BsomeDomainObject%5D=123';
+        $expectedResult = '/typo3/index.php?route=%2Ftest%2FPath&token=dummyToken&somePrefix%5BsomeDomainObject%5D=123';
         $actualResult = $this->uriBuilder->buildBackendUri();
         $this->assertEquals($expectedResult, $actualResult);
     }
@@ -368,9 +373,9 @@ class UriBuilderTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
      */
     public function buildBackendUriRespectsSection()
     {
-        GeneralUtility::_GETset(['M' => 'moduleKey']);
+        GeneralUtility::_GETset(['route' => '/test/Path']);
         $this->uriBuilder->setSection('someSection');
-        $expectedResult = '/typo3/index.php?M=moduleKey&moduleToken=dummyToken#someSection';
+        $expectedResult = '/typo3/index.php?route=%2Ftest%2FPath&token=dummyToken#someSection';
         $actualResult = $this->uriBuilder->buildBackendUri();
         $this->assertEquals($expectedResult, $actualResult);
     }
@@ -381,12 +386,12 @@ class UriBuilderTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
     public function buildBackendUriCreatesAbsoluteUrisIfSpecified()
     {
         GeneralUtility::flushInternalRuntimeCaches();
-        GeneralUtility::_GETset(['M' => 'moduleKey']);
+        GeneralUtility::_GETset(['route' => '/test/Path']);
         $_SERVER['HTTP_HOST'] = 'baseuri';
         $_SERVER['SCRIPT_NAME'] = '/typo3/index.php';
         $this->mockRequest->expects($this->any())->method('getBaseUri')->will($this->returnValue('http://baseuri'));
         $this->uriBuilder->setCreateAbsoluteUri(true);
-        $expectedResult = 'http://baseuri/' . TYPO3_mainDir . 'index.php?M=moduleKey&moduleToken=dummyToken';
+        $expectedResult = 'http://baseuri/' . TYPO3_mainDir . 'index.php?route=%2Ftest%2FPath&token=dummyToken';
         $actualResult = $this->uriBuilder->buildBackendUri();
         $this->assertSame($expectedResult, $actualResult);
     }
@@ -419,7 +424,7 @@ class UriBuilderTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
         ];
         $this->uriBuilder->setAddQueryString(true);
         $this->uriBuilder->setAddQueryStringMethod('POST,GET');
-        $expectedResult = $this->rawUrlEncodeSquareBracketsInUrl('/typo3/index.php?M=&moduleToken=dummyToken&key1=POST1&key2=GET2&key3[key31]=POST31&key3[key32]=GET32&key3[key33][key331]=GET331&key3[key33][key332]=POST332');
+        $expectedResult = $this->rawUrlEncodeSquareBracketsInUrl('/typo3/index.php?route=&token=dummyToken&key1=POST1&key2=GET2&key3[key31]=POST31&key3[key32]=GET32&key3[key33][key331]=GET331&key3[key33][key332]=POST332');
         $actualResult = $this->uriBuilder->buildBackendUri();
         $this->assertEquals($expectedResult, $actualResult);
     }
@@ -452,7 +457,7 @@ class UriBuilderTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
         ];
         $this->uriBuilder->setAddQueryString(true);
         $this->uriBuilder->setAddQueryStringMethod('GET,POST');
-        $expectedResult = $this->rawUrlEncodeSquareBracketsInUrl('/typo3/index.php?M=&moduleToken=dummyToken&key1=GET1&key2=POST2&key3[key31]=GET31&key3[key32]=POST32&key3[key33][key331]=POST331&key3[key33][key332]=GET332');
+        $expectedResult = $this->rawUrlEncodeSquareBracketsInUrl('/typo3/index.php?route=&token=dummyToken&key1=GET1&key2=POST2&key3[key31]=GET31&key3[key32]=POST32&key3[key33][key331]=POST331&key3[key33][key332]=GET332');
         $actualResult = $this->uriBuilder->buildBackendUri();
         $this->assertEquals($expectedResult, $actualResult);
     }
diff --git a/typo3/sysext/filelist/Classes/FileList.php b/typo3/sysext/filelist/Classes/FileList.php
index fe821d987900..207bd6b3be77 100644
--- a/typo3/sysext/filelist/Classes/FileList.php
+++ b/typo3/sysext/filelist/Classes/FileList.php
@@ -16,7 +16,6 @@ namespace TYPO3\CMS\Filelist;
 
 use TYPO3\CMS\Backend\Clipboard\Clipboard;
 use TYPO3\CMS\Backend\Configuration\TranslationConfigurationProvider;
-use TYPO3\CMS\Backend\Routing\Router;
 use TYPO3\CMS\Backend\Routing\UriBuilder;
 use TYPO3\CMS\Backend\Utility\BackendUtility;
 use TYPO3\CMS\Core\Database\ConnectionPool;
@@ -1489,12 +1488,8 @@ class FileList
     protected function determineScriptUrl()
     {
         if ($routePath = GeneralUtility::_GP('route')) {
-            $router = GeneralUtility::makeInstance(Router::class);
-            $route = $router->match($routePath);
             $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
-            $this->thisScript = (string)$uriBuilder->buildUriFromRoute($route->getOption('_identifier'));
-        } elseif ($moduleName = GeneralUtility::_GP('M')) {
-            $this->thisScript = BackendUtility::getModuleUrl($moduleName);
+            $this->thisScript = (string)$uriBuilder->buildUriFromRoutePath($routePath);
         } else {
             $this->thisScript = GeneralUtility::getIndpEnv('SCRIPT_NAME');
         }
diff --git a/typo3/sysext/fluid/Classes/ViewHelpers/Be/Buttons/ShortcutViewHelper.php b/typo3/sysext/fluid/Classes/ViewHelpers/Be/Buttons/ShortcutViewHelper.php
index 9e838b9c21c3..12ba44799249 100644
--- a/typo3/sysext/fluid/Classes/ViewHelpers/Be/Buttons/ShortcutViewHelper.php
+++ b/typo3/sysext/fluid/Classes/ViewHelpers/Be/Buttons/ShortcutViewHelper.php
@@ -34,7 +34,7 @@ use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
  * </output>
  *
  * <code title="Explicitly set parameters to be stored in the shortcut">
- * <f:be.buttons.shortcut getVars="{0: 'M', 1: 'myOwnPrefix'}" setVars="{0: 'function'}" />
+ * <f:be.buttons.shortcut getVars="{0: 'route', 1: 'myOwnPrefix'}" setVars="{0: 'function'}" />
  * </code>
  * <output>
  * Shortcut button as known from the TYPO3 backend.
@@ -99,7 +99,7 @@ class ShortcutViewHelper extends AbstractBackendViewHelper
             $moduleName = $currentRequest->getPluginName();
             if (count($getVars) === 0) {
                 $modulePrefix = strtolower('tx_' . $extensionName . '_' . $moduleName);
-                $getVars = ['id', 'M', $modulePrefix];
+                $getVars = ['id', 'route', $modulePrefix];
             }
             $getList = implode(',', $getVars);
             $setList = implode(',', $setVars);
diff --git a/typo3/sysext/form/Classes/Controller/FormManagerController.php b/typo3/sysext/form/Classes/Controller/FormManagerController.php
index 353eb05a6b72..80b473f97689 100644
--- a/typo3/sysext/form/Classes/Controller/FormManagerController.php
+++ b/typo3/sysext/form/Classes/Controller/FormManagerController.php
@@ -427,7 +427,7 @@ class FormManagerController extends AbstractBackendController
             $extensionName = $currentRequest->getControllerExtensionName();
             if (count($getVars) === 0) {
                 $modulePrefix = strtolower('tx_' . $extensionName . '_' . $moduleName);
-                $getVars = ['id', 'M', $modulePrefix];
+                $getVars = ['id', 'route', $modulePrefix];
             }
 
             $shortcutButton = $buttonBar->makeShortcutButton()
diff --git a/typo3/sysext/info/Classes/Controller/InfoModuleController.php b/typo3/sysext/info/Classes/Controller/InfoModuleController.php
index 309fe694305d..24426a349ed4 100644
--- a/typo3/sysext/info/Classes/Controller/InfoModuleController.php
+++ b/typo3/sysext/info/Classes/Controller/InfoModuleController.php
@@ -172,7 +172,7 @@ class InfoModuleController extends BaseScriptClass
             ->setModuleName($this->moduleName)
             ->setDisplayName($this->MOD_MENU['function'][$this->MOD_SETTINGS['function']])
             ->setGetVariables([
-                'M',
+                'route',
                 'id',
                 'edit_record',
                 'pointer',
diff --git a/typo3/sysext/install/Configuration/ExtensionScanner/Php/ClassNameMatcher.php b/typo3/sysext/install/Configuration/ExtensionScanner/Php/ClassNameMatcher.php
index ba0746a6be33..3f2af70a4755 100644
--- a/typo3/sysext/install/Configuration/ExtensionScanner/Php/ClassNameMatcher.php
+++ b/typo3/sysext/install/Configuration/ExtensionScanner/Php/ClassNameMatcher.php
@@ -379,6 +379,11 @@ return [
             'Breaking-55298-DecoupledHistoryFunctionality.rst',
         ],
     ],
+    'TYPO3\CMS\Backend\Http\BackendModuleRequestHandler' => [
+        'restFiles' => [
+            'Breaking-82406-RoutingBackendModulesRunThroughRegularDispatcher.rst',
+        ],
+    ],
 
     // Removed interfaces
     'TYPO3\CMS\Backend\Form\DatabaseFileIconsHookInterface' => [
diff --git a/typo3/sysext/lowlevel/Classes/Utility/ArrayBrowser.php b/typo3/sysext/lowlevel/Classes/Utility/ArrayBrowser.php
index 01700abf698c..a7051053c318 100644
--- a/typo3/sysext/lowlevel/Classes/Utility/ArrayBrowser.php
+++ b/typo3/sysext/lowlevel/Classes/Utility/ArrayBrowser.php
@@ -109,7 +109,7 @@ class ArrayBrowser
             $output .= '<li' . ($isResult ? ' class="active"' : '') . '>';
             if ($isArray && !$this->expAll) {
                 $goto = 'a' . substr(md5($depth), 0, 6);
-                $output .= '<a class="list-tree-control' . ($isExpanded ? ' list-tree-control-open' : ' list-tree-control-closed') . '" id="' . $goto . '" href="' . htmlspecialchars((BackendUtility::getModuleUrl(GeneralUtility::_GP('M')) . '&node[' . $depth . ']=' . ($isExpanded ? 0 : 1) . '#' . $goto)) . '"><i class="fa"></i></a> ';
+                $output .= '<a class="list-tree-control' . ($isExpanded ? ' list-tree-control-open' : ' list-tree-control-closed') . '" id="' . $goto . '" href="' . htmlspecialchars((BackendUtility::getModuleUrl(GeneralUtility::_GP('route')) . '&node[' . $depth . ']=' . ($isExpanded ? 0 : 1) . '#' . $goto)) . '"><i class="fa"></i></a> ';
             }
             $output .= '<span class="list-tree-group">';
             $output .= $this->wrapArrayKey($key, $depth, !$isArray ? $value : '');
@@ -149,7 +149,7 @@ class ArrayBrowser
                 . (!MathUtility::canBeInterpretedAsInteger($theValue) ? '\''
                 . addslashes($theValue) . '\'' : $theValue) . '; ';
             $label = '<a class="list-tree-label" href="'
-                . htmlspecialchars((BackendUtility::getModuleUrl(GeneralUtility::_GP('M'))
+                . htmlspecialchars((BackendUtility::getModuleUrl(GeneralUtility::_GP('route'))
                 . '&varname=' . urlencode($variableName)))
                 . '#varname">' . $label . '</a>';
         }
diff --git a/typo3/sysext/recordlist/Classes/Browser/AbstractElementBrowser.php b/typo3/sysext/recordlist/Classes/Browser/AbstractElementBrowser.php
index f1b277fd86a0..2e819dad6fd0 100644
--- a/typo3/sysext/recordlist/Classes/Browser/AbstractElementBrowser.php
+++ b/typo3/sysext/recordlist/Classes/Browser/AbstractElementBrowser.php
@@ -14,10 +14,8 @@ namespace TYPO3\CMS\Recordlist\Browser;
  * The TYPO3 project - inspiring people to share!
  */
 
-use TYPO3\CMS\Backend\Routing\Router;
 use TYPO3\CMS\Backend\Routing\UriBuilder;
 use TYPO3\CMS\Backend\Template\DocumentTemplate;
-use TYPO3\CMS\Backend\Utility\BackendUtility;
 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
 use TYPO3\CMS\Core\Imaging\IconFactory;
 use TYPO3\CMS\Core\Localization\LanguageService;
@@ -106,12 +104,8 @@ abstract class AbstractElementBrowser
     protected function determineScriptUrl()
     {
         if ($routePath = GeneralUtility::_GP('route')) {
-            $router = GeneralUtility::makeInstance(Router::class);
-            $route = $router->match($routePath);
             $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
-            $this->thisScript = (string)$uriBuilder->buildUriFromRoute($route->getOption('_identifier'));
-        } elseif ($moduleName = GeneralUtility::_GP('M')) {
-            $this->thisScript = BackendUtility::getModuleUrl($moduleName);
+            $this->thisScript = (string)$uriBuilder->buildUriFromRoutePath($routePath);
         } else {
             $this->thisScript = GeneralUtility::getIndpEnv('SCRIPT_NAME');
         }
diff --git a/typo3/sysext/recordlist/Classes/Controller/AbstractLinkBrowserController.php b/typo3/sysext/recordlist/Classes/Controller/AbstractLinkBrowserController.php
index c1dbc264a8dd..fe40625bb78f 100644
--- a/typo3/sysext/recordlist/Classes/Controller/AbstractLinkBrowserController.php
+++ b/typo3/sysext/recordlist/Classes/Controller/AbstractLinkBrowserController.php
@@ -16,7 +16,6 @@ namespace TYPO3\CMS\Recordlist\Controller;
 
 use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ServerRequestInterface;
-use TYPO3\CMS\Backend\Routing\Router;
 use TYPO3\CMS\Backend\Routing\UriBuilder;
 use TYPO3\CMS\Backend\Template\DocumentTemplate;
 use TYPO3\CMS\Backend\Utility\BackendUtility;
@@ -209,12 +208,8 @@ abstract class AbstractLinkBrowserController
     protected function determineScriptUrl(ServerRequestInterface $request)
     {
         if ($routePath = $request->getQueryParams()['route']) {
-            $router = GeneralUtility::makeInstance(Router::class);
-            $route = $router->match($routePath);
             $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
-            $this->thisScript = (string)$uriBuilder->buildUriFromRoute($route->getOption('_identifier'));
-        } elseif ($moduleName = $request->getQueryParams()['M']) {
-            $this->thisScript = BackendUtility::getModuleUrl($moduleName);
+            $this->thisScript = (string)$uriBuilder->buildUriFromRoutePath($routePath);
         } else {
             $this->thisScript = GeneralUtility::getIndpEnv('SCRIPT_NAME');
         }
diff --git a/typo3/sysext/recordlist/Classes/RecordList/AbstractDatabaseRecordList.php b/typo3/sysext/recordlist/Classes/RecordList/AbstractDatabaseRecordList.php
index 1b3b1dde87cf..672ff22b063b 100644
--- a/typo3/sysext/recordlist/Classes/RecordList/AbstractDatabaseRecordList.php
+++ b/typo3/sysext/recordlist/Classes/RecordList/AbstractDatabaseRecordList.php
@@ -15,7 +15,6 @@ namespace TYPO3\CMS\Recordlist\RecordList;
  */
 
 use TYPO3\CMS\Backend\RecordList\AbstractRecordList;
-use TYPO3\CMS\Backend\Routing\Router;
 use TYPO3\CMS\Backend\Routing\UriBuilder;
 use TYPO3\CMS\Backend\Tree\View\PageTreeView;
 use TYPO3\CMS\Backend\Utility\BackendUtility;
@@ -1189,12 +1188,8 @@ class AbstractDatabaseRecordList extends AbstractRecordList
         $urlParameters = array_merge_recursive($urlParameters, $this->overrideUrlParameters);
 
         if ($routePath = GeneralUtility::_GP('route')) {
-            $router = GeneralUtility::makeInstance(Router::class);
-            $route = $router->match($routePath);
             $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
-            $url = (string)$uriBuilder->buildUriFromRoute($route->getOption('_identifier'), $urlParameters);
-        } elseif ($moduleName = GeneralUtility::_GP('M')) {
-            $url = BackendUtility::getModuleUrl($moduleName, $urlParameters);
+            $url = (string)$uriBuilder->buildUriFromRoutePath($routePath, $urlParameters);
         } else {
             $url = GeneralUtility::getIndpEnv('SCRIPT_NAME') . '?' . ltrim(GeneralUtility::implodeArrayForUrl('', $urlParameters), '&');
         }
diff --git a/typo3/sysext/recordlist/Classes/RecordList/DatabaseRecordList.php b/typo3/sysext/recordlist/Classes/RecordList/DatabaseRecordList.php
index 4da1c9e42d07..bd6d78f10e3c 100644
--- a/typo3/sysext/recordlist/Classes/RecordList/DatabaseRecordList.php
+++ b/typo3/sysext/recordlist/Classes/RecordList/DatabaseRecordList.php
@@ -17,7 +17,6 @@ namespace TYPO3\CMS\Recordlist\RecordList;
 use TYPO3\CMS\Backend\Configuration\TranslationConfigurationProvider;
 use TYPO3\CMS\Backend\Module\BaseScriptClass;
 use TYPO3\CMS\Backend\RecordList\RecordListGetTableHookInterface;
-use TYPO3\CMS\Backend\Routing\Router;
 use TYPO3\CMS\Backend\Routing\UriBuilder;
 use TYPO3\CMS\Backend\Template\Components\ButtonBar;
 use TYPO3\CMS\Backend\Template\DocumentTemplate;
@@ -869,7 +868,7 @@ class DatabaseRecordList
                     ->setModuleName('web_list')
                     ->setGetVariables([
                         'id',
-                        'M',
+                        'route',
                         'imagemode',
                         'pointer',
                         'table',
@@ -3580,12 +3579,8 @@ class DatabaseRecordList
         $urlParameters = array_merge_recursive($urlParameters, $this->overrideUrlParameters);
 
         if ($routePath = GeneralUtility::_GP('route')) {
-            $router = GeneralUtility::makeInstance(Router::class);
-            $route = $router->match($routePath);
             $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
-            $url = (string)$uriBuilder->buildUriFromRoute($route->getOption('_identifier'), $urlParameters);
-        } elseif ($moduleName = GeneralUtility::_GP('M')) {
-            $url = BackendUtility::getModuleUrl($moduleName, $urlParameters);
+            $url = (string)$uriBuilder->buildUriFromRoutePath($routePath, $urlParameters);
         } else {
             $url = GeneralUtility::getIndpEnv('SCRIPT_NAME') . '?' . ltrim(
                     GeneralUtility::implodeArrayForUrl('', $urlParameters),
@@ -4152,12 +4147,8 @@ class DatabaseRecordList
     protected function determineScriptUrl()
     {
         if ($routePath = GeneralUtility::_GP('route')) {
-            $router = GeneralUtility::makeInstance(Router::class);
-            $route = $router->match($routePath);
             $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
-            $this->thisScript = (string)$uriBuilder->buildUriFromRoute($route->getOption('_identifier'));
-        } elseif ($moduleName = GeneralUtility::_GP('M')) {
-            $this->thisScript = BackendUtility::getModuleUrl($moduleName);
+            $this->thisScript = (string)$uriBuilder->buildUriFromRoutePath($routePath);
         } else {
             $this->thisScript = GeneralUtility::getIndpEnv('SCRIPT_NAME');
         }
diff --git a/typo3/sysext/recycler/Classes/Controller/RecyclerModuleController.php b/typo3/sysext/recycler/Classes/Controller/RecyclerModuleController.php
index f6e98fc21527..a60914e29ece 100644
--- a/typo3/sysext/recycler/Classes/Controller/RecyclerModuleController.php
+++ b/typo3/sysext/recycler/Classes/Controller/RecyclerModuleController.php
@@ -148,7 +148,7 @@ class RecyclerModuleController extends ActionController
         $extensionName = $currentRequest->getControllerExtensionName();
         if (count($getVars) === 0) {
             $modulePrefix = strtolower('tx_' . $extensionName . '_' . $moduleName);
-            $getVars = ['id', 'M', $modulePrefix];
+            $getVars = ['id', 'route', $modulePrefix];
         }
         $shortcutButton = $buttonBar->makeShortcutButton()
             ->setModuleName($moduleName)
diff --git a/typo3/sysext/reports/Classes/Controller/ReportController.php b/typo3/sysext/reports/Classes/Controller/ReportController.php
index 36f1cf864cb4..6a2054231b5b 100644
--- a/typo3/sysext/reports/Classes/Controller/ReportController.php
+++ b/typo3/sysext/reports/Classes/Controller/ReportController.php
@@ -172,7 +172,7 @@ class ReportController extends ActionController
         $setVars = $this->request->hasArgument('setVars') ? $this->request->getArgument('setVars') : [];
         if (count($getVars) === 0) {
             $modulePrefix = strtolower('tx_' . $this->request->getControllerExtensionName() . '_' . $moduleName);
-            $getVars = ['id', 'M', $modulePrefix];
+            $getVars = ['id', 'route', $modulePrefix];
         }
         $shortcutButton = $buttonBar->makeShortcutButton()
             ->setModuleName($moduleName)
diff --git a/typo3/sysext/tstemplate/Classes/Controller/TypoScriptTemplateModuleController.php b/typo3/sysext/tstemplate/Classes/Controller/TypoScriptTemplateModuleController.php
index 7b669e00204d..d3532ba7b161 100644
--- a/typo3/sysext/tstemplate/Classes/Controller/TypoScriptTemplateModuleController.php
+++ b/typo3/sysext/tstemplate/Classes/Controller/TypoScriptTemplateModuleController.php
@@ -362,7 +362,7 @@ class TypoScriptTemplateModuleController extends BaseScriptClass
         // Shortcut
         $shortcutButton = $buttonBar->makeShortcutButton()
             ->setModuleName($this->MCONF['name'])
-            ->setGetVariables(['id', 'M']);
+            ->setGetVariables(['id', 'route']);
         $buttonBar->addButton($shortcutButton);
     }
 
diff --git a/typo3/sysext/viewpage/Classes/Controller/ViewModuleController.php b/typo3/sysext/viewpage/Classes/Controller/ViewModuleController.php
index 8c2725e1bc6a..de87dd2f468c 100644
--- a/typo3/sysext/viewpage/Classes/Controller/ViewModuleController.php
+++ b/typo3/sysext/viewpage/Classes/Controller/ViewModuleController.php
@@ -103,7 +103,7 @@ class ViewModuleController extends ActionController
         $extensionName = $currentRequest->getControllerExtensionName();
         if (count($getVars) === 0) {
             $modulePrefix = strtolower('tx_' . $extensionName . '_' . $moduleName);
-            $getVars = ['id', 'M', $modulePrefix];
+            $getVars = ['id', 'route', $modulePrefix];
         }
         $shortcutButton = $buttonBar->makeShortcutButton()
             ->setModuleName($moduleName)
diff --git a/typo3/sysext/workspaces/Classes/Controller/PreviewController.php b/typo3/sysext/workspaces/Classes/Controller/PreviewController.php
index 079b4a81625e..7945dcc6d0bb 100644
--- a/typo3/sysext/workspaces/Classes/Controller/PreviewController.php
+++ b/typo3/sysext/workspaces/Classes/Controller/PreviewController.php
@@ -87,7 +87,7 @@ class PreviewController extends AbstractController
 
         // Remove the GET parameters related to the workspaces module and the page id
         unset($queryParameters['tx_workspaces_web_workspacesworkspaces']);
-        unset($queryParameters['M']);
+        unset($queryParameters['route']);
         unset($queryParameters['id']);
 
         // Assemble a query string from the retrieved parameters
diff --git a/typo3/sysext/workspaces/Classes/Controller/ReviewController.php b/typo3/sysext/workspaces/Classes/Controller/ReviewController.php
index 22279a17ad41..6c21f5a0dd2a 100644
--- a/typo3/sysext/workspaces/Classes/Controller/ReviewController.php
+++ b/typo3/sysext/workspaces/Classes/Controller/ReviewController.php
@@ -50,7 +50,7 @@ class ReviewController extends AbstractController
         $extensionName = $currentRequest->getControllerExtensionName();
         if (count($getVars) === 0) {
             $modulePrefix = strtolower('tx_' . $extensionName . '_' . $moduleName);
-            $getVars = ['id', 'M', $modulePrefix];
+            $getVars = ['id', 'route', $modulePrefix];
         }
         $shortcutButton = $buttonBar->makeShortcutButton()
             ->setModuleName($moduleName)
diff --git a/typo3/sysext/workspaces/Classes/Service/WorkspaceService.php b/typo3/sysext/workspaces/Classes/Service/WorkspaceService.php
index f03a1c48d539..ba257964be14 100644
--- a/typo3/sysext/workspaces/Classes/Service/WorkspaceService.php
+++ b/typo3/sysext/workspaces/Classes/Service/WorkspaceService.php
@@ -863,10 +863,10 @@ class WorkspaceService implements SingletonInterface
         $uriBuilder = $this->getObjectManager()->get(\TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder::class);
         $redirect = 'index.php?redirect_url=';
         // @todo this should maybe be changed so that the extbase URI Builder can deal with module names directly
-        $originalM = GeneralUtility::_GET('M');
-        GeneralUtility::_GETset('web_WorkspacesWorkspaces', 'M');
+        $originalM = GeneralUtility::_GET('route');
+        GeneralUtility::_GETset('web_WorkspacesWorkspaces', 'route');
         $viewScript = $uriBuilder->uriFor('index', [], 'Preview', 'workspaces', 'web_workspacesworkspaces') . '&id=';
-        GeneralUtility::_GETset($originalM, 'M');
+        GeneralUtility::_GETset($originalM, 'route');
         if ($addDomain === true) {
             return BackendUtility::getViewDomain($uid) . $redirect . urlencode($viewScript) . $uid;
         }
-- 
GitLab