[!!!][FEATURE] Introduce Backend Routing 76/37476/10
authorBenjamin Mack <benni@typo3.org>
Tue, 3 Mar 2015 14:02:28 +0000 (15:02 +0100)
committerHelmut Hummel <helmut.hummel@typo3.org>
Mon, 23 Mar 2015 18:33:58 +0000 (19:33 +0100)
A new Routing API is introduced in order to streamline
the entrypoints to the TYPO3 Backend.

All previous patches by Wouter for the dispatched modules
are the foundation for this change.

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.

The Routing API is inspired by the Symfony Routing
framework and mostly compatible for now
but the TYPO3 implementation only takes around
20% of the needed logic.

There are three new classes:
- Route (a single route with a path and some options)
- Router (API to match paths and generate URLs)
- UrlGenerator (Generates the URL)

This patch changes the entrypoint for
login/logout to typo3/index.php/myroute/?token=...
making index.php the only entrypoint to the
TYPO3 Backend in the future and using the PATH_INFO
functionality of the browser to store the route.

The main RequestHandler of all Backend modules
detects where a PATH_INFO is given and
then resolves to a controller/action logic and checks
for a valid token.

Once this patch is in, all non-module entrypoints
are moved to the new format.

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

Resolves: #65493
Releases: master
Change-Id: I91b5812c833c558794f70fd4504f2da452b1c3ce
Reviewed-on: http://review.typo3.org/37476
Reviewed-by: Nicole Cordes <typo3@cordes.co>
Tested-by: Nicole Cordes <typo3@cordes.co>
Reviewed-by: Helmut Hummel <helmut.hummel@typo3.org>
Tested-by: Helmut Hummel <helmut.hummel@typo3.org>
19 files changed:
typo3/backend.php
typo3/index.php
typo3/sysext/backend/Classes/AjaxRequestHandler.php
typo3/sysext/backend/Classes/BackendModuleRequestHandler.php
typo3/sysext/backend/Classes/Controller/BackendController.php
typo3/sysext/backend/Classes/Controller/LoginController.php
typo3/sysext/backend/Classes/Controller/LogoutController.php
typo3/sysext/backend/Classes/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/Generator/UrlGenerator.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/Template/DocumentTemplate.php
typo3/sysext/backend/Classes/Utility/BackendUtility.php
typo3/sysext/backend/Configuration/Backend/Routes.php [new file with mode: 0644]
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]

index 4c17926..1dcae0c 100644 (file)
  * The TYPO3 project - inspiring people to share!
  */
 require_once 'init.php';
-$GLOBALS['LANG']->includeLLFile('EXT:lang/locallang_misc.xlf');
 
 // Document generation
 $TYPO3backend = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Controller\BackendController::class);
-// 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]);
-                       }
-               }
-       }
-}
 $TYPO3backend->render();
 \TYPO3\CMS\Core\Core\Bootstrap::getInstance()->shutdown();
index 14882c2..709fec5 100644 (file)
  */
 
 /**
- * Login-screen of TYPO3.
- *
- * @author Kasper Skårhøj <kasperYYYY@typo3.com>
+ * Main entry point for the TYPO3 Backend.
+ * See Backend/RequestHandler.php on how the setup and the Routing is done.
  */
-define('TYPO3_PROCEED_IF_NO_USER', 1);
-require __DIR__ . '/init.php';
-
-$loginController = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Controller\LoginController::class);
-$loginController->main();
-$loginController->printContent();
+define('TYPO3_MODE', 'BE');
+require __DIR__ . '/sysext/core/Classes/Core/Bootstrap.php';
+\TYPO3\CMS\Core\Core\Bootstrap::getInstance()->run('typo3/')->shutdown();
index c4fa586..9e9bd18 100644 (file)
@@ -78,6 +78,7 @@ class AjaxRequestHandler implements RequestHandlerInterface {
                        ->checkBackendIpOrDie()
                        ->checkSslBackendAndRedirectIfNeeded()
                        ->checkValidBrowserOrDie()
+                       ->initializeBackendRouter()
                        ->loadExtensionTables(TRUE)
                        ->initializeSpriteManager()
                        ->initializeBackendUser()
index 6d40660..8a976cb 100644 (file)
@@ -86,6 +86,7 @@ class BackendModuleRequestHandler implements \TYPO3\CMS\Core\Core\RequestHandler
                        ->checkBackendIpOrDie()
                        ->checkSslBackendAndRedirectIfNeeded()
                        ->checkValidBrowserOrDie()
+                       ->initializeBackendRouter()
                        ->loadExtensionTables(TRUE)
                        ->initializeSpriteManager()
                        ->initializeBackendUser()
index 4476de7..16ed497 100644 (file)
@@ -96,6 +96,7 @@ class BackendController {
         * Constructor
         */
        public function __construct() {
+               $GLOBALS['LANG']->includeLLFile('EXT:lang/locallang_misc.xlf');
                $this->backendModuleRepository = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Domain\Repository\Module\BackendModuleRepository::class);
 
                // Set debug flag for BE development only
@@ -110,6 +111,7 @@ class BackendController {
                $this->pageRenderer->enableExtJSQuickTips();
                $this->pageRenderer->addJsInlineCode('consoleOverrideWithDebugPanel', '//already done', FALSE);
                $this->pageRenderer->addExtDirectCode();
+               $this->pageRenderer->setBaseUrl(GeneralUtility::getIndpEnv('TYPO3_REQUEST_DIR'));
                // Add default BE javascript
                $this->jsFiles = array(
                        'locallang' => $this->getLocalLangFileName(),
@@ -156,6 +158,42 @@ 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]);
+                                       }
+                               }
+                       }
+               }
        }
 
        /**
index a26b0f0..2325083 100644 (file)
@@ -186,7 +186,7 @@ class LoginController {
                }
                $GLOBALS['LANG']->includeLLFile('EXT:lang/locallang_login.xlf');
                // Setting the redirect URL to "backend.php" if no alternative input is given
-               $this->redirectToURL = $this->redirect_url ?: 'backend.php';
+               $this->redirectToURL = $this->redirect_url ?: BackendUtility::getModuleUrl('backend');
                // Do a logout if the command is set
                if ($this->L == 'OUT' && is_object($GLOBALS['BE_USER'])) {
                        $GLOBALS['BE_USER']->logoff();
@@ -251,12 +251,25 @@ class LoginController {
        /**
         * Outputting the accumulated content to screen
         *
-        * @return void
+        * @return string
+        * @deprecated since TYPO3 CMS 7, will be removed in TYPO3 CMS 8, use $this->indexAction()
         */
        public function printContent() {
+               GeneralUtility::logDeprecatedFunction();
                echo $this->content;
        }
 
+       /**
+        * Calls the main method and returns the content
+        *
+        * @return string
+        */
+       public function indexAction() {
+               $this->main();
+               return $this->content;
+       }
+
+
        /*****************************
         *
         * Various functions
@@ -411,7 +424,7 @@ class LoginController {
                        // Based on specific setting of interface we set the redirect script:
                        switch ($this->GPinterface) {
                                case 'backend':
-                                       $this->redirectToURL = 'backend.php';
+                                       $this->redirectToURL = BackendUtility::getModuleUrl('backend');
                                        break;
                                case 'frontend':
                                        $this->redirectToURL = '../';
@@ -460,7 +473,7 @@ class LoginController {
                                        'frontend' => $GLOBALS['LANG']->getLL('interface.frontend')
                                );
                                $jumpScript = array(
-                                       'backend' => 'backend.php',
+                                       'backend' => BackendUtility::getModuleUrl('backend'),
                                        'frontend' => '../'
                                );
                                // Traverse the interface keys:
index 0e50af1..e5a22b3 100644 (file)
@@ -15,6 +15,7 @@ namespace TYPO3\CMS\Backend\Controller;
  */
 
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\HttpUtility;
 
 /**
  * Script Class for logging a user out.
@@ -34,9 +35,19 @@ class LogoutController {
                $GLOBALS['BE_USER']->writelog(255, 2, 0, 1, 'User %s logged out from TYPO3 Backend', array($GLOBALS['BE_USER']->user['username']));
                \TYPO3\CMS\Core\FormProtection\FormProtectionFactory::get()->removeSessionTokenFromRegistry();
                $GLOBALS['BE_USER']->logoff();
-               $redirect = GeneralUtility::sanitizeLocalUrl(GeneralUtility::_GP('redirect'));
-               $redirectUrl = $redirect ? $redirect : 'index.php';
-               \TYPO3\CMS\Core\Utility\HttpUtility::redirect($redirectUrl);
+               $this->redirect();
+       }
+
+       /**
+        * Redirects based on the "redirect" parameter, otherwise falls back to the login page
+        */
+       protected function redirect() {
+               $redirectUrl = GeneralUtility::sanitizeLocalUrl(GeneralUtility::_GP('redirect'));
+               if (empty($redirectUrl)) {
+                       // @todo: use the UrlGenerator in the future for that
+                       $redirectUrl = \TYPO3\CMS\Backend\Utility\BackendUtility::getModuleUrl('login', array(), FALSE, TRUE);
+               }
+               HttpUtility::redirect($redirectUrl);
        }
 
 }
index 23be5e2..90c929f 100644 (file)
@@ -14,15 +14,26 @@ namespace TYPO3\CMS\Backend;
  * The TYPO3 project - inspiring people to share!
  */
 
+use TYPO3\CMS\Backend\Routing\Exception\RouteNotFoundException;
 use TYPO3\CMS\Core\Core\Bootstrap;
 use TYPO3\CMS\Core\Core\RequestHandlerInterface;
+use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
 
 /**
- * General RequestHandler for the TYPO3 Backend. This is used for all Backend requests except for CLI
- * or AJAX calls. Unlike all other RequestHandlers in the TYPO3 CMS Core, the actual logic for choosing
- * the controller is still done inside places like mod.php and 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.
+ * General RequestHandler for the TYPO3 Backend. This is used for all backend requests except for CLI, AJAX or
+ * calls to mod.php (currently handled by BackendModuleRequestHandler).
+ *
+ * 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 {
 
@@ -44,14 +55,61 @@ class RequestHandler implements RequestHandlerInterface {
        /**
         * Handles any backend request
         *
-        * @return void
+        * @throws \Exception
+        * @throws \TYPO3\CMS\Core\Exception
         */
        public function handleRequest() {
+               // Only enable routing for typo3/index.php
+               $routingEnabled = GeneralUtility::getIndpEnv('SCRIPT_FILENAME') === PATH_typo3 . 'index.php';
+               $pathToRoute = (string)GeneralUtility::getIndpEnv('PATH_INFO');
+
+               // Allow the login page to be displayed if routing is not used and on index.php
+               if ($routingEnabled && empty($pathToRoute)) {
+                       define('TYPO3_PROCEED_IF_NO_USER', 1);
+                       $pathToRoute = '/login';
+               }
+
+               $this->boot();
+
+               // Check if the router has the available route and dispatch.
+               if ($routingEnabled) {
+                       $this->dispatchRoute($pathToRoute);
+                       $this->bootstrap->shutdown();
+               }
+
+               // No route found, so the system proceeds in called entrypoint as fallback.
+       }
+
+       /**
+        * This request handler can handle any backend request (but not CLI).
+        *
+        * @return bool If the request is not a CLI script, TRUE otherwise FALSE
+        */
+       public function canHandleRequest() {
+               return (TYPO3_REQUESTTYPE & TYPO3_REQUESTTYPE_BE && !(TYPO3_REQUESTTYPE & TYPO3_REQUESTTYPE_CLI));
+       }
+
+       /**
+        * Returns the priority - how eager the handler is to actually handle the request.
+        *
+        * @return int The priority of the request handler.
+        */
+       public function getPriority() {
+               return 50;
+       }
+
+       /**
+        * Does the main work for setting up the backend environment for any Backend request
+        *
+        * @return void
+        */
+       protected function boot() {
                $this->bootstrap
                        ->checkLockedBackendAndRedirectOrDie()
                        ->checkBackendIpOrDie()
                        ->checkSslBackendAndRedirectIfNeeded()
                        ->checkValidBrowserOrDie()
+                       ->initializeBackendRouter()
                        ->loadExtensionTables(TRUE)
                        ->initializeSpriteManager()
                        ->initializeBackendUser()
@@ -64,21 +122,49 @@ class RequestHandler implements RequestHandlerInterface {
        }
 
        /**
-        * This request handler can handle any backend request (but not CLI).
+        * If the request can be handled by the routing framework, dispatch to the correct
+        * controller and action, and then echo the content.
         *
-        * @return bool If the request is not a CLI script, TRUE otherwise FALSE
+        * @param string $pathToRoute
+        *
+        * @throws RouteNotFoundException
+        * @throws \TYPO3\CMS\Core\Exception
         */
-       public function canHandleRequest() {
-               return (TYPO3_REQUESTTYPE & TYPO3_REQUESTTYPE_BE && !(TYPO3_REQUESTTYPE & TYPO3_REQUESTTYPE_CLI));
+       protected function dispatchRoute($pathToRoute) {
+               $route = $this->bootstrap->getEarlyInstance(Routing\Router::class)->match($pathToRoute);
+               $routeToken = (string)GeneralUtility::_GP('token');
+               $routeName = $route->getOption('_identifier');
+               $isPublicRoute = $route->getOption('public') || (defined('TYPO3_PROCEED_IF_NO_USER') && TYPO3_PROCEED_IF_NO_USER > 0);
+               if ($isPublicRoute || $this->isValidRequest($routeToken, $routeName)) {
+                       list($className, $methodName) = $route->getOption('controller');
+                       // parameters can be used at a later point to define the available request parameters
+                       $parameters = array();
+                       echo GeneralUtility::callUserFunction($className . '->' . $methodName, $parameters, $this);
+               } else {
+                       throw new RouteNotFoundException('Invalid request for route "' . $pathToRoute . '"', 1425389455);
+               }
        }
 
        /**
-        * Returns the priority - how eager the handler is to actually handle the
-        * request.
+        * Wrapper method for static form protection utility
         *
-        * @return int The priority of the request handler.
+        * @return \TYPO3\CMS\Core\FormProtection\AbstractFormProtection
         */
-       public function getPriority() {
-               return 50;
+       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 string $token
+        * @param string $name
+        * @return bool
+        * @see \TYPO3\CMS\Backend\Routing\Generator\UrlGenerator::generate() where the token is generated.
+        */
+       protected function isValidRequest($token, $name) {
+               return $this->getFormProtection()->validateToken($token, 'route', $name);
        }
 }
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/Generator/UrlGenerator.php b/typo3/sysext/backend/Classes/Routing/Generator/UrlGenerator.php
new file mode 100644 (file)
index 0000000..e8784f7
--- /dev/null
@@ -0,0 +1,115 @@
+<?php
+namespace TYPO3\CMS\Backend\Routing\Generator;
+
+/*
+ * 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\Backend\Routing\RouteCollection;
+use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * 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.
+ *
+ * The architecture is highly inspired by the Symfony Routing Component.
+ * The general integration approach into the TYPO3 Backend is to only
+ * fetch the minimal necessary code and stay as close as possible to the Symfony
+ * implementation to have maximum interchangeability at a later point.
+ */
+class UrlGenerator {
+
+       /**
+        * Generates an absolute URL
+        */
+       const ABSOLUTE_URL = 'url';
+
+       /**
+        * Generates an absolute path
+        */
+       const ABSOLUTE_PATH = 'absolute';
+
+       /**
+        * Generates a relative path
+        * It is discouraged to use this method, as $BACK_PATH is still needed currently
+        */
+       const RELATIVE_PATH = 'relative';
+
+       /**
+        * @var array
+        */
+       protected $routes;
+
+       /**
+        * Constructor which always receives the available routes
+        *
+        * @param array $routes
+        */
+       public function __construct(array $routes) {
+               $this->routes = $routes;
+       }
+
+       /**
+        * 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 string The generated URL
+        * @throws RouteNotFoundException If the named route doesn't exist
+        */
+       public function generate($name, $parameters = array(), $referenceType = self::ABSOLUTE_PATH) {
+               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];
+               $path = $route->getPath();
+
+               // 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;
+               }
+               // Build the base URL by adding the Route path as PATH_INFO. No prefix added yet, see below.
+               $url = 'index.php' . $path;
+
+               // Add parameters if there are any
+               if (!empty($parameters)) {
+                       $url .= '?' . ltrim(GeneralUtility::implodeArrayForUrl('', $parameters, '', TRUE, TRUE), '&');
+               }
+
+               // Build the prefix for the URL
+               $prefix = '';
+               switch ($referenceType) {
+                       case self::RELATIVE_PATH:
+                               $prefix = $GLOBALS['BACK_PATH'];
+                               break;
+                       case self::ABSOLUTE_PATH:
+                               $prefix = \TYPO3\CMS\Core\Utility\PathUtility::stripPathSitePrefix(GeneralUtility::getIndpEnv('TYPO3_SITE_PATH') . PATH_typo3);
+                               break;
+                       case self::ABSOLUTE_URL:
+                               $prefix = GeneralUtility::getIndpEnv('TYPO3_REQUEST_DIR');
+                               break;
+               }
+               return $prefix . $url;
+       }
+}
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..e47c2e9
--- /dev/null
@@ -0,0 +1,127 @@
+<?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.
+ * Usually only called from within the RouteCollection or the RouteManager.
+ *
+ * The architecture is highly inspired by the Symfony Routing Component.
+ * The general integration approach into the TYPO3 Backend is to only
+ * fetch the minimal necessary code and stay as close as possible to the Symfony
+ * implementation to have maximum interchangeability at a later point.
+ */
+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..7d84961
--- /dev/null
@@ -0,0 +1,192 @@
+<?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\Generator\UrlGenerator;
+use TYPO3\CMS\Backend\Routing\Exception\RouteNotFoundException;
+use TYPO3\CMS\Backend\Routing\Exception\ResourceNotFoundException;
+use TYPO3\CMS\Core\Cache\CacheManager;
+use TYPO3\CMS\Core\SingletonInterface;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Package\PackageManager;
+
+/**
+ * Implementation of a class for registering routes, used throughout the Bootstrap
+ * to register all sorts of Backend Routes, and to fetch the main Collection in order
+ * to resolve a route (see ->match()).
+ *
+ * For the TYPO3 Backend there is currently only one "main" RouteCollection
+ * collecting all routes and caches them away.
+ *
+ * See the main methods currently relevant:
+ * ->generate()
+ * ->match()
+ *
+ * Ideally, the Router is solely instantiated and accessed via the Bootstrap but is currently
+ * also used inside BackendUtility for generating URLs. Although this serves as single entrypoint for the routing
+ * logic currently, it should only be used within the RequestHandler, Bootstrap and the BackendUtility.
+ *
+ * See \TYPO3\CMS\Backend\RequestHandler for more details on route matching() and Bootstrap->initializeBackendRouting().
+ *
+ * The architecture is highly inspired by the Symfony Routing Component.
+ * The general integration approach into the TYPO3 Backend is to only
+ * fetch the minimal necessary code and stay as close as possible to the Symfony
+ * implementation to have maximum interchangeability at a later point.
+ */
+class Router implements SingletonInterface {
+
+       /**
+        * All routes used in the Backend
+        *
+        * @var array|null
+        */
+       protected $routes = array();
+
+       /**
+        * Available options
+        *
+        * @var array
+        */
+       protected $options = array();
+
+       /**
+        * @var PackageManager
+        */
+       protected $packageManager = NULL;
+
+       /**
+        * @param PackageManager $packageManager
+        */
+       public function __construct(PackageManager $packageManager) {
+               $this->packageManager = $packageManager;
+               $this->loadFromPackages();
+       }
+
+       /**
+        * Loads all routes registered inside all packages
+        * In the future, the Route objects could be stored directly in the Cache
+        */
+       protected function loadFromPackages() {
+               // See if the Routes.php from the Packages have been built together already
+               $cacheIdentifier = 'BackendRoutesFromPackages' . sha1((TYPO3_version . PATH_site . 'BackendRoutesFromPackages'));
+
+               /** @var $codeCache \TYPO3\CMS\Core\Cache\Frontend\PhpFrontend */
+               $codeCache = $this->getCacheManager()->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 = $this->packageManager->getActivePackages();
+                       foreach ($packages as $package) {
+                               $routesFileNameForPackage = $package->getConfigurationPath() . '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
+               foreach ($routesFromPackages as $name => $options) {
+                       $path = $options['path'];
+                       unset($options['path']);
+                       $route = GeneralUtility::makeInstance(Route::class, $path, $options);
+                       $this->routes[$name] = $route;
+               }
+       }
+
+       /**
+        * Sets multiple options for this router at once
+        *
+        * @param array $options An array of options
+        */
+       public function setOptions(array $options) {
+               $this->options = $options;
+       }
+
+       /**
+        * Sets a single option
+        *
+        * @param string $key The key
+        * @param mixed $value The value
+        */
+       public function setOption($key, $value) {
+               $this->options[$key] = $value;
+       }
+
+       /**
+        * Gets the value of an option
+        *
+        * @param string $key The key
+        * @return mixed The value
+        */
+       public function getOption($key) {
+               return $this->options[$key];
+       }
+
+       /**
+        * Generates a URL or path for a specific route based on the given parameters.
+        * This call needs to be publically accessable in the future for doing routing through the Router.
+        * @see Generator/UrlGenerator->generate for more details
+        *
+        * @param string $name The name of the route
+        * @param array $parameters An array of parameters
+        * @param bool|string $referenceType The type of URL to be generated
+        * @return string The generated URL
+        * @throws RouteNotFoundException If the named route doesn't exist
+        * @api
+        */
+       public function generate($name, $parameters = array(), $referenceType = UrlGenerator::ABSOLUTE_PATH) {
+               $urlGenerator = GeneralUtility::makeInstance(UrlGenerator::class, $this->routes);
+               return $urlGenerator->generate($name, $parameters, $referenceType);
+       }
+
+       /**
+        * Tries to match a URL path with a set of routes.
+        * Should go into its own Matcher class later on
+        *
+        * @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
+        * @api
+        */
+       public function match($pathInfo) {
+               foreach ($this->routes as $routeName => $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', $routeName);
+                               return $route;
+                       }
+               }
+               throw new ResourceNotFoundException('The requested resource "' . htmlspecialchars($pathInfo) . '" was not found.', 1425389240);
+       }
+
+       /**
+        * Create and returns an instance of the CacheManager
+        *
+        * @return CacheManager
+        */
+       protected function getCacheManager() {
+               return GeneralUtility::makeInstance(CacheManager::class);
+       }
+}
index 3e39d0e..80cd1d8 100644 (file)
@@ -466,6 +466,7 @@ function jumpToUrl(URL) {
                if ((int)$GLOBALS['TYPO3_CONF_VARS']['BE']['debug'] === 1) {
                        $this->pageRenderer->enableDebugMode();
                }
+               $this->pageRenderer->setBaseUrl(GeneralUtility::getIndpEnv('TYPO3_REQUEST_DIR'));
                return $this->pageRenderer;
        }
 
index 7db8d14..59ed2af 100644 (file)
@@ -15,6 +15,8 @@ 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\Template\DocumentTemplate;
 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
 use TYPO3\CMS\Core\Cache\CacheManager;
@@ -56,6 +58,22 @@ class BackendUtility {
         */
        static protected $tcaTableTypeConfigurationCache = array();
 
+       /**
+        * @var Router
+        */
+       static protected $router;
+
+       /**
+        * Sets the router for all that backwards compatibility stuff,
+        * so it doesn't have to be fetched through the bootstrap.
+        *
+        * @param Router $router
+        * @internal
+        */
+       static public function setRouter(Router $router) {
+               static::$router = $router;
+       }
+
        /*******************************************
         *
         * SQL-related, selecting records, searching
@@ -3139,6 +3157,13 @@ class BackendUtility {
         * @return string Calculated URL
         */
        static public function getModuleUrl($moduleName, $urlParameters = array(), $backPathOverride = FALSE, $returnAbsoluteUrl = FALSE) {
+               if (self::$router !== NULL) {
+                       try {
+                               return self::$router->generate($moduleName, $urlParameters, $returnAbsoluteUrl ? UrlGenerator::ABSOLUTE_PATH : UrlGenerator::RELATIVE_PATH);
+                       } catch (\TYPO3\CMS\Backend\Routing\Exception\RouteNotFoundException $e) {
+                               // do nothing, to have a proper fallback
+                       }
+               }
                if ($backPathOverride === FALSE) {
                        $backPath = isset($GLOBALS['BACK_PATH']) ? $GLOBALS['BACK_PATH'] : '';
                } else {
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..549b356
--- /dev/null
@@ -0,0 +1,44 @@
+<?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,
+                       'indexAction'
+               ]
+       ],
+
+       // Main backend rendering setup (backend.php) for the TYPO3 Backend
+       'backend' => [
+               'path' => '/main',
+               'controller' => [
+                       Controller\BackendController::class,
+                       'render'
+               ]
+       ],
+
+       // Logout script for the TYPO3 Backend
+       'logout' => [
+               'path' => '/logout',
+               'controller' => [
+                       Controller\LogoutController::class,
+                       'logout'
+               ]
+       ]
+];
index 8f4b403..c9d52e0 100644 (file)
@@ -14,12 +14,6 @@ if (TYPO3_MODE === 'BE') {
                \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::extPath($_EXTKEY) . 'Modules/LoginFrameset/'
        );
 
-       // Register logout
-       \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addModulePath(
-               'logout',
-               \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::extPath($_EXTKEY) . 'Modules/Logout/'
-       );
-
        // Register file_navframe
        \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addCoreNavigationComponent('file', 'file_navframe');
        \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addModulePath(
index d44c0c6..f1c2d31 100644 (file)
@@ -1112,6 +1112,20 @@ class Bootstrap {
        }
 
        /**
+        * Initialize the Routing for the TYPO3 Backend
+        *
+        * @return Bootstrap
+        * @internal This is not a public API method, do not use in own extensions
+        */
+       public function initializeBackendRouter() {
+               $packageManager = $this->getEarlyInstance(\TYPO3\Flow\Package\PackageManager::class);
+               $router = Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\Router::class, $packageManager);
+               \TYPO3\CMS\Backend\Utility\BackendUtility::setRouter($router);
+               $this->setEarlyInstance(\TYPO3\CMS\Backend\Routing\Router::class, $router);
+               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..5021053
--- /dev/null
@@ -0,0 +1,34 @@
+=========================================
+Feature: #58621 - Unified Backend Routing
+=========================================
+
+Description
+===========
+
+A new Routing component was added to the TYPO3 Backend which handles addressing different pages / 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 class and a method.
+
+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 to generate an URL to resolve a route.
+
+Routes are defined inside the file "Configuration/Backend/Routes.php" of any extension.
+
+An example can be found within EXT:backend.
+
+The entry point for Routes is typo3/index.php/myroute/?token=.... The main RequestHandler for all Backend requests
+detects where a PATH_INFO from the server is given and uses this as the route identifier and then resolves to a
+controller/action defined inside the Route.
+
+See http://wiki.typo3.org/Blueprints/BackendRouting for more details.
+
+The API is not public and should not be used for 3rd-party code except for the registration of new routes. The Routing
+concept is built so it works solely inside the Bootstrap and in the URL generation, which is centralized through
+``BackendUtility::getModuleUrl``.
+
+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.