[!!!][FEATURE] Introduce Backend Routing 95/38095/15
authorBenjamin Mack <benni@typo3.org>
Tue, 3 Mar 2015 14:02:28 +0000 (15:02 +0100)
committerBenni Mack <benni@typo3.org>
Fri, 7 Aug 2015 09:26:18 +0000 (11:26 +0200)
A new Routing API is introduced in order to streamline
the entrypoints to the TYPO3 Backend.

Instead of using the term "module" for anything linkable
in the backend, the term "routes" fits more. A "module"
or an ajax call is a derivative of a route, which will
build on this foundation.

Routes can be registered via
Configuration/Backend/Routes.php in any extension
and are loaded solely on Backend requests.

There are three new classes:
- Route (a single route with a path and some options)
- Router (API to match paths)
- UriBuilder (Generates a Backend URI)

This patch changes the entrypoint for
login/logout to typo3/index.php?route=...&token=....

The main RequestHandler of all Backend modules
detects where a route parameter is given and
then resolves to a controller which inherits the ControllerInterface
introduced with PSR-7, and checks for a valid token.

See http://wiki.typo3.org/Blueprints/BackendRouting
for implementation details.

Resolves: #65493
Releases: master
Change-Id: I39257df45b177793c5e8f57970b4088183b78c73
Reviewed-on: http://review.typo3.org/38095
Reviewed-by: Wouter Wolters <typo3@wouterwolters.nl>
Tested-by: Wouter Wolters <typo3@wouterwolters.nl>
Reviewed-by: Susanne Moog <typo3@susannemoog.de>
Tested-by: Susanne Moog <typo3@susannemoog.de>
Reviewed-by: Daniel Maier <dani-maier@gmx.de>
Tested-by: Daniel Maier <dani-maier@gmx.de>
Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch>
Tested-by: Christian Kuhn <lolli@schwarzbu.ch>
Reviewed-by: Benni Mack <benni@typo3.org>
Tested-by: Benni Mack <benni@typo3.org>
22 files changed:
typo3/logout.php
typo3/sysext/backend/Classes/Controller/BackendController.php
typo3/sysext/backend/Classes/Controller/LogoutController.php
typo3/sysext/backend/Classes/Http/AjaxRequestHandler.php
typo3/sysext/backend/Classes/Http/BackendModuleRequestHandler.php
typo3/sysext/backend/Classes/Http/RequestHandler.php
typo3/sysext/backend/Classes/Routing/Exception/ResourceNotFoundException.php [new file with mode: 0644]
typo3/sysext/backend/Classes/Routing/Exception/RouteNotFoundException.php [new file with mode: 0644]
typo3/sysext/backend/Classes/Routing/Route.php [new file with mode: 0644]
typo3/sysext/backend/Classes/Routing/Router.php [new file with mode: 0644]
typo3/sysext/backend/Classes/Routing/UriBuilder.php [new file with mode: 0644]
typo3/sysext/backend/Classes/Utility/BackendUtility.php
typo3/sysext/backend/Configuration/Backend/Routes.php [new file with mode: 0644]
typo3/sysext/backend/Modules/Logout/conf.php [deleted file]
typo3/sysext/backend/Modules/Logout/index.php [deleted file]
typo3/sysext/backend/Modules/Main/conf.php [deleted file]
typo3/sysext/backend/Modules/Main/index.php [deleted file]
typo3/sysext/backend/ext_tables.php
typo3/sysext/core/Classes/Core/Bootstrap.php
typo3/sysext/core/Documentation/Changelog/master/Feature-65493-BackendRouting.rst [new file with mode: 0644]
typo3/sysext/extbase/Classes/Mvc/Web/Routing/UriBuilder.php
typo3/sysext/extbase/Tests/Unit/Mvc/Web/Routing/UriBuilderTest.php

index 9ec3920..c753408 100644 (file)
@@ -25,5 +25,9 @@ call_user_func(function() {
 
                $logoutController = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Controller\LogoutController::class);
                $logoutController->logout();
+               // do the redirect
+               $redirect = \TYPO3\CMS\Core\Utility\GeneralUtility::sanitizeLocalUrl(\TYPO3\CMS\Core\Utility\GeneralUtility::_GP('redirect'));
+               $redirectUrl = $redirect ?: 'index.php';
+               \TYPO3\CMS\Core\Utility\HttpUtility::redirect($redirectUrl);
        });
 });
index 34bf85c..60fbfd9 100644 (file)
@@ -14,10 +14,12 @@ namespace TYPO3\CMS\Backend\Controller;
  * The TYPO3 project - inspiring people to share!
  */
 
+use Psr\Http\Message\ServerRequestInterface;
 use TYPO3\CMS\Backend\Domain\Repository\Module\BackendModuleRepository;
 use TYPO3\CMS\Backend\Module\ModuleLoader;
 use TYPO3\CMS\Backend\Toolbar\ToolbarItemInterface;
 use TYPO3\CMS\Backend\Utility\BackendUtility;
+use TYPO3\CMS\Core\Http\Response;
 use TYPO3\CMS\Core\Page\PageRenderer;
 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
@@ -28,7 +30,7 @@ use TYPO3\CMS\Rsaauth\RsaEncryptionEncoder;
 /**
  * Class for rendering the TYPO3 backend
  */
-class BackendController {
+class BackendController implements \TYPO3\CMS\Core\Http\ControllerInterface {
 
        /**
         * @var string
@@ -103,6 +105,7 @@ class BackendController {
         * Constructor
         */
        public function __construct() {
+               $this->getLanguageService()->includeLLFile('EXT:lang/locallang_misc.xlf');
                $this->backendModuleRepository = GeneralUtility::makeInstance(BackendModuleRepository::class);
 
                // Set debug flag for BE development only
@@ -167,6 +170,41 @@ class BackendController {
                        $this->menuWidth = (int)$GLOBALS['TBE_STYLES']['dims']['leftMenuFrameW'];
                }
                $this->executeHook('constructPostProcess');
+               $this->includeLegacyBackendItems();
+       }
+
+       /**
+        * Add hooks from the additional backend items to load certain things for the main backend.
+        * This was previously called from the global scope from backend.php.
+        */
+       protected function includeLegacyBackendItems() {
+               $TYPO3backend = $this;
+               // Include extensions which may add css, javascript or toolbar items
+               if (is_array($GLOBALS['TYPO3_CONF_VARS']['typo3/backend.php']['additionalBackendItems'])) {
+                       foreach ($GLOBALS['TYPO3_CONF_VARS']['typo3/backend.php']['additionalBackendItems'] as $additionalBackendItem) {
+                               include_once $additionalBackendItem;
+                       }
+               }
+
+               // Process ExtJS module js and css
+               if (is_array($GLOBALS['TBE_MODULES']['_configuration'])) {
+                       foreach ($GLOBALS['TBE_MODULES']['_configuration'] as $moduleConfig) {
+                               if (is_array($moduleConfig['cssFiles'])) {
+                                       foreach ($moduleConfig['cssFiles'] as $cssFileName => $cssFile) {
+                                               $files = array(\TYPO3\CMS\Core\Utility\GeneralUtility::getFileAbsFileName($cssFile));
+                                               $files = \TYPO3\CMS\Core\Utility\GeneralUtility::removePrefixPathFromList($files, PATH_site);
+                                               $TYPO3backend->addCssFile($cssFileName, '../' . $files[0]);
+                                       }
+                               }
+                               if (is_array($moduleConfig['jsFiles'])) {
+                                       foreach ($moduleConfig['jsFiles'] as $jsFile) {
+                                               $files = array(\TYPO3\CMS\Core\Utility\GeneralUtility::getFileAbsFileName($jsFile));
+                                               $files = \TYPO3\CMS\Core\Utility\GeneralUtility::removePrefixPathFromList($files, PATH_site);
+                                               $TYPO3backend->addJavascriptFile('../' . $files[0]);
+                                       }
+                               }
+                       }
+               }
        }
 
        /**
@@ -205,40 +243,27 @@ class BackendController {
        }
 
        /**
+        * Injects the request object for the current request or subrequest
+        * As this controller goes only through the render() method, it is rather simple for now
+        * This will be split up in an abstract controller once proper routing/dispatcher is in place.
+        *
+        * @param ServerRequestInterface $request
+        * @return \Psr\Http\Message\ResponseInterface $response
+        */
+       public function processRequest(ServerRequestInterface $request) {
+               $this->render();
+               /** @var Response $response */
+               $response = GeneralUtility::makeInstance(Response::class);
+               $response->getBody()->write($this->content);
+               return $response;
+       }
+
+       /**
         * Main function generating the BE scaffolding
         *
         * @return void
         */
        public function render() {
-               // Needed for the hooks below, as they previously were located in the global scope
-               // Caution: do not use the global variable anymore but only reference "$this", or use the "renderPreProcess"
-               $GLOBALS['TYPO3backend'] = $TYPO3backend = $this;
-               // Include extensions which may add css, javascript or toolbar items
-               if (is_array($GLOBALS['TYPO3_CONF_VARS']['typo3/backend.php']['additionalBackendItems'])) {
-                       foreach ($GLOBALS['TYPO3_CONF_VARS']['typo3/backend.php']['additionalBackendItems'] as $additionalBackendItem) {
-                               include_once $additionalBackendItem;
-                       }
-               }
-
-               // Process ExtJS module js and css
-               if (is_array($GLOBALS['TBE_MODULES']['_configuration'])) {
-                       foreach ($GLOBALS['TBE_MODULES']['_configuration'] as $moduleConfig) {
-                               if (is_array($moduleConfig['cssFiles'])) {
-                                       foreach ($moduleConfig['cssFiles'] as $cssFileName => $cssFile) {
-                                               $files = array(GeneralUtility::getFileAbsFileName($cssFile));
-                                               $files = GeneralUtility::removePrefixPathFromList($files, PATH_site);
-                                               $this->addCssFile($cssFileName, '../' . $files[0]);
-                                       }
-                               }
-                               if (is_array($moduleConfig['jsFiles'])) {
-                                       foreach ($moduleConfig['jsFiles'] as $jsFile) {
-                                               $files = array(GeneralUtility::getFileAbsFileName($jsFile));
-                                               $files = GeneralUtility::removePrefixPathFromList($files, PATH_site);
-                                               $this->addJavascriptFile('../' . $files[0]);
-                                       }
-                               }
-                       }
-               }
                $this->executeHook('renderPreProcess');
 
                // Prepare the scaffolding, at this point extension may still add javascript and css
@@ -311,7 +336,6 @@ class BackendController {
                $this->content = $this->getDocumentTemplate()->render($title, $view->render());
                $hookConfiguration = array('content' => &$this->content);
                $this->executeHook('renderPostProcess', $hookConfiguration);
-               echo $this->content;
        }
 
        /**
index ecf78c2..3e1fa10 100644 (file)
@@ -14,15 +14,42 @@ namespace TYPO3\CMS\Backend\Controller;
  * The TYPO3 project - inspiring people to share!
  */
 
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use TYPO3\CMS\Backend\Utility\BackendUtility;
+use TYPO3\CMS\Core\Http\Response;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
-use TYPO3\CMS\Core\Utility\HttpUtility;
 
 /**
  * Script Class for logging a user out.
  * Does not display any content, just calls the logout-function for the current user and then makes a redirect.
  */
-class LogoutController {
+class LogoutController implements \TYPO3\CMS\Core\Http\ControllerInterface {
+
+       /**
+        * Injects the request object for the current request or subrequest
+        * As this controller goes only through the main() method, it is rather simple for now
+        * This will be split up in an abstract controller once proper routing/dispatcher is in place.
+        *
+        * @param ServerRequestInterface $request
+        * @return ResponseInterface $response
+        */
+       public function processRequest(ServerRequestInterface $request) {
+               $this->logout();
+
+               $redirectUrl = isset($request->getParsedBody()['redirect']) ? $request->getParsedBody()['redirect'] : $request->getQueryParams()['redirect'];
+               $redirectUrl = GeneralUtility::sanitizeLocalUrl($redirectUrl);
+               if (empty($redirectUrl)) {
+                       /** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
+                       $uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
+                       $redirectUrl = (string)$uriBuilder->buildUriFromRoute('login', array(), $uriBuilder::ABSOLUTE_URL);
+               }
+               /** @var Response $response */
+               $response = GeneralUtility::makeInstance(Response::class);
+               $response =     $response->withHeader('Location', GeneralUtility::locationHeaderUrl($redirectUrl));
+               return $response->withStatus(303);
+       }
 
        /**
         * Performs the logout processing
@@ -30,17 +57,15 @@ class LogoutController {
         * @return void
         */
        public function logout() {
-               if (!empty($this->getBackendUser()->user['username'])) {
-                       // Logout written to log
-                       $this->getBackendUser()->writelog(255, 2, 0, 1, 'User %s logged out from TYPO3 Backend', array($this->getBackendUser()->user['username']));
-                       /** @var \TYPO3\CMS\Core\FormProtection\BackendFormProtection $backendFormProtection */
-                       $backendFormProtection = FormProtectionFactory::get();
-                       $backendFormProtection->removeSessionTokenFromRegistry();
-                       $this->getBackendUser()->logoff();
+               if (empty($this->getBackendUser()->user['username'])) {
+                       return;
                }
-               $redirect = GeneralUtility::sanitizeLocalUrl(GeneralUtility::_GP('redirect'));
-               $redirectUrl = $redirect ? $redirect : 'index.php';
-               HttpUtility::redirect($redirectUrl);
+               // Logout written to log
+               $this->getBackendUser()->writelog(255, 2, 0, 1, 'User %s logged out from TYPO3 Backend', array($this->getBackendUser()->user['username']));
+               /** @var \TYPO3\CMS\Core\FormProtection\BackendFormProtection $backendFormProtection */
+               $backendFormProtection = FormProtectionFactory::get();
+               $backendFormProtection->removeSessionTokenFromRegistry();
+               $this->getBackendUser()->logoff();
        }
 
        /**
index d58123d..9e1acbc 100644 (file)
@@ -150,6 +150,7 @@ class AjaxRequestHandler implements RequestHandlerInterface {
                        ->checkLockedBackendAndRedirectOrDie($proceedIfNoUserIsLoggedIn)
                        ->checkBackendIpOrDie()
                        ->checkSslBackendAndRedirectIfNeeded()
+                       ->initializeBackendRouter()
                        ->loadExtensionTables(TRUE)
                        ->initializeSpriteManager()
                        ->initializeBackendUser()
index d4b5717..bf39621 100644 (file)
@@ -106,6 +106,7 @@ class BackendModuleRequestHandler implements RequestHandlerInterface {
                $this->bootstrap->checkLockedBackendAndRedirectOrDie()
                        ->checkBackendIpOrDie()
                        ->checkSslBackendAndRedirectIfNeeded()
+                       ->initializeBackendRouter()
                        ->loadExtensionTables(TRUE)
                        ->initializeSpriteManager()
                        ->initializeBackendUser()
index afb6cfd..f2f030b 100644 (file)
@@ -16,7 +16,11 @@ namespace TYPO3\CMS\Backend\Http;
 
 use TYPO3\CMS\Core\Core\Bootstrap;
 use TYPO3\CMS\Core\Http\RequestHandlerInterface;
+use TYPO3\CMS\Backend\Routing\Exception\RouteNotFoundException;
+use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Backend\Routing\Router;
+use TYPO3\CMS\Backend\Routing\Route;
 
 /**
  * General RequestHandler for the TYPO3 Backend. This is used for all Backend requests except for CLI
@@ -24,6 +28,17 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
  * the controller is still done inside places like each single file.
  * This RequestHandler here serves solely to check and set up all requirements needed for a TYPO3 Backend.
  * This class might be changed in the future.
+ *
+ * At first, this request handler serves as a replacement to typo3/init.php. It is called but does not exit
+ * so any typical script that is not dispatched, is just running through the handleRequest() method and then
+ * calls its own code.
+ *
+ * However, if a get/post parameter "route" is set, the unified Backend Routing is called and searches for a
+ * matching route inside the Router. The corresponding controller / action is called then which returns content.
+ *
+ * The following get/post parameters are evaluated here:
+ *   - route
+ *   - token
  */
 class RequestHandler implements RequestHandlerInterface {
 
@@ -49,21 +64,50 @@ class RequestHandler implements RequestHandlerInterface {
         * @return NULL|\Psr\Http\Message\ResponseInterface
         */
        public function handleRequest(\Psr\Http\Message\ServerRequestInterface $request) {
-               // enable dispatching via Request/Response logic only for typo3/index.php currently
+               // enable dispatching via Request/Response logic only for typo3/index.php
+               // This fallback will be removed in TYPO3 CMS 8, as only index.php will be allowed
                $path = substr($request->getUri()->getPath(), strlen(GeneralUtility::getIndpEnv('TYPO3_SITE_PATH')));
                $routingEnabled = ($path === TYPO3_mainDir . 'index.php' || $path === TYPO3_mainDir);
+               $proceedIfNoUserIsLoggedIn = FALSE;
+
+               if ($routingEnabled) {
+                       $pathToRoute = (string)$request->getQueryParams()['route'];
+                       // Allow the login page to be displayed if routing is not used and on index.php
+                       if (empty($pathToRoute)) {
+                               $pathToRoute = '/login';
+                       }
+                       $request = $request->withAttribute('routePath', $pathToRoute);
 
-               // Evaluate the constant for skipping the BE user check for the bootstrap
-               if (defined('TYPO3_PROCEED_IF_NO_USER') && TYPO3_PROCEED_IF_NO_USER) {
-                       $proceedIfNoUserIsLoggedIn = TRUE;
-               } else {
-                       $proceedIfNoUserIsLoggedIn = FALSE;
+                       // Evaluate the constant for skipping the BE user check for the bootstrap
+                       // should be handled differently in the future by checking the Bootstrap directly
+                       if ($pathToRoute === '/login') {
+                               $proceedIfNoUserIsLoggedIn = TRUE;
+                       }
+               }
+
+               $this->boot($proceedIfNoUserIsLoggedIn);
+
+               // Check if the router has the available route and dispatch.
+               if ($routingEnabled) {
+                       return $this->dispatch($request);
                }
 
+               // No route found, so the system proceeds in called entrypoint as fallback.
+               return NULL;
+       }
+
+       /**
+        * Does the main work for setting up the backend environment for any Backend request
+        *
+        * @param bool $proceedIfNoUserIsLoggedIn option to allow to render the request even if no user is logged in
+        * @return void
+        */
+       protected function boot($proceedIfNoUserIsLoggedIn) {
                $this->bootstrap
                        ->checkLockedBackendAndRedirectOrDie()
                        ->checkBackendIpOrDie()
                        ->checkSslBackendAndRedirectIfNeeded()
+                       ->initializeBackendRouter()
                        ->loadExtensionTables(TRUE)
                        ->initializeSpriteManager()
                        ->initializeBackendUser()
@@ -73,17 +117,12 @@ class RequestHandler implements RequestHandlerInterface {
                        ->endOutputBufferingAndCleanPreviousOutput()
                        ->initializeOutputCompression()
                        ->sendHttpHeaders();
-
-               if ($routingEnabled) {
-                       return $this->dispatch($request);
-               }
-               return NULL;
        }
 
        /**
         * This request handler can handle any backend request (but not CLI).
         *
-        * @param \Psr\Http\Message\ServerRequestInterface|\TYPO3\CMS\Core\Console\Request $request
+        * @param \Psr\Http\Message\ServerRequestInterface $request
         * @return bool If the request is not a CLI script, TRUE otherwise FALSE
         */
        public function canHandleRequest(\Psr\Http\Message\ServerRequestInterface $request) {
@@ -101,17 +140,50 @@ class RequestHandler implements RequestHandlerInterface {
        }
 
        /**
-        * Dispatch the request to the appropriate controller, will go to a proper dispatcher/router class in the future
+        * Wrapper method for static form protection utility
         *
-        * @internal
-        * @param \Psr\Http\Message\RequestInterface $request
-        * @return NULL|\Psr\Http\Message\ResponseInterface
+        * @return \TYPO3\CMS\Core\FormProtection\AbstractFormProtection
+        */
+       protected function getFormProtection() {
+               return FormProtectionFactory::get();
+       }
+
+       /**
+        * Checks if the request token is valid. This is checked to see if the route is really
+        * created by the same instance. Should be called for all routes in the backend except
+        * for the ones that don't require a login.
+        *
+        * @param \Psr\Http\Message\ServerRequestInterface $request
+        * @return bool
+        * @see \TYPO3\CMS\Backend\Routing\UriBuilder where the token is generated.
+        */
+       protected function isValidRequest($request) {
+               $token = (string)(isset($request->getParsedBody()['token']) ? $request->getParsedBody()['token'] : $request->getQueryParams()['token']);
+               $route = $request->getAttribute('route');
+               return ($route->getOption('access') === 'public' || $this->getFormProtection()->validateToken($token, 'route', $route->getOption('_identifier')));
+       }
+
+       /**
+        * Dispatch the request to the appropriate controller
+        *
+        * @param \Psr\Http\Message\ServerRequestInterface $request
+        * @return \Psr\Http\Message\ResponseInterface
+        * @throws RouteNotFoundException when no route is registered
+        * @throws \RuntimeException when a route is found but the controller to be called does not implement the Controller Interface
         */
        protected function dispatch($request) {
-               $controller = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Controller\LoginController::class);
-               if ($controller instanceof \TYPO3\CMS\Core\Http\ControllerInterface) {
-                       return $controller->processRequest($request);
+               /** @var Route $route */
+               $router = GeneralUtility::makeInstance(Router::class);
+               $route = $router->matchRequest($request);
+               $request = $request->withAttribute('route', $route);
+               if (!$this->isValidRequest($request)) {
+                       throw new RouteNotFoundException('Invalid request for route "' . $route->getPath() . '"', 1425389455);
                }
-               return NULL;
+               $className = $route->getOption('controller');
+               $controller = GeneralUtility::makeInstance($className);
+               if (!$controller instanceof \TYPO3\CMS\Core\Http\ControllerInterface) {
+                       throw new \RuntimeException('Requested controller "' . $className . '" does not implement the ControllerInterface', 1425389452);
+               }
+               return $controller->processRequest($request);
        }
 }
diff --git a/typo3/sysext/backend/Classes/Routing/Exception/ResourceNotFoundException.php b/typo3/sysext/backend/Classes/Routing/Exception/ResourceNotFoundException.php
new file mode 100644 (file)
index 0000000..7f6eea9
--- /dev/null
@@ -0,0 +1,20 @@
+<?php
+namespace TYPO3\CMS\Backend\Routing\Exception;
+
+/*
+ * 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!
+ */
+
+/**
+ * Exception thrown when a resource was not found.
+ */
+class ResourceNotFoundException extends \TYPO3\CMS\Core\Exception {}
diff --git a/typo3/sysext/backend/Classes/Routing/Exception/RouteNotFoundException.php b/typo3/sysext/backend/Classes/Routing/Exception/RouteNotFoundException.php
new file mode 100644 (file)
index 0000000..8e9e32f
--- /dev/null
@@ -0,0 +1,20 @@
+<?php
+namespace TYPO3\CMS\Backend\Routing\Exception;
+
+/*
+ * 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!
+ */
+
+/**
+ * Exception thrown when a route does not exist
+ */
+class RouteNotFoundException extends \TYPO3\CMS\Core\Exception {}
diff --git a/typo3/sysext/backend/Classes/Routing/Route.php b/typo3/sysext/backend/Classes/Routing/Route.php
new file mode 100644 (file)
index 0000000..5a8a9eb
--- /dev/null
@@ -0,0 +1,123 @@
+<?php
+namespace TYPO3\CMS\Backend\Routing;
+
+/*
+ * 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!
+ */
+
+/**
+ * This is a single entity for a Route.
+ *
+ * The architecture is highly inspired by the Symfony Routing Component.
+ */
+class Route {
+
+       /**
+        * @var string
+        */
+       protected $path = '/';
+
+       /**
+        * @var array
+        */
+       protected $options = array();
+
+       /**
+        * Constructor setting up the required path and options
+        *
+        * @param string $path The path pattern to match
+        * @param array $options An array of options
+        */
+       public function __construct($path, $options) {
+               $this->setPath($path)->setOptions($options);
+       }
+
+       /**
+        * Returns the path
+        *
+        * @return string The path pattern
+        */
+       public function getPath() {
+               return $this->path;
+       }
+
+       /**
+        * Sets the pattern for the path
+        * A pattern must start with a slash and must not have multiple slashes at the beginning because the
+        * generated path for this route would be confused with a network path, e.g. '//domain.com/path'.
+        *
+        * This method implements a fluent interface.
+        *
+        * @param string $pattern The path pattern
+        * @return Route The current Route instance
+        */
+       public function setPath($pattern) {
+               $this->path = '/' . ltrim(trim($pattern), '/');
+               return $this;
+       }
+
+       /**
+        * Returns the options set
+        *
+        * @return array The options
+        */
+       public function getOptions() {
+               return $this->options;
+       }
+
+       /**
+        * Sets the options
+        *
+        * This method implements a fluent interface.
+        *
+        * @param array $options The options
+        * @return Route The current Route instance
+        */
+       public function setOptions(array $options) {
+               $this->options = $options;
+               return $this;
+       }
+
+       /**
+        * Sets an option value
+        *
+        * This method implements a fluent interface.
+        *
+        * @param string $name An option name
+        * @param mixed $value The option value
+        * @return Route The current Route instance
+        */
+       public function setOption($name, $value) {
+               $this->options[$name] = $value;
+               return $this;
+       }
+
+       /**
+        * Get an option value
+        *
+        * @param string $name An option name
+        * @return mixed The option value or NULL when not given
+        */
+       public function getOption($name) {
+               return isset($this->options[$name]) ? $this->options[$name] : NULL;
+       }
+
+       /**
+        * Checks if an option has been set
+        *
+        * @param string $name An option name
+        * @return bool TRUE if the option is set, FALSE otherwise
+        */
+       public function hasOption($name) {
+               return array_key_exists($name, $this->options);
+       }
+}
diff --git a/typo3/sysext/backend/Classes/Routing/Router.php b/typo3/sysext/backend/Classes/Routing/Router.php
new file mode 100644 (file)
index 0000000..1621b19
--- /dev/null
@@ -0,0 +1,91 @@
+<?php
+namespace TYPO3\CMS\Backend\Routing;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Psr\Http\Message\ServerRequestInterface;
+use TYPO3\CMS\Backend\Routing\Exception\ResourceNotFoundException;
+use TYPO3\CMS\Core\Cache\CacheManager;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Package\PackageManager;
+
+/**
+ * Implementation of a class for adding routes, collecting throughout the Bootstrap
+ * to register all sorts of Backend Routes, and to fetch the main Collection in order
+ * to resolve a route (see ->match() and ->matchRequest()).
+ *
+ * Ideally, the Router is solely instantiated and accessed via the Bootstrap, the RequestHandler and the UriBuilder.
+ *
+ * See \TYPO3\CMS\Backend\RequestHandler for more details on route matching() and Bootstrap->initializeBackendRouting().
+ *
+ * The architecture is inspired by the Symfony Routing Component.
+ */
+class Router implements \TYPO3\CMS\Core\SingletonInterface {
+
+       /**
+        * All routes used in the Backend
+        *
+        * @var array|null
+        */
+       protected $routes = array();
+
+       /**
+        * Adds a new route with the identifiers
+        *
+        * @param string $routeIdentifier
+        * @param Route $route
+        */
+       public function addRoute($routeIdentifier, $route) {
+               $this->routes[$routeIdentifier] = $route;
+       }
+
+       /**
+        * Fetch all registered routes, only use in UriBuilder
+        *
+        * @return array|null
+        */
+       public function getRoutes() {
+               return $this->routes;
+       }
+
+       /**
+        * Tries to match a URL path with a set of routes.
+        *
+        * @param string $pathInfo The path info to be parsed
+        * @return Route the first Route object found
+        * @throws ResourceNotFoundException If the resource could not be found
+        */
+       public function match($pathInfo) {
+               $pathInfo = rawurldecode($pathInfo);
+               foreach ($this->routes as $routeIdentifier => $route) {
+                       // This check is done in a simple way as there are no parameters yet (get parameters only)
+                       if ($route->getPath() === $pathInfo) {
+                               // Store the name of the Route in the _identifier option so the token can be checked against that
+                               $route->setOption('_identifier', $routeIdentifier);
+                               return $route;
+                       }
+               }
+               throw new ResourceNotFoundException('The requested resource "' . $pathInfo . '" was not found.', 1425389240);
+       }
+
+       /**
+        * Tries to match a URI against the registered routes
+        *
+        * @param ServerRequestInterface $request
+        * @return Route the first Route object found
+        */
+       public function matchRequest(ServerRequestInterface $request) {
+               return $this->match($request->getAttribute('routePath'));
+       }
+}
diff --git a/typo3/sysext/backend/Classes/Routing/UriBuilder.php b/typo3/sysext/backend/Classes/Routing/UriBuilder.php
new file mode 100644 (file)
index 0000000..7045bea
--- /dev/null
@@ -0,0 +1,148 @@
+<?php
+namespace TYPO3\CMS\Backend\Routing;
+
+/*
+ * 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\Exception\RouteNotFoundException;
+use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
+use TYPO3\CMS\Core\Http\Uri;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\PathUtility;
+
+/**
+ * Main UrlGenerator for creating URLs for the Backend. Generates a URL based on
+ * an identifier defined by Configuration/Backend/Routes.php of an extension,
+ * and adds some more parameters to the URL.
+ *
+ * Currently only available and useful when called from Router->generate() as the information
+ * about possible routes needs to be handed over.
+ */
+class UriBuilder {
+
+       /**
+        * Generates an absolute URL
+        */
+       const ABSOLUTE_URL = 'url';
+
+       /**
+        * Generates an absolute path
+        */
+       const ABSOLUTE_PATH = 'absolute';
+
+       /**
+        * @var array
+        */
+       protected $routes;
+
+       /**
+        * Fetches the available routes from the Router to be used for generating routes
+        */
+       protected function loadBackendRoutes() {
+               $router = GeneralUtility::makeInstance(Router::class);
+               $this->routes = $router->getRoutes();
+       }
+
+       /**
+        * Generates a URL or path for a specific route based on the given parameters.
+        * When the route is configured with "access=public" then the token generation is left out.
+        *
+        * If there is no route with the given name, the generator throws the RouteNotFoundException.
+        *
+        * @param string $name The name of the route
+        * @param array $parameters An array of parameters
+        * @param string $referenceType The type of reference to be generated (one of the constants)
+        * @return Uri The generated Uri
+        * @throws RouteNotFoundException If the named route doesn't exist
+        */
+       public function buildUriFromRoute($name, $parameters = array(), $referenceType = self::ABSOLUTE_PATH) {
+               $this->loadBackendRoutes();
+               if (!isset($this->routes[$name])) {
+                       throw new RouteNotFoundException('Unable to generate a URL for the named route "' . $name . '" because this route was not found.');
+               }
+
+               $route = $this->routes[$name];
+
+               // If the route has the "public" option set, no token is generated.
+               if ($route->getOption('access') !== 'public') {
+                       $parameters = array(
+                               'token' => FormProtectionFactory::get()->generateToken('route', $name)
+                       ) + $parameters;
+               }
+
+               // Add the Route path as &route=XYZ
+               $parameters = array(
+                       'route' => rawurlencode($route->getPath())
+               ) + $parameters;
+
+               return $this->buildUri($parameters, $referenceType);
+       }
+
+       /**
+        * Generate a URI for a backend module, does not check if a module is available though
+        *
+        * @param string $moduleName The name of the module
+        * @param array $parameters An array of parameters
+        * @param string $referenceType The type of reference to be generated (one of the constants)
+        *
+        * @return Uri The generated Uri
+        */
+       public function buildUriFromModule($moduleName, $parameters = array(), $referenceType = self::ABSOLUTE_PATH) {
+               $parameters = array(
+                       'M' => $moduleName,
+                       'moduleToken' => FormProtectionFactory::get()->generateToken('moduleCall', $moduleName)
+               ) + $parameters;
+               return $this->buildUri($parameters, $referenceType);
+       }
+
+       /**
+        * Returns the Ajax URL for a given AjaxID including a CSRF token.
+        *
+        * This method is only called by the core and must not be used by extensions.
+        * Ajax URLs of all registered backend Ajax handlers are automatically published
+        * to JavaScript inline settings: TYPO3.settings.ajaxUrls['ajaxId']
+        *
+        * @param string $ajaxIdentifier the ajaxID (used as GET parameter)
+        * @param array $parameters An array of parameters
+        * @param string $referenceType The type of reference to be generated (one of the constants)
+        *
+        * @return Uri The generated Uri
+        */
+       public function buildUriFromAjaxId($ajaxIdentifier, $parameters = array(), $referenceType = self::ABSOLUTE_PATH) {
+               $parameters = array(
+                       'ajaxID' => $ajaxIdentifier
+               ) + $parameters;
+               if (!empty($GLOBALS['TYPO3_CONF_VARS']['BE']['AJAX'][$ajaxIdentifier]['csrfTokenCheck'])) {
+                       $parameters['ajaxToken'] = FormProtectionFactory::get()->generateToken('ajaxCall', $ajaxIdentifier);
+               }
+               return $this->buildUri($parameters, $referenceType);
+       }
+
+       /**
+        * Internal method building a Uri object, merging the GET parameters array into a flat queryString
+        *
+        * @param array $parameters An array of GET parameters
+        * @param string $referenceType The type of reference to be generated (one of the constants)
+        *
+        * @return Uri
+        */
+       protected function buildUri($parameters, $referenceType) {
+               $uri = 'index.php?' . ltrim(GeneralUtility::implodeArrayForUrl('', $parameters, '', TRUE, TRUE), '&');
+               if ($referenceType === self::ABSOLUTE_PATH) {
+                       $uri = PathUtility::getAbsoluteWebPath(PATH_typo3 . $uri);
+               } else {
+                       $uri = GeneralUtility::getIndpEnv('TYPO3_REQUEST_DIR') . $uri;
+               }
+               return GeneralUtility::makeInstance(Uri::class, $uri);
+       }
+}
index d8e2f4d..4532396 100755 (executable)
@@ -15,6 +15,9 @@ namespace TYPO3\CMS\Backend\Utility;
  */
 
 use TYPO3\CMS\Backend\Configuration\TranslationConfigurationProvider;
+use TYPO3\CMS\Backend\Routing\Generator\UrlGenerator;
+use TYPO3\CMS\Backend\Routing\Router;
+use TYPO3\CMS\Backend\Routing\UriBuilder;
 use TYPO3\CMS\Backend\Template\DocumentTemplate;
 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
 use TYPO3\CMS\Core\Cache\CacheManager;
@@ -3292,16 +3295,15 @@ class BackendUtility {
         * @return string Calculated URL
         */
        static public function getModuleUrl($moduleName, $urlParameters = array(), $backPathOverride = FALSE, $returnAbsoluteUrl = FALSE) {
-               $urlParameters = array(
-                       'M' => $moduleName,
-                       'moduleToken' => FormProtectionFactory::get()->generateToken('moduleCall', $moduleName)
-               ) + $urlParameters;
-               $url = 'index.php?' . ltrim(GeneralUtility::implodeArrayForUrl('', $urlParameters, '', TRUE, TRUE), '&');
-               if ($returnAbsoluteUrl) {
-                       return GeneralUtility::getIndpEnv('TYPO3_REQUEST_DIR') . $url;
-               } else {
-                       return PathUtility::getAbsoluteWebPath(PATH_typo3 . $url);
+               /** @var UriBuilder $uriBuilder */
+               $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
+               try {
+                       $uri = $uriBuilder->buildUriFromRoute($moduleName, $urlParameters, $returnAbsoluteUrl ? UriBuilder::ABSOLUTE_URL : UriBuilder::ABSOLUTE_PATH);
+               } catch (\TYPO3\CMS\Backend\Routing\Exception\RouteNotFoundException $e) {
+                       // no route registered, use the fallback logic to check for a module
+                       $uri = $uriBuilder->buildUriFromModule($moduleName, $urlParameters, $returnAbsoluteUrl ? UriBuilder::ABSOLUTE_URL : UriBuilder::ABSOLUTE_PATH);
                }
+               return (string)$uri;
        }
 
        /**
@@ -3319,18 +3321,9 @@ class BackendUtility {
         * @internal
         */
        static public function getAjaxUrl($ajaxIdentifier, array $urlParameters = array(), $backPathOverride = FALSE, $returnAbsoluteUrl = FALSE) {
-               $additionalUrlParameters = array(
-                       'ajaxID' => $ajaxIdentifier
-               );
-               if (!empty($GLOBALS['TYPO3_CONF_VARS']['BE']['AJAX'][$ajaxIdentifier]['csrfTokenCheck'])) {
-                       $additionalUrlParameters['ajaxToken'] = FormProtectionFactory::get()->generateToken('ajaxCall', $ajaxIdentifier);
-               }
-               $url = 'index.php?' . ltrim(GeneralUtility::implodeArrayForUrl('', ($additionalUrlParameters + $urlParameters), '', TRUE, TRUE), '&');
-               if ($returnAbsoluteUrl) {
-                       return GeneralUtility::getIndpEnv('TYPO3_REQUEST_DIR') . $url;
-               } else {
-                       return PathUtility::getAbsoluteWebPath(PATH_typo3 . $url);
-               }
+               /** @var UriBuilder $uriBuilder */
+               $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
+               return (string)$uriBuilder->buildUriFromAjaxId($ajaxIdentifier, $urlParameters, $returnAbsoluteUrl ? UriBuilder::ABSOLUTE_URL : UriBuilder::ABSOLUTE_PATH);
        }
 
        /**
diff --git a/typo3/sysext/backend/Configuration/Backend/Routes.php b/typo3/sysext/backend/Configuration/Backend/Routes.php
new file mode 100644 (file)
index 0000000..9b2aea2
--- /dev/null
@@ -0,0 +1,35 @@
+<?php
+use TYPO3\CMS\Backend\Controller as Controller;
+
+/**
+ * Definitions for routes provided by EXT:backend
+ * Contains all "regular" routes for entry points
+ *
+ * Please note that this setup is preliminary until all core use-cases are set up here.
+ * Especially some more properties regarding modules will be added until TYPO3 CMS 7 LTS, and might change.
+ *
+ * Currently the "access" property is only used so no token creation + validation is made,
+ * but will be extended further.
+ *
+ * @internal This is not a public API yet until TYPO3 CMS 7 LTS.
+ */
+return [
+       // Login screen of the TYPO3 Backend
+       'login' => [
+               'path' => '/login',
+               'access' => 'public',
+               'controller' => Controller\LoginController::class
+       ],
+
+       // Main backend rendering setup (backend.php) for the TYPO3 Backend
+       'main' => [
+               'path' => '/main',
+               'controller' => Controller\BackendController::class
+       ],
+
+       // Logout script for the TYPO3 Backend
+       'logout' => [
+               'path' => '/logout',
+               'controller' => Controller\LogoutController::class
+       ]
+];
diff --git a/typo3/sysext/backend/Modules/Logout/conf.php b/typo3/sysext/backend/Modules/Logout/conf.php
deleted file mode 100644 (file)
index 34db869..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-<?php
-$MCONF['script'] = '_DISPATCH';
-$MCONF['name'] = 'logout';
\ No newline at end of file
diff --git a/typo3/sysext/backend/Modules/Logout/index.php b/typo3/sysext/backend/Modules/Logout/index.php
deleted file mode 100644 (file)
index bdfd417..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-<?php
-/*
- * 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!
- */
-
-/**
- * Logout script for the backend
- * This script saves the interface positions and calls the closeTypo3Windows in the frameset
- */
-
-$logoutController = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Controller\LogoutController::class);
-$logoutController->logout();
diff --git a/typo3/sysext/backend/Modules/Main/conf.php b/typo3/sysext/backend/Modules/Main/conf.php
deleted file mode 100644 (file)
index 8022f84..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-<?php
-$MCONF['name'] = 'main';
-$MCONF['script'] = '_DISPATCH';
\ No newline at end of file
diff --git a/typo3/sysext/backend/Modules/Main/index.php b/typo3/sysext/backend/Modules/Main/index.php
deleted file mode 100644 (file)
index 5951748..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-<?php
-/*
- * 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!
- */
-
-$GLOBALS['LANG']->includeLLFile('EXT:lang/locallang_misc.xlf');
-
-// Document generation
-$GLOBALS['TYPO3backend'] = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(
-       \TYPO3\CMS\Backend\Controller\BackendController::class
-);
-$GLOBALS['TYPO3backend']->render();
\ No newline at end of file
index b2dd8d3..88620a9 100644 (file)
@@ -2,11 +2,6 @@
 defined('TYPO3_MODE') or die();
 
 if (TYPO3_MODE === 'BE') {
-       // Main module
-       \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addModulePath(
-               'main',
-               'EXT:backend/Modules/Main/'
-       );
 
        // Register record edit module
        \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addModulePath(
@@ -26,12 +21,6 @@ if (TYPO3_MODE === 'BE') {
                'EXT:backend/Modules/LoginFrameset/'
        );
 
-       // Register logout
-       \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addModulePath(
-               'logout',
-               'EXT:backend/Modules/Logout/'
-       );
-
        // Register file_navframe
        \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addCoreNavigationComponent('file', 'file_navframe');
        \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addModulePath(
index fb2ff47..c6a9db0 100644 (file)
@@ -1027,6 +1027,52 @@ class Bootstrap {
        }
 
        /**
+        * Initialize the Routing for the TYPO3 Backend
+        * Loads all routes registered inside all packages and stores them inside the Router
+        *
+        * @return Bootstrap
+        * @internal This is not a public API method, do not use in own extensions
+        */
+       public function initializeBackendRouter() {
+               $packageManager = $this->getEarlyInstance(\TYPO3\CMS\Core\Package\PackageManager::class);
+
+               // See if the Routes.php from all active packages have been built together already
+               $cacheIdentifier = 'BackendRoutesFromPackages_' . sha1((TYPO3_version . PATH_site . 'BackendRoutesFromPackages'));
+
+               /** @var $codeCache \TYPO3\CMS\Core\Cache\Frontend\PhpFrontend */
+               $codeCache = $this->getEarlyInstance(\TYPO3\CMS\Core\Cache\CacheManager::class)->getCache('cache_core');
+               $routesFromPackages = array();
+               if ($codeCache->has($cacheIdentifier)) {
+                       // substr is necessary, because the php frontend wraps php code around the cache value
+                       $routesFromPackages = unserialize(substr($codeCache->get($cacheIdentifier), 6, -2));
+               } else {
+                       // Loop over all packages and check for a Configuration/Backend/Routes.php file
+                       $packages = $packageManager->getActivePackages();
+                       foreach ($packages as $package) {
+                               $routesFileNameForPackage = $package->getPackagePath() . 'Configuration/Backend/Routes.php';
+                               if (file_exists($routesFileNameForPackage)) {
+                                       $definedRoutesInPackage = require $routesFileNameForPackage;
+                                       if (is_array($definedRoutesInPackage)) {
+                                               $routesFromPackages += $definedRoutesInPackage;
+                                       }
+                               }
+                       }
+                       // Store the data from all packages in the cache
+                       $codeCache->set($cacheIdentifier, serialize($routesFromPackages));
+               }
+
+               // Build Route objects from the data
+               $router = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\Router::class);
+               foreach ($routesFromPackages as $name => $options) {
+                       $path = $options['path'];
+                       unset($options['path']);
+                       $route = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\Route::class, $path, $options);
+                       $router->addRoute($name, $route);
+               }
+               return $this;
+       }
+
+       /**
         * Initialize backend user object in globals
         *
         * @return Bootstrap
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-65493-BackendRouting.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-65493-BackendRouting.rst
new file mode 100644 (file)
index 0000000..2f0cea3
--- /dev/null
@@ -0,0 +1,50 @@
+=========================================
+Feature: #58621 - Unified Backend Routing
+=========================================
+
+Description
+===========
+
+A new Routing component was added to the TYPO3 Backend which handles addressing different calls / modules inside TYPO3.
+
+A Route is the smallest entity consisting of a path (e.g. "/records/edit/") as well as an identifier for addressing
+the route, and the information about how to dispatch the route to a PHP controller.
+
+A Route can be a module, wizard or any page inside the TYPO3 Backend. The Router contains the public API for matching
+paths to fetch a Route, and is resolved inside the RequestHandler of the Backend.
+
+The entry point for Routes is typo3/index.php?route=myroute&token=.... The main RequestHandler for all Backend requests
+detects where a route parameter from the server is given and uses this as the route identifier and then resolves to a
+controller defined inside the Route.
+
+Routes are defined inside the file "Configuration/Backend/Routes.php" of any extension.
+
+Example of a Configuration/Backend/Routes.php file:
+
+.. code-block:: php
+
+       return [
+               'myRouteIdentifier' => [
+                       'path' => '/document/edit',
+                       'controller' => Acme\MyExtension\Controller\MyExampleController::class
+               ]
+       ];
+
+The controller to be called receives a PSR-7 compliant Request object, and returns a PSR-7 Response object.
+The UriBuilder generates any kind of URL for the Backend, may it be a module, a typical route or an AJAX call. The
+UriBuilder returns a PSR-7-conform Uri object that can be casted to string when needed.
+
+Usage:
+
+.. code-block:: php
+
+       $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
+       $uri = $uriBuilder->buildUriFromRoute('myRouteIdentifier', array('foo' => 'bar'));
+
+See http://wiki.typo3.org/Blueprints/BackendRouting for more details.
+
+Impact
+======
+
+Handling of existing modules works the same as before and fully transparent. Any existing registration of entrypoints
+can be moved to the new registration file in Configuration/Backend/Routes.php.
\ No newline at end of file
index 338c552..5d11980 100644 (file)
@@ -659,10 +659,11 @@ class UriBuilder {
                $this->lastArguments = $arguments;
                $moduleName = $arguments['M'];
                unset($arguments['M'], $arguments['moduleToken']);
+               $backendUriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
                if ($this->request instanceof WebRequest && $this->createAbsoluteUri) {
-                       $uri = BackendUtility::getModuleUrl($moduleName, $arguments, NULL, TRUE);
+                       $uri = (string)$backendUriBuilder->buildUriFromModule($moduleName, $arguments, \TYPO3\CMS\Backend\Routing\UriBuilder::ABSOLUTE_URL);
                } else {
-                       $uri = BackendUtility::getModuleUrl($moduleName, $arguments);
+                       $uri = (string)$backendUriBuilder->buildUriFromModule($moduleName, $arguments);
                }
                if ($this->section !== '') {
                        $uri .= '#' . $this->section;
index 611ec11..91dc532 100644 (file)
@@ -234,7 +234,7 @@ class UriBuilderTest extends UnitTestCase {
                $_POST['foo2'] = 'bar2';
                $this->uriBuilder->setAddQueryString(TRUE);
                $this->uriBuilder->setAddQueryStringMethod('GET,POST');
-               $expectedResult = 'typo3/index.php?M=moduleKey&moduleToken=dummyToken&id=pageId&foo=bar&foo2=bar2';
+               $expectedResult = '/typo3/index.php?M=moduleKey&moduleToken=dummyToken&id=pageId&foo=bar&foo2=bar2';
                $actualResult = $this->uriBuilder->buildBackendUri();
                $this->assertEquals($expectedResult, $actualResult);
        }
@@ -248,7 +248,7 @@ class UriBuilderTest extends UnitTestCase {
                $_POST['foo2'] = 'bar2';
                $this->uriBuilder->setAddQueryString(TRUE);
                $this->uriBuilder->setAddQueryStringMethod(NULL);
-               $expectedResult = 'typo3/index.php?M=moduleKey&moduleToken=dummyToken&id=pageId&foo=bar';
+               $expectedResult = '/typo3/index.php?M=moduleKey&moduleToken=dummyToken&id=pageId&foo=bar';
                $actualResult = $this->uriBuilder->buildBackendUri();
                $this->assertEquals($expectedResult, $actualResult);
        }
@@ -271,7 +271,7 @@ class UriBuilderTest extends UnitTestCase {
                                        'M',
                                        'id'
                                ),
-                               'typo3/index.php?moduleToken=dummyToken&foo=bar&foo2=bar2'
+                               '/typo3/index.php?moduleToken=dummyToken&foo=bar&foo2=bar2'
                        ),
                        'Arguments to be excluded in the end' => array(
                                array(
@@ -286,7 +286,7 @@ class UriBuilderTest extends UnitTestCase {
                                        'M',
                                        'id'
                                ),
-                               'typo3/index.php?moduleToken=dummyToken&foo=bar&foo2=bar2'
+                               '/typo3/index.php?moduleToken=dummyToken&foo=bar&foo2=bar2'
                        ),
                        'Arguments in nested array to be excluded' => array(
                                array(
@@ -303,7 +303,7 @@ class UriBuilderTest extends UnitTestCase {
                                        'id',
                                        'tx_foo[bar]'
                                ),
-                               'typo3/index.php?M=moduleKey&moduleToken=dummyToken&foo2=bar2'
+                               '/typo3/index.php?M=moduleKey&moduleToken=dummyToken&foo2=bar2'
                        ),
                        'Arguments in multidimensional array to be excluded' => array(
                                array(
@@ -322,7 +322,7 @@ class UriBuilderTest extends UnitTestCase {
                                        'id',
                                        'tx_foo[bar][baz]'
                                ),
-                               'typo3/index.php?M=moduleKey&moduleToken=dummyToken&foo2=bar2'
+                               '/typo3/index.php?M=moduleKey&moduleToken=dummyToken&foo2=bar2'
                        ),
                );
        }
@@ -350,7 +350,7 @@ class UriBuilderTest extends UnitTestCase {
         */
        public function buildBackendUriKeepsModuleQueryParametersIfAddQueryStringIsNotSet() {
                GeneralUtility::_GETset(array('M' => 'moduleKey', 'id' => 'pageId', 'foo' => 'bar'));
-               $expectedResult = 'typo3/index.php?M=moduleKey&moduleToken=dummyToken&id=pageId';
+               $expectedResult = '/typo3/index.php?M=moduleKey&moduleToken=dummyToken&id=pageId';
                $actualResult = $this->uriBuilder->buildBackendUri();
                $this->assertEquals($expectedResult, $actualResult);
        }
@@ -361,7 +361,7 @@ class UriBuilderTest extends UnitTestCase {
        public function buildBackendUriMergesAndOverrulesQueryParametersWithArguments() {
                GeneralUtility::_GETset(array('M' => 'moduleKey', 'id' => 'pageId', 'foo' => 'bar'));
                $this->uriBuilder->setArguments(array('M' => 'overwrittenModuleKey', 'somePrefix' => array('bar' => 'baz')));
-               $expectedResult = 'typo3/index.php?M=overwrittenModuleKey&moduleToken=dummyToken&id=pageId&somePrefix%5Bbar%5D=baz';
+               $expectedResult = '/typo3/index.php?M=overwrittenModuleKey&moduleToken=dummyToken&id=pageId&somePrefix%5Bbar%5D=baz';
                $actualResult = $this->uriBuilder->buildBackendUri();
                $this->assertEquals($expectedResult, $actualResult);
        }
@@ -374,7 +374,7 @@ class UriBuilderTest extends UnitTestCase {
                $mockDomainObject = $this->getAccessibleMock(AbstractEntity::class, array('dummy'));
                $mockDomainObject->_set('uid', '123');
                $this->uriBuilder->setArguments(array('somePrefix' => array('someDomainObject' => $mockDomainObject)));
-               $expectedResult = 'typo3/index.php?M=moduleKey&moduleToken=dummyToken&somePrefix%5BsomeDomainObject%5D=123';
+               $expectedResult = '/typo3/index.php?M=moduleKey&moduleToken=dummyToken&somePrefix%5BsomeDomainObject%5D=123';
                $actualResult = $this->uriBuilder->buildBackendUri();
                $this->assertEquals($expectedResult, $actualResult);
        }
@@ -385,7 +385,7 @@ class UriBuilderTest extends UnitTestCase {
        public function buildBackendUriRespectsSection() {
                GeneralUtility::_GETset(array('M' => 'moduleKey'));
                $this->uriBuilder->setSection('someSection');
-               $expectedResult = 'typo3/index.php?M=moduleKey&moduleToken=dummyToken#someSection';
+               $expectedResult = '/typo3/index.php?M=moduleKey&moduleToken=dummyToken#someSection';
                $actualResult = $this->uriBuilder->buildBackendUri();
                $this->assertEquals($expectedResult, $actualResult);
        }
@@ -431,7 +431,7 @@ class UriBuilderTest extends UnitTestCase {
                );
                $this->uriBuilder->setAddQueryString(TRUE);
                $this->uriBuilder->setAddQueryStringMethod('POST,GET');
-               $expectedResult = $this->rawUrlEncodeSquareBracketsInUrl('typo3/index.php?moduleToken=dummyToken&key1=POST1&key2=GET2&key3[key31]=POST31&key3[key32]=GET32&key3[key33][key331]=GET331&key3[key33][key332]=POST332');
+               $expectedResult = $this->rawUrlEncodeSquareBracketsInUrl('/typo3/index.php?moduleToken=dummyToken&key1=POST1&key2=GET2&key3[key31]=POST31&key3[key32]=GET32&key3[key33][key331]=GET331&key3[key33][key332]=POST332');
                $actualResult = $this->uriBuilder->buildBackendUri();
                $this->assertEquals($expectedResult, $actualResult);
        }
@@ -463,7 +463,7 @@ class UriBuilderTest extends UnitTestCase {
                );
                $this->uriBuilder->setAddQueryString(TRUE);
                $this->uriBuilder->setAddQueryStringMethod('GET,POST');
-               $expectedResult = $this->rawUrlEncodeSquareBracketsInUrl('typo3/index.php?moduleToken=dummyToken&key1=GET1&key2=POST2&key3[key31]=GET31&key3[key32]=POST32&key3[key33][key331]=POST331&key3[key33][key332]=GET332');
+               $expectedResult = $this->rawUrlEncodeSquareBracketsInUrl('/typo3/index.php?moduleToken=dummyToken&key1=GET1&key2=POST2&key3[key31]=GET31&key3[key32]=POST32&key3[key33][key331]=POST331&key3[key33][key332]=GET332');
                $actualResult = $this->uriBuilder->buildBackendUri();
                $this->assertEquals($expectedResult, $actualResult);
        }