From e4833fdaff300915df506d69b56b552e46b96d80 Mon Sep 17 00:00:00 2001
From: Oliver Bartsch <bo@cedev.de>
Date: Tue, 15 Dec 2020 17:01:45 +0100
Subject: [PATCH] [!!!][TASK] Rework shortcut PHP API functionality

To be able to introduce URL rewrites for the backend,
the internal handling and registration of the shortcut
PHP API is reworked.

The Shortcut PHP API previously has the full URL of
the shortcut target stored in the database. This lead
to many problems such as shortcuts got invalid as soon
as their target module changed its route path. Furthermore,
this required unnecessary functionality like replacing
tokens on URL creation.

Therefore, a shortcut record now stores only the route
identifier of the module to link to and necessary arguments
in two new database columns. A upgrade wizard is in place
to migrate existing data.

The rework also required to deprecate some methods in
the ShortcutButton API and a parameter signature change
of the JavaScript function `TYPO3.ShortcutMenu.createShortcut()`
which performs the AJAX call to create new shortcuts.

Side effect, this also deprecated the last remains of
xMOD_alt_doc.php in the core.

Resolves: #93093
Releases: master
Change-Id: I07666a299651e4953b4adf2987fcd3469094c288
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/67143
Tested-by: TYPO3com <noreply@typo3.com>
Tested-by: Daniel Goerz <daniel.goerz@posteo.de>
Tested-by: Christian Kuhn <lolli@schwarzbu.ch>
Reviewed-by: Daniel Goerz <daniel.goerz@posteo.de>
Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch>
---
 .../Public/TypeScript/Toolbar/ShortcutMenu.ts |  19 +-
 .../Backend/Shortcut/ShortcutRepository.php   | 355 +++++++-----------
 .../Controller/EditDocumentController.php     |   3 +-
 .../Classes/Controller/HelpController.php     |   3 +-
 .../Controller/PageLayoutController.php       |   3 +-
 .../Classes/Controller/ShortcutController.php |  14 +-
 .../SiteConfigurationController.php           |   2 +-
 .../Buttons/Action/ShortcutButton.php         | 184 +++++++--
 .../Classes/Template/ModuleTemplate.php       |  66 +++-
 .../Button/ShortcutButtonViewHelper.php       |  29 +-
 .../backend/Configuration/Services.yaml       |   3 +
 .../Public/JavaScript/Toolbar/ShortcutMenu.js |   2 +-
 .../Backend/Fixtures/ShortcutsAddedResult.csv |  11 +
 .../Backend/Fixtures/ShortcutsBase.csv        |   8 +
 .../Shortcut/ShortcutRepositoryTest.php       | 181 +++++++++
 .../Controller/ShortcutControllerTest.php     | 105 ++++++
 .../Functional/Fixtures/sys_be_shortcuts.xml  |   9 +
 .../Buttons/Action/ShortcutButtonTest.php     | 122 ++++++
 .../Template/Fixtures/RecordList.html         |  10 +
 .../Fixtures/RecordListSingleTable.html       |  10 +
 .../Fixtures/SpecialRouteIdentifier.html      |  10 +
 .../SpecialRouteIdentifierWithArguments.html  |  10 +
 .../Controller/PermissionController.php       |   3 +-
 .../Resources/Private/Layouts/Default.html    |   2 +-
 .../Breaking-93093-ReworkShortcutPHPAPI.rst   | 131 +++++++
 ...ecation-92132-DeprecatedShortcutPHPAPI.rst |  22 +-
 ...60-ShortcutTitleMustBeSetByControllers.rst |   2 +
 ...93-DeprecateMethodNameInShortcutPHPAPI.rst |  67 ++++
 typo3/sysext/core/ext_tables.sql              |   4 +-
 .../Classes/Controller/FileListController.php |   2 +-
 .../Controller/FormManagerController.php      |   4 +-
 .../Controller/InfoModuleController.php       |   2 +-
 .../Updates/ShortcutRecordsMigration.php      | 191 ++++++++++
 .../Php/MethodCallMatcher.php                 |  14 +
 .../Updates/Fixtures/ShortcutsBase.csv        |  24 ++
 .../Fixtures/ShortcutsMigratedToRoutes.csv    |  24 ++
 .../Updates/ShortcutRecordsMigrationTest.php  |  75 ++++
 typo3/sysext/install/ext_localconf.php        |   2 +
 .../Controller/ConfigurationController.php    |   3 +-
 .../DatabaseIntegrityController.php           |   2 +-
 .../Classes/RecordList/DatabaseRecordList.php |   2 +-
 .../Controller/RecyclerModuleController.php   |   2 +-
 .../Controller/ManagementController.php       |   2 +-
 .../Classes/Controller/ReportController.php   |   2 +-
 .../Controller/SchedulerModuleController.php  |   2 +-
 .../Controller/SetupModuleController.php      |   2 +-
 .../TypoScriptTemplateModuleController.php    |   2 +-
 .../Controller/ViewModuleController.php       |   2 +-
 .../Classes/Controller/ReviewController.php   |   2 +-
 49 files changed, 1429 insertions(+), 322 deletions(-)
 create mode 100644 typo3/sysext/backend/Tests/Functional/Backend/Fixtures/ShortcutsAddedResult.csv
 create mode 100644 typo3/sysext/backend/Tests/Functional/Backend/Fixtures/ShortcutsBase.csv
 create mode 100644 typo3/sysext/backend/Tests/Functional/Backend/Shortcut/ShortcutRepositoryTest.php
 create mode 100644 typo3/sysext/backend/Tests/Functional/Controller/ShortcutControllerTest.php
 create mode 100644 typo3/sysext/backend/Tests/Functional/Fixtures/sys_be_shortcuts.xml
 create mode 100644 typo3/sysext/backend/Tests/Functional/Template/Components/Buttons/Action/ShortcutButtonTest.php
 create mode 100644 typo3/sysext/backend/Tests/Functional/Template/Fixtures/RecordList.html
 create mode 100644 typo3/sysext/backend/Tests/Functional/Template/Fixtures/RecordListSingleTable.html
 create mode 100644 typo3/sysext/backend/Tests/Functional/Template/Fixtures/SpecialRouteIdentifier.html
 create mode 100644 typo3/sysext/backend/Tests/Functional/Template/Fixtures/SpecialRouteIdentifierWithArguments.html
 create mode 100644 typo3/sysext/core/Documentation/Changelog/master/Breaking-93093-ReworkShortcutPHPAPI.rst
 create mode 100644 typo3/sysext/core/Documentation/Changelog/master/Deprecation-93093-DeprecateMethodNameInShortcutPHPAPI.rst
 create mode 100644 typo3/sysext/install/Classes/Updates/ShortcutRecordsMigration.php
 create mode 100644 typo3/sysext/install/Tests/Functional/Updates/Fixtures/ShortcutsBase.csv
 create mode 100644 typo3/sysext/install/Tests/Functional/Updates/Fixtures/ShortcutsMigratedToRoutes.csv
 create mode 100644 typo3/sysext/install/Tests/Functional/Updates/ShortcutRecordsMigrationTest.php

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 289b33554416..cab8f00a6b1f 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 01aa444c6422..30d46a147c25 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 45a8787d08be..c0b4ae3cfa89 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 c0e41b648a39..c516e5d97ae7 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 ba560d93af88..f33c01e361d3 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 eb6ec8357696..093c87dbe564 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 2aec52beab17..45e9cff49ac8 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 3d4753bdb8c3..9cb219764076 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 fc19116df4b3..0aa03794a2ae 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 e342d05f4245..d5431f529e73 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 4b04fb44760f..29fc73a5a67f 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 57f414b7211d..0ca34a5707f1 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 000000000000..bc7aed9a77ae
--- /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 000000000000..ffe0d2cf721e
--- /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 000000000000..639acf01e42e
--- /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 000000000000..e1af847e0983
--- /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 000000000000..6eabf0bea49d
--- /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 000000000000..9671eb798f4d
--- /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 000000000000..ef705a2044f9
--- /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 000000000000..74931a73eed6
--- /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 000000000000..ade7e7a57744
--- /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 000000000000..631533788f8c
--- /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 48ea870d4276..4bfdc371427e 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 e95d0c2ce25f..19ae9915619c 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 000000000000..161c759c93b8
--- /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 0d2555f7810c..0e86d36b6524 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 6455bfb42ef9..8d2545490c73 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 000000000000..a235b2b27c34
--- /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 c5cb7ec53667..5d95f477154f 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 5610bbbc48b2..487ebd94276f 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 f39367660404..c78817c3c0d5 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 6c5729a86344..5ae8246d49d9 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 000000000000..2f4bde1068e4
--- /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 ddc6e54c2ab2..74d07e3516a5 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 000000000000..44a06457f4c6
--- /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 000000000000..e73e43a6f706
--- /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 000000000000..0d5d31af8a18
--- /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 d04eefeaaea3..19b5e1a175ad 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 f9e3767b0840..5a6caf117b9b 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 e7928a7827f0..175f16b110df 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 b734e056d595..edb8686c5ccf 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 088d96ea4e16..a96818d00e53 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 63772e557519..9b24cf15a609 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 bc1549cc4c2e..cab8e4fbe3ff 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 41133aaa2e47..c496ac9dfa51 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 6752f38347f3..5d31394ddef1 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 f4a197478732..b6405e12fa53 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 74f3666b7189..bc1d619a3ac9 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 6cb11e4556e9..2c5451ed5bbe 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'),
-- 
GitLab