Using DI for certain classes
authorAlexander Ebert <ebert@woltlab.com>
Tue, 17 Nov 2015 18:24:25 +0000 (19:24 +0100)
committerAlexander Ebert <ebert@woltlab.com>
Tue, 17 Nov 2015 18:24:25 +0000 (19:24 +0100)
12 files changed:
wcfsetup/install/files/lib/system/SingletonFactory.class.php
wcfsetup/install/files/lib/system/application/ApplicationHandler.class.php
wcfsetup/install/files/lib/system/event/EventHandler.class.php
wcfsetup/install/files/lib/system/request/ControllerMap.class.php
wcfsetup/install/files/lib/system/request/FlexibleRoute.class.php
wcfsetup/install/files/lib/system/request/IRoute.class.php
wcfsetup/install/files/lib/system/request/RequestHandler.class.php
wcfsetup/install/files/lib/system/request/RouteHandler.class.php
wcfsetup/install/files/lib/system/request/StaticRoute.class.php
wcfsetup/install/files/lib/system/request/route/DynamicRequestRoute.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/request/route/IRequestRoute.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/request/route/StaticRequestRoute.class.php [new file with mode: 0644]

index d15fc3f2f662a215c6962c94ac2c0f36155fe8f3..69ac610654ab1cd4d7a7ebe3084424fc19576c7f 100644 (file)
@@ -11,13 +11,14 @@ use wcf\system\exception\SystemException;
  * @package    com.woltlab.wcf
  * @subpackage system
  * @category   Community Framework
+ * @deprecated  2.2
  */
 abstract class SingletonFactory {
        /**
         * Singletons do not support a public constructor. Override init() if
         * your class needs to initialize components on creation.
         */
-       public final function __construct() {
+       public function __construct() {
                $this->init();
        }
        
index d43aac53a810fbeb98bbf1fdb58fedb9aa928700..b0043f54eab7537458c71ec1513ba98693028824 100644 (file)
@@ -17,6 +17,10 @@ use wcf\system\SingletonFactory;
  * @category   Community Framework
  */
 class ApplicationHandler extends SingletonFactory {
+       /**
+        * @var ApplicationCacheBuilder
+        */
+       protected $applicationCacheBuilder;
        /**
         * application cache
         * @var array<array>
@@ -29,17 +33,28 @@ class ApplicationHandler extends SingletonFactory {
         */
        protected $pageURLs = array();
        
+       /**
+        * ApplicationHandler constructor.
+        * 
+        * @param       ApplicationCacheBuilder $applicationCacheBuilder
+        */
+       public function __construct(ApplicationCacheBuilder $applicationCacheBuilder) {
+               $this->applicationCacheBuilder = $applicationCacheBuilder;
+               
+               parent::__construct();
+       }
+       
        /**
         * Initializes cache.
         */
        protected function init() {
-               $this->cache = ApplicationCacheBuilder::getInstance()->getData();
+               $this->cache = $this->applicationCacheBuilder->getData();
        }
        
        /**
         * Returns the primary application.
         * 
-        * @return      \wcf\data\application\Application
+        * @return      Application
         */
        public function getPrimaryApplication() {
                $packageID = ($this->cache['primary']) ?: PACKAGE_ID;
@@ -55,7 +70,8 @@ class ApplicationHandler extends SingletonFactory {
         * Returns an application based upon it's abbreviation. Will return the
         * primary application if $abbreviation equals to 'wcf'
         * 
-        * @return      \wcf\data\application\Application
+        * @param       string  $abbreviation   package abbreviation, e.g. `wbb` for `com.woltlab.wbb`
+        * @return      Application
         */
        public function getApplication($abbreviation) {
                if ($abbreviation == 'wcf') {
@@ -77,7 +93,7 @@ class ApplicationHandler extends SingletonFactory {
         * Returns pseudo-application representing WCF used for special cases,
         * e.g. cross-domain files requestable through the webserver.
         * 
-        * @return      \wcf\data\application\Application
+        * @return      Application
         */
        public function getWCF() {
                return $this->cache['wcf'];
@@ -86,7 +102,7 @@ class ApplicationHandler extends SingletonFactory {
        /**
         * Returns the currently active application.
         * 
-        * @return      \wcf\data\application\Application
+        * @return      Application
         */
        public function getActiveApplication() {
                // work-around during WCFSetup
@@ -100,7 +116,7 @@ class ApplicationHandler extends SingletonFactory {
        /**
         * Returns a list of dependent applications.
         * 
-        * @return      array<\wcf\data\application\Application>
+        * @return      Application[]
         */
        public function getDependentApplications() {
                $applications = $this->getApplications();
@@ -117,7 +133,7 @@ class ApplicationHandler extends SingletonFactory {
        /**
         * Returns a list of all active applications.
         * 
-        * @return      array<\wcf\data\application\Application>
+        * @return      Application[]
         */
        public function getApplications() {
                return $this->cache['application'];
@@ -126,6 +142,7 @@ class ApplicationHandler extends SingletonFactory {
        /**
         * Returns abbreviation for a given package id or null if application is unknown.
         * 
+        * @param       int     $packageID      unique package id
         * @return      string
         */
        public function getAbbreviation($packageID) {
index f04a97b8c9e9b90fe839faacdf3f60a59124a08f..9a218134ab8bc0dea4442aebbc3923b8fc619699 100644 (file)
@@ -23,6 +23,11 @@ class EventHandler extends SingletonFactory {
         */
        protected $actions = null;
        
+       /**
+        * @var EventListenerCacheBuilder
+        */
+       protected $eventListenerCacheBuilder;
+       
        /**
         * registered inherit actions
         * @var array
@@ -33,26 +38,37 @@ class EventHandler extends SingletonFactory {
         * instances of registerd actions
         * @var array
         */
-       protected $actionsObjects = array();
+       protected $actionsObjects = [];
        
        /**
         * instances of registered inherit actions
         * @var array
         */
-       protected $inheritedActionsObjects = array();
+       protected $inheritedActionsObjects = [];
        
        /**
         * instances of listener objects
-        * @var array<\wcf\system\event\IEventListener>
+        * @var IEventListener[]
         */
-       protected $listenerObjects = array();
+       protected $listenerObjects = [];
+       
+       /**
+        * EventHandler constructor.
+        * 
+        * @param       EventListenerCacheBuilder       $eventListenerCacheBuilder
+        */
+       public function __construct(EventListenerCacheBuilder $eventListenerCacheBuilder) {
+               $this->eventListenerCacheBuilder = $eventListenerCacheBuilder;
+               
+               parent::__construct();
+       }
        
        /**
         * Loads all registered actions of the active package.
         */
        protected function loadActions() {
                $environment = ((class_exists('wcf\system\WCFACP', false) || class_exists('wcf\system\CLIWCF', false)) ? 'admin' : 'user');
-               $cache = EventListenerCacheBuilder::getInstance()->getData();
+               $cache = $this->eventListenerCacheBuilder->getData();
                
                if (isset($cache['actions'][$environment])) {
                        $this->actions = $cache['actions'][$environment];
@@ -63,10 +79,10 @@ class EventHandler extends SingletonFactory {
                unset($cache);
                
                if (!is_array($this->actions)) {
-                       $this->actions = array();
+                       $this->actions = [];
                }
                if (!is_array($this->inheritedActions)) {
-                       $this->inheritedActions = array();
+                       $this->inheritedActions = [];
                }
        }
        
@@ -78,14 +94,15 @@ class EventHandler extends SingletonFactory {
         * @param       string          $className
         * @param       string          $name
         * @param       array           &$parameters
+        * @throws      SystemException
         */
        protected function executeInheritedActions($eventObj, $eventName, $className, $name, array &$parameters) {
                // create objects of the actions
                if (!isset($this->inheritedActionsObjects[$name]) || !is_array($this->inheritedActionsObjects[$name])) {
-                       $this->inheritedActionsObjects[$name] = array();
+                       $this->inheritedActionsObjects[$name] = [];
                        
                        // get parent classes
-                       $familyTree = array();
+                       $familyTree = [];
                        $member = (is_object($eventObj) ? get_class($eventObj) : $eventObj);
                        while ($member != false) {
                                $familyTree[] = $member;
@@ -155,6 +172,7 @@ class EventHandler extends SingletonFactory {
         * @param       mixed           $eventObj
         * @param       string          $eventName
         * @param       array           &$parameters
+        * @throws      SystemException
         */
        public function fireAction($eventObj, $eventName, array &$parameters = array()) {
                // get class name
@@ -178,10 +196,10 @@ class EventHandler extends SingletonFactory {
                if (!isset($this->actionsObjects[$name]) || !is_array($this->actionsObjects[$name])) {
                        if (!isset($this->actions[$name]) || !is_array($this->actions[$name])) {
                                // no action registered
-                               return false;
+                               return;
                        }
                        
-                       $this->actionsObjects[$name] = array();
+                       $this->actionsObjects[$name] = [];
                        foreach ($this->actions[$name] as $eventListener) {
                                if ($eventListener->validateOptions() && $eventListener->validatePermissions()) {
                                        if (isset($this->actionsObjects[$name][$eventListener->listenerClassName])) continue;
@@ -229,8 +247,9 @@ class EventHandler extends SingletonFactory {
        /**
         * Generates an unique name for an action.
         * 
-        * @param       string          $className
-        * @param       string          $eventName
+        * @param       string  $className
+        * @param       string  $eventName
+        * @return      string  unique action name
         */
        public static function generateKey($className, $eventName) {
                return $eventName.'@'.$className;
index 408fcf58fe833c2abd15430d3e124e946f992655..c36abb030bbf57458c35aa8cc93d35b58a5093d0 100644 (file)
@@ -30,6 +30,7 @@ class ControllerMap {
         * @param       string          $controller     url controller
         * @param       boolean         $isAcpRequest   true if this is an ACP request
         * @return      array<string>   className, controller and pageType
+        * @throws      SystemException
         */
        public function resolve($application, $controller, $isAcpRequest) {
                // validate controller
index 301c0bbadec54e14cc5bde9b701e823578d6bf88..664310fe683df8f05f14de71921db3c2eaf10456 100644 (file)
@@ -15,6 +15,7 @@ use wcf\system\menu\page\PageMenu;
  * @package    com.woltlab.wcf
  * @subpackage system.request
  * @category   Community Framework
+ * @deprecated  2.2:2.3 Consider using \wcf\system\request\route\DynamicRequestRoute
  */
 class FlexibleRoute implements IRoute {
        /**
index 7ba99e62a12d375f60377854993d6a31e93f8afa..2c92e1fe5202adb99aa8205689df95f82590db15 100644 (file)
@@ -10,6 +10,7 @@ namespace wcf\system\request;
  * @package    com.woltlab.wcf
  * @subpackage system.request
  * @category   Community Framework
+ * @deprecated 2.2:2.3 will be replaced with \wcf\system\request\route\IRequestRoute
  */
 interface IRoute {
        /**
index 2d423e7e923a2bedf5d686e86f757d7bfe608f7c..457cc89807a779fef39e449569c1e06de314876a 100644 (file)
@@ -23,14 +23,19 @@ use wcf\util\HeaderUtil;
 class RequestHandler extends SingletonFactory {
        /**
         * active request object
-        * @var \wcf\system\request\Request
+        * @var Request
         */
        protected $activeRequest = null;
        
        /**
-        * @var \wcf\system\request\ControllerMap
+        * @var ApplicationHandler
         */
-       protected $controllerMap = null;
+       protected $applicationHandler;
+       
+       /**
+        * @var ControllerMap
+        */
+       protected $controllerMap;
        
        /**
         * true, if current domain mismatch any known domain
@@ -44,14 +49,32 @@ class RequestHandler extends SingletonFactory {
         */
        protected $isACPRequest = false;
        
+       /**
+        * @var RouteHandler
+        */
+       protected $routeHandler;
+       
+       /**
+        * RequestHandler constructor.
+        * 
+        * @param       ApplicationHandler      $applicationHandler
+        * @param       ControllerMap           $controllerMap
+        * @param       RouteHandler            $routeHandler
+        */
+       public function __construct(ApplicationHandler $applicationHandler, ControllerMap $controllerMap, RouteHandler $routeHandler) {
+               $this->applicationHandler = $applicationHandler;
+               $this->controllerMap = $controllerMap;
+               $this->routeHandler = $routeHandler;
+               
+               parent::__construct();
+       }
+       
        /**
         * @see \wcf\system\SingletonFactory::init()
         */
        protected function init() {
-               $this->controllerMap = new ControllerMap();
-               
                if (isset($_SERVER['HTTP_HOST'])) {
-                       foreach (ApplicationHandler::getInstance()->getApplications() as $application) {
+                       foreach ($this->applicationHandler->getApplications() as $application) {
                                if ($application->domainName == $_SERVER['HTTP_HOST']) {
                                        $this->inRescueMode = false;
                                        break;
@@ -60,7 +83,7 @@ class RequestHandler extends SingletonFactory {
                        
                        // check if WCF is running as standalone
                        if ($this->inRescueMode() && PACKAGE_ID == 1) {
-                               if (ApplicationHandler::getInstance()->getWCF()->domainName == $_SERVER['HTTP_HOST']) {
+                               if ($this->applicationHandler->getWCF()->domainName == $_SERVER['HTTP_HOST']) {
                                        $this->inRescueMode = false;
                                }
                        }
@@ -77,14 +100,21 @@ class RequestHandler extends SingletonFactory {
        
        /**
         * Handles a http request.
-        * 
+        *
         * @param       string          $application
         * @param       boolean         $isACPRequest
+        * @throws      AJAXException
+        * @throws      IllegalLinkException
+        * @throws      SystemException
         */
        public function handle($application = 'wcf', $isACPRequest = false) {
                $this->isACPRequest = $isACPRequest;
                
-               if (!RouteHandler::getInstance()->matches()) {
+               // initialize route handler
+               $this->routeHandler->setRequestHandler($this);
+               $this->routeHandler->setDefaultRoutes();
+               
+               if (!$this->routeHandler->matches()) {
                        if (ENABLE_DEBUG_MODE) {
                                throw new SystemException("Cannot handle request, no valid route provided.");
                        }
@@ -123,10 +153,11 @@ class RequestHandler extends SingletonFactory {
         * Builds a new request.
         * 
         * @param       string          $application
+        * @throws      IllegalLinkException
         */
        protected function buildRequest($application) {
                try {
-                       $routeData = RouteHandler::getInstance()->getRouteData();
+                       $routeData = $this->routeHandler->getRouteData();
                        
                        // handle landing page for frontend requests
                        if (!$this->isACPRequest()) {
@@ -134,7 +165,7 @@ class RequestHandler extends SingletonFactory {
                                
                                // check if accessing from the wrong domain (e.g. "www." omitted but domain was configured with)
                                if (!defined('WCF_RUN_MODE') || WCF_RUN_MODE != 'embedded') {
-                                       $applicationObject = ApplicationHandler::getInstance()->getApplication($application);
+                                       $applicationObject = $this->applicationHandler->getApplication($application);
                                        if ($applicationObject->domainName != $_SERVER['HTTP_HOST']) {
                                                // build URL, e.g. http://example.net/forum/
                                                $url = FileUtil::addTrailingSlash(RouteHandler::getProtocol() . $applicationObject->domainName . RouteHandler::getPath());
@@ -232,7 +263,7 @@ class RequestHandler extends SingletonFactory {
         * @param       array           $routeData
         */
        protected function handleDefaultController($application, array &$routeData) {
-               if (!RouteHandler::getInstance()->isDefaultController()) {
+               if (!$this->routeHandler->isDefaultController()) {
                        return;
                }
                
@@ -245,8 +276,8 @@ class RequestHandler extends SingletonFactory {
                
                // resolve implicit application abbreviation for landing page controller
                $landingPageApplication = $landingPage->getApplication();
-               $primaryApplication = ApplicationHandler::getInstance()->getPrimaryApplication();
-               $primaryApplicationAbbr = ApplicationHandler::getInstance()->getAbbreviation($primaryApplication->packageID);
+               $primaryApplication = $this->applicationHandler->getPrimaryApplication();
+               $primaryApplicationAbbr = $this->applicationHandler->getAbbreviation($primaryApplication->packageID);
                if ($landingPageApplication == 'wcf') {
                        $landingPageApplication = $primaryApplicationAbbr;
                }
@@ -266,7 +297,7 @@ class RequestHandler extends SingletonFactory {
                }
                
                // set default controller
-               $applicationObj = WCF::getApplicationObject(ApplicationHandler::getInstance()->getApplication($application));
+               $applicationObj = WCF::getApplicationObject($this->applicationHandler->getApplication($application));
                $routeData['controller'] = preg_replace('~^.*?\\\([^\\\]+)(?:Action|Form|Page)$~', '\\1', $applicationObj->getPrimaryController());
                $routeData['controller'] = $this->getControllerMap()->lookup($routeData['controller']);
        }
index e5cf7dfe96d13ed753257a7f8148d5a38f34b94f..3f9090d5f73b30cca7b12e1b6d86a910acb80312 100644 (file)
@@ -3,6 +3,7 @@ namespace wcf\system\request;
 use wcf\system\application\ApplicationHandler;
 use wcf\system\event\EventHandler;
 use wcf\system\exception\SystemException;
+use wcf\system\request\route\DynamicRequestRoute;
 use wcf\system\SingletonFactory;
 use wcf\system\WCF;
 use wcf\util\FileUtil;
@@ -51,21 +52,36 @@ class RouteHandler extends SingletonFactory {
         */
        protected static $secure = null;
        
+       /**
+        * @var ApplicationHandler
+        */
+       protected $applicationHandler;
+       
        /**
         * list of application abbreviation and default controller name
         * @var array<string>
         */
        protected $defaultControllers = null;
        
+       /**
+        * @var EventHandler
+        */
+       protected $eventHandler;
+       
        /**
         * true, if default controller is used (support for custom landing page)
         * @var boolean
         */
        protected $isDefaultController = false;
        
+       /**
+        * @var RequestHandler
+        */
+       protected $requestHandler;
+       
        /**
         * list of available routes
-        * @var array<\wcf\system\request\IRoute>
+        * @var IRoute[]
         */
        protected $routes = array();
        
@@ -76,39 +92,48 @@ class RouteHandler extends SingletonFactory {
        protected $routeData = null;
        
        /**
-        * @see \wcf\system\SingletonFactory::init()
+        * RouteHandler constructor.
+        * 
+        * @param       ApplicationHandler      $applicationHandler
+        * @param       EventHandler            $eventHandler
+        */
+       public function __construct(ApplicationHandler $applicationHandler, EventHandler $eventHandler) {
+               $this->applicationHandler = $applicationHandler;
+               $this->eventHandler = $eventHandler;
+               
+               parent::__construct();
+       }
+       
+       /**
+        * Sets default routes.
         */
-       protected function init() {
-               $this->addDefaultRoutes();
+       public function setDefaultRoutes() {
+               /** @var \wcf\system\request\route\DynamicRequestRoute $route */
+               $route = WCF::getDIContainer()->make(DynamicRequestRoute::class);
+               $route->setIsACP(true);
+               $this->addRoute($route);
+               
+               $route = WCF::getDIContainer()->make(DynamicRequestRoute::class);
+               $route->setIsACP(false);
+               $this->addRoute($route);
                
                // fire event
-               EventHandler::getInstance()->fireAction($this, 'didInit');
+               $this->eventHandler->fireAction($this, 'didInit');
        }
        
        /**
-        * Adds default routes.
+        * Sets the required request handler, setter function to avoid circular dependencies.
+        * 
+        * @param       RequestHandler  $requestHandler
         */
-       protected function addDefaultRoutes() {
-               $acpRoute = new FlexibleRoute(true);
-               $this->addRoute($acpRoute);
-               
-               if (URL_LEGACY_MODE) {
-                       $defaultRoute = new Route('default');
-                       $defaultRoute->setSchema('/{controller}/{id}');
-                       $defaultRoute->setParameterOption('controller', null, null, true);
-                       $defaultRoute->setParameterOption('id', null, '\d+', true);
-                       $this->addRoute($defaultRoute);
-               }
-               else {
-                       $defaultRoute = new FlexibleRoute(false);
-                       $this->addRoute($defaultRoute);
-               }
+       public function setRequestHandler(RequestHandler $requestHandler) {
+               $this->requestHandler = $requestHandler;
        }
        
        /**
         * Adds a new route to the beginning of all routes.
         * 
-        * @param       \wcf\system\request\IRoute      $route
+        * @param       IRoute  $route
         */
        public function addRoute(IRoute $route) {
                array_unshift($this->routes, $route);
@@ -117,7 +142,7 @@ class RouteHandler extends SingletonFactory {
        /**
         * Returns all registered routes. 
         * 
-        * @return      array<\wcf\system\request\IRoute>
+        * @return      IRoute[]
         **/
        public function getRoutes() {
                return $this->routes; 
@@ -132,7 +157,7 @@ class RouteHandler extends SingletonFactory {
         */
        public function matches() {
                foreach ($this->routes as $route) {
-                       if (RequestHandler::getInstance()->isACPRequest() != $route->isACP()) {
+                       if ($this->requestHandler->isACPRequest() != $route->isACP()) {
                                continue;
                        }
                        
@@ -185,9 +210,10 @@ class RouteHandler extends SingletonFactory {
         * @param       array           $components
         * @param       boolean         $isACP
         * @return      string
+        * @throws      SystemException
         */
        public function buildRoute(array $components, $isACP = null) {
-               if ($isACP === null) $isACP = RequestHandler::getInstance()->isACPRequest();
+               if ($isACP === null) $isACP = $this->requestHandler->isACPRequest();
                
                foreach ($this->routes as $route) {
                        if ($isACP != $route->isACP()) {
@@ -282,8 +308,9 @@ class RouteHandler extends SingletonFactory {
        public static function getPathInfo() {
                if (self::$pathInfo === null) {
                        self::$pathInfo = '';
+                       $requestHandler = WCF::getDIContainer()->get(RequestHandler::class);
                        
-                       if (!URL_LEGACY_MODE || RequestHandler::getInstance()->isACPRequest()) {
+                       if (!URL_LEGACY_MODE || $requestHandler->isACPRequest()) {
                                // WCF 2.1: ?Foo/Bar/
                                if (!empty($_SERVER['QUERY_STRING'])) {
                                        // don't use parse_str as it replaces dots with underscores
@@ -306,7 +333,7 @@ class RouteHandler extends SingletonFactory {
                        }
                        
                        // WCF 2.0: index.php/Foo/Bar/
-                       if ((URL_LEGACY_MODE && !RequestHandler::getInstance()->isACPRequest()) || (RequestHandler::getInstance()->isACPRequest() && empty(self::$pathInfo))) {
+                       if ((URL_LEGACY_MODE && !$requestHandler->isACPRequest()) || ($requestHandler->isACPRequest() && empty(self::$pathInfo))) {
                                if (isset($_SERVER['PATH_INFO'])) {
                                        self::$pathInfo = $_SERVER['PATH_INFO'];
                                }
@@ -358,7 +385,7 @@ class RouteHandler extends SingletonFactory {
                if ($this->defaultControllers === null) {
                        $this->defaultControllers = array();
                        
-                       foreach (ApplicationHandler::getInstance()->getApplications() as $application) {
+                       foreach ($this->applicationHandler->getApplications() as $application) {
                                $app = WCF::getApplicationObject($application);
                                
                                if (!$app) {
index d014d840db9c580ef56212f49bd1ade245c387ef..1e47a254fcc78c9d2b6a81c1dfcf06850b90081c 100644 (file)
@@ -10,6 +10,7 @@ namespace wcf\system\request;
  * @package    com.woltlab.wcf
  * @subpackage system.request
  * @category   Community Framework
+ * @deprecated  2.2:2.3 Consider using \wcf\system\request\route\StaticRequestRoute
  */
 class StaticRoute extends FlexibleRoute {
        /**
diff --git a/wcfsetup/install/files/lib/system/request/route/DynamicRequestRoute.class.php b/wcfsetup/install/files/lib/system/request/route/DynamicRequestRoute.class.php
new file mode 100644 (file)
index 0000000..add7f76
--- /dev/null
@@ -0,0 +1,340 @@
+<?php
+namespace wcf\system\request\route;
+
+use wcf\system\application\ApplicationHandler;
+use wcf\system\menu\page\PageMenu;
+use wcf\system\request\RequestHandler;
+use wcf\system\request\RouteHandler;
+
+class DynamicRequestRoute implements IRequestRoute {
+       /**
+        * @var \wcf\system\application\ApplicationHandler
+        */
+       protected $applicationHandler;
+       
+       /**
+        * schema for outgoing links
+        * @var array<array>
+        */
+       protected $buildSchema = [];
+       
+       /**
+        * route is restricted to ACP
+        * @var boolean
+        */
+       protected $isACP = false;
+       
+       /**
+        * @var \wcf\system\menu\page\PageMenu;
+        */
+       protected $pageMenu;
+       
+       /**
+        * pattern for incoming requests
+        * @var string
+        */
+       protected $pattern = '';
+       
+       /**
+        * primary application's abbreviation (e.g. "wbb")
+        * @var string
+        */
+       protected $primaryApplication = '';
+       
+       /**
+        * @var \wcf\system\request\RequestHandler
+        */
+       protected $requestHandler;
+       
+       /**
+        * list of required components
+        * @var array<string>
+        */
+       protected $requireComponents = [];
+       
+       /**
+        * parsed request data
+        * @var array<mixed>
+        */
+       protected $routeData = [];
+       
+       /**
+        * @var \wcf\system\request\RouteHandler;
+        */
+       protected $routeHandler;
+       
+       /**
+        * DynamicRequestRoute constructor.
+        * 
+        * @param       \wcf\system\application\ApplicationHandler      $applicationHandler
+        * @param       \wcf\system\menu\page\PageMenu                  $pageMenu
+        * @param       \wcf\system\request\RequestHandler              $requestHandler
+        * @param       \wcf\system\request\RouteHandler                $routeHandler
+        */
+       public function __construct(ApplicationHandler $applicationHandler, PageMenu $pageMenu, RequestHandler $requestHandler, RouteHandler $routeHandler) {
+               $this->applicationHandler = $applicationHandler;
+               $this->pageMenu = $pageMenu;
+               $this->requestHandler = $requestHandler;
+               $this->routeHandler = $routeHandler;
+               
+               $this->init();
+       }
+       
+       /**
+        * Sets default routing information.
+        */
+       protected function init() {
+               $this->setPattern('~
+                       /?
+                       (?:
+                               (?P<controller>[A-Za-z0-9\-]+)
+                               (?:
+                                       /
+                                       (?P<id>\d+)
+                                       (?:
+                                               -
+                                               (?P<title>[^/]+)
+                                       )?
+                               )?
+                       )?
+               ~x');
+               $this->setBuildSchema('/{controller}/{id}-{title}/');
+       }
+       
+       /**
+        * {@inheritdoc}
+        */
+       public function setIsACP($isACP) {
+               $this->isACP = $isACP;
+       }
+       
+       /**
+        * Sets the build schema used to build outgoing links.
+        *
+        * @param       string          $buildSchema
+        */
+       public function setBuildSchema($buildSchema) {
+               $this->buildSchema = [];
+               
+               $buildSchema = ltrim($buildSchema, '/');
+               $components = preg_split('~({(?:[a-z]+)})~', $buildSchema, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
+               $delimiters = ['/', '-', '.', '_'];
+               
+               foreach ($components as $component) {
+                       $type = 'component';
+                       if (preg_match('~{([a-z]+)}~', $component, $matches)) {
+                               $component = $matches[1];
+                       }
+                       else {
+                               $type = 'separator';
+                       }
+                       
+                       $this->buildSchema[] = [
+                               'type' => $type,
+                               'value' => $component
+                       ];
+               }
+       }
+       
+       /**
+        * Sets the route pattern used to evaluate an incoming request.
+        *
+        * @param       string          $pattern
+        */
+       public function setPattern($pattern) {
+               $this->pattern = $pattern;
+       }
+       
+       /**
+        * Sets the list of required components.
+        *
+        * @param       array<string>   $requiredComponents
+        */
+       public function setRequiredComponents(array $requiredComponents) {
+               $this->requireComponents = $requiredComponents;
+       }
+       
+       /**
+        * @see IRoute::buildLink()
+        */
+       public function buildLink(array $components) {
+               $application = (isset($components['application'])) ? $components['application'] : null;
+               
+               // drop application component to avoid being appended as query string
+               unset($components['application']);
+               
+               // handle default values for controller
+               $useBuildSchema = true;
+               if (count($components) == 1 && isset($components['controller'])) {
+                       $ignoreController = false;
+                       
+                       if (!RequestHandler::getInstance()->isACPRequest()) {
+                               $landingPage = PageMenu::getInstance()->getLandingPage();
+                               if ($this->primaryApplication === '') {
+                                       $primaryApplication = ApplicationHandler::getInstance()->getPrimaryApplication();
+                                       $this->primaryApplication = ApplicationHandler::getInstance()->getAbbreviation($primaryApplication->packageID);
+                               }
+                               
+                               // check if this is the default controller
+                               if (strcasecmp(RouteHandler::getInstance()->getDefaultController($application), $components['controller']) === 0) {
+                                       // check if this matches the primary application
+                                       if ($this->primaryApplication === $application) {
+                                               if (strcasecmp($landingPage->getController(), $components['controller']) === 0) {
+                                                       // skip controller if it matches the default controller
+                                                       $ignoreController = true;
+                                               }
+                                       }
+                                       else {
+                                               // skip default controller
+                                               $ignoreController = true;
+                                       }
+                               }
+                               else if (strcasecmp($landingPage->getController(), $components['controller']) === 0) {
+                                       // landing page
+                                       $ignoreController = true;
+                               }
+                       }
+                       
+                       // drops controller from route
+                       if ($ignoreController) {
+                               $useBuildSchema = false;
+                               
+                               // unset the controller, since it would otherwise be added with http_build_query()
+                               unset($components['controller']);
+                       }
+               }
+               
+               return $this->buildRoute($components, $application, $useBuildSchema);
+       }
+       
+       /**
+        * Builds the actual link, the parameter $useBuildSchema can be set to false for
+        * empty routes, e.g. for the default page.
+        *
+        * @param       array           $components
+        * @param       string          $application
+        * @param       boolean         $useBuildSchema
+        * @return      string
+        */
+       protected function buildRoute(array $components, $application, $useBuildSchema) {
+               $link = '';
+               
+               if ($useBuildSchema) {
+                       $lastSeparator = null;
+                       $skipToLastSeparator = false;
+                       foreach ($this->buildSchema as $component) {
+                               $value = $component['value'];
+                               
+                               if ($component['type'] === 'separator') {
+                                       $lastSeparator = $value;
+                               }
+                               else if ($skipToLastSeparator === false) {
+                                       // routes are build from left-to-right
+                                       if (empty($components[$value])) {
+                                               $skipToLastSeparator = true;
+                                               
+                                               // drop empty components to avoid them being appended as query string argument
+                                               unset($components[$value]);
+                                               
+                                               continue;
+                                       }
+                                       
+                                       if ($lastSeparator !== null) {
+                                               $link .= $lastSeparator;
+                                               $lastSeparator = null;
+                                       }
+                                       
+                                       // handle controller names
+                                       if ($value === 'controller') {
+                                               $components[$value] = $this->getControllerName($application, $components[$value]);
+                                       }
+                                       
+                                       $link .= $components[$value];
+                                       unset($components[$value]);
+                               }
+                       }
+                       
+                       if (!empty($link) && $lastSeparator !== null) {
+                               $link .= $lastSeparator;
+                       }
+               }
+               
+               if ($this->isACP || !URL_OMIT_INDEX_PHP) {
+                       if (!empty($link)) {
+                               $link = 'index.php?' . $link;
+                       }
+               }
+               
+               if (!empty($components)) {
+                       if (strpos($link, '?') === false) $link .= '?';
+                       else $link .= '&';
+                       
+                       $link .= http_build_query($components, '', '&');
+               }
+               
+               return $link;
+       }
+       
+       /**
+        * @see IRoute::canHandle()
+        */
+       public function canHandle(array $components) {
+               if (!empty($this->requireComponents)) {
+                       foreach ($this->requireComponents as $component => $pattern) {
+                               if (empty($components[$component])) {
+                                       return false;
+                               }
+                               
+                               if ($pattern && !preg_match($pattern, $components[$component])) {
+                                       return false;
+                               }
+                       }
+               }
+               
+               return true;
+       }
+       
+       /**
+        * @see IRoute::getRouteData()
+        */
+       public function getRouteData() {
+               return $this->routeData;
+       }
+       
+       /**
+        * @see IRoute::isACP()
+        */
+       public function isACP() {
+               return $this->isACP;
+       }
+       
+       /**
+        * @see IRoute::matches()
+        */
+       public function matches($requestURL) {
+               if (preg_match($this->pattern, $requestURL, $matches)) {
+                       foreach ($matches as $key => $value) {
+                               if (!is_numeric($key)) {
+                                       $this->routeData[$key] = $value;
+                               }
+                       }
+                       
+                       $this->routeData['isDefaultController'] = (!isset($this->routeData['controller']));
+                       
+                       return true;
+               }
+               
+               return false;
+       }
+       
+       /**
+        * Returns the transformed controller name.
+        *
+        * @param       string          $application
+        * @param       string          $controller
+        * @return      string
+        */
+       protected function getControllerName($application, $controller) {
+               return RequestHandler::getInstance()->getControllerMap()->lookup($controller);
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/request/route/IRequestRoute.class.php b/wcfsetup/install/files/lib/system/request/route/IRequestRoute.class.php
new file mode 100644 (file)
index 0000000..d1a37eb
--- /dev/null
@@ -0,0 +1,22 @@
+<?php
+namespace wcf\system\request\route;
+use wcf\system\request\IRoute;
+
+/**
+ * Default interface for route implementations.
+ * 
+ * @author      Alexander Ebert
+ * @copyright   2001-2015 WoltLab GmbH
+ * @license     GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package     com.woltlab.wcf
+ * @subpackage  system.request
+ * @category    Community Framework
+ */
+interface IRequestRoute extends IRoute {
+       /**
+        * Configures this route to handle either ACP or frontend requests.
+        * 
+        * @param boolean $isACP true if route handles ACP requests
+        */
+       public function setIsACP($isACP);
+}
diff --git a/wcfsetup/install/files/lib/system/request/route/StaticRequestRoute.class.php b/wcfsetup/install/files/lib/system/request/route/StaticRequestRoute.class.php
new file mode 100644 (file)
index 0000000..3b16940
--- /dev/null
@@ -0,0 +1,111 @@
+<?php
+namespace wcf\system\request\route;
+use wcf\system\application\ApplicationHandler;
+use wcf\system\menu\page\PageMenu;
+use wcf\system\request\ControllerMap;
+use wcf\system\request\RequestHandler;
+use wcf\system\request\RouteHandler;
+
+/**
+ * Static route implementation to resolve HTTP requests, handling a single controller.
+ *
+ * @author     Alexander Ebert
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage system.request
+ * @category   Community Framework
+ */
+class StaticRequestRoute extends DynamicRequestRoute {
+       /**
+        * @var \wcf\system\request\ControllerMap
+        */
+       protected $controllerMap;
+       
+       /**
+        * static application identifier
+        * @var string
+        */
+       protected $staticApplication = '';
+       
+       /**
+        * static controller name, not the FQN
+        * @var string
+        */
+       protected $staticController = '';
+       
+       /**
+        * StaticRequestRoute constructor.
+        * 
+        * @param       \wcf\system\application\ApplicationHandler      $applicationHandler
+        * @param       \wcf\system\request\ControllerMap               $controllerMap
+        * @param       \wcf\system\menu\page\PageMenu                  $pageMenu
+        * @param       \wcf\system\request\RequestHandler              $requestHandler
+        * @param       \wcf\system\request\RouteHandler                $routeHandler
+        */
+       public function __construct(ApplicationHandler $applicationHandler, ControllerMap $controllerMap, PageMenu $pageMenu, RequestHandler $requestHandler, RouteHandler $routeHandler) {
+               parent::__construct($applicationHandler, $pageMenu, $requestHandler, $routeHandler);
+               
+               $this->controllerMap = $controllerMap;
+       }
+       
+       /**
+        * @see \wcf\system\request\route\IRequestRoute::setIsACP()
+        */
+       public function setIsACP($isACP) {
+               // static routes are disallowed for ACP
+               parent::setIsACP(false);
+       }
+       
+       /**
+        * Sets the static controller for this route.
+        *
+        * @param       string          $application
+        * @param       string          $controller
+        */
+       public function setStaticController($application, $controller) {
+               $this->staticApplication = $application;
+               $this->staticController = $controller;
+               
+               $this->requireComponents['controller'] = '~^' . $this->staticController . '$~';
+       }
+       
+       /**
+        * @see \wcf\system\request\IRoute::buildLink()
+        */
+       public function buildLink(array $components) {
+               // static routes don't have these components
+               unset($components['application']);
+               unset($components['controller']);
+               
+               return $this->buildRoute($components, '', true);
+       }
+       
+       /**
+        * @see \wcf\system\request\IRoute::canHandle()
+        */
+       public function canHandle(array $components) {
+               if (isset($components['application']) && $components['application'] == $this->staticApplication) {
+                       if (isset($components['controller']) && $components['controller'] == $this->staticController) {
+                               return parent::canHandle($components);
+                       }
+               }
+               
+               return false;
+       }
+       
+       /**
+        * @see \wcf\system\request\IRoute::matches()
+        */
+       public function matches($requestURL) {
+               if (parent::matches($requestURL)) {
+                       $this->routeData['application'] = $this->staticApplication;
+                       $this->routeData['controller'] = $this->controllerMap->lookup($this->staticController);
+                       $this->routeData['isDefaultController'] = false;
+                       
+                       return true;
+               }
+               
+               return false;
+       }
+}