diff --git a/Build/Sources/TypeScript/backend/Resources/Public/TypeScript/Toolbar/ShortcutMenu.ts b/Build/Sources/TypeScript/backend/Resources/Public/TypeScript/Toolbar/ShortcutMenu.ts
index 289b33554416e449e846856b17e2434cf6c0a2f7..cab8f00a6b1fa11ae69af4f4a648967d4bbd6c4f 100644
--- a/Build/Sources/TypeScript/backend/Resources/Public/TypeScript/Toolbar/ShortcutMenu.ts
+++ b/Build/Sources/TypeScript/backend/Resources/Public/TypeScript/Toolbar/ShortcutMenu.ts
@@ -49,20 +49,18 @@ class ShortcutMenu {
    * makes a call to the backend class to create a new shortcut,
    * when finished it reloads the menu
    *
-   * @param {String} moduleName
-   * @param {String} url
+   * @param {String} routeIdentifier
+   * @param {String} routeArguments
+   * @param {String} displayName
    * @param {String} confirmationText
-   * @param {String} motherModule
    * @param {Object} shortcutButton
-   * @param {String} displayName
    */
   public createShortcut(
-    moduleName: string,
-    url: string,
+    routeIdentifier: string,
+    routeArguments: string,
+    displayName: string,
     confirmationText: string,
-    motherModule: string,
     shortcutButton: JQuery,
-    displayName: string,
   ): void {
     if (typeof confirmationText !== 'undefined') {
       Modal.confirm(TYPO3.lang['bookmark.create'], confirmationText).on('confirm.button.ok', (e: JQueryEventObject): void => {
@@ -74,9 +72,8 @@ class ShortcutMenu {
         });
 
         (new AjaxRequest(TYPO3.settings.ajaxUrls.shortcut_create)).post({
-          module: moduleName,
-          url: url,
-          motherModName: motherModule,
+          routeIdentifier: routeIdentifier,
+          arguments: routeArguments,
           displayName: displayName,
         }).then((): void => {
           this.refreshMenu();
diff --git a/typo3/sysext/backend/Classes/Backend/Shortcut/ShortcutRepository.php b/typo3/sysext/backend/Classes/Backend/Shortcut/ShortcutRepository.php
index 01aa444c6422f938ffadd24b1ea10356f006b1d5..30d46a147c2532637ecbb8105ebb2f4b1ae3bb55 100644
--- a/typo3/sysext/backend/Classes/Backend/Shortcut/ShortcutRepository.php
+++ b/typo3/sysext/backend/Classes/Backend/Shortcut/ShortcutRepository.php
@@ -17,8 +17,8 @@ declare(strict_types=1);
 
 namespace TYPO3\CMS\Backend\Backend\Shortcut;
 
+use Symfony\Component\Routing\Route;
 use TYPO3\CMS\Backend\Module\ModuleLoader;
-use TYPO3\CMS\Backend\Routing\Exception\ResourceNotFoundException;
 use TYPO3\CMS\Backend\Routing\Router;
 use TYPO3\CMS\Backend\Routing\UriBuilder;
 use TYPO3\CMS\Backend\Utility\BackendUtility;
@@ -32,7 +32,6 @@ use TYPO3\CMS\Core\Resource\Exception\ResourceDoesNotExistException;
 use TYPO3\CMS\Core\Resource\ResourceFactory;
 use TYPO3\CMS\Core\Type\Bitmask\Permission;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
-use TYPO3\CMS\Core\Utility\MathUtility;
 
 /**
  * Repository for backend shortcuts
@@ -46,33 +45,23 @@ class ShortcutRepository
      */
     protected const SUPERGLOBAL_GROUP = -100;
 
-    /**
-     * @var array
-     */
-    protected $shortcuts;
+    protected const TABLE_NAME = 'sys_be_shortcuts';
 
-    /**
-     * @var array
-     */
-    protected $shortcutGroups;
+    protected array $shortcuts;
 
-    /**
-     * @var IconFactory
-     */
-    protected $iconFactory;
+    protected array $shortcutGroups;
 
-    /**
-     * @var ModuleLoader
-     */
-    protected $moduleLoader;
+    protected ConnectionPool $connectionPool;
 
-    /**
-     * Constructor
-     */
-    public function __construct()
+    protected IconFactory $iconFactory;
+
+    protected ModuleLoader $moduleLoader;
+
+    public function __construct(ConnectionPool $connectionPool, IconFactory $iconFactory, ModuleLoader $moduleLoader)
     {
-        $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
-        $this->moduleLoader = GeneralUtility::makeInstance(ModuleLoader::class);
+        $this->connectionPool = $connectionPool;
+        $this->iconFactory = $iconFactory;
+        $this->moduleLoader = $moduleLoader;
         $this->moduleLoader->load($GLOBALS['TBE_MODULES']);
 
         $this->shortcutGroups = $this->initShortcutGroups();
@@ -154,26 +143,24 @@ class ShortcutRepository
     /**
      * Returns if there already is a shortcut entry for a given TYPO3 URL
      *
-     * @param string $url
+     * @param string $routeIdentifier
+     * @param string $arguments
      * @return bool
      */
-    public function shortcutExists(string $url): bool
+    public function shortcutExists(string $routeIdentifier, string $arguments): bool
     {
-        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
-            ->getQueryBuilderForTable('sys_be_shortcuts');
+        $queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::TABLE_NAME);
         $queryBuilder->getRestrictions()->removeAll();
 
         $uid = $queryBuilder->select('uid')
-            ->from('sys_be_shortcuts')
+            ->from(self::TABLE_NAME)
             ->where(
                 $queryBuilder->expr()->eq(
                     'userid',
                     $queryBuilder->createNamedParameter($this->getBackendUser()->user['uid'], \PDO::PARAM_INT)
                 ),
-                $queryBuilder->expr()->eq(
-                    'url',
-                    $queryBuilder->createNamedParameter($url, \PDO::PARAM_STR)
-                )
+                $queryBuilder->expr()->eq('route', $queryBuilder->createNamedParameter($routeIdentifier)),
+                $queryBuilder->expr()->eq('arguments', $queryBuilder->createNamedParameter($arguments))
             )
             ->execute()
             ->fetchColumn();
@@ -184,58 +171,49 @@ class ShortcutRepository
     /**
      * Add a shortcut
      *
-     * @param string $url URL of the new shortcut
-     * @param string $module module identifier of the new shortcut
-     * @param string $parentModule parent module identifier of the new shortcut
+     * @param string $routeIdentifier route identifier of the new shortcut
+     * @param string $arguments arguments of the new shortcut
      * @param string $title title of the new shortcut
      * @return bool
      * @throws \RuntimeException if the given URL is invalid
      */
-    public function addShortcut(string $url, string $module, string $parentModule = '', string $title = ''): bool
+    public function addShortcut(string $routeIdentifier, string $arguments = '', string $title = ''): bool
     {
-        // @todo $parentModule can not longer be set using public API.
-
-        if (empty($url) || empty($module)) {
+        // Do not add shortcuts for routes which do not exist
+        if (!$this->routeExists($routeIdentifier)) {
             return false;
         }
 
-        $queryParts = parse_url($url);
-        $queryParameters = [];
-        parse_str($queryParts['query'] ?? '', $queryParameters);
-
-        if (!empty($queryParameters['scheme'])) {
-            throw new \RuntimeException('Shortcut URLs must be relative', 1518785877);
-        }
-
         $languageService = $this->getLanguageService();
-        $titlePrefix = '';
-        $type = 'other';
-        $table = '';
-        $recordId = 0;
-        $pageId = 0;
-
-        if (is_array($queryParameters['edit'])) {
-            $table = (string)key($queryParameters['edit']);
-            $recordId = (int)key($queryParameters['edit'][$table]);
-            $pageId = (int)BackendUtility::getRecord($table, $recordId)['pid'];
-            $languageFile = 'LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf';
-            $action = $queryParameters['edit'][$table][$recordId];
-
-            switch ($action) {
-                case 'edit':
-                    $type = 'edit';
-                    $titlePrefix = $languageService->sL($languageFile . ':shortcut_edit');
-                    break;
-                case 'new':
-                    $type = 'new';
-                    $titlePrefix = $languageService->sL($languageFile . ':shortcut_create');
-                    break;
-            }
-        }
 
         // Only apply "magic" if title is not set
         // @todo This is deprecated and can be removed in v12
         if ($title === '') {
+            $queryParameters = json_decode($arguments, true);
+            $titlePrefix = '';
+            $type = 'other';
+            $table = '';
+            $recordId = 0;
+            $pageId = 0;
+
+            if ($queryParameters && is_array($queryParameters['edit'])) {
+                $table = (string)key($queryParameters['edit']);
+                $recordId = (int)key($queryParameters['edit'][$table]);
+                $pageId = (int)BackendUtility::getRecord($table, $recordId)['pid'];
+                $languageFile = 'LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf';
+                $action = $queryParameters['edit'][$table][$recordId];
+
+                switch ($action) {
+                    case 'edit':
+                        $type = 'edit';
+                        $titlePrefix = $languageService->sL($languageFile . ':shortcut_edit');
+                        break;
+                    case 'new':
+                        $type = 'new';
+                        $titlePrefix = $languageService->sL($languageFile . ':shortcut_create');
+                        break;
+                }
+            }
             // Check if given id is a combined identifier
             if (!empty($queryParameters['id']) && preg_match('/^[\d]+:/', $queryParameters['id'])) {
                 try {
@@ -250,7 +228,7 @@ class ShortcutRepository
                 }
             } else {
                 // Lookup the title of this page and use it as default description
-                $pageId = $pageId ?: $recordId ?: $this->extractPageIdFromShortcutUrl($url);
+                $pageId = $pageId ?: $recordId ?: (int)($queryParameters['id'] ?? 0);
                 $page = $pageId ? BackendUtility::getRecord('pages', $pageId) : null;
 
                 if (!empty($page)) {
@@ -282,21 +260,19 @@ class ShortcutRepository
         // In case title is still empty try to set the modules short description label
         // @todo This is deprecated and can be removed in v12
         if ($title === '') {
-            $moduleLabels = $this->moduleLoader->getLabelsForModule($module);
-
+            $moduleLabels = $this->moduleLoader->getLabelsForModule($this->getModuleNameFromRouteIdentifier($routeIdentifier));
             if (!empty($moduleLabels['shortdescription'])) {
                 $title = $this->getLanguageService()->sL($moduleLabels['shortdescription']);
             }
         }
 
-        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
-            ->getQueryBuilderForTable('sys_be_shortcuts');
+        $queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::TABLE_NAME);
         $affectedRows = $queryBuilder
-            ->insert('sys_be_shortcuts')
+            ->insert(self::TABLE_NAME)
             ->values([
                 'userid' => $this->getBackendUser()->user['uid'],
-                'module_name' => $module . '|' . $parentModule,
-                'url' => $url,
+                'route' => $routeIdentifier,
+                'arguments' => $arguments,
                 'description' => $title ?: 'Shortcut',
                 'sorting' => $GLOBALS['EXEC_TIME'],
             ])
@@ -316,9 +292,8 @@ class ShortcutRepository
     public function updateShortcut(int $id, string $title, int $groupId): bool
     {
         $backendUser = $this->getBackendUser();
-        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
-            ->getQueryBuilderForTable('sys_be_shortcuts');
-        $queryBuilder->update('sys_be_shortcuts')
+        $queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::TABLE_NAME);
+        $queryBuilder->update(self::TABLE_NAME)
             ->where(
                 $queryBuilder->expr()->eq(
                     'uid',
@@ -358,10 +333,9 @@ class ShortcutRepository
         $shortcut = $this->getShortcutById($id);
         $success = false;
 
-        if ($shortcut['raw']['userid'] == $this->getBackendUser()->user['uid']) {
-            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
-                ->getQueryBuilderForTable('sys_be_shortcuts');
-            $affectedRows = $queryBuilder->delete('sys_be_shortcuts')
+        if ((int)$shortcut['raw']['userid'] === (int)$this->getBackendUser()->user['uid']) {
+            $queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::TABLE_NAME);
+            $affectedRows = $queryBuilder->delete(self::TABLE_NAME)
                 ->where(
                     $queryBuilder->expr()->eq(
                         'uid',
@@ -459,13 +433,12 @@ class ShortcutRepository
     protected function initShortcuts(): array
     {
         $backendUser = $this->getBackendUser();
-        // Traverse shortcuts
         $lastGroup = 0;
         $shortcuts = [];
-        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
-            ->getQueryBuilderForTable('sys_be_shortcuts');
+
+        $queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::TABLE_NAME);
         $result = $queryBuilder->select('*')
-            ->from('sys_be_shortcuts')
+            ->from(self::TABLE_NAME)
             ->where(
                 $queryBuilder->expr()->andX(
                     $queryBuilder->expr()->eq(
@@ -493,21 +466,16 @@ class ShortcutRepository
 
         while ($row = $result->fetch()) {
             $shortcut = ['raw' => $row];
+            $routeIdentifier = $row['route'] ?? '';
+            $arguments = json_decode($row['arguments'] ?? '', true) ?? [];
 
-            [$row['module_name'], $row['M_module_name']] = explode('|', $row['module_name']);
-
-            $queryParts = parse_url($row['url']);
-            // Explode GET vars recursively
-            $queryParameters = [];
-            parse_str($queryParts['query'] ?? '', $queryParameters);
+            if ($routeIdentifier === 'record_edit' && is_array($arguments['edit'])) {
+                $shortcut['table'] = key($arguments['edit']);
+                $shortcut['recordid'] = key($arguments['edit'][$shortcut['table']]);
 
-            if ($row['module_name'] === 'xMOD_alt_doc.php' && is_array($queryParameters['edit'])) {
-                $shortcut['table'] = key($queryParameters['edit']);
-                $shortcut['recordid'] = key($queryParameters['edit'][$shortcut['table']]);
-
-                if ($queryParameters['edit'][$shortcut['table']][$shortcut['recordid']] === 'edit') {
+                if ($arguments['edit'][$shortcut['table']][$shortcut['recordid']] === 'edit') {
                     $shortcut['type'] = 'edit';
-                } elseif ($queryParameters['edit'][$shortcut['table']][$shortcut['recordid']] === 'new') {
+                } elseif ($arguments['edit'][$shortcut['table']][$shortcut['recordid']] === 'new') {
                     $shortcut['type'] = 'new';
                 }
 
@@ -518,56 +486,57 @@ class ShortcutRepository
                 $shortcut['type'] = 'other';
             }
 
-            // Check for module access
-            $moduleName = $row['M_module_name'] ?: $row['module_name'];
+            $moduleName = $this->getModuleNameFromRouteIdentifier($routeIdentifier);
+
+            // Skip shortcut if module name can not be resolved
+            if ($moduleName === '') {
+                continue;
+            }
 
             // Check if the user has access to this module
             // @todo Hack for EditDocumentController / FormEngine, see issues #91368 and #91210
-            if (!is_array($this->moduleLoader->checkMod($moduleName)) && $moduleName !== 'xMOD_alt_doc.php') {
+            if ($routeIdentifier !== 'record_edit' && !is_array($this->moduleLoader->checkMod($moduleName))) {
                 continue;
             }
-
-            $pageId = $this->extractPageIdFromShortcutUrl($row['url']);
-
-            if (!$backendUser->isAdmin()) {
-                if (MathUtility::canBeInterpretedAsInteger($pageId)) {
-                    // Check for webmount access
-                    if ($backendUser->isInWebMount($pageId) === null) {
-                        continue;
-                    }
-                    // Check for record access
-                    $pageRow = BackendUtility::getRecord('pages', $pageId);
-
-                    if ($pageRow === null) {
-                        continue;
-                    }
-
-                    if (!$backendUser->doesUserHaveAccess($pageRow, Permission::PAGE_SHOW)) {
-                        continue;
-                    }
+            if (($pageId = ((int)($arguments['id'] ?? 0))) > 0 && !$backendUser->isAdmin()) {
+                // Check for webmount access
+                if ($backendUser->isInWebMount($pageId) === null) {
+                    continue;
+                }
+                // Check for record access
+                $pageRow = BackendUtility::getRecord('pages', $pageId);
+                if ($pageRow === null || !$backendUser->doesUserHaveAccess($pageRow, Permission::PAGE_SHOW)) {
+                    continue;
                 }
             }
 
-            $moduleParts = explode('_', $moduleName);
             $shortcutGroup = (int)$row['sc_group'];
-
             if ($shortcutGroup && $lastGroup !== $shortcutGroup && $shortcutGroup !== self::SUPERGLOBAL_GROUP) {
                 $shortcut['groupLabel'] = $this->getShortcutGroupLabel($shortcutGroup);
             }
-
             $lastGroup = $shortcutGroup;
 
-            if ($row['description']) {
-                $shortcut['label'] = $row['description'];
-            } else {
-                $shortcut['label'] = GeneralUtility::fixed_lgd_cs(rawurldecode($queryParts['query']), 150);
+            $description = $row['description'] ?? '';
+            // Empty description should usually never happen since not defining such, is deprecated and a
+            // fallback is in place, at least for v11. Only manual inserts could lead to an empty description.
+            // @todo Can be removed in v12 since setting a display name is mandatory then
+            if ($description === '') {
+                $moduleLabel = (string)($this->moduleLoader->getLabelsForModule($moduleName)['shortdescription'] ?? '');
+                if ($moduleLabel !== '') {
+                    $description = $this->getLanguageService()->sL($moduleLabel);
+                }
             }
 
-            $shortcut['group'] = $shortcutGroup;
-            $shortcut['icon'] = $this->getShortcutIcon($row, $shortcut);
-            $shortcut['iconTitle'] = $this->getShortcutIconTitle($shortcut['label'], $row['module_name'], $row['M_module_name']);
-            $shortcut['action'] = 'jump(' . GeneralUtility::quoteJSvalue($this->getTokenUrl($row['url'])) . ',' . GeneralUtility::quoteJSvalue($moduleName) . ',' . GeneralUtility::quoteJSvalue($moduleParts[0]) . ', ' . (int)$pageId . ');';
+            $shortcutUrl = (string)GeneralUtility::makeInstance(UriBuilder::class)->buildUriFromRoute($routeIdentifier, $arguments);
 
+            $shortcut['group'] = $shortcutGroup;
+            $shortcut['icon'] = $this->getShortcutIcon($routeIdentifier, $moduleName, $shortcut);
+            $shortcut['label'] = $description;
+            $shortcut['action'] = 'jump('
+                . GeneralUtility::quoteJSvalue($shortcutUrl)
+                . ',' . GeneralUtility::quoteJSvalue($moduleName)
+                . ',' . GeneralUtility::quoteJSvalue($moduleName)
+                . ', ' . $pageId . ');';
             $shortcuts[] = $shortcut;
         }
 
@@ -606,15 +575,16 @@ class ShortcutRepository
     /**
      * Gets the icon for the shortcut
      *
-     * @param array $row
+     * @param string $routeIdentifier
+     * @param string $moduleName
      * @param array $shortcut
      * @return string Shortcut icon as img tag
      */
-    protected function getShortcutIcon(array $row, array $shortcut): string
+    protected function getShortcutIcon(string $routeIdentifier, string $moduleName, array $shortcut): string
     {
         $selectFields = [];
-        switch ($row['module_name']) {
-            case 'xMOD_alt_doc.php':
+        switch ($routeIdentifier) {
+            case 'record_edit':
                 $table = $shortcut['table'];
                 $recordid = $shortcut['recordid'];
                 $icon = '';
@@ -644,8 +614,7 @@ class ShortcutRepository
                         $selectFields[] = 't3ver_oid';
                     }
 
-                    $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
-                        ->getQueryBuilderForTable($table);
+                    $queryBuilder = $this->connectionPool->getQueryBuilderForTable($table);
                     $queryBuilder->select(...array_unique(array_values($selectFields)))
                         ->from($table)
                         ->where(
@@ -670,12 +639,11 @@ class ShortcutRepository
                 break;
             default:
                 $iconIdentifier = '';
-                $moduleName = $row['module_name'];
 
                 if (strpos($moduleName, '_') !== false) {
                     [$mainModule, $subModule] = explode('_', $moduleName, 2);
                     $iconIdentifier = $this->moduleLoader->modules[$mainModule]['sub'][$subModule]['iconIdentifier'];
-                } elseif (!empty($moduleName)) {
+                } elseif ($moduleName !== '') {
                     $iconIdentifier = $this->moduleLoader->modules[$moduleName]['iconIdentifier'];
                 }
 
@@ -690,95 +658,52 @@ class ShortcutRepository
     }
 
     /**
-     * Returns title for the shortcut icon
+     * Get the module name from the resolved route or by static mapping for some special cases.
      *
-     * @param string $shortcutLabel Shortcut label
-     * @param string $moduleName Backend module name (key)
-     * @param string $parentModuleName Parent module label
-     * @return string Title for the shortcut icon
+     * @param string $routeIdentifier
+     * @return string
      */
-    protected function getShortcutIconTitle(string $shortcutLabel, string $moduleName, string $parentModuleName = ''): string
+    protected function getModuleNameFromRouteIdentifier(string $routeIdentifier): string
     {
-        $languageService = $this->getLanguageService();
-
-        if (strpos($moduleName, 'xMOD_') === 0) {
-            $title = substr($moduleName, 5);
-        } else {
-            [$mainModule, $subModule] = explode('_', $moduleName);
-            $mainModuleLabels = $this->moduleLoader->getLabelsForModule($mainModule);
-            $title = $languageService->sL($mainModuleLabels['title']);
-
-            if (!empty($subModule)) {
-                $subModuleLabels = $this->moduleLoader->getLabelsForModule($moduleName);
-                $title .= '>' . $languageService->sL($subModuleLabels['title']);
-            }
-        }
-
-        if ($parentModuleName) {
-            $title .= ' (' . $parentModuleName . ')';
+        if ($this->isSpecialRoute($routeIdentifier)) {
+            return $routeIdentifier;
         }
 
-        $title .= ': ' . $shortcutLabel;
-
-        return $title;
+        $route = $this->getRoute($routeIdentifier);
+        return $route !== null ? (string)($route->getOption('moduleName') ?? '') : '';
     }
 
     /**
-     * Return the ID of the page in the URL if found.
+     * Get the route for a given route identifier
      *
-     * @param string $url The URL of the current shortcut link
-     * @return int If a page ID was found, it is returned. Otherwise: 0
+     * @param string $routeIdentifier
+     * @return Route|null
      */
-    protected function extractPageIdFromShortcutUrl(string $url): int
+    protected function getRoute(string $routeIdentifier): ?Route
     {
-        return (int)preg_replace('/.*[\\?&]id=([^&]+).*/', '$1', $url);
+        return GeneralUtility::makeInstance(Router::class)->getRoutes()[$routeIdentifier] ?? null;
     }
 
     /**
-     * Adds the correct token, if the url is an index.php script
-     * @todo: this needs love
+     * Check if a route for the given identifier exists
      *
-     * @param string $url
-     * @return string
+     * @param string $routeIdentifier
+     * @return bool
      */
-    protected function getTokenUrl(string $url): string
+    protected function routeExists(string $routeIdentifier): bool
     {
-        $parsedUrl = parse_url($url);
-        $parameters = [];
-        parse_str($parsedUrl['query'] ?? '', $parameters);
-
-        $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
-        // parse the returnUrl and replace the module token of it
-        if (!empty($parameters['returnUrl'])) {
-            $parsedReturnUrl = parse_url($parameters['returnUrl']);
-            $returnUrlParameters = [];
-            parse_str($parsedReturnUrl['query'] ?? '', $returnUrlParameters);
-
-            if (strpos($parsedReturnUrl['path'] ?? '', 'index.php') !== false && !empty($returnUrlParameters['route'])) {
-                $module = $returnUrlParameters['route'];
-                $parameters['returnUrl'] = (string)$uriBuilder->buildUriFromRoutePath($module, $returnUrlParameters);
-                $url = $parsedUrl['path'] . '?' . http_build_query($parameters, '', '&', PHP_QUERY_RFC3986);
-            }
-        }
-
-        if (strpos($parsedUrl['path'], 'index.php') !== false && isset($parameters['route'])) {
-            $routePath = $parameters['route'];
-            /** @var \TYPO3\CMS\Backend\Routing\Router $router */
-            $router = GeneralUtility::makeInstance(Router::class);
-
-            try {
-                $route = $router->match($routePath);
+        return $this->getRoute($routeIdentifier) !== null;
+    }
 
-                if ($route) {
-                    $routeIdentifier = $route->getOption('_identifier');
-                    unset($parameters['route']);
-                    $url = (string)$uriBuilder->buildUriFromRoute($routeIdentifier, $parameters);
-                }
-            } catch (ResourceNotFoundException $e) {
-                $url = '';
-            }
-        }
-        return $url;
+    /**
+     * Check if given route identifier is a special "no module" route
+     *
+     * @param string $routeIdentifier
+     * @return bool
+     */
+    protected function isSpecialRoute(string $routeIdentifier): bool
+    {
+        return in_array($routeIdentifier, ['record_edit', 'file_edit', 'wizard_rte'], true);
     }
 
     /**
diff --git a/typo3/sysext/backend/Classes/Controller/EditDocumentController.php b/typo3/sysext/backend/Classes/Controller/EditDocumentController.php
index 45a8787d08be0ba10e35686d0cd8ea8317436928..c0b4ae3cfa892e0e4e12995110f3a5a4050c6e82 100644
--- a/typo3/sysext/backend/Classes/Controller/EditDocumentController.php
+++ b/typo3/sysext/backend/Classes/Controller/EditDocumentController.php
@@ -1788,7 +1788,6 @@ class EditDocumentController
         if ($this->returnUrl !== $this->getCloseUrl()) {
             $queryParams = $request->getQueryParams();
             $potentialArguments = [
-                'returnUrl',
                 'edit',
                 'defVals',
                 'overrideVals',
@@ -1806,7 +1805,7 @@ class EditDocumentController
             }
             $shortCutButton = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar()->makeShortcutButton();
             $shortCutButton
-                ->setModuleName('xMOD_alt_doc.php')
+                ->setRouteIdentifier('record_edit')
                 ->setDisplayName($this->getShortcutTitle($request))
                 ->setArguments($arguments);
             $buttonBar->addButton($shortCutButton, $position, $group);
diff --git a/typo3/sysext/backend/Classes/Controller/HelpController.php b/typo3/sysext/backend/Classes/Controller/HelpController.php
index c0e41b648a391b3e0c327ca7ac413644dcc7159b..c516e5d97ae7e4e5f58017f4225e82ef331572e5 100644
--- a/typo3/sysext/backend/Classes/Controller/HelpController.php
+++ b/typo3/sysext/backend/Classes/Controller/HelpController.php
@@ -174,10 +174,9 @@ class HelpController
 
         $action = $request->getQueryParams()['action'] ?? $request->getParsedBody()['action'] ?? 'index';
         $shortcutButton = $buttonBar->makeShortcutButton()
-            ->setModuleName('help_cshmanual')
+            ->setRouteIdentifier('help_cshmanual')
             ->setDisplayName($this->getShortcutTitle($request))
             ->setArguments([
-                'route' => $request->getQueryParams()['route'],
                 'action' => $action,
                 'table' => $request->getQueryParams()['table'] ?? '',
                 'field' => $request->getQueryParams()['field'] ?? ''
diff --git a/typo3/sysext/backend/Classes/Controller/PageLayoutController.php b/typo3/sysext/backend/Classes/Controller/PageLayoutController.php
index ba560d93af8886e697a632b7fc238b4e4297ede6..f33c01e361d32cdba9d3e57edbbbb1187a4b6e7d 100644
--- a/typo3/sysext/backend/Classes/Controller/PageLayoutController.php
+++ b/typo3/sysext/backend/Classes/Controller/PageLayoutController.php
@@ -764,10 +764,9 @@ class PageLayoutController
         }
         // Shortcut
         $shortcutButton = $this->buttonBar->makeShortcutButton()
-            ->setModuleName($this->moduleName)
+            ->setRouteIdentifier($this->moduleName)
             ->setDisplayName($this->getShortcutTitle())
             ->setArguments([
-                'route' => $request->getQueryParams()['route'],
                 'id' => (int)$this->id,
                 'SET' => [
                     'tt_content_showHidden' => (bool)$this->MOD_SETTINGS['tt_content_showHidden'],
diff --git a/typo3/sysext/backend/Classes/Controller/ShortcutController.php b/typo3/sysext/backend/Classes/Controller/ShortcutController.php
index eb6ec83576967f0ff4624e3d2d1fc4d5bdac1b63..093c87dbe564d33f24689a9659668185f6eabd17 100644
--- a/typo3/sysext/backend/Classes/Controller/ShortcutController.php
+++ b/typo3/sysext/backend/Classes/Controller/ShortcutController.php
@@ -83,16 +83,16 @@ class ShortcutController
         $result = 'success';
         $parsedBody = $request->getParsedBody();
         $queryParams = $request->getQueryParams();
-        $url = rawurldecode($parsedBody['url'] ?? $queryParams['url'] ?? '');
+        $routeIdentifier = $parsedBody['routeIdentifier'] ?? $queryParams['routeIdentifier'] ?? '';
+        $arguments = $parsedBody['arguments'] ?? $queryParams['arguments'] ?? '';
 
-        if ($this->shortcutRepository->shortcutExists($url)) {
+        if ($routeIdentifier === '') {
+            $result = 'missingRoute';
+        } elseif ($this->shortcutRepository->shortcutExists($routeIdentifier, $arguments)) {
             $result = 'alreadyExists';
         } else {
-            $moduleName = $parsedBody['module'] ?? '';
-            $parentModuleName = $parsedBody['motherModName'] ?? '';
-            $shortcutName = $parsedBody['displayName'] ?? '';
-            $success = $this->shortcutRepository->addShortcut($url, $moduleName, $parentModuleName, $shortcutName);
-
+            $shortcutName = $parsedBody['displayName'] ?? $queryParams['arguments'] ?? '';
+            $success = $this->shortcutRepository->addShortcut($routeIdentifier, $arguments, $shortcutName);
             if (!$success) {
                 $result = 'failed';
             }
diff --git a/typo3/sysext/backend/Classes/Controller/SiteConfigurationController.php b/typo3/sysext/backend/Classes/Controller/SiteConfigurationController.php
index 2aec52beab1752d40770a91c41da115a0e558c88..45e9cff49ac8be42f75672d5a129c85f87eb3cd7 100644
--- a/typo3/sysext/backend/Classes/Controller/SiteConfigurationController.php
+++ b/typo3/sysext/backend/Classes/Controller/SiteConfigurationController.php
@@ -640,7 +640,7 @@ class SiteConfigurationController
             ->setIcon($iconFactory->getIcon('actions-refresh', Icon::SIZE_SMALL));
         $buttonBar->addButton($reloadButton, ButtonBar::BUTTON_POSITION_RIGHT);
         $shortcutButton = $buttonBar->makeShortcutButton()
-            ->setModuleName('site_configuration')
+            ->setRouteIdentifier('site_configuration')
             ->setDisplayName($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_module.xlf:mlang_labels_tablabel'))
             ->setArguments([
                 'route' => $route
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 3d4753bdb8c3d51d5d54d4c8d971c21be4bbde2a..9cb2197640761f7ceb2f7135f7e6b8592d1bb1a7 100644
--- a/typo3/sysext/backend/Classes/Template/Components/Buttons/Action/ShortcutButton.php
+++ b/typo3/sysext/backend/Classes/Template/Components/Buttons/Action/ShortcutButton.php
@@ -16,6 +16,7 @@
 namespace TYPO3\CMS\Backend\Template\Components\Buttons\Action;
 
 use TYPO3\CMS\Backend\Backend\Shortcut\ShortcutRepository;
+use TYPO3\CMS\Backend\Routing\Router;
 use TYPO3\CMS\Backend\Template\Components\ButtonBar;
 use TYPO3\CMS\Backend\Template\Components\Buttons\ButtonInterface;
 use TYPO3\CMS\Backend\Template\Components\Buttons\PositionInterface;
@@ -25,7 +26,6 @@ use TYPO3\CMS\Core\Imaging\Icon;
 use TYPO3\CMS\Core\Imaging\IconFactory;
 use TYPO3\CMS\Core\Localization\LanguageService;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
-use TYPO3\CMS\Core\Utility\HttpUtility;
 
 /**
  * ShortcutButton
@@ -36,17 +36,25 @@ use TYPO3\CMS\Core\Utility\HttpUtility;
  * EXAMPLE USAGE TO ADD A SHORTCUT BUTTON:
  *
  * $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
+ * $pageId = (int)($request->getQueryParams()['id'] ?? 0);
  * $myButton = $buttonBar->makeShortcutButton()
+ *       ->setRouteIdentifier('web_view')
+ *       ->setDisplayName('View page ' . $pageId)
  *       ->setArguments([
- *          'route' => $request->getQueryParams()['route']
- *       ])
- *       ->setModuleName('my_info');
+ *          'id' => $pageId
+ *       ]);
  * $buttonBar->addButton($myButton);
  */
 class ShortcutButton implements ButtonInterface, PositionInterface
 {
+    /**
+     * @var string The route identifier of the shortcut
+     */
+    protected string $routeIdentifier = '';
+
     /**
      * @var string
+     * @deprecated since v11, will be removed in v12
      */
     protected $moduleName = '';
 
@@ -72,13 +80,37 @@ class ShortcutButton implements ButtonInterface, PositionInterface
      */
     protected $getVariables = [];
 
+    /**
+     * Gets the route identifier for the shortcut.
+     *
+     * @return string
+     */
+    public function getRouteIdentifier(): string
+    {
+        return $this->routeIdentifier;
+    }
+
+    /**
+     * Sets the route identifier for the shortcut.
+     *
+     * @param string $routeIdentifier
+     * @return ShortcutButton
+     */
+    public function setRouteIdentifier(string $routeIdentifier): self
+    {
+        $this->routeIdentifier = $routeIdentifier;
+        return $this;
+    }
+
     /**
      * Gets the name of the module.
      *
      * @return string
+     * @deprecated since v11, will be removed in v12
      */
     public function getModuleName()
     {
+        trigger_error('Method getModuleName() is deprecated and will be removed in v12. Use getRouteIdentifier() instead.', E_USER_DEPRECATED);
         return $this->moduleName;
     }
 
@@ -87,9 +119,11 @@ class ShortcutButton implements ButtonInterface, PositionInterface
      *
      * @param string $moduleName
      * @return ShortcutButton
+     * @deprecated since v11, will be removed in v12
      */
     public function setModuleName($moduleName)
     {
+        trigger_error('Method setModuleName() is deprecated and will be removed in v12. Use setRouteIdentifier() instead.', E_USER_DEPRECATED);
         $this->moduleName = $moduleName;
         return $this;
     }
@@ -213,7 +247,7 @@ class ShortcutButton implements ButtonInterface, PositionInterface
      */
     public function isValid()
     {
-        return !empty($this->moduleName);
+        return $this->moduleName !== '' || $this->routeIdentifier !== '' || (string)($this->arguments['route'] ?? '') !== '';
     }
 
     /**
@@ -237,7 +271,7 @@ class ShortcutButton implements ButtonInterface, PositionInterface
             if ($this->displayName === '') {
                 trigger_error('Creating a shortcut button without a display name is deprecated and fallbacks will be removed in v12. Please use ShortcutButton->setDisplayName() to set a display name.', E_USER_DEPRECATED);
             }
-            if (!empty($this->arguments)) {
+            if (!empty($this->routeIdentifier) || !empty($this->arguments)) {
                 $shortcutMarkup = $this->createShortcutMarkup();
             } else {
                 // @deprecated since v11, the else branch will be removed in v12. Deprecation thrown by makeShortcutIcon() below
@@ -261,36 +295,124 @@ class ShortcutButton implements ButtonInterface, PositionInterface
 
     protected function createShortcutMarkup(): string
     {
-        $moduleName = $this->moduleName;
-        $storeUrl = HttpUtility::buildQueryString($this->arguments, '&');
+        $routeIdentifier = $this->routeIdentifier;
+        $arguments = $this->arguments;
+        $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
 
-        // Find out if this shortcut exists already. Note this is a hack based on the fact
-        // that sys_be_shortcuts stores the entire request string and not just needed params as array.
-        $pathInfo = parse_url(GeneralUtility::getIndpEnv('REQUEST_URI'));
-        $shortcutUrl = $pathInfo['path'] . '?' . $storeUrl;
-        $shortcutRepository = GeneralUtility::makeInstance(ShortcutRepository::class);
-        $shortcutExist = $shortcutRepository->shortcutExists($shortcutUrl);
+        if (strpos($routeIdentifier, '/') !== false) {
+            trigger_error('Automatic fallback for the route path is deprecated and will be removed in v12.', E_USER_DEPRECATED);
+            $routeIdentifier = $this->getRouteIdentifierByRoutePath($routeIdentifier);
+        }
 
-        $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
-        if ($shortcutExist) {
-            $shortcutMarkup = '<a class="active btn btn-default btn-sm" title="">'
+        if ($routeIdentifier === '' && $this->moduleName !== '') {
+            trigger_error('Using ShortcutButton::$moduleNname is deprecated and will be removed in v12. Use ShortcutButton::$routeIdentifier instead.', E_USER_DEPRECATED);
+            $routeIdentifier = $this->getRouteIdentifierByModuleName($this->moduleName);
+        }
+
+        if (isset($arguments['route'])) {
+            trigger_error('Using route as an argument is deprecated and will be removed in v12. Set the route identifier with ShortcutButton::setRouteIdentifier() instead.', E_USER_DEPRECATED);
+            if ($routeIdentifier === '' && is_string($arguments['route'])) {
+                $routeIdentifier = $this->getRouteIdentifierByRoutePath($arguments['route']);
+            }
+            unset($arguments['route']);
+        }
+
+        // No route found so no shortcut button will be rendered
+        if ($routeIdentifier === '' || !$this->routeExists($routeIdentifier)) {
+            return '';
+        }
+
+        // returnUrl will not longer be stored in the database
+        unset($arguments['returnUrl']);
+
+        // Encode arguments to be stored in the database
+        $arguments = json_encode($arguments);
+
+        if (GeneralUtility::makeInstance(ShortcutRepository::class)->shortcutExists($routeIdentifier, $arguments)) {
+            return '<a class="active btn btn-default btn-sm" title="">'
                 . $iconFactory->getIcon('actions-system-shortcut-active', Icon::SIZE_SMALL)->render()
                 . '</a>';
-        } else {
-            $languageService = $this->getLanguageService();
-            $confirmationText = $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.makeBookmark');
-            $onClick = 'top.TYPO3.ShortcutMenu.createShortcut('
-                . GeneralUtility::quoteJSvalue(rawurlencode($moduleName))
-                . ', ' . GeneralUtility::quoteJSvalue(rawurlencode($shortcutUrl))
-                . ', ' . GeneralUtility::quoteJSvalue($confirmationText)
-                . ', \'\''
-                . ', this'
-                . ', ' . GeneralUtility::quoteJSvalue($this->displayName) . ');return false;';
-            $shortcutMarkup = '<a href="#" class="btn btn-default btn-sm" onclick="' . htmlspecialchars($onClick) . '" title="'
-                . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.makeBookmark')) . '">'
-                . $iconFactory->getIcon('actions-system-shortcut-new', Icon::SIZE_SMALL)->render() . '</a>';
         }
-        return $shortcutMarkup;
+
+        $confirmationText = $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.makeBookmark');
+        $onClick = 'top.TYPO3.ShortcutMenu.createShortcut('
+            . GeneralUtility::quoteJSvalue($routeIdentifier)
+            . ', ' . GeneralUtility::quoteJSvalue($arguments)
+            . ', ' . GeneralUtility::quoteJSvalue($this->displayName)
+            . ', ' . GeneralUtility::quoteJSvalue($confirmationText)
+            . ', this);return false;';
+
+        return '<a href="#" class="btn btn-default btn-sm" onclick="' . htmlspecialchars($onClick) . '" title="' . htmlspecialchars($confirmationText) . '">'
+            . $iconFactory->getIcon('actions-system-shortcut-new', Icon::SIZE_SMALL)->render()
+            . '</a>';
+    }
+
+    /**
+     * Map a given route path to its route identifier
+     *
+     * @param string $routePath
+     * @return string
+     * @deprecated Only for backwards compatibility. Can be removed in v12.
+     */
+    protected function getRouteIdentifierByRoutePath(string $routePath): string
+    {
+        foreach ($this->getRoutes() as $identifier => $route) {
+            if ($route->getPath() === $routePath
+                && (
+                    $route->hasOption('moduleName')
+                    || in_array($identifier, ['record_edit', 'file_edit', 'wizard_rte'], true)
+                )
+            ) {
+                return $identifier;
+            }
+        }
+
+        return '';
+    }
+
+    /**
+     * Map a given module name to its route identifier by respecting some special cases
+     *
+     * @param string $moduleName
+     * @return string
+     * @deprecated Only for backwards compatibility. Can be removed in v12.
+     */
+    protected function getRouteIdentifierByModuleName(string $moduleName): string
+    {
+        $identifier = '';
+
+        // Special case module names
+        switch ($moduleName) {
+            case 'xMOD_alt_doc.php':
+                $identifier = 'record_edit';
+                break;
+            case 'file_edit':
+            case 'wizard_rte':
+                $identifier = $moduleName;
+                break;
+        }
+
+        if ($identifier !== '') {
+            return $identifier;
+        }
+
+        foreach ($this->getRoutes() as $identifier => $route) {
+            if ($route->hasOption('moduleName') && $route->getOption('moduleName') === $moduleName) {
+                return $identifier;
+            }
+        }
+
+        return '';
+    }
+
+    protected function routeExists(string $routeIdentifier): bool
+    {
+        return (bool)($this->getRoutes()[$routeIdentifier] ?? false);
+    }
+
+    protected function getRoutes(): iterable
+    {
+        return GeneralUtility::makeInstance(Router::class)->getRoutes();
     }
 
     protected function getBackendUser(): BackendUserAuthentication
diff --git a/typo3/sysext/backend/Classes/Template/ModuleTemplate.php b/typo3/sysext/backend/Classes/Template/ModuleTemplate.php
index fc19116df4b39d9f94c660e13c4b5b3861fe78aa..0aa03794a2ae0381b3a554e307ff193a37b769fd 100644
--- a/typo3/sysext/backend/Classes/Template/ModuleTemplate.php
+++ b/typo3/sysext/backend/Classes/Template/ModuleTemplate.php
@@ -16,6 +16,7 @@
 namespace TYPO3\CMS\Backend\Template;
 
 use TYPO3\CMS\Backend\Backend\Shortcut\ShortcutRepository;
+use TYPO3\CMS\Backend\Routing\Router;
 use TYPO3\CMS\Backend\Template\Components\DocHeaderComponent;
 use TYPO3\CMS\Backend\Utility\BackendUtility;
 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
@@ -540,32 +541,28 @@ class ModuleTemplate
         if (GeneralUtility::_GET('route') !== null) {
             $storeUrl = '&route=' . $moduleName . $storeUrl;
         }
-        if ((int)$motherModName === 1) {
-            $motherModule = 'top.currentModuleLoaded';
-        } elseif (is_string($motherModName) && $motherModName !== '') {
-            $motherModule = GeneralUtility::quoteJSvalue($motherModName);
-        } else {
-            $motherModule = '\'\'';
-        }
-        $confirmationText = GeneralUtility::quoteJSvalue(
-            $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.makeBookmark')
-        );
 
         $shortcutUrl = $pathInfo['path'] . '?' . $storeUrl;
-        $shortcutRepository = GeneralUtility::makeInstance(ShortcutRepository::class);
-        $shortcutExist = $shortcutRepository->shortcutExists($shortcutUrl);
 
-        if ($shortcutExist) {
+        // We simply let the above functionality as it is for maximum backwards compatibility and now
+        // just process the generated $shortcutUrl to match the new format (routeIdentifier & arguments)
+        [$routeIdentifier, $arguments] = $this->getCreateShortcutProperties($shortcutUrl);
+
+        if (GeneralUtility::makeInstance(ShortcutRepository::class)->shortcutExists($routeIdentifier, $arguments)) {
             return '<a class="active ' . htmlspecialchars($classes) . '" title="">' .
             $this->iconFactory->getIcon('actions-system-shortcut-active', Icon::SIZE_SMALL)->render() . '</a>';
         }
 
-        $url = GeneralUtility::quoteJSvalue(rawurlencode($shortcutUrl));
-        $onClick = 'top.TYPO3.ShortcutMenu.createShortcut(' . GeneralUtility::quoteJSvalue(rawurlencode($modName)) .
-            ', ' . $url . ', ' . $confirmationText . ', ' . $motherModule . ', this, ' . GeneralUtility::quoteJSvalue($displayName) . ');return false;';
+        $confirmationText =  $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.makeBookmark');
+        $onClick = 'top.TYPO3.ShortcutMenu.createShortcut('
+            . GeneralUtility::quoteJSvalue($routeIdentifier)
+            . ', ' . GeneralUtility::quoteJSvalue($arguments)
+            . ', ' . GeneralUtility::quoteJSvalue($displayName)
+            . ', ' . GeneralUtility::quoteJSvalue($confirmationText)
+            . ', this);return false;';
 
         return '<a href="#" class="' . htmlspecialchars($classes) . '" onclick="' . htmlspecialchars($onClick) . '" title="' .
-        htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.makeBookmark')) . '">' .
+        htmlspecialchars($confirmationText) . '">' .
         $this->iconFactory->getIcon('actions-system-shortcut-new', Icon::SIZE_SMALL)->render() . '</a>';
     }
 
@@ -592,6 +589,41 @@ class ModuleTemplate
         return HttpUtility::buildQueryString($storeArray, '&');
     }
 
+    /**
+     * Process the generated shortcut url and return properties needed for the
+     * shortcut registration with route identifier and JSON encoded arguments.
+     *
+     * @param string $shortcutUrl
+     *
+     * @return array
+     * @deprecated Only for backwards compatibility. Can be removed in v12
+     */
+    protected function getCreateShortcutProperties(string $shortcutUrl): array
+    {
+        $routeIdentifier = '';
+        $arguments = [];
+
+        parse_str(parse_url($shortcutUrl)['query'] ?? '', $arguments);
+        $routePath = (string)($arguments['route'] ?? '');
+
+        if ($routePath !== '') {
+            foreach (GeneralUtility::makeInstance(Router::class)->getRoutes() as $identifier => $route) {
+                if ($route->getPath() === $routePath
+                    && (
+                        $route->hasOption('moduleName')
+                        || in_array($identifier, ['record_edit', 'file_edit', 'wizard_rte'], true)
+                    )
+                ) {
+                    $routeIdentifier = $identifier;
+                }
+            }
+        }
+
+        unset($arguments['route'], $arguments['returnUrl']);
+
+        return [$routeIdentifier, json_encode($arguments)];
+    }
+
     /**
      * Retrieves configured favicon for backend (with fallback)
      *
diff --git a/typo3/sysext/backend/Classes/ViewHelpers/ModuleLayout/Button/ShortcutButtonViewHelper.php b/typo3/sysext/backend/Classes/ViewHelpers/ModuleLayout/Button/ShortcutButtonViewHelper.php
index e342d05f4245c53941b112a3a4605dcd44aa3187..d5431f529e73ec20b488865f10be7b97fc133ace 100644
--- a/typo3/sysext/backend/Classes/ViewHelpers/ModuleLayout/Button/ShortcutButtonViewHelper.php
+++ b/typo3/sysext/backend/Classes/ViewHelpers/ModuleLayout/Button/ShortcutButtonViewHelper.php
@@ -17,6 +17,7 @@ declare(strict_types=1);
 
 namespace TYPO3\CMS\Backend\ViewHelpers\ModuleLayout\Button;
 
+use TYPO3\CMS\Backend\Routing\Router;
 use TYPO3\CMS\Backend\Template\Components\ButtonBar;
 use TYPO3\CMS\Backend\Template\Components\Buttons\ButtonInterface;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
@@ -37,7 +38,7 @@ use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
  * Default::
  *
  *    <be:moduleLayout>
- *        <be:moduleLayout.button.shortcutButton displayName="Shortcut label" arguments="{route: '{route}'"/>
+ *        <be:moduleLayout.button.shortcutButton displayName="Shortcut label" arguments="{parameter: '{someValue}'}"/>
  *    </be:moduleLayout>
  */
 class ShortcutButtonViewHelper extends AbstractButtonViewHelper
@@ -50,7 +51,8 @@ class ShortcutButtonViewHelper extends AbstractButtonViewHelper
     public function initializeArguments(): void
     {
         parent::initializeArguments();
-        $this->registerArgument('displayName', 'string', 'Name for the shortcut');
+        // This will be required in v12. Deprecation for empty argument logged by ModuleTemplate->makeShortcutIcon()
+        $this->registerArgument('displayName', 'string', 'Name for the shortcut', false, '');
         $this->registerArgument('arguments', 'array', 'List of relevant GET variables as key/values list to store', false, []);
         // @deprecated since v11, will be removed in v12. Use 'arguments' instead. Deprecation logged by ModuleTemplate->makeShortcutIcon()
         $this->registerArgument('getVars', 'array', 'List of additional GET variables to store. The current id, module and all module arguments will always be stored', false, []);
@@ -62,9 +64,11 @@ class ShortcutButtonViewHelper extends AbstractButtonViewHelper
         $moduleName = $currentRequest->getPluginName();
         $displayName = $arguments['displayName'];
 
-        $shortcutButton = $buttonBar->makeShortcutButton()
+        // Initialize the shortcut button
+        $shortcutButton = $buttonBar
+            ->makeShortcutButton()
             ->setDisplayName($displayName)
-            ->setModuleName($moduleName);
+            ->setRouteIdentifier(self::getRouteIdentifierForModuleName($moduleName));
 
         if (!empty($arguments['arguments'])) {
             $shortcutButton->setArguments($arguments['arguments']);
@@ -81,4 +85,21 @@ class ShortcutButtonViewHelper extends AbstractButtonViewHelper
 
         return $shortcutButton;
     }
+
+    /**
+     * Tries to fetch the route identifier for a given module name
+     *
+     * @param string $moduleName
+     * @return string
+     */
+    protected static function getRouteIdentifierForModuleName(string $moduleName): string
+    {
+        foreach (GeneralUtility::makeInstance(Router::class)->getRoutes() as $identifier => $route) {
+            if ($route->hasOption('moduleName') && $route->getOption('moduleName') === $moduleName) {
+                return $identifier;
+            }
+        }
+
+        return '';
+    }
 }
diff --git a/typo3/sysext/backend/Configuration/Services.yaml b/typo3/sysext/backend/Configuration/Services.yaml
index 4b04fb44760f4044d5a9201ab2336e0d6c89f2b1..29fc73a5a67f6bc9401d1133c9b36e69c390e7d5 100644
--- a/typo3/sysext/backend/Configuration/Services.yaml
+++ b/typo3/sysext/backend/Configuration/Services.yaml
@@ -38,6 +38,9 @@ services:
   TYPO3\CMS\Backend\History\RecordHistoryRollback:
     public: true
 
+  TYPO3\CMS\Backend\Backend\Shortcut\ShortcutRepository:
+    public: true
+
   TYPO3\CMS\Backend\Controller\AboutController:
     tags: ['backend.controller']
 
diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/Toolbar/ShortcutMenu.js b/typo3/sysext/backend/Resources/Public/JavaScript/Toolbar/ShortcutMenu.js
index 57f414b7211df9a78f8d8b618841f9b45945473b..0ca34a5707f199424a406b3e63b3444d21f3bb91 100644
--- a/typo3/sysext/backend/Resources/Public/JavaScript/Toolbar/ShortcutMenu.js
+++ b/typo3/sysext/backend/Resources/Public/JavaScript/Toolbar/ShortcutMenu.js
@@ -10,4 +10,4 @@
  *
  * The TYPO3 project - inspiring people to share!
  */
-var __importDefault=this&&this.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};define(["require","exports","jquery","TYPO3/CMS/Core/Ajax/AjaxRequest","../Icons","../Modal","../Notification","../Viewport"],(function(t,e,o,r,c,a,s,l){"use strict";var n;o=__importDefault(o),function(t){t.containerSelector="#typo3-cms-backend-backend-toolbaritems-shortcuttoolbaritem",t.toolbarIconSelector=".dropdown-toggle span.icon",t.toolbarMenuSelector=".dropdown-menu",t.shortcutItemSelector=".t3js-topbar-shortcut",t.shortcutDeleteSelector=".t3js-shortcut-delete",t.shortcutEditSelector=".t3js-shortcut-edit",t.shortcutFormTitleSelector='input[name="shortcut-title"]',t.shortcutFormGroupSelector='select[name="shortcut-group"]',t.shortcutFormSaveSelector=".shortcut-form-save",t.shortcutFormCancelSelector=".shortcut-form-cancel",t.shortcutFormSelector=".shortcut-form"}(n||(n={}));let u=new class{constructor(){this.initializeEvents=()=>{o.default(n.containerSelector).on("click",n.shortcutDeleteSelector,t=>{t.preventDefault(),t.stopImmediatePropagation(),this.deleteShortcut(o.default(t.currentTarget).closest(n.shortcutItemSelector))}).on("click",n.shortcutFormGroupSelector,t=>{t.preventDefault(),t.stopImmediatePropagation()}).on("click",n.shortcutEditSelector,t=>{t.preventDefault(),t.stopImmediatePropagation(),this.editShortcut(o.default(t.currentTarget).closest(n.shortcutItemSelector))}).on("click",n.shortcutFormSaveSelector,t=>{t.preventDefault(),t.stopImmediatePropagation(),this.saveShortcutForm(o.default(t.currentTarget).closest(n.shortcutFormSelector))}).on("submit",n.shortcutFormSelector,t=>{t.preventDefault(),t.stopImmediatePropagation(),this.saveShortcutForm(o.default(t.currentTarget).closest(n.shortcutFormSelector))}).on("click",n.shortcutFormCancelSelector,t=>{t.preventDefault(),t.stopImmediatePropagation(),this.refreshMenu()})},l.Topbar.Toolbar.registerEvent(this.initializeEvents)}createShortcut(t,e,s,l,u,i){void 0!==s&&a.confirm(TYPO3.lang["bookmark.create"],s).on("confirm.button.ok",a=>{const s=o.default(n.toolbarIconSelector,n.containerSelector),h=s.clone();c.getIcon("spinner-circle-light",c.sizes.small).then(t=>{s.replaceWith(t)}),new r(TYPO3.settings.ajaxUrls.shortcut_create).post({module:t,url:e,motherModName:l,displayName:i}).then(()=>{this.refreshMenu(),o.default(n.toolbarIconSelector,n.containerSelector).replaceWith(h),"object"==typeof u&&(c.getIcon("actions-system-shortcut-active",c.sizes.small).then(t=>{o.default(u).html(t)}),o.default(u).addClass("active"),o.default(u).attr("title",null),o.default(u).attr("onclick",null))}),o.default(a.currentTarget).trigger("modal-dismiss")}).on("confirm.button.cancel",t=>{o.default(t.currentTarget).trigger("modal-dismiss")})}deleteShortcut(t){a.confirm(TYPO3.lang["bookmark.delete"],TYPO3.lang["bookmark.confirmDelete"]).on("confirm.button.ok",e=>{new r(TYPO3.settings.ajaxUrls.shortcut_remove).post({shortcutId:t.data("shortcutid")}).then(()=>{this.refreshMenu()}),o.default(e.currentTarget).trigger("modal-dismiss")}).on("confirm.button.cancel",t=>{o.default(t.currentTarget).trigger("modal-dismiss")})}editShortcut(t){new r(TYPO3.settings.ajaxUrls.shortcut_editform).withQueryArguments({shortcutId:t.data("shortcutid"),shortcutGroup:t.data("shortcutgroup")}).get({cache:"no-cache"}).then(async t=>{o.default(n.containerSelector).find(n.toolbarMenuSelector).html(await t.resolve())})}saveShortcutForm(t){new r(TYPO3.settings.ajaxUrls.shortcut_saveform).post({shortcutId:t.data("shortcutid"),shortcutTitle:t.find(n.shortcutFormTitleSelector).val(),shortcutGroup:t.find(n.shortcutFormGroupSelector).val()}).then(()=>{s.success(TYPO3.lang["bookmark.savedTitle"],TYPO3.lang["bookmark.savedMessage"]),this.refreshMenu()})}refreshMenu(){new r(TYPO3.settings.ajaxUrls.shortcut_list).get({cache:"no-cache"}).then(async t=>{o.default(n.toolbarMenuSelector,n.containerSelector).html(await t.resolve())})}};return TYPO3.ShortcutMenu=u,u}));
\ No newline at end of file
+var __importDefault=this&&this.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};define(["require","exports","jquery","TYPO3/CMS/Core/Ajax/AjaxRequest","../Icons","../Modal","../Notification","../Viewport"],(function(t,e,o,r,c,a,s,l){"use strict";var n;o=__importDefault(o),function(t){t.containerSelector="#typo3-cms-backend-backend-toolbaritems-shortcuttoolbaritem",t.toolbarIconSelector=".dropdown-toggle span.icon",t.toolbarMenuSelector=".dropdown-menu",t.shortcutItemSelector=".t3js-topbar-shortcut",t.shortcutDeleteSelector=".t3js-shortcut-delete",t.shortcutEditSelector=".t3js-shortcut-edit",t.shortcutFormTitleSelector='input[name="shortcut-title"]',t.shortcutFormGroupSelector='select[name="shortcut-group"]',t.shortcutFormSaveSelector=".shortcut-form-save",t.shortcutFormCancelSelector=".shortcut-form-cancel",t.shortcutFormSelector=".shortcut-form"}(n||(n={}));let u=new class{constructor(){this.initializeEvents=()=>{o.default(n.containerSelector).on("click",n.shortcutDeleteSelector,t=>{t.preventDefault(),t.stopImmediatePropagation(),this.deleteShortcut(o.default(t.currentTarget).closest(n.shortcutItemSelector))}).on("click",n.shortcutFormGroupSelector,t=>{t.preventDefault(),t.stopImmediatePropagation()}).on("click",n.shortcutEditSelector,t=>{t.preventDefault(),t.stopImmediatePropagation(),this.editShortcut(o.default(t.currentTarget).closest(n.shortcutItemSelector))}).on("click",n.shortcutFormSaveSelector,t=>{t.preventDefault(),t.stopImmediatePropagation(),this.saveShortcutForm(o.default(t.currentTarget).closest(n.shortcutFormSelector))}).on("submit",n.shortcutFormSelector,t=>{t.preventDefault(),t.stopImmediatePropagation(),this.saveShortcutForm(o.default(t.currentTarget).closest(n.shortcutFormSelector))}).on("click",n.shortcutFormCancelSelector,t=>{t.preventDefault(),t.stopImmediatePropagation(),this.refreshMenu()})},l.Topbar.Toolbar.registerEvent(this.initializeEvents)}createShortcut(t,e,s,l,u){void 0!==l&&a.confirm(TYPO3.lang["bookmark.create"],l).on("confirm.button.ok",a=>{const l=o.default(n.toolbarIconSelector,n.containerSelector),i=l.clone();c.getIcon("spinner-circle-light",c.sizes.small).then(t=>{l.replaceWith(t)}),new r(TYPO3.settings.ajaxUrls.shortcut_create).post({routeIdentifier:t,arguments:e,displayName:s}).then(()=>{this.refreshMenu(),o.default(n.toolbarIconSelector,n.containerSelector).replaceWith(i),"object"==typeof u&&(c.getIcon("actions-system-shortcut-active",c.sizes.small).then(t=>{o.default(u).html(t)}),o.default(u).addClass("active"),o.default(u).attr("title",null),o.default(u).attr("onclick",null))}),o.default(a.currentTarget).trigger("modal-dismiss")}).on("confirm.button.cancel",t=>{o.default(t.currentTarget).trigger("modal-dismiss")})}deleteShortcut(t){a.confirm(TYPO3.lang["bookmark.delete"],TYPO3.lang["bookmark.confirmDelete"]).on("confirm.button.ok",e=>{new r(TYPO3.settings.ajaxUrls.shortcut_remove).post({shortcutId:t.data("shortcutid")}).then(()=>{this.refreshMenu()}),o.default(e.currentTarget).trigger("modal-dismiss")}).on("confirm.button.cancel",t=>{o.default(t.currentTarget).trigger("modal-dismiss")})}editShortcut(t){new r(TYPO3.settings.ajaxUrls.shortcut_editform).withQueryArguments({shortcutId:t.data("shortcutid"),shortcutGroup:t.data("shortcutgroup")}).get({cache:"no-cache"}).then(async t=>{o.default(n.containerSelector).find(n.toolbarMenuSelector).html(await t.resolve())})}saveShortcutForm(t){new r(TYPO3.settings.ajaxUrls.shortcut_saveform).post({shortcutId:t.data("shortcutid"),shortcutTitle:t.find(n.shortcutFormTitleSelector).val(),shortcutGroup:t.find(n.shortcutFormGroupSelector).val()}).then(()=>{s.success(TYPO3.lang["bookmark.savedTitle"],TYPO3.lang["bookmark.savedMessage"]),this.refreshMenu()})}refreshMenu(){new r(TYPO3.settings.ajaxUrls.shortcut_list).get({cache:"no-cache"}).then(async t=>{o.default(n.toolbarMenuSelector,n.containerSelector).html(await t.resolve())})}};return TYPO3.ShortcutMenu=u,u}));
\ No newline at end of file
diff --git a/typo3/sysext/backend/Tests/Functional/Backend/Fixtures/ShortcutsAddedResult.csv b/typo3/sysext/backend/Tests/Functional/Backend/Fixtures/ShortcutsAddedResult.csv
new file mode 100644
index 0000000000000000000000000000000000000000..bc7aed9a77ae77efce5714cf2bd84e582e5a1df3
--- /dev/null
+++ b/typo3/sysext/backend/Tests/Functional/Backend/Fixtures/ShortcutsAddedResult.csv
@@ -0,0 +1,11 @@
+"sys_be_shortcuts",,,,,,
+,"uid","userid","description","sc_group","route","arguments"
+,1,1,"Recordlist",1,"web_list","{""id"":123,""GET"":{""clipBoard"":1}}"
+,2,1,"Edit Content",1,"record_edit","{""edit"":{""tt_content"":{""113"":""edit""}}}"
+,3,1,"Page Layout - Group 2",2,"web_layout","{""id"":""123"",""SET"":{""tt_content_showHidden"":""1"",""function"":""1"",""language"":""0""}}"
+,4,1,"Invalid route identifier",1,"web_proc","{""id"":""123""}"
+,5,2,"Wrong user",1,"web_layout","{""id"":""123""}"
+,6,1,,1,"web_layout","{""id"":""123""}"
+,7,1,"Recordlist of id 111",0,"web_list","{""id"":111,""GET"":{""clipBoard"":1}}"
+,8,1,"Edit Page",0,"record_edit","{""edit"":{""pages"":{""112"":""edit""}}}"
+,9,1,"Page content",0,"web_layout","[]"
diff --git a/typo3/sysext/backend/Tests/Functional/Backend/Fixtures/ShortcutsBase.csv b/typo3/sysext/backend/Tests/Functional/Backend/Fixtures/ShortcutsBase.csv
new file mode 100644
index 0000000000000000000000000000000000000000..ffe0d2cf721ef2e37adc9988c58b786cdb832da6
--- /dev/null
+++ b/typo3/sysext/backend/Tests/Functional/Backend/Fixtures/ShortcutsBase.csv
@@ -0,0 +1,8 @@
+"sys_be_shortcuts",,,,,,
+,"uid","userid","description","sc_group","route","arguments"
+,1,1,"Recordlist",1,"web_list","{""id"":123,""GET"":{""clipBoard"":1}}"
+,2,1,"Edit Content",1,"record_edit","{""edit"":{""tt_content"":{""113"":""edit""}}}"
+,3,1,"Page Layout - Group 2",2,"web_layout","{""id"":""123"",""SET"":{""tt_content_showHidden"":""1"",""function"":""1"",""language"":""0""}}"
+,4,1,"Invalid route identifier",1,"web_proc","{""id"":""123""}"
+,5,2,"Wrong user",1,"web_layout","{""id"":""123""}"
+,6,1,,1,"web_layout","{""id"":""123""}"
diff --git a/typo3/sysext/backend/Tests/Functional/Backend/Shortcut/ShortcutRepositoryTest.php b/typo3/sysext/backend/Tests/Functional/Backend/Shortcut/ShortcutRepositoryTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..639acf01e42eef1cd804f95ab574bd40a8e08c59
--- /dev/null
+++ b/typo3/sysext/backend/Tests/Functional/Backend/Shortcut/ShortcutRepositoryTest.php
@@ -0,0 +1,181 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * 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!
+ */
+
+namespace TYPO3\CMS\Backend\Tests\Functional\Backend\Shortcut;
+
+use TYPO3\CMS\Backend\Backend\Shortcut\ShortcutRepository;
+use TYPO3\CMS\Backend\Module\ModuleLoader;
+use TYPO3\CMS\Core\Core\Bootstrap;
+use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\Core\Imaging\IconFactory;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
+
+class ShortcutRepositoryTest extends FunctionalTestCase
+{
+    protected ShortcutRepository $subject;
+
+    protected function setUp(): void
+    {
+        parent::setUp();
+        $this->importCSVDataSet(__DIR__ . '/../Fixtures/ShortcutsBase.csv');
+
+        $this->setUpBackendUserFromFixture(1);
+        Bootstrap::initializeLanguageObject();
+
+        $this->subject = new ShortcutRepository(
+            GeneralUtility::makeInstance(ConnectionPool::class),
+            GeneralUtility::makeInstance(IconFactory::class),
+            GeneralUtility::makeInstance(ModuleLoader::class)
+        );
+    }
+
+    /**
+     * @dataProvider shortcutExistsTestDataProvider
+     * @test
+     *
+     * @param string $routeIdentifier
+     * @param array  $arguments
+     * @param int    $userid
+     * @param bool   $exists
+     */
+    public function shortcutExistsTest(string $routeIdentifier, array $arguments, int $userid, bool $exists): void
+    {
+        $GLOBALS['BE_USER']->user['uid'] = $userid;
+        self::assertEquals($exists, $this->subject->shortcutExists($routeIdentifier, json_encode($arguments)));
+    }
+
+    public function shortcutExistsTestDataProvider(): \Generator
+    {
+        yield 'Shortcut exists' => [
+            'web_list',
+            ['id' => 123, 'GET' => ['clipBoard' => 1]],
+            1,
+            true
+        ];
+        yield 'Not this user' => [
+            'web_list',
+            ['id' => 123, 'GET' => ['clipBoard' => 1]],
+            2,
+            false
+        ];
+        yield 'Wrong route identifer' => [
+            'web_layout',
+            ['id' => 123, 'GET' => ['clipBoard' => 1]],
+            1,
+            false
+        ];
+        yield 'Wrong arguments' => [
+            'web_list',
+            ['id' => 321, 'GET' => ['clipBoard' => 1]],
+            1,
+            false
+        ];
+    }
+
+    /**
+     * @test
+     */
+    public function addShortcutTest(): void
+    {
+        foreach ($this->getShortcutsToAdd() as $shortcut) {
+            $this->subject->addShortcut(
+                $shortcut['routeIdentifier'],
+                json_encode($shortcut['arguments']),
+                $shortcut['title']
+            );
+        }
+
+        $this->assertCSVDataSet('typo3/sysext/backend/Tests/Functional/Backend/Fixtures/ShortcutsAddedResult.csv');
+    }
+
+    public function getShortcutsToAdd(): array
+    {
+        return [
+            'Basic shortcut with all information' => [
+                'routeIdentifier' => 'web_list',
+                'arguments' => ['id' => 111, 'GET' => ['clipBoard' => 1]],
+                'title' => 'Recordlist of id 111'
+            ],
+            // @todo Below is deprecated functionality which only provides backwards compatibility for v11. Remove in v12!
+            'FormEngine without title' => [
+                'routeIdentifier' => 'record_edit',
+                'arguments' => ['edit' => ['pages' => [112 => 'edit']]],
+                'title' => ''
+            ],
+            // @todo Below is deprecated functionality which only provides backwards compatibility for v11. Remove in v12!
+            'Page Layout without title' => [
+                'routeIdentifier' => 'web_layout',
+                'arguments' => [],
+                'title' => ''
+            ]
+        ];
+    }
+
+    /**
+     * This effectively also tests ShortcutRepository::initShortcuts()
+     *
+     * @test
+     */
+    public function getShortcutsByGroupTest(): void
+    {
+        $expected = [
+            1 => [
+                'table' => null,
+                'recordid' => null,
+                'groupLabel' => 'Pages',
+                'type' => 'other',
+                'icon' => 'data-identifier="module-web_list"',
+                'label' => 'Recordlist',
+                'action' => 'id=123\u0026GET%5BclipBoard%5D=1\',\'web_list\',\'web_list\', 123);'
+            ],
+            2 => [
+                'table' => 'tt_content',
+                'recordid' => 113,
+                'groupLabel' => null,
+                'type' => 'edit',
+                'label' => 'Edit Content',
+                'icon' => 'data-identifier="mimetypes-x-content-text"',
+                'action' => '\u0026edit%5Btt_content%5D%5B113%5D=edit\',\'record_edit\',\'record_edit\', 0);'
+            ],
+            6 => [
+                'table' => null,
+                'recordid' => null,
+                'groupLabel' => null,
+                'type' => 'other',
+                'label' => 'Page content',
+                'icon' => 'data-identifier="module-web_layout"',
+                'action' => '\u0026id=123\',\'web_layout\',\'web_layout\', 123);'
+            ]
+        ];
+
+        $shortcuts = $this->subject->getShortcutsByGroup(1);
+        self::assertCount(3, $shortcuts);
+
+        foreach ($shortcuts as $shortcut) {
+            $id = (int)$shortcut['raw']['uid'];
+            self::assertEquals(1, $shortcut['group']);
+            self::assertEquals($expected[$id]['table'], $shortcut['table'] ?? null);
+            self::assertEquals($expected[$id]['recordid'], $shortcut['recordid'] ?? null);
+            self::assertEquals($expected[$id]['groupLabel'], $shortcut['groupLabel'] ?? null);
+            self::assertEquals($expected[$id]['type'], $shortcut['type']);
+            self::assertEquals($expected[$id]['label'], $shortcut['label']);
+            self::assertStringContainsString($expected[$id]['icon'], $shortcut['icon']);
+            self::assertStringContainsString($expected[$id]['action'], $shortcut['action']);
+        }
+    }
+}
diff --git a/typo3/sysext/backend/Tests/Functional/Controller/ShortcutControllerTest.php b/typo3/sysext/backend/Tests/Functional/Controller/ShortcutControllerTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..e1af847e0983724c4125adc2409d3faa5ec03c31
--- /dev/null
+++ b/typo3/sysext/backend/Tests/Functional/Controller/ShortcutControllerTest.php
@@ -0,0 +1,105 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * 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!
+ */
+
+namespace TYPO3\CMS\Backend\Tests\Functional\Controller;
+
+use TYPO3\CMS\Backend\Controller\ShortcutController;
+use TYPO3\CMS\Core\Core\Bootstrap;
+use TYPO3\CMS\Core\Http\NormalizedParams;
+use TYPO3\CMS\Core\Http\ServerRequest;
+use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
+
+class ShortcutControllerTest extends FunctionalTestCase
+{
+    protected ShortcutController $subject;
+    protected ServerRequest $request;
+
+    protected function setUp(): void
+    {
+        parent::setUp();
+
+        $this->importDataSet(__DIR__ . '/../Fixtures/sys_be_shortcuts.xml');
+
+        $this->setUpBackendUserFromFixture(1);
+        Bootstrap::initializeLanguageObject();
+
+        $this->subject = new ShortcutController();
+        $this->request = (new ServerRequest())->withAttribute('normalizedParams', new NormalizedParams([], [], '', ''));
+    }
+
+    /**
+     * @dataProvider addShortcutTestDataProvide
+     * @test
+     *
+     * @param array $parsedBody
+     * @param array $queryParams
+     * @param string $expectedResponseBody
+     */
+    public function addShortcutTest(array $parsedBody, array $queryParams, string $expectedResponseBody): void
+    {
+        $request = $this->request->withParsedBody($parsedBody)->withQueryParams($queryParams);
+        self::assertEquals($expectedResponseBody, $this->subject->addAction($request)->getBody());
+    }
+
+    public function addShortcutTestDataProvide(): \Generator
+    {
+        yield 'No route defined' => [
+            [],
+            [],
+            'missingRoute'
+        ];
+        yield 'Existing data as parsed body' => [
+            [
+                'routeIdentifier' => 'web_layout',
+                'arguments' => '{"id":"123"}'
+            ],
+            [],
+            'alreadyExists'
+        ];
+        yield 'Existing data as query parameters' => [
+            [],
+            [
+                'routeIdentifier' => 'web_layout',
+                'arguments' => '{"id":"123"}'
+            ],
+            'alreadyExists'
+        ];
+        yield 'Invalid route identifier' => [
+            [],
+            [
+                'routeIdentifier' => 'invalid_route_identifier',
+            ],
+            'failed'
+        ];
+        yield 'New data as parsed body' => [
+            [
+                'routeIdentifier' => 'web_list',
+                'arguments' => '{"id":"123","GET":{"clipBoard":"1"}}'
+            ],
+            [],
+            'success'
+        ];
+        yield 'New data as query parameters' => [
+            [],
+            [
+                'routeIdentifier' => 'web_list',
+                'arguments' => '{"id":"321","GET":{"clipBoard":"1"}}'
+            ],
+            'success'
+        ];
+    }
+}
diff --git a/typo3/sysext/backend/Tests/Functional/Fixtures/sys_be_shortcuts.xml b/typo3/sysext/backend/Tests/Functional/Fixtures/sys_be_shortcuts.xml
new file mode 100644
index 0000000000000000000000000000000000000000..6eabf0bea49d96bc2f5cc973019b9ea4a40e6912
--- /dev/null
+++ b/typo3/sysext/backend/Tests/Functional/Fixtures/sys_be_shortcuts.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<dataset>
+    <sys_be_shortcuts>
+        <uid>1</uid>
+        <userid>1</userid>
+        <route>web_layout</route>
+        <arguments>{"id":"123"}</arguments>
+    </sys_be_shortcuts>
+</dataset>
diff --git a/typo3/sysext/backend/Tests/Functional/Template/Components/Buttons/Action/ShortcutButtonTest.php b/typo3/sysext/backend/Tests/Functional/Template/Components/Buttons/Action/ShortcutButtonTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..9671eb798f4d2fc29384127b4fdb1b15f08d9950
--- /dev/null
+++ b/typo3/sysext/backend/Tests/Functional/Template/Components/Buttons/Action/ShortcutButtonTest.php
@@ -0,0 +1,122 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * 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!
+ */
+
+namespace TYPO3\CMS\Backend\Tests\Functional\Template\Components\Buttons\Action;
+
+use TYPO3\CMS\Backend\Template\Components\Buttons\Action\ShortcutButton;
+use TYPO3\CMS\Core\Core\Bootstrap;
+use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
+
+class ShortcutButtonTest extends FunctionalTestCase
+{
+    private const FIXTURES_PATH_PATTERN = __DIR__ . '/../../../Fixtures/%s.html';
+
+    protected function setUp(): void
+    {
+        parent::setUp();
+
+        $this->setUpBackendUserFromFixture(1);
+        Bootstrap::initializeLanguageObject();
+    }
+
+    /**
+     * @test
+     */
+    public function isButtonValid(): void
+    {
+        self::assertFalse((new ShortcutButton())->isValid());
+        self::assertTrue((new ShortcutButton())->setRouteIdentifier('web_list')->isValid());
+        // @todo Remove below in v12
+        self::assertTrue((new ShortcutButton())->setArguments(['route' => 'web_list'])->isValid());
+        self::assertTrue((new ShortcutButton())->setModuleName('web_list')->isValid());
+    }
+
+    /**
+     * @dataProvider rendersCorrectMarkupDataProvider
+     * @test
+     *
+     * @param ShortcutButton $button
+     * @param string $expectedMarkupFile
+     */
+    public function rendersCorrectMarkup(ShortcutButton $button, string $expectedMarkupFile): void
+    {
+        self::assertEquals(
+            preg_replace('/\s+/', '', file_get_contents(sprintf(self::FIXTURES_PATH_PATTERN, $expectedMarkupFile))),
+            preg_replace('/\s+/', '', $button->render())
+        );
+    }
+
+    public function rendersCorrectMarkupDataProvider(): \Generator
+    {
+        yield 'Recordlist' => [
+            (new ShortcutButton())->setRouteIdentifier('web_list')->setDisplayName('Recordlist'),
+            'RecordList'
+        ];
+        // @todo Below is deprecated functionality which only provides backwards compatibility for v11. Remove in v12!
+        yield 'Recordlist as route path' => [
+            (new ShortcutButton())->setRouteIdentifier('/module/web/list')->setDisplayName('Recordlist'),
+            'RecordList'
+        ];
+        yield 'Recordlist - single table view' => [
+            (new ShortcutButton())
+                ->setRouteIdentifier('web_list')
+                ->setDisplayName('Recordlist - single table view')
+                ->setArguments([
+                    'id' => 123,
+                    'table' => 'some_table',
+                    'GET' => [
+                        'clipBoard' => 1
+                    ]
+                ]),
+            'RecordListSingleTable'
+        ];
+        yield 'With special route identifier' => [
+            (new ShortcutButton())->setRouteIdentifier('record_edit')->setDisplayName('Edit record'),
+            'SpecialRouteIdentifier'
+        ];
+        yield 'With special route identifier and arguments' => [
+            (new ShortcutButton())
+                ->setRouteIdentifier('record_edit')
+                ->setDisplayName('Edit record')
+                ->setArguments([
+                    'id' => 123,
+                    'edit' => [
+                        'pages' => [
+                            123 => 'edit',
+                        ],
+                        'overrideVals' => [
+                            'pages' => [
+                                'sys_language_uid' => 1
+                            ]
+                        ]
+                    ],
+                    'returnUrl' => 'some/url'
+                ]),
+            'SpecialRouteIdentifierWithArguments'
+        ];
+        // @todo Below is deprecated functionality which only provides backwards compatibility for v11. Remove in v12!
+        yield 'With special route path' => [
+            (new ShortcutButton())->setRouteIdentifier('/record/edit')->setDisplayName('Edit record'),
+            'SpecialRouteIdentifier'
+        ];
+        // @todo Below is deprecated functionality which only provides backwards compatibility for v11. Remove in v12!
+        yield 'With special route path as Argument' => [
+            (new ShortcutButton())->setArguments(['route' => '/record/edit'])->setDisplayName('Edit record'),
+            'SpecialRouteIdentifier'
+        ];
+    }
+}
diff --git a/typo3/sysext/backend/Tests/Functional/Template/Fixtures/RecordList.html b/typo3/sysext/backend/Tests/Functional/Template/Fixtures/RecordList.html
new file mode 100644
index 0000000000000000000000000000000000000000..ef705a2044f98ce1e6fb626e2de5a47219692619
--- /dev/null
+++ b/typo3/sysext/backend/Tests/Functional/Template/Fixtures/RecordList.html
@@ -0,0 +1,10 @@
+<a href="#"
+   class="btn btn-default btn-sm"
+   onclick="top.TYPO3.ShortcutMenu.createShortcut('web_list', '[]', 'Recordlist', 'Create\u0020a\u0020bookmark\u0020to\u0020this\u0020page', this);return false;"
+   title="Create a bookmark to this page">
+    <span class="t3js-icon icon icon-size-small icon-state-default icon-actions-system-shortcut-new" data-identifier="actions-system-shortcut-new">
+        <span class="icon-markup">
+            <svg class="icon-color" role="img"><use xlink:href="typo3/sysext/core/Resources/Public/Icons/T3Icons/sprites/actions.svg#actions-star" /></svg>
+        </span>
+    </span>
+</a>
diff --git a/typo3/sysext/backend/Tests/Functional/Template/Fixtures/RecordListSingleTable.html b/typo3/sysext/backend/Tests/Functional/Template/Fixtures/RecordListSingleTable.html
new file mode 100644
index 0000000000000000000000000000000000000000..74931a73eed6dd3a36cf051f0825685bec18dfd6
--- /dev/null
+++ b/typo3/sysext/backend/Tests/Functional/Template/Fixtures/RecordListSingleTable.html
@@ -0,0 +1,10 @@
+<a href="#"
+   class="btn btn-default btn-sm"
+   onclick="top.TYPO3.ShortcutMenu.createShortcut('web_list', '{\u0022id\u0022:123,\u0022table\u0022:\u0022some_table\u0022,\u0022GET\u0022:{\u0022clipBoard\u0022:1}}', 'Recordlist\u0020-\u0020single\u0020table\u0020view', 'Create\u0020a\u0020bookmark\u0020to\u0020this\u0020page', this);return false;"
+   title="Create a bookmark to this page">
+    <span class="t3js-icon icon icon-size-small icon-state-default icon-actions-system-shortcut-new" data-identifier="actions-system-shortcut-new">
+        <span class="icon-markup">
+            <svg class="icon-color" role="img"><use xlink:href="typo3/sysext/core/Resources/Public/Icons/T3Icons/sprites/actions.svg#actions-star" /></svg>
+        </span>
+    </span>
+</a>
diff --git a/typo3/sysext/backend/Tests/Functional/Template/Fixtures/SpecialRouteIdentifier.html b/typo3/sysext/backend/Tests/Functional/Template/Fixtures/SpecialRouteIdentifier.html
new file mode 100644
index 0000000000000000000000000000000000000000..ade7e7a577440326b3f8b0c9aeea21c6d426ec29
--- /dev/null
+++ b/typo3/sysext/backend/Tests/Functional/Template/Fixtures/SpecialRouteIdentifier.html
@@ -0,0 +1,10 @@
+<a href="#"
+   class="btn btn-default btn-sm"
+   onclick="top.TYPO3.ShortcutMenu.createShortcut('record_edit', '[]', 'Edit\u0020record', 'Create\u0020a\u0020bookmark\u0020to\u0020this\u0020page', this);return false;"
+   title="Create a bookmark to this page">
+    <span class="t3js-icon icon icon-size-small icon-state-default icon-actions-system-shortcut-new" data-identifier="actions-system-shortcut-new">
+        <span class="icon-markup">
+            <svg class="icon-color" role="img"><use xlink:href="typo3/sysext/core/Resources/Public/Icons/T3Icons/sprites/actions.svg#actions-star" /></svg>
+        </span>
+    </span>
+</a>
diff --git a/typo3/sysext/backend/Tests/Functional/Template/Fixtures/SpecialRouteIdentifierWithArguments.html b/typo3/sysext/backend/Tests/Functional/Template/Fixtures/SpecialRouteIdentifierWithArguments.html
new file mode 100644
index 0000000000000000000000000000000000000000..631533788f8cfed748a717d040780a90aa579983
--- /dev/null
+++ b/typo3/sysext/backend/Tests/Functional/Template/Fixtures/SpecialRouteIdentifierWithArguments.html
@@ -0,0 +1,10 @@
+<a href="#"
+   class="btn btn-default btn-sm"
+   onclick="top.TYPO3.ShortcutMenu.createShortcut('record_edit', '{\u0022id\u0022:123,\u0022edit\u0022:{\u0022pages\u0022:{\u0022123\u0022:\u0022edit\u0022},\u0022overrideVals\u0022:{\u0022pages\u0022:{\u0022sys_language_uid\u0022:1}}}}', 'Edit\u0020record', 'Create\u0020a\u0020bookmark\u0020to\u0020this\u0020page', this);return false;"
+   title="Create a bookmark to this page">
+    <span class="t3js-icon icon icon-size-small icon-state-default icon-actions-system-shortcut-new" data-identifier="actions-system-shortcut-new">
+        <span class="icon-markup">
+            <svg class="icon-color" role="img"><use xlink:href="typo3/sysext/core/Resources/Public/Icons/T3Icons/sprites/actions.svg#actions-star" /></svg>
+        </span>
+    </span>
+</a>
diff --git a/typo3/sysext/beuser/Classes/Controller/PermissionController.php b/typo3/sysext/beuser/Classes/Controller/PermissionController.php
index 48ea870d4276d1fe61985a87ecb6a64c1066360f..4bfdc371427e22d0d668617feb30e84527979688 100644
--- a/typo3/sysext/beuser/Classes/Controller/PermissionController.php
+++ b/typo3/sysext/beuser/Classes/Controller/PermissionController.php
@@ -142,7 +142,6 @@ class PermissionController extends ActionController
         /** @var ButtonBar $buttonBar */
         $buttonBar = $this->view->getModuleTemplate()->getDocHeaderComponent()->getButtonBar();
         $currentRequest = $this->request;
-        $moduleName = $currentRequest->getPluginName();
         $lang = $this->getLanguageService();
 
         if ($currentRequest->getControllerActionName() === 'edit') {
@@ -174,7 +173,7 @@ class PermissionController extends ActionController
         }
 
         $shortcutButton = $buttonBar->makeShortcutButton()
-            ->setModuleName($moduleName)
+            ->setRouteIdentifier('system_BeuserTxPermission')
             ->setDisplayName($this->getShortcutTitle())
             ->setArguments([
                 'route' => GeneralUtility::_GP('route'),
diff --git a/typo3/sysext/beuser/Resources/Private/Layouts/Default.html b/typo3/sysext/beuser/Resources/Private/Layouts/Default.html
index e95d0c2ce25f6cf01194cd33cc6eab7fed4afc1c..19ae9915619c440c43cff2a3ffb0f9ac1ffcb3e9 100644
--- a/typo3/sysext/beuser/Resources/Private/Layouts/Default.html
+++ b/typo3/sysext/beuser/Resources/Private/Layouts/Default.html
@@ -24,7 +24,7 @@
     <f:render section="Buttons" />
     <be:moduleLayout.button.shortcutButton
         displayName="{f:translate(id: shortcutLabel)}"
-        arguments="{route: '{route}', tx_beuser_system_beusertxbeuser: '{action: \'{action}\', controller: \'{controller}\'}' }"
+        arguments="{tx_beuser_system_beusertxbeuser: '{action: \'{action}\', controller: \'{controller}\'}' }"
     />
 
     <div id="beuser-main-content">
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Breaking-93093-ReworkShortcutPHPAPI.rst b/typo3/sysext/core/Documentation/Changelog/master/Breaking-93093-ReworkShortcutPHPAPI.rst
new file mode 100644
index 0000000000000000000000000000000000000000..161c759c93b8521a61e2b7ec027250b6687ae086
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/master/Breaking-93093-ReworkShortcutPHPAPI.rst
@@ -0,0 +1,131 @@
+.. include:: ../../Includes.txt
+
+.. _changelog-Breaking-93093-ReworkShortcutPHPAPI:
+
+==========================================
+Breaking: #93093 - Rework Shortcut PHP API
+==========================================
+
+See :issue:`93093`
+
+Description
+===========
+
+The Shortcut PHP API previously has the full URL of the shortcut target
+stored in the :sql:`sys_be_shortcuts` table. It however turned out that
+this is not working well, error-prone and laborious. For example, all
+created shortcuts are automatically invalid as soon as the corresponding
+module changed its route path. Furthermore the :sql:`url` column included
+the token which was actually never used but regenerated on every link
+generation, e.g. when reloading the backend. Since even the initial
+`returnUrl` was stored in the database, a shortcut which linked to
+FormEngine has returned to this initial url forever. Even after years
+when the initial target may no longer available.
+
+All these characteristics opposes the introduction of speaking urls for
+the TYPO3 backend. Therefore, the internal handling and registration of
+the Shortcut PHP API was reworked.
+
+A shortcut record does now not longer store the full url of the shortcut
+target but instead only the modules route identifier and the necessary
+arguments (parameters) for the URL.
+
+The columns :sql:`module_name` and :sql:`url` of the :php:`sys_be_shortcuts`
+table have been replaced with:
+
+* :sql:`route` - Contains the route identifier of the module to link to
+* :sql:`arguments` - Contains all necessary arguments (parameters) for the
+link as JSON encoded string
+
+The :sql:`arguments` column does therefore not longer store any of the
+following parameters:
+
+* `route`
+* `token`
+* `returnUrl`
+
+Shortcuts are usually created through the JavaScript function
+:js:`TYPO3.ShortcutMenu.createShortcut()` which performs an AJAX call to
+:php:`ShortcutController->addAction()`. The parameter signature of the
+JavaScript function has therefore been changed and the :php:`addAction()`
+method does now feature an additional result string `missingRoute`, in case
+no `routeIdentifier` was provided in the AJAX call.
+
+The parameter signature changed as followed:
+
+.. code-block:: javascript
+
+   // Old signature:
+	public createShortcut(
+      moduleName: string,
+      url: string,
+      confirmationText: string,
+      motherModule: string,
+      shortcutButton: JQuery,
+      displayName: string,
+	)
+
+	// New signature:
+	public createShortcut(
+      routeIdentifier: string,
+      routeArguments: string,
+      displayName: string,
+      confirmationText: string,
+      shortcutButton: JQuery,
+	)
+
+The :php:`TYPO3\CMS\Backend\Template\Components\Buttons\Action\ShortcutButton`
+API for generating such creation links now provides a new public method
+:php:`setRouteIdentifier()` which replaces the deprecated
+:php:`setModuleName()` method. See
+:ref:`changelog-Deprecation-93093-DeprecateMethodNameInShortcutPHPAPI` for
+all deprecations done during the rework.
+
+
+Impact
+======
+
+Directly calling :js:`TYPO3.ShortcutMenu.createShortcut()` with the old
+parameter signature will result in a JavaScript error.
+
+Already created shortcuts won't be available prior to running the provided
+upgrade wizard.
+
+The columns :sql:`module_name` and :sql:`url` have been removed. Directly
+querying these columns will raise a doctrine dbal exception.
+
+
+Affected Installations
+======================
+
+Installations with already created shortcuts.
+
+Installations with custom extensions directly calling
+:js:`TYPO3.ShortcutMenu.createShortcut()` with the old parameter signatur.
+
+Installations with custom extensions, directly using the columns
+:sql:`module_name` and :sql:`url` or relying on them being filled.
+
+Installations with custom extensions using deprecated functionality of
+the Shortcut PHP API.
+
+
+Migration
+=========
+
+Update the database schema (only "Add fields to tables") and run the
+`shortcutRecordsMigration` upgrade wizard either in the install tool or on
+CLI with
+:shell:`./typo3/sysext/core/bin/typo3 upgrade:run shortcutRecordsMigration`.
+Remove the unused :sql:`module_name` and :sql:`url` columns only after running
+the wizard.
+
+Change any call to :js:`TYPO3.ShortcutMenu.createShortcut()` to use the new
+parameter signature.
+
+Migrate custom extension code to use :sql:`route` and :sql:`arguments` instead
+of :sql:`module_name` and :sql:`url`.
+
+Migrate any call to deprecated functionality of the Shortcut PHP API.
+
+.. index:: Backend, PHP-API, PartiallyScanned, ext:backend
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Deprecation-92132-DeprecatedShortcutPHPAPI.rst b/typo3/sysext/core/Documentation/Changelog/master/Deprecation-92132-DeprecatedShortcutPHPAPI.rst
index 0d2555f7810c249bbe58ef0beac90ae9812a5ed7..0e86d36b6524d1b6f7ee4e01a9e50df1d193ad21 100644
--- a/typo3/sysext/core/Documentation/Changelog/master/Deprecation-92132-DeprecatedShortcutPHPAPI.rst
+++ b/typo3/sysext/core/Documentation/Changelog/master/Deprecation-92132-DeprecatedShortcutPHPAPI.rst
@@ -18,6 +18,11 @@ Some methods related to :php:`ext:backend` shortcut / bookmark handling have bee
 * :php:`TYPO3\CMS\Backend\Template\Components\Buttons\Action\ShortcutButton->setGetVariables()`
 * :php:`TYPO3\CMS\Backend\Template\Components\Buttons\Action\ShortcutButton->setSetVariables()`
 
+See also:
+
+- :ref:`changelog-Deprecation-93060-ShortcutTitleMustBeSetByControllers`
+- :ref:`changelog-Deprecation-93093-DeprecateMethodNameInShortcutPHPAPI`
+
 
 Impact
 ======
@@ -40,13 +45,14 @@ introduced. This method expects the full set of arguments and values to create a
 
 .. code-block:: php
 
-    $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
-    $shortCutButton = $buttonBar->makeShortcutButton()
-        ->setModuleName('web_view')
-        ->setArguments([
-            'route' => $request->getQueryParams['route'],
-            'id' => (int)($request->getQueryParams()['id'] ?? 0),
-         ]);
-        $buttonBar->addButton($shortCutButton, ButtonBar::BUTTON_POSITION_RIGHT);
+   $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
+   $pageId = (int)($request->getQueryParams()['id'] ?? 0);
+   $shortCutButton = $buttonBar->makeShortcutButton()
+       ->setRouteIdentifier('web_view')
+       ->setDisplayName('View page ' . $pageId)
+       ->setArguments([
+          'id' => $pageId,
+       ]);
+   $buttonBar->addButton($shortCutButton, ButtonBar::BUTTON_POSITION_RIGHT);
 
 .. index:: Backend, PHP-API, FullyScanned, ext:backend
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Deprecation-93060-ShortcutTitleMustBeSetByControllers.rst b/typo3/sysext/core/Documentation/Changelog/master/Deprecation-93060-ShortcutTitleMustBeSetByControllers.rst
index 6455bfb42ef905a74cbf75f5fdf337cf68425f4f..8d2545490c73ae919735e16f7f28f7dd296053ee 100644
--- a/typo3/sysext/core/Documentation/Changelog/master/Deprecation-93060-ShortcutTitleMustBeSetByControllers.rst
+++ b/typo3/sysext/core/Documentation/Changelog/master/Deprecation-93060-ShortcutTitleMustBeSetByControllers.rst
@@ -1,5 +1,7 @@
 .. include:: ../../Includes.txt
 
+.. _changelog-Deprecation-93060-ShortcutTitleMustBeSetByControllers:
+
 ===============================================================
 Deprecation: #93060 - Shortcut title must be set by controllers
 ===============================================================
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Deprecation-93093-DeprecateMethodNameInShortcutPHPAPI.rst b/typo3/sysext/core/Documentation/Changelog/master/Deprecation-93093-DeprecateMethodNameInShortcutPHPAPI.rst
new file mode 100644
index 0000000000000000000000000000000000000000..a235b2b27c34d088b477e26c6f470ee0fa3d1ab6
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/master/Deprecation-93093-DeprecateMethodNameInShortcutPHPAPI.rst
@@ -0,0 +1,67 @@
+.. include:: ../../Includes.txt
+
+.. _changelog-Deprecation-93093-DeprecateMethodNameInShortcutPHPAPI:
+
+==============================================================
+Deprecation: #93093 - Deprecate MethodName in Shortcut PHP API
+==============================================================
+
+See :issue:`93093`
+
+Description
+===========
+
+Since :issue:`92723` the TYPO3 backend uses symfony routing for resolving
+internal endpoints, e.g. modules. This will allow speaking urls and also
+deep-linking in the future. To achieve this, the shortcut PHP API had to
+be reworked to be fully compatible with the new routing.
+See :ref:`changelog-Breaking-93093-ReworkShortcutPHPAPI` for more information
+regarding the rework.
+
+In the course of the rework, following methods within :php:`ShortcutButton`
+have been deprecated:
+
+* :php:`TYPO3\CMS\Backend\Template\Components\Buttons\Action\ShortcutButton->setModuleName()`
+* :php:`TYPO3\CMS\Backend\Template\Components\Buttons\Action\ShortcutButton->getModuleName()`
+
+Impact
+======
+
+Using those methods directly or indirectly will trigger deprecation log
+warnings.
+
+
+Affected Installations
+======================
+
+Installations with custom extensions, adding a shortcut button in the module
+header of their backend modules using the mentioned methods. The extension
+scanner will find all PHP usages as weak match.
+
+
+Migration
+=========
+
+Use the new methods :php:`ShortcutButton->setRouteIdentifier()` and
+:php:`ShortcutButton->getRouteIdentifier()` as replacement. Please note
+that these methods require the route identifier of the backend module
+which may differ from the module name. To find out the route identifier,
+the "Backend Routes" section within the configuration module can be used.
+
+Before:
+
+.. code-block:: php
+
+   $shortCutButton = $buttonBar
+       ->makeShortcutButton()
+       ->setModuleName('web_list');
+
+After:
+
+.. code-block:: php
+
+   $shortCutButton = $buttonBar
+       ->makeShortcutButton()
+       ->setRouteIdentifier('web_list');
+
+.. index:: Backend, PHP-API, FullyScanned, ext:backend
diff --git a/typo3/sysext/core/ext_tables.sql b/typo3/sysext/core/ext_tables.sql
index c5cb7ec53667a6704e4677081caca8a6b9e65994..5d95f477154f57bde665fe8ee8b33ff5aeb9a5b4 100644
--- a/typo3/sysext/core/ext_tables.sql
+++ b/typo3/sysext/core/ext_tables.sql
@@ -130,8 +130,8 @@ CREATE TABLE sys_registry (
 CREATE TABLE sys_be_shortcuts (
 	uid int(11) unsigned NOT NULL auto_increment,
 	userid int(11) unsigned DEFAULT '0' NOT NULL,
-	module_name varchar(255) DEFAULT '' NOT NULL,
-	url text,
+	route varchar(255) DEFAULT '' NOT NULL,
+	arguments text,
 	description varchar(255) DEFAULT '' NOT NULL,
 	sorting int(11) DEFAULT '0' NOT NULL,
 	sc_group tinyint(4) DEFAULT '0' NOT NULL,
diff --git a/typo3/sysext/filelist/Classes/Controller/FileListController.php b/typo3/sysext/filelist/Classes/Controller/FileListController.php
index 5610bbbc48b213d06ecfed5a53acd62e4b633508..487ebd94276f79ee5ae0af4707ad30f78393dbb6 100644
--- a/typo3/sysext/filelist/Classes/Controller/FileListController.php
+++ b/typo3/sysext/filelist/Classes/Controller/FileListController.php
@@ -663,7 +663,7 @@ class FileListController extends ActionController implements LoggerAwareInterfac
 
         // Shortcut
         $shortCutButton = $buttonBar->makeShortcutButton()
-            ->setModuleName('file_FilelistList')
+            ->setRouteIdentifier('file_FilelistList')
             ->setDisplayName($this->getShortcutTitle())
             ->setArguments([
                 'route' => GeneralUtility::_GP('route'),
diff --git a/typo3/sysext/form/Classes/Controller/FormManagerController.php b/typo3/sysext/form/Classes/Controller/FormManagerController.php
index f393676604044578bf80a3d063fbf1ce8ad65804..c78817c3c0d56fccbf9bd9b180838582c0b0d452 100644
--- a/typo3/sysext/form/Classes/Controller/FormManagerController.php
+++ b/typo3/sysext/form/Classes/Controller/FormManagerController.php
@@ -458,8 +458,6 @@ class FormManagerController extends AbstractBackendController
     {
         /** @var ButtonBar $buttonBar */
         $buttonBar = $this->view->getModuleTemplate()->getDocHeaderComponent()->getButtonBar();
-        $currentRequest = $this->request;
-        $moduleName = $currentRequest->getPluginName();
 
         // Create new
         $addFormButton = $buttonBar->makeLinkButton()
@@ -478,7 +476,7 @@ class FormManagerController extends AbstractBackendController
 
         // Shortcut
         $shortcutButton = $buttonBar->makeShortcutButton()
-            ->setModuleName($moduleName)
+            ->setRouteIdentifier('web_FormFormbuilder')
             ->setDisplayName($this->getLanguageService()->sL('LLL:EXT:form/Resources/Private/Language/Database.xlf:module.shortcut_name'))
             ->setArguments([
                 'route' => GeneralUtility::_GP('route')
diff --git a/typo3/sysext/info/Classes/Controller/InfoModuleController.php b/typo3/sysext/info/Classes/Controller/InfoModuleController.php
index 6c5729a86344c7adbb8a5e69d4b5a50ea6f9b9be..5ae8246d49d934cdce17f9f0e838d3327f139830 100644
--- a/typo3/sysext/info/Classes/Controller/InfoModuleController.php
+++ b/typo3/sysext/info/Classes/Controller/InfoModuleController.php
@@ -286,7 +286,7 @@ class InfoModuleController
             }
         }
         $shortCutButton = $buttonBar->makeShortcutButton()
-            ->setModuleName($this->moduleName)
+            ->setRouteIdentifier($this->moduleName)
             ->setDisplayName($this->MOD_MENU['function'][$this->MOD_SETTINGS['function']])
             ->setArguments($shortcutArguments);
         $buttonBar->addButton($shortCutButton, ButtonBar::BUTTON_POSITION_RIGHT);
diff --git a/typo3/sysext/install/Classes/Updates/ShortcutRecordsMigration.php b/typo3/sysext/install/Classes/Updates/ShortcutRecordsMigration.php
new file mode 100644
index 0000000000000000000000000000000000000000..2f4bde1068e4f6a4b52409bd99fe07d369aa0f01
--- /dev/null
+++ b/typo3/sysext/install/Classes/Updates/ShortcutRecordsMigration.php
@@ -0,0 +1,191 @@
+<?php
+
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Install\Updates;
+
+/*
+ * 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 TYPO3\CMS\Backend\Routing\Router;
+use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\Core\Database\Query\QueryBuilder;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
+ */
+class ShortcutRecordsMigration implements UpgradeWizardInterface
+{
+    private const TABLE_NAME = 'sys_be_shortcuts';
+
+    protected ?iterable $routes = null;
+
+    public function getIdentifier(): string
+    {
+        return 'shortcutRecordsMigration';
+    }
+
+    public function getTitle(): string
+    {
+        return 'Migrate shortcut records to new format.';
+    }
+
+    public function getDescription(): string
+    {
+        return 'To support speaking urls in the backend, some fields need to be changed in sys_be_shortcuts.';
+    }
+
+    public function getPrerequisites(): array
+    {
+        return [
+            DatabaseUpdatedPrerequisite::class
+        ];
+    }
+
+    public function updateNecessary(): bool
+    {
+        return $this->columnsExistInTable() && $this->hasRecordsToUpdate();
+    }
+
+    public function executeUpdate(): bool
+    {
+        $this->routes = GeneralUtility::makeInstance(Router::class)->getRoutes();
+        $connection = $this->getConnectionPool()->getConnectionForTable(self::TABLE_NAME);
+
+        foreach ($this->getRecordsToUpdate() as $record) {
+            [$moduleName] = explode('|', (string)$record['module_name'], 2);
+
+            if (!is_string($moduleName) || $moduleName === '') {
+                continue;
+            }
+
+            if (($routeIdentifier = $this->getRouteIdentifierForModuleName($moduleName)) === '') {
+                continue;
+            }
+
+            // Parse the url and reveal the arguments (query parameters)
+            $parsedUrl = parse_url((string)$record['url']) ?: [];
+            $arguments = [];
+            parse_str($parsedUrl['query'] ?? '', $arguments);
+
+            // Unset not longer needed arguments
+            unset($arguments['route'], $arguments['returnUrl']);
+
+            try {
+                $encodedArguments = json_encode($arguments, JSON_THROW_ON_ERROR) ?: '';
+            } catch (\JsonException $e) {
+                // Skip the row if arguments can not be encoded
+                continue;
+            }
+
+            // Update the record - Note: The "old" values won't be unset
+            $connection->update(
+                self::TABLE_NAME,
+                ['route' => $routeIdentifier, 'arguments' => $encodedArguments],
+                ['uid' => (int)$record['uid']]
+            );
+        }
+
+        return true;
+    }
+
+    protected function columnsExistInTable(): bool
+    {
+        $schemaManager = $this->getConnectionPool()->getConnectionForTable(self::TABLE_NAME)->getSchemaManager();
+
+        if ($schemaManager === null) {
+            return false;
+        }
+
+        $tableColumns = $schemaManager->listTableColumns(self::TABLE_NAME);
+
+        foreach (['module_name', 'url', 'route', 'arguments'] as $column) {
+            if (!isset($tableColumns[$column])) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    protected function hasRecordsToUpdate(): bool
+    {
+        return (bool)$this->getPreparedQueryBuilder()->count('uid')->execute()->fetchColumn();
+    }
+
+    protected function getRecordsToUpdate(): array
+    {
+        return $this->getPreparedQueryBuilder()->select(...['uid', 'module_name', 'url'])->execute()->fetchAll();
+    }
+
+    protected function getPreparedQueryBuilder(): QueryBuilder
+    {
+        $queryBuilder = $this->getConnectionPool()->getQueryBuilderForTable(self::TABLE_NAME);
+        $queryBuilder->getRestrictions()->removeAll();
+        $queryBuilder
+            ->from(self::TABLE_NAME)
+            ->where(
+                $queryBuilder->expr()->neq('module_name', $queryBuilder->createNamedParameter('')),
+                $queryBuilder->expr()->andX(
+                    $queryBuilder->expr()->neq('url', $queryBuilder->createNamedParameter('')),
+                    $queryBuilder->expr()->isNotNull('url')
+                ),
+                $queryBuilder->expr()->eq('route', $queryBuilder->createNamedParameter('')),
+                $queryBuilder->expr()->orX(
+                    $queryBuilder->expr()->eq('arguments', $queryBuilder->createNamedParameter('')),
+                    $queryBuilder->expr()->isNull('arguments')
+                )
+            );
+
+        return $queryBuilder;
+    }
+
+    protected function getRouteIdentifierForModuleName(string $moduleName): string
+    {
+        // Check static special cases first
+        switch ($moduleName) {
+            case 'xMOD_alt_doc.php':
+                return 'record_edit';
+            case 'file_edit':
+            case 'wizard_rte':
+                return $moduleName;
+        }
+
+        // Get identifier from module configuration
+        $routeIdentifier = $GLOBALS['TBE_MODULES']['_configuration'][$moduleName]['id'] ?? $moduleName;
+
+        // Check if a route with the identifier exist
+        if (isset($this->routes[$routeIdentifier])
+            && $this->routes[$routeIdentifier]->hasOption('moduleName')
+            && $this->routes[$routeIdentifier]->getOption('moduleName') === $moduleName
+        ) {
+            return $routeIdentifier;
+        }
+
+        // If the defined route identifier can't be fetched, try from the other side
+        // by iterating over the routes to match a route by the defined module name
+        foreach ($this->routes as $identifier => $route) {
+            if ($route->hasOption('moduleName') && $route->getOption('moduleName') === $moduleName) {
+                return $routeIdentifier;
+            }
+        }
+
+        return '';
+    }
+
+    protected function getConnectionPool(): ConnectionPool
+    {
+        return GeneralUtility::makeInstance(ConnectionPool::class);
+    }
+}
diff --git a/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallMatcher.php b/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallMatcher.php
index ddc6e54c2ab27fe2d400d7759c887a35ee48f19d..74d07e3516a51790542ab1fe511e65947a3d709a 100644
--- a/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallMatcher.php
+++ b/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallMatcher.php
@@ -4690,4 +4690,18 @@ return [
             'Breaking-93080-RelationHandlerInternalsProtected.rst',
         ],
     ],
+    'TYPO3\CMS\Backend\Template\Components\Buttons\Action\ShortcutButton->getModuleName' => [
+        'numberOfMandatoryArguments' => 0,
+        'maximumNumberOfArguments' => 0,
+        'restFiles' => [
+            'Deprecation-93093-DeprecateMethodNameInShortcutPHPAPI.rst'
+        ],
+    ],
+    'TYPO3\CMS\Backend\Template\Components\Buttons\Action\ShortcutButton->setModuleName' => [
+        'numberOfMandatoryArguments' => 1,
+        'maximumNumberOfArguments' => 1,
+        'restFiles' => [
+            'Deprecation-93093-DeprecateMethodNameInShortcutPHPAPI.rst'
+        ],
+    ],
 ];
diff --git a/typo3/sysext/install/Tests/Functional/Updates/Fixtures/ShortcutsBase.csv b/typo3/sysext/install/Tests/Functional/Updates/Fixtures/ShortcutsBase.csv
new file mode 100644
index 0000000000000000000000000000000000000000..44a06457f4c61d6e2cbb5a9adda2898b5adecce7
--- /dev/null
+++ b/typo3/sysext/install/Tests/Functional/Updates/Fixtures/ShortcutsBase.csv
@@ -0,0 +1,24 @@
+"sys_be_shortcuts",,,,,,,,,
+,"uid","userid","module_name","url","description","sorting","sc_group","route","arguments"
+,1,1,"system_config|","/typo3/index.php?&route=%2Fmodule%2Fsystem%2Fconfig&tree=siteConfiguration","Site Configuration",0,0,,
+,2,1,"web_FormFormbuilder|","/typo3/index.php?&route=%2Fmodule%2Fweb%2FFormFormbuilder","Form manager",0,0,,
+,3,1,"system_BeuserTxPermission|","/typo3/index.php?&route=%2Fmodule%2Fsystem%2FBeuserTxPermission&id=123","Permissions",0,0,,
+,4,1,"file_FilelistList|","/typo3/index.php?&route=%2Fmodule%2Ffile%2FFilelistList&id=1%3A%2F","Filelist root",0,0,,
+,5,1,"web_layout|","/typo3/index.php?&route=%2Fmodule%2Fweb%2Flayout&id=123&SET%5Btt_content_showHidden%5D=1&SET%5Bfunction%5D=1&SET%5Blanguage%5D=0","Web layout",0,0,,
+,6,1,"xMOD_alt_doc.php|","/typo3/index.php?&route=%2Frecord%2Fedit&returnUrl=%2Ftypo3%2Findex.php%3Froute%3D%252Fmodule%252Fweb%252Flayout%26token%3D123456%26id%3D123%26%23element-tt_content-3&edit%5Btt_content%5D%5B3%5D=edit","Edit Page Content",0,0,,
+,7,1,"xMOD_alt_doc.php|","/typo3/index.php?&route=%2Frecord%2Fedit&returnUrl=%2Ftypo3%2Findex.php%3Froute%3D%252Fmodule%252Fweb%252Flist%26token%3D123456%26id%3D0%26table%3D&edit%5Bbe_groups%5D%5B3%5D=edit","Edit Backend usergroup on root level",0,0,,
+,8,1,"xMOD_alt_doc.php|","/typo3/index.php?&route=%2Frecord%2Fedit&returnUrl=%2Ftypo3%2Findex.php%3Froute%3D%252Fmodule%252Fweb%252Flist%26token%3D123456%26id%3D0%26table%3D&edit%5Bbe_groups%5D%5B0%5D=new","Create new Backend usergroup",0,0,,
+,9,1,"xMOD_alt_doc.php|","/typo3/index.php?&route=%2Frecord%2Fedit&returnUrl=%2Ftypo3%2Findex.php%3Froute%3D%252Fmodule%252Fsystem%252FBeuserTxBeuser%26token%3D123456&edit%5Bbe_users%5D%5B1%5D=edit","Edit Backend user on root level",0,0,,
+,10,1,"xMOD_alt_doc.php|","/typo3/index.php?&route=%2Frecord%2Fedit&returnUrl=%2Ftypo3%2Findex.php%3Froute%3D%252Fmodule%252Fweb%252Flist%26token%3D123456%26id%3D123%26table%3D&edit%5Bpages%5D%5B416%5D=edit&overrideVals%5Bpages%5D%5Bsys_language_uid%5D=1","Edit Page with overrides",0,0,,
+,11,1,"web_info|","/typo3/index.php?&route=%2Fmodule%2Fweb%2Finfo&id=123&SET%5Bfunction%5D=TYPO3%5CCMS%5CInfo%5CController%5CTranslationStatusController&SET%5Bdepth%5D=3","Localization Overview",0,0,,
+,12,1,"system_dbint|","/typo3/index.php?&route=%2Fmodule%2Fsystem%2Fdbint&SET%5Bfunction%5D=search&SET%5Bsearch%5D=raw&SET%5Bsearch_query_makeQuery%5D=all","Db int full search",0,0,,
+,13,1,"system_BeuserTxBeuser|","/typo3/index.php?&route=%2Fmodule%2Fsystem%2FBeuserTxBeuser&tx_beuser_system_beusertxbeuser%5Baction%5D=index&tx_beuser_system_beusertxbeuser%5Bcontroller%5D=BackendUser","Backend users",0,0,,
+,14,1,"web_list|","/typo3/index.php?&route=%2Fmodule%2Fweb%2Flist&id=123&GET%5BclipBoard%5D=1","Recordlist",0,0,,
+,15,1,"web_list|","/typo3/index.php?&route=%2Fmodule%2Fweb%2Flist&id=123&table=tx_styleguide_inline_mnsymmetric&GET%5BclipBoard%5D=1","Single table view",0,0,,
+,16,1,,"/typo3/index.php?&route=%2Fmodule%2Fweb%2Flist&id=123&table=tx_styleguide_inline_mnsymmetric&GET%5BclipBoard%5D=1","Invalid no module name",0,0,,
+,17,1,"web_list|",,"Invalid no url",0,0,,
+,18,1,"web_list|","/typo3/index.php?&route=%2Fmodule%2Fweb%2Flist&id=123&table=tx_styleguide_inline_mnsymmetric&GET%5BclipBoard%5D=1","Invalid new field route not empty",0,0,"route_identifier",
+,19,1,"web_list|","/typo3/index.php?&route=%2Fmodule%2Fweb%2Flist&id=123&table=tx_styleguide_inline_mnsymmetric&GET%5BclipBoard%5D=1","Invalid new field arguments not empty",0,0,,"[]"
+,20,1,,,"New record",0,0,"web_list","{""id"":123,""GET"":{""clipBoard"":true}}"
+,21,1,,,"New record with empty arguments",0,0,"record_edit","[]"
+,22,1,,,"New record with empty route",0,0,,"{""id"":123,""GET"":{""clipBoard"":true}}"
diff --git a/typo3/sysext/install/Tests/Functional/Updates/Fixtures/ShortcutsMigratedToRoutes.csv b/typo3/sysext/install/Tests/Functional/Updates/Fixtures/ShortcutsMigratedToRoutes.csv
new file mode 100644
index 0000000000000000000000000000000000000000..e73e43a6f7060d33c20e0cc69d43425b053b4b8e
--- /dev/null
+++ b/typo3/sysext/install/Tests/Functional/Updates/Fixtures/ShortcutsMigratedToRoutes.csv
@@ -0,0 +1,24 @@
+"sys_be_shortcuts",,,,,,,,,
+,"uid","userid","module_name","url","description","sorting","sc_group","route","arguments"
+,1,1,"system_config|","/typo3/index.php?&route=%2Fmodule%2Fsystem%2Fconfig&tree=siteConfiguration","Site Configuration",0,0,"system_config","{""tree"":""siteConfiguration""}"
+,2,1,"web_FormFormbuilder|","/typo3/index.php?&route=%2Fmodule%2Fweb%2FFormFormbuilder","Form manager",0,0,"web_FormFormbuilder","[]"
+,3,1,"system_BeuserTxPermission|","/typo3/index.php?&route=%2Fmodule%2Fsystem%2FBeuserTxPermission&id=123","Permissions",0,0,"system_BeuserTxPermission","{""id"":""123""}"
+,4,1,"file_FilelistList|","/typo3/index.php?&route=%2Fmodule%2Ffile%2FFilelistList&id=1%3A%2F","Filelist root",0,0,"file_FilelistList","{""id"":""1:\/""}"
+,5,1,"web_layout|","/typo3/index.php?&route=%2Fmodule%2Fweb%2Flayout&id=123&SET%5Btt_content_showHidden%5D=1&SET%5Bfunction%5D=1&SET%5Blanguage%5D=0","Web layout",0,0,"web_layout","{""id"":""123"",""SET"":{""tt_content_showHidden"":""1"",""function"":""1"",""language"":""0""}}"
+,6,1,"xMOD_alt_doc.php|","/typo3/index.php?&route=%2Frecord%2Fedit&returnUrl=%2Ftypo3%2Findex.php%3Froute%3D%252Fmodule%252Fweb%252Flayout%26token%3D123456%26id%3D123%26%23element-tt_content-3&edit%5Btt_content%5D%5B3%5D=edit","Edit Page Content",0,0,"record_edit","{""edit"":{""tt_content"":{""3"":""edit""}}}"
+,7,1,"xMOD_alt_doc.php|","/typo3/index.php?&route=%2Frecord%2Fedit&returnUrl=%2Ftypo3%2Findex.php%3Froute%3D%252Fmodule%252Fweb%252Flist%26token%3D123456%26id%3D0%26table%3D&edit%5Bbe_groups%5D%5B3%5D=edit","Edit Backend usergroup on root level",0,0,"record_edit","{""edit"":{""be_groups"":{""3"":""edit""}}}"
+,8,1,"xMOD_alt_doc.php|","/typo3/index.php?&route=%2Frecord%2Fedit&returnUrl=%2Ftypo3%2Findex.php%3Froute%3D%252Fmodule%252Fweb%252Flist%26token%3D123456%26id%3D0%26table%3D&edit%5Bbe_groups%5D%5B0%5D=new","Create new Backend usergroup",0,0,"record_edit","{""edit"":{""be_groups"":[""new""]}}"
+,9,1,"xMOD_alt_doc.php|","/typo3/index.php?&route=%2Frecord%2Fedit&returnUrl=%2Ftypo3%2Findex.php%3Froute%3D%252Fmodule%252Fsystem%252FBeuserTxBeuser%26token%3D123456&edit%5Bbe_users%5D%5B1%5D=edit","Edit Backend user on root level",0,0,"record_edit","{""edit"":{""be_users"":{""1"":""edit""}}}"
+,10,1,"xMOD_alt_doc.php|","/typo3/index.php?&route=%2Frecord%2Fedit&returnUrl=%2Ftypo3%2Findex.php%3Froute%3D%252Fmodule%252Fweb%252Flist%26token%3D123456%26id%3D123%26table%3D&edit%5Bpages%5D%5B416%5D=edit&overrideVals%5Bpages%5D%5Bsys_language_uid%5D=1","Edit Page with overrides",0,0,"record_edit","{""edit"":{""pages"":{""416"":""edit""}},""overrideVals"":{""pages"":{""sys_language_uid"":""1""}}}"
+,11,1,"web_info|","/typo3/index.php?&route=%2Fmodule%2Fweb%2Finfo&id=123&SET%5Bfunction%5D=TYPO3%5CCMS%5CInfo%5CController%5CTranslationStatusController&SET%5Bdepth%5D=3","Localization Overview",0,0,"web_info","{""id"":""123"",""SET"":{""function"":""TYPO3\\CMS\\Info\\Controller\\TranslationStatusController"",""depth"":""3""}}"
+,12,1,"system_dbint|","/typo3/index.php?&route=%2Fmodule%2Fsystem%2Fdbint&SET%5Bfunction%5D=search&SET%5Bsearch%5D=raw&SET%5Bsearch_query_makeQuery%5D=all","Db int full search",0,0,"system_dbint","{""SET"":{""function"":""search"",""search"":""raw"",""search_query_makeQuery"":""all""}}"
+,13,1,"system_BeuserTxBeuser|","/typo3/index.php?&route=%2Fmodule%2Fsystem%2FBeuserTxBeuser&tx_beuser_system_beusertxbeuser%5Baction%5D=index&tx_beuser_system_beusertxbeuser%5Bcontroller%5D=BackendUser","Backend users",0,0,"system_BeuserTxBeuser","{""tx_beuser_system_beusertxbeuser"":{""action"":""index"",""controller"":""BackendUser""}}"
+,14,1,"web_list|","/typo3/index.php?&route=%2Fmodule%2Fweb%2Flist&id=123&GET%5BclipBoard%5D=1","Recordlist",0,0,"web_list","{""id"":""123"",""GET"":{""clipBoard"":""1""}}"
+,15,1,"web_list|","/typo3/index.php?&route=%2Fmodule%2Fweb%2Flist&id=123&table=tx_styleguide_inline_mnsymmetric&GET%5BclipBoard%5D=1","Single table view",0,0,"web_list","{""id"":""123"",""table"":""tx_styleguide_inline_mnsymmetric"",""GET"":{""clipBoard"":""1""}}"
+,16,1,"","/typo3/index.php?&route=%2Fmodule%2Fweb%2Flist&id=123&table=tx_styleguide_inline_mnsymmetric&GET%5BclipBoard%5D=1","Invalid no module name",0,0,,
+,17,1,"web_list|",,"Invalid no url",0,0,,
+,18,1,"web_list|","/typo3/index.php?&route=%2Fmodule%2Fweb%2Flist&id=123&table=tx_styleguide_inline_mnsymmetric&GET%5BclipBoard%5D=1","Invalid new field route not empty",0,0,"route_identifier",
+,19,1,"web_list|","/typo3/index.php?&route=%2Fmodule%2Fweb%2Flist&id=123&table=tx_styleguide_inline_mnsymmetric&GET%5BclipBoard%5D=1","Invalid new field arguments not empty",0,0,,"[]"
+,20,1,,,"New record",0,0,"web_list","{""id"":123,""GET"":{""clipBoard"":true}}"
+,21,1,,,"New record with empty arguments",0,0,"record_edit","[]"
+,22,1,,,"New record with empty route",0,0,,"{""id"":123,""GET"":{""clipBoard"":true}}"
diff --git a/typo3/sysext/install/Tests/Functional/Updates/ShortcutRecordsMigrationTest.php b/typo3/sysext/install/Tests/Functional/Updates/ShortcutRecordsMigrationTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..0d5d31af8a1814787bc78c86abe3de4e47c0cb37
--- /dev/null
+++ b/typo3/sysext/install/Tests/Functional/Updates/ShortcutRecordsMigrationTest.php
@@ -0,0 +1,75 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * 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!
+ */
+
+namespace TYPO3\CMS\Install\Tests\Functional\Updates;
+
+use Doctrine\DBAL\Schema\Column;
+use Doctrine\DBAL\Types\StringType;
+use Doctrine\DBAL\Types\TextType;
+use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\Core\Database\Schema\TableDiff;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Install\Updates\ShortcutRecordsMigration;
+use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
+
+class ShortcutRecordsMigrationTest extends FunctionalTestCase
+{
+    private const TABLE_NAME = 'sys_be_shortcuts';
+
+    /**
+     * Require additional core extensions so the routes
+     * of the modules in the fixture are available.
+     *
+     * @var string[]
+     */
+    protected $coreExtensionsToLoad = ['beuser', 'filelist', 'form', 'info', 'lowlevel'];
+
+    protected string $baseDataSet = 'typo3/sysext/install/Tests/Functional/Updates/Fixtures/ShortcutsBase.csv';
+    protected string $resultDataSet = 'typo3/sysext/install/Tests/Functional/Updates/Fixtures/ShortcutsMigratedToRoutes.csv';
+
+    /**
+     * @test
+     */
+    public function shortcutRecordsUpdated(): void
+    {
+        $subject = new ShortcutRecordsMigration();
+
+        $schemaManager = GeneralUtility::makeInstance(ConnectionPool::class)
+            ->getConnectionForTable(self::TABLE_NAME)
+            ->getSchemaManager();
+
+        $schemaManager->alterTable(
+            new TableDiff(
+                self::TABLE_NAME,
+                [
+                    new Column('module_name', new StringType(), ['length' => 255, 'default' => '']),
+                    new Column('url', new TextType(), ['notnull' => false])
+                ]
+            )
+        );
+
+        $this->importCSVDataSet(GeneralUtility::getFileAbsFileName($this->baseDataSet));
+        self::assertTrue($subject->updateNecessary());
+        $subject->executeUpdate();
+        self::assertFalse($subject->updateNecessary());
+        $this->assertCSVDataSet(GeneralUtility::getFileAbsFileName($this->resultDataSet));
+
+        // Just ensure that running the upgrade again does not change anything
+        $subject->executeUpdate();
+        $this->assertCSVDataSet(GeneralUtility::getFileAbsFileName($this->resultDataSet));
+    }
+}
diff --git a/typo3/sysext/install/ext_localconf.php b/typo3/sysext/install/ext_localconf.php
index d04eefeaaea397040a1be572129327042bb05f0f..19b5e1a175ad26342517b85e1956ef73809b110c 100644
--- a/typo3/sysext/install/ext_localconf.php
+++ b/typo3/sysext/install/ext_localconf.php
@@ -11,6 +11,8 @@ $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update']['taskcenterEx
     = \TYPO3\CMS\Install\Updates\TaskcenterExtractionUpdate::class;
 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update']['sysActionExtension']
     = \TYPO3\CMS\Install\Updates\SysActionExtractionUpdate::class;
+$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update']['shortcutRecordsMigration']
+    = \TYPO3\CMS\Install\Updates\ShortcutRecordsMigration::class;
 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update']['databaseRowsUpdateWizard']
     = \TYPO3\CMS\Install\Updates\DatabaseRowsUpdateWizard::class;
 
diff --git a/typo3/sysext/lowlevel/Classes/Controller/ConfigurationController.php b/typo3/sysext/lowlevel/Classes/Controller/ConfigurationController.php
index f9e3767b0840c3976205239ef71e60bcf9a39545..5a6caf117b9bfe3bc719ce5469ec147b398b4814 100644
--- a/typo3/sysext/lowlevel/Classes/Controller/ConfigurationController.php
+++ b/typo3/sysext/lowlevel/Classes/Controller/ConfigurationController.php
@@ -121,7 +121,8 @@ class ConfigurationController
 
         // Shortcut in doc header
         $shortcutButton = $moduleTemplate->getDocHeaderComponent()->getButtonBar()->makeShortcutButton();
-        $shortcutButton->setModuleName('system_config')
+        $shortcutButton
+            ->setRouteIdentifier('system_config')
             ->setDisplayName($configurationProvider->getLabel())
             ->setArguments([
                 'route' => $request->getQueryParams()['route'],
diff --git a/typo3/sysext/lowlevel/Classes/Controller/DatabaseIntegrityController.php b/typo3/sysext/lowlevel/Classes/Controller/DatabaseIntegrityController.php
index e7928a7827f04bd673b2f7f1ad9d362b4013b9ef..175f16b110df898301b59385175f1cd42d8ee37f 100644
--- a/typo3/sysext/lowlevel/Classes/Controller/DatabaseIntegrityController.php
+++ b/typo3/sysext/lowlevel/Classes/Controller/DatabaseIntegrityController.php
@@ -140,7 +140,7 @@ class DatabaseIntegrityController
         $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
         // Shortcut
         $shortCutButton = $buttonBar->makeShortcutButton()
-            ->setModuleName($this->moduleName)
+            ->setRouteIdentifier($this->moduleName)
             ->setDisplayName($this->MOD_MENU['function'][$this->MOD_SETTINGS['function']])
             ->setArguments([
                 'route' => $request->getQueryParams()['route'],
diff --git a/typo3/sysext/recordlist/Classes/RecordList/DatabaseRecordList.php b/typo3/sysext/recordlist/Classes/RecordList/DatabaseRecordList.php
index b734e056d595639c8da3b04407fbc85c1e3f6137..edb8686c5ccf028a7bd0313fb54f9bb5fde62682 100644
--- a/typo3/sysext/recordlist/Classes/RecordList/DatabaseRecordList.php
+++ b/typo3/sysext/recordlist/Classes/RecordList/DatabaseRecordList.php
@@ -601,7 +601,7 @@ class DatabaseRecordList
             $buttonBar->addButton($reloadButton, ButtonBar::BUTTON_POSITION_RIGHT);
 
             // Shortcut
-            $shortCutButton = $buttonBar->makeShortcutButton()->setModuleName('web_list');
+            $shortCutButton = $buttonBar->makeShortcutButton()->setRouteIdentifier('web_list');
             $queryParams = $request->getQueryParams();
             $arguments = [
                 'route' => $queryParams['route'],
diff --git a/typo3/sysext/recycler/Classes/Controller/RecyclerModuleController.php b/typo3/sysext/recycler/Classes/Controller/RecyclerModuleController.php
index 088d96ea4e164bbe8cc7412bc66b2dceef54e24f..a96818d00e5371d9d24efd5f5c047edf4f3a90e2 100644
--- a/typo3/sysext/recycler/Classes/Controller/RecyclerModuleController.php
+++ b/typo3/sysext/recycler/Classes/Controller/RecyclerModuleController.php
@@ -157,7 +157,7 @@ class RecyclerModuleController
         $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
 
         $shortcutButton = $buttonBar->makeShortcutButton()
-            ->setModuleName('web_RecyclerRecycler')
+            ->setRouteIdentifier('web_RecyclerRecycler')
             ->setDisplayName($this->getShortcutTitle())
             ->setArguments([
                 'route' => $route,
diff --git a/typo3/sysext/redirects/Classes/Controller/ManagementController.php b/typo3/sysext/redirects/Classes/Controller/ManagementController.php
index 63772e557519f7f6427ea5a37074aa94773cf66d..9b24cf15a6094d16c15cdb88b882b8e7d3acb128 100644
--- a/typo3/sysext/redirects/Classes/Controller/ManagementController.php
+++ b/typo3/sysext/redirects/Classes/Controller/ManagementController.php
@@ -178,7 +178,7 @@ class ManagementController
 
         // Shortcut
         $shortcutButton = $buttonBar->makeShortcutButton()
-            ->setModuleName('site_redirects')
+            ->setRouteIdentifier('site_redirects')
             ->setDisplayName($this->getLanguageService()->sL('LLL:EXT:redirects/Resources/Private/Language/locallang_module_redirect.xlf:mlang_labels_tablabel'))
             ->setArguments([
                 'route' => $this->request->getQueryParams()['route'],
diff --git a/typo3/sysext/reports/Classes/Controller/ReportController.php b/typo3/sysext/reports/Classes/Controller/ReportController.php
index bc1549cc4c2ecbd87b4eab038c446e6df90a524a..cab8e4fbe3ff658215e01723e3f35244bce2bafa 100644
--- a/typo3/sysext/reports/Classes/Controller/ReportController.php
+++ b/typo3/sysext/reports/Classes/Controller/ReportController.php
@@ -113,7 +113,7 @@ class ReportController
 
         $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
         $shortcutButton = $buttonBar->makeShortcutButton()
-            ->setModuleName('system_reports')
+            ->setRouteIdentifier('system_reports')
             ->setDisplayName($this->shortcutName)
             ->setArguments([
                 'route' => $request->getQueryParams()['route'],
diff --git a/typo3/sysext/scheduler/Classes/Controller/SchedulerModuleController.php b/typo3/sysext/scheduler/Classes/Controller/SchedulerModuleController.php
index 41133aaa2e4723df5a8dd12bd78e375a0f577526..c496ac9dfa51b9a2f155020d43b8f198622be387 100644
--- a/typo3/sysext/scheduler/Classes/Controller/SchedulerModuleController.php
+++ b/typo3/sysext/scheduler/Classes/Controller/SchedulerModuleController.php
@@ -1383,7 +1383,7 @@ class SchedulerModuleController
             $shortcutArguments['tx_scheduler']['uid'] = $queryParams['tx_scheduler']['uid'];
         }
         $shortcutButton = $buttonBar->makeShortcutButton()
-            ->setModuleName('system_txschedulerM1')
+            ->setRouteIdentifier('system_txschedulerM1')
             ->setDisplayName($this->MOD_MENU['function'][$this->MOD_SETTINGS['function']])
             ->setArguments($shortcutArguments);
         $buttonBar->addButton($shortcutButton);
diff --git a/typo3/sysext/setup/Classes/Controller/SetupModuleController.php b/typo3/sysext/setup/Classes/Controller/SetupModuleController.php
index 6752f38347f38588508c51ff016f1708bba9953c..5d31394ddef1c13cdde45b5565bcd1cb91e3e2bf 100644
--- a/typo3/sysext/setup/Classes/Controller/SetupModuleController.php
+++ b/typo3/sysext/setup/Classes/Controller/SetupModuleController.php
@@ -430,7 +430,7 @@ class SetupModuleController
 
         $buttonBar->addButton($saveButton);
         $shortcutButton = $buttonBar->makeShortcutButton()
-            ->setModuleName($this->moduleName)
+            ->setRouteIdentifier($this->moduleName)
             ->setDisplayName($this->getLanguageService()->sL('LLL:EXT:setup/Resources/Private/Language/locallang_mod.xlf:mlang_labels_tablabel'))
             ->setArguments([
                 'route' => $route,
diff --git a/typo3/sysext/tstemplate/Classes/Controller/TypoScriptTemplateModuleController.php b/typo3/sysext/tstemplate/Classes/Controller/TypoScriptTemplateModuleController.php
index f4a197478732fce0812fff393a94c675e02ee835..b6405e12fa538e3ef91af97b28b57aadfd57ec1d 100644
--- a/typo3/sysext/tstemplate/Classes/Controller/TypoScriptTemplateModuleController.php
+++ b/typo3/sysext/tstemplate/Classes/Controller/TypoScriptTemplateModuleController.php
@@ -373,7 +373,7 @@ class TypoScriptTemplateModuleController
         }
         // Shortcut
         $shortcutButton = $buttonBar->makeShortcutButton()
-            ->setModuleName('web_ts')
+            ->setRouteIdentifier('web_ts')
             ->setDisplayName($this->getShortcutTitle())
             ->setArguments([
                 'route' => $this->request->getQueryParams()['route'],
diff --git a/typo3/sysext/viewpage/Classes/Controller/ViewModuleController.php b/typo3/sysext/viewpage/Classes/Controller/ViewModuleController.php
index 74f3666b7189dc5c775c738f8969c732acab08ce..bc1d619a3ac9b2d01817040591dbe2754b2e64ef 100644
--- a/typo3/sysext/viewpage/Classes/Controller/ViewModuleController.php
+++ b/typo3/sysext/viewpage/Classes/Controller/ViewModuleController.php
@@ -136,7 +136,7 @@ class ViewModuleController
 
         // Shortcut
         $shortcutButton = $buttonBar->makeShortcutButton()
-            ->setModuleName('web_ViewpageView')
+            ->setRouteIdentifier('web_ViewpageView')
             ->setDisplayName($this->getShortcutTitle($pageId))
             ->setArguments([
                 'route' => $route,
diff --git a/typo3/sysext/workspaces/Classes/Controller/ReviewController.php b/typo3/sysext/workspaces/Classes/Controller/ReviewController.php
index 6cb11e4556e9bef3d542326c9376868fb44e1bea..2c5451ed5bbe8299182abd73c16c3c3483849a33 100644
--- a/typo3/sysext/workspaces/Classes/Controller/ReviewController.php
+++ b/typo3/sysext/workspaces/Classes/Controller/ReviewController.php
@@ -186,7 +186,7 @@ class ReviewController
             $buttonBar->addButton($showButton);
         }
         $shortcutButton = $buttonBar->makeShortcutButton()
-            ->setModuleName('web_WorkspacesWorkspaces')
+            ->setRouteIdentifier('web_WorkspacesWorkspaces')
             ->setDisplayName(sprintf('%s: %s [%d]', $activeWorkspaceTitle, $pageTitle, $this->pageId))
             ->setArguments([
                 'route' => (string)GeneralUtility::_GP('route'),