Overhauled WCF 2.1 route system
authorAlexander Ebert <ebert@woltlab.com>
Tue, 10 Feb 2015 10:51:09 +0000 (11:51 +0100)
committerAlexander Ebert <ebert@woltlab.com>
Tue, 10 Feb 2015 10:51:09 +0000 (11:51 +0100)
com.woltlab.wcf/option.xml
wcfsetup/install/files/lib/system/cache/builder/ControllerCacheBuilder.class.php
wcfsetup/install/files/lib/system/option/UrlControllerReplacementOptionType.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/request/FlexibleRoute.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/request/IRoute.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/request/LinkHandler.class.php
wcfsetup/install/files/lib/system/request/RequestHandler.class.php
wcfsetup/install/files/lib/system/request/Route.class.php
wcfsetup/install/files/lib/system/request/RouteHandler.class.php
wcfsetup/install/lang/de.xml
wcfsetup/install/lang/en.xml

index 6eb2698d870c10340f1e8db0aad6880842aff5b6..6f8daebd2290b2a0910ad5969fe2bf3751733ee2 100644 (file)
                                <categoryname>general.page.seo</categoryname>
                                <optiontype>boolean</optiontype>
                                <defaultvalue>0</defaultvalue>
+                               <enableoptions>!url_controller_replacement</enableoptions>
                        </option>
                        <option name="url_omit_index_php">
                                <categoryname>general.page.seo</categoryname>
                                <optiontype>boolean</optiontype>
                                <defaultvalue>0</defaultvalue>
                        </option>
-                       <option name="url_to_lowercase">
+                       <option name="url_controller_replacement">
                                <categoryname>general.page.seo</categoryname>
-                               <optiontype>boolean</optiontype>
-                               <defaultvalue>1</defaultvalue>
+                               <optiontype>urlControllerReplacement</optiontype>
                        </option>
                        <option name="url_title_component_replacement">
                                <categoryname>general.page.seo</categoryname>
@@ -1402,5 +1402,6 @@ DESC:wcf.global.sortOrder.descending]]></selectoptions>
        <delete>
                <option name="cache_source_memcached_use_pconnect" />
                <option name="http_gzip_level" />
+               <option name="url_to_lowercase" />
        </delete>
 </data>
index 78c75151fef29783c578b6330a9f767daf0bbaeb..e10123e3301ef5c081502aeb11542839f886a15a 100644 (file)
@@ -51,20 +51,21 @@ class ControllerCacheBuilder extends AbstractCacheBuilder {
                $controllers = array();
                $path .= $type . '/';
                
-               $files = glob($path . '*' . ucfirst($type) . '.class.php');
+               $type = ucfirst($type);
+               $files = glob($path . '*' . $type . '.class.php');
                if ($files === false) {
                        return array();
                }
                
                foreach ($files as $file) {
                        $file = basename($file);
-                       if (preg_match('~^([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)(Action|Form|Page)\.class\.php$~', $file, $match)) {
+                       if (preg_match('~^([A-Z][A-Za-z0-9]*)' . $type . '\.class\.php$~', $file, $match)) {
                                if ($match[1] === 'I') {
                                        continue;
                                }
                                
                                $controller = mb_strtolower($match[1]);
-                               $fqn = '\\' . $abbreviation . '\\' . ($isACP ? 'acp\\' : '') . $type . '\\' . $match[1] . $match[2];
+                               $fqn = '\\' . $abbreviation . '\\' . ($isACP ? 'acp\\' : '') . $type . '\\' . $match[1] . $type;
                                
                                $controllers[$controller] = $fqn;
                        }
diff --git a/wcfsetup/install/files/lib/system/option/UrlControllerReplacementOptionType.class.php b/wcfsetup/install/files/lib/system/option/UrlControllerReplacementOptionType.class.php
new file mode 100644 (file)
index 0000000..c212f83
--- /dev/null
@@ -0,0 +1,120 @@
+<?php
+namespace wcf\system\option;
+use wcf\data\option\Option;
+use wcf\util\StringUtil;
+use wcf\system\exception\UserInputException;
+use wcf\system\WCF;
+use wcf\system\cache\builder\ControllerCacheBuilder;
+
+/**
+ * Option type implementation for URL controller replacements.
+ * 
+ * @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.option
+ * @category   Community Framework
+ */
+class UrlControllerReplacementOptionType extends TextareaOptionType {
+       /**
+        * list of known controllers grouped by application
+        * @var array<array>
+        */
+       protected $controllers = null;
+       
+       /**
+        * @see \wcf\system\option\IOptionType::getData()
+        */
+       public function getData(Option $option, $newValue) {
+               return $this->cleanup($newValue);
+       }
+       
+       /**
+        * @see \wcf\system\option\IOptionType::validate()
+        */
+       public function validate(Option $option, $newValue) {
+               $newValue = $this->cleanup($newValue);
+               if (!empty($newValue)) {
+                       $lines = explode("\n", $newValue);
+                       
+                       $aliases = array();
+                       $controllers = array();
+                       for ($i = 0, $length = count($lines); $i < $length; $i++) {
+                               $line = $lines[$i];
+                               if (preg_match('~^(?P<controller>[a-z0-9\-]+)=(?P<alias>[a-z0-9\-]+)$~', $line, $matches)) {
+                                       // check if there is already a replacement for given controller
+                                       if (in_array($matches['controller'], $controllers)) {
+                                               WCF::getTPL()->assign('urlControllerReplacementError', $matches['controller']);
+                                               throw new UserInputException($option->optionName, 'controllerReplacementDuplicateController', array('controller' => $matches['controller']));
+                                       }
+                                       
+                                       // check if there is already the same alias for a different controller
+                                       if (in_array($matches['alias'], $aliases)) {
+                                               WCF::getTPL()->assign('urlControllerReplacementError', $matches['alias']);
+                                               throw new UserInputException($option->optionName, 'controllerReplacementDuplicateAlias', array('alias' => $matches['alias']));
+                                       }
+                                       
+                                       $aliases[] = $matches['alias'];
+                                       $controllers[] = $matches['controller'];
+                                       
+                                       // check if controller exists
+                                       if (!$this->isKnownController($matches['controller'])) {
+                                               WCF::getTPL()->assign('urlControllerReplacementError', $matches['controller']);
+                                               throw new UserInputException($option->optionName, 'controllerReplacementUnknown', array('controller' => $matches['controller']));
+                                       }
+                                       
+                                       // check if alias collides with an existing controller name
+                                       if ($this->isKnownController($matches['alias'])) {
+                                               WCF::getTPL()->assign('urlControllerReplacementError', $matches['alias']);
+                                               throw new UserInputException($option->optionName, 'controllerReplacementCollision', array('alias' => $matches['alias']));
+                                       }
+                               }
+                               else {
+                                       throw new UserInputException($option->optionName, 'controllerReplacementInvalidFormat', array('line' => $line));
+                               }
+                       }
+               }
+       }
+       
+       /**
+        * Cleans up newlines and converts input to lower-case.
+        * 
+        * @param       string          $newValue
+        * @return      string
+        */
+       protected function cleanup($newValue) {
+               $newValue = StringUtil::unifyNewlines($newValue);
+               $newValue = trim($newValue);
+               $newValue = preg_replace('~\n+~', "\n", $newValue);
+               $newValue = mb_strtolower($newValue);
+               
+               return $newValue;
+       }
+       
+       /**
+        * Returns true if given controller name is known to the system, used to
+        * prevent aliases colliding with existing ones.
+        * 
+        * @param       string          $controller
+        * @return      boolean
+        */
+       protected function isKnownController($controller) {
+               if ($this->controllers === null) {
+                       $this->controllers = ControllerCacheBuilder::getInstance()->getData(array(
+                               'environment' => 'user'
+                       ));
+               }
+               
+               $controller = str_replace('-', '', $controller);
+               foreach ($this->controllers as $types) {
+                       foreach ($types as $controllers) {
+                               if (isset($controllers[$controller])) {
+                                       return true;
+                               }
+                       }
+               }
+               
+               return false;
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/request/FlexibleRoute.class.php b/wcfsetup/install/files/lib/system/request/FlexibleRoute.class.php
new file mode 100644 (file)
index 0000000..163a479
--- /dev/null
@@ -0,0 +1,289 @@
+<?php
+namespace wcf\system\request;
+use wcf\system\application\ApplicationHandler;
+use wcf\system\menu\page\PageMenu;
+
+/**
+ * Flexible route implementation to resolve HTTP requests.
+ * 
+ * Inspired by routing mechanism used by ASP.NET MVC and released under the terms of
+ * the Microsoft Public License (MS-PL) http://www.opensource.org/licenses/ms-pl.html
+ * 
+ * @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 FlexibleRoute implements IRoute {
+       /**
+        * schema for outgoing links
+        * @var array<array>
+        */
+       protected $buildSchema = array();
+       
+       /**
+        * route is restricted to ACP
+        * @var boolean
+        */
+       protected $isACP = false;
+       
+       /**
+        * pattern for incoming requests
+        * @var string
+        */
+       protected $pattern = '';
+       
+       /**
+        * list of required components
+        * @var array<string>
+        */
+       protected $requireComponents = array();
+       
+       /**
+        * parsed request data
+        * @var array<mixed>
+        */
+       protected $routeData = array();
+       
+       /**
+        * cached list of transformed controller names
+        * @var array<string>
+        */
+       protected static $controllerNames = array();
+       
+       /**
+        * Creates a new flexible route instace.
+        * 
+        * @param       boolean         $isACP
+        */
+       public function __construct($isACP) {
+               $this->isACP = $isACP;
+               
+               $this->pattern = '~
+                       /?
+                       (?:
+                               (?P<controller>[A-Za-z0-9\-]+)
+                               (?:
+                                       /
+                                       (?P<id>\d+)     
+                               )?
+                       )?
+               ~x';
+               $this->setBuildSchema('/{controller}/{id}-{title}/');
+       }
+       
+       /**
+        * Sets the build schema used to build outgoing links.
+        * 
+        * @param       string          $buildSchema
+        */
+       public function setBuildSchema($buildSchema) {
+               $this->buildSchema = array();
+               
+               $buildSchema = ltrim($buildSchema, '/');
+               $components = preg_split('~{([a-z]+)}~', $buildSchema, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
+               $delimiters = array('/', '-', '.', '_');
+               
+               foreach ($components as $component) {
+                       $type = 'component';
+                       if (in_array($component, $delimiters)) {
+                               $type = 'separator';
+                       }
+                       
+                       $this->buildSchema[] = array(
+                               '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 \wcf\system\request\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']);
+               
+               $link = '';
+               
+               // handle default values for controller
+               $buildRoute = true;
+               if (count($components) == 1 && isset($components['controller'])) {
+                       $ignoreController = false;
+                       
+                       if (!RequestHandler::getInstance()->isACPRequest()) {
+                               $landingPage = PageMenu::getInstance()->getLandingPage();
+                               if ($landingPage !== null && strcasecmp($landingPage->getController(), $components['controller']) == 0) {
+                                       $ignoreController = true;
+                               }
+                               
+                               // check if this is the default controller of the requested application
+                               if (!$ignoreController && $application !== null) {
+                                       if (RouteHandler::getInstance()->getDefaultController($application) == $components['controller']) {
+                                               // check if this is the primary application and the landing page originates to the same application
+                                               $primaryApplication = ApplicationHandler::getInstance()->getPrimaryApplication();
+                                               $abbreviation = ApplicationHandler::getInstance()->getAbbreviation($primaryApplication->packageID);
+                                               if ($abbreviation != $application || $landingPage === null || $landingPage->getApplication() != 'wcf') {
+                                                       $ignoreController = true;
+                                               }
+                                       }
+                               }
+                       }
+                       
+                       // drops controller from route
+                       if ($ignoreController) {
+                               $buildRoute = false;
+                               
+                               // unset the controller, since it would otherwise be added with http_build_query()
+                               unset($components['controller']);
+                       }
+               }
+               
+               if ($buildRoute) {
+                       $lastSeparator = null;
+                       foreach ($this->buildSchema as $component) {
+                               $value = $component['value'];
+                               
+                               if ($component['type'] === 'separator') {
+                                       $lastSeparator = $value;
+                               }
+                               else {
+                                       // routes are build from left-to-right
+                                       if (empty($components[$value])) {
+                                               break;
+                                       }
+                                       
+                                       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)) {
+                               $link .= '/';
+                       }
+               }
+               
+               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 \wcf\system\request\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 \wcf\system\request\IRoute::getRouteData()
+        */
+       public function getRouteData() {
+               return $this->routeData;
+       }
+       
+       /**
+        * @see \wcf\system\request\IRoute::isACP()
+        */
+       public function isACP() {
+               return $this->isACP;
+       }
+       
+       /**
+        * @see \wcf\system\request\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) {
+               if (!isset(self::$controllerNames[$controller])) {
+                       $parts = preg_split('~([A-Z][a-z0-9]+)~', $controller, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
+                       $controllerName = '';
+                       for ($i = 0, $length = count($parts); $i < $length; $i++) {
+                               if (!empty($controllerName)) $controllerName .= '-';
+                               $controllerName .= strtolower($parts[$i]);
+                       }
+                       
+                       $alias = RequestHandler::getInstance()->getAliasByController($controllerName);
+                       
+                       self::$controllerNames[$controller] = ($alias) ?: $controllerName;
+               }
+               
+               return self::$controllerNames[$controller];
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/request/IRoute.class.php b/wcfsetup/install/files/lib/system/request/IRoute.class.php
new file mode 100644 (file)
index 0000000..7ba99e6
--- /dev/null
@@ -0,0 +1,52 @@
+<?php
+namespace wcf\system\request;
+
+/**
+ * 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 IRoute {
+       /**
+        * Builds a link upon route components.
+        * 
+        * @param       array           $components
+        * @return      string
+        */
+       public function buildLink(array $components);
+       
+       /**
+        * Returns true if current route can handle the build request.
+        * 
+        * @param       array           $components
+        * @return      boolean
+        */
+       public function canHandle(array $components);
+       
+       /**
+        * Returns parsed route data.
+        * 
+        * @return      array
+        */
+       public function getRouteData();
+       
+       /**
+        * Returns true if route applies for ACP.
+        * 
+        * @return      boolean
+        */
+       public function isACP();
+       
+       /**
+        * Returns true if given request url matches this route.
+        * 
+        * @param       string          $requestURL
+        * @return      boolean
+        */
+       public function matches($requestURL);
+}
index 1db4ae1429eadb30180a4dcf8334ac988b4a8a42..353f7b7999316faf0318285908cdc99e8e28cbe2 100644 (file)
@@ -12,7 +12,7 @@ use wcf\util\StringUtil;
  * Handles relative links within the wcf.
  * 
  * @author     Marcel Werk
- * @copyright  2001-2014 WoltLab GmbH
+ * @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
@@ -156,7 +156,7 @@ class LinkHandler extends SingletonFactory {
                        // trim to 80 characters
                        $parameters['title'] = rtrim(mb_substr($parameters['title'], 0, 80), '-');
                        
-                       if (URL_TO_LOWERCASE) {
+                       if (!URL_LEGACY_MODE) {
                                $parameters['title'] = mb_strtolower($parameters['title']);
                        }
                        
@@ -164,7 +164,7 @@ class LinkHandler extends SingletonFactory {
                        if ($encodeTitle) $parameters['title'] = rawurlencode($parameters['title']);
                }
                
-               $parameters['controller'] = (URL_TO_LOWERCASE) ? mb_strtolower($controller) : $controller;
+               $parameters['controller'] = $controller;
                $routeURL = RouteHandler::getInstance()->buildRoute($parameters, $isACP);
                if (!$isRaw && !empty($url)) {
                        $routeURL .= (strpos($routeURL, '?') === false) ? '?' : '&';
index 17734a7dfcdd99afccb849b1c2f1d9409757a3fd..eb099df7e79df8df840cf155d71f0be8a6300dda 100644 (file)
@@ -15,7 +15,7 @@ use wcf\util\HeaderUtil;
  * Handles http requests.
  * 
  * @author     Marcel Werk
- * @copyright  2001-2014 WoltLab GmbH
+ * @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
@@ -34,6 +34,12 @@ class RequestHandler extends SingletonFactory {
         */
        protected $controllers = null;
        
+       /**
+        * list of controller aliases
+        * @var array<string>
+        */
+       protected $controllerAliases = array();
+       
        /**
         * true, if current domain mismatch any known domain
         * @var boolean
@@ -78,6 +84,14 @@ class RequestHandler extends SingletonFactory {
                        $this->controllers = ControllerCacheBuilder::getInstance()->getData(array(
                                'environment' => ($this->isACPRequest ? 'admin' : 'user')
                        ));
+                       
+                       if (!URL_LEGACY_MODE && URL_CONTROLLER_REPLACEMENT) {
+                               $controllerAliases = explode("\n", URL_CONTROLLER_REPLACEMENT);
+                               for ($i = 0, $length = count($controllerAliases); $i < $length; $i++) {
+                                       $tmp = explode('=', $controllerAliases[$i]);
+                                       $this->controllerAliases[$tmp[0]] = $tmp[1];
+                               }
+                       }
                }
        }
        
@@ -162,12 +176,28 @@ class RequestHandler extends SingletonFactory {
                                                exit;
                                        }
                                }
+                               
+                               // handle controller aliasing
+                               if (!URL_LEGACY_MODE && isset($routeData['controller'])) {
+                                       // aliased controller, pretend it does not exist
+                                       if ($this->getAliasByController($routeData['controller']) !== null) {
+                                               throw new IllegalLinkException();
+                                       }
+                                       
+                                       $controller = $this->getControllerByAlias($routeData['controller']);
+                                       if ($controller !== null) {
+                                               $routeData['controller'] = $controller;
+                                       }
+                               }
+                       }
+                       else if (empty($routeData['controller'])) {
+                               $routeData['controller'] = 'Index';
                        }
                        
                        $controller = $routeData['controller'];
                        
                        // validate class name
-                       if (!preg_match('~^[a-z0-9_]+$~i', $controller)) {
+                       if (!preg_match('~^[a-z0-9' . (URL_LEGACY_MODE ? '' : '\-') . ']+$~i', $controller)) {
                                throw new SystemException("Illegal class name '".$controller."'");
                        }
                        
@@ -249,7 +279,6 @@ class RequestHandler extends SingletonFactory {
         */
        protected function getClassData($controller, $pageType, $application) {
                $className = false;
-               
                if ($this->controllers !== null) {
                        $className = $this->lookupController($controller, $pageType, $application);
                        if ($className === false && $application != 'wcf') {
@@ -293,6 +322,8 @@ class RequestHandler extends SingletonFactory {
        protected function lookupController($controller, $pageType, $application) {
                if (isset($this->controllers[$application]) && isset($this->controllers[$application][$pageType])) {
                        $ciController = mb_strtolower($controller);
+                       if (!URL_LEGACY_MODE) $ciController = str_replace('-', '', $ciController);
+                       
                        if (isset($this->controllers[$application][$pageType][$ciController])) {
                                return $this->controllers[$application][$pageType][$ciController];
                        }
@@ -327,4 +358,33 @@ class RequestHandler extends SingletonFactory {
        public function inRescueMode() {
                return $this->inRescueMode;
        }
+       
+       /**
+        * Returns the alias by controller or null if there is no match.
+        * 
+        * @param       string          $controller
+        * @return      string
+        */
+       public function getAliasByController($controller) {
+               if (isset($this->controllerAliases[$controller])) {
+                       return $this->controllerAliases[$controller];
+               }
+               
+               return null;
+       }
+       
+       /**
+        * Returns the controller by alias or null if there is no match.
+        * 
+        * @param       string          $alias
+        * @return      string
+        */
+       public function getControllerByAlias($alias) {
+               $controller = array_search($alias, $this->controllerAliases);
+               if ($controller !== false) {
+                       return $controller;
+               }
+               
+               return null;
+       }
 }
index 3b529626b8e615adf17194d1f3527192656f3ab6..32cdfdf30d0c2589352a8e41b3d31efcf2e22ed8 100644 (file)
@@ -12,13 +12,13 @@ use wcf\system\WCF;
  * the Microsoft Public License (MS-PL) http://www.opensource.org/licenses/ms-pl.html
  * 
  * @author     Alexander Ebert
- * @copyright  2001-2014 WoltLab GmbH
+ * @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 Route {
+class Route implements IRoute {
        /**
         * route controller if controller is no part of the route schema
         * @var string
@@ -128,10 +128,7 @@ class Route {
        }
        
        /**
-        * Returns true if given request url matches this route.
-        * 
-        * @param       string          $requestURL
-        * @return      boolean
+        * @see \wcf\system\request\IRoute::matches()
         */
        public function matches($requestURL) {
                $urlParts = $this->getParts($requestURL);
@@ -194,9 +191,7 @@ class Route {
        }
        
        /**
-        * Returns parsed route data.
-        * 
-        * @return      array
+        * @see \wcf\system\request\IRoute::getRouteData()
         */
        public function getRouteData() {
                return $this->routeData;
@@ -221,10 +216,7 @@ class Route {
        }
        
        /**
-        * Returns true if current route can handle the build request.
-        * 
-        * @param       array           $components
-        * @return      boolean
+        * @see \wcf\system\request\IRoute::canHandle()
         */
        public function canHandle(array $components) {
                foreach ($this->routeSchema as $schemaPart) {
@@ -256,10 +248,7 @@ class Route {
        }
        
        /**
-        * Builds a link upon route components.
-        * 
-        * @param       array           $components
-        * @return      string
+        * @see \wcf\system\request\IRoute::buildLink()
         */
        public function buildLink(array $components) {
                $application = (isset($components['application'])) ? $components['application'] : null;
@@ -345,9 +334,7 @@ class Route {
        }
        
        /**
-        * Returns true if route applies for ACP.
-        * 
-        * @return      boolean
+        * @see \wcf\system\request\IRoute::isACP()
         */
        public function isACP() {
                return $this->isACP;
index e626f9bb5c6065b71fe01975d2c93dc28f79ff05..5ad3a35bb6f3791d1158e797c8da8260ba2ff380 100644 (file)
@@ -1,8 +1,10 @@
 <?php
 namespace wcf\system\request;
+use wcf\system\application\ApplicationHandler;
 use wcf\system\event\EventHandler;
 use wcf\system\exception\SystemException;
 use wcf\system\SingletonFactory;
+use wcf\system\WCF;
 use wcf\util\FileUtil;
 
 /**
@@ -12,7 +14,7 @@ use wcf\util\FileUtil;
  * the Microsoft Public License (MS-PL) http://www.opensource.org/licenses/ms-pl.html
  * 
  * @author     Alexander Ebert
- * @copyright  2001-2014 WoltLab GmbH
+ * @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
@@ -49,6 +51,12 @@ class RouteHandler extends SingletonFactory {
         */
        protected static $secure = null;
        
+       /**
+        * list of application abbreviation and default controller name
+        * @var array<string>
+        */
+       protected $defaultControllers = null;
+       
        /**
         * true, if default controller is used (support for custom landing page)
         * @var boolean
@@ -57,7 +65,7 @@ class RouteHandler extends SingletonFactory {
        
        /**
         * list of available routes
-        * @var array<\wcf\system\request\Route>
+        * @var array<\wcf\system\request\IRoute>
         */
        protected $routes = array();
        
@@ -81,32 +89,41 @@ class RouteHandler extends SingletonFactory {
         * Adds default routes.
         */
        protected function addDefaultRoutes() {
-               $acpRoute = new Route('ACP_default', true);
-               $acpRoute->setSchema('/{controller}/{id}');
-               $acpRoute->setParameterOption('controller', 'Index', null, true);
-               $acpRoute->setParameterOption('id', null, '\d+', true);
-               $this->addRoute($acpRoute);
-               
-               $defaultRoute = new Route('default');
-               $defaultRoute->setSchema('/{controller}/{id}');
-               $defaultRoute->setParameterOption('controller', null, null, true);
-               $defaultRoute->setParameterOption('id', null, '\d+', true);
-               $this->addRoute($defaultRoute);
+               if (URL_LEGACY_MODE) {
+                       $acpRoute = new Route('ACP_default', true);
+                       $acpRoute->setSchema('/{controller}/{id}');
+                       $acpRoute->setParameterOption('controller', 'Index', null, true);
+                       $acpRoute->setParameterOption('id', null, '\d+', true);
+                       $this->addRoute($acpRoute);
+                       
+                       $defaultRoute = new Route('default');
+                       $defaultRoute->setSchema('/{controller}/{id}');
+                       $defaultRoute->setParameterOption('controller', null, null, true);
+                       $defaultRoute->setParameterOption('id', null, '\d+', true);
+                       $this->addRoute($defaultRoute);
+               }
+               else {
+                       $acpRoute = new FlexibleRoute(true);
+                       $this->addRoute($acpRoute);
+                       
+                       $defaultRoute = new FlexibleRoute(false);
+                       $this->addRoute($defaultRoute);
+               }
        }
        
        /**
         * Adds a new route to the beginning of all routes.
         * 
-        * @param       \wcf\system\request\Route       $route
+        * @param       \wcf\system\request\IRoute      $route
         */
-       public function addRoute(Route $route) {
+       public function addRoute(IRoute $route) {
                array_unshift($this->routes, $route);
        }
        
        /**
         * Returns all registered routes. 
         * 
-        * @return      array<\wcf\system\request\Route>
+        * @return      array<\wcf\system\request\IRoute>
         **/
        public function getRoutes() {
                return $this->routes; 
@@ -314,4 +331,48 @@ class RouteHandler extends SingletonFactory {
                
                return self::$pathInfo;
        }
+       
+       /**
+        * Returns the default controller name for given application.
+        * 
+        * @param       string          $application
+        * @return      string
+        */
+       public function getDefaultController($application) {
+               $this->loadDefaultControllers();
+               
+               if (isset($this->defaultControllers[$application])) {
+                       return $this->defaultControllers[$application];
+               }
+               
+               return '';
+       }
+       
+       /**
+        * Loads the default controllers for each active application.
+        */
+       protected function loadDefaultControllers() {
+               if ($this->defaultControllers === null) {
+                       $this->defaultControllers = array();
+                       
+                       foreach (ApplicationHandler::getInstance()->getApplications() as $application) {
+                               $app = WCF::getApplicationObject($application);
+                               
+                               if (!$app) {
+                                       continue;
+                               }
+                               
+                               $controller = $app->getPrimaryController();
+                               
+                               if (!$controller) {
+                                       continue;
+                               }
+                               
+                               $controller = explode('\\', $controller);
+                               $controllerName = preg_replace('~(Action|Form|Page)$~', '', array_pop($controller));
+                               
+                               $this->defaultControllers[$controller[0]] = $controllerName;
+                       }
+               }
+       }
 }
index deacd860af3c6ace7d30437f700de908e4516aef..402e8fa838fe92c3567d5bd68288d95951844bfe 100644 (file)
                <item name="wcf.acp.option.cookie_path"><![CDATA[Cookiepfad]]></item>
                <item name="wcf.acp.option.cookie_path.description"><![CDATA[Der Cookiepfad wird absolut zum Document Root angegeben - also z.B. "/forum" für http://www.woltlab.de/forum.]]></item>
                <item name="wcf.acp.option.cookie_prefix"><![CDATA[Präfix für Cookienamen]]></item>
+               <item name="wcf.acp.option.error.controllerReplacementCollision"><![CDATA[Das Alias „{$urlControllerReplacementError}“ kollidiert mit einem real existierenden Controller und ist daher unzulässig.]]></item>
+               <item name="wcf.acp.option.error.controllerReplacementDuplicateAlias"><![CDATA[Das Alias „{$urlControllerReplacementError}“ wird bereits verwendet.]]></item>
+               <item name="wcf.acp.option.error.controllerReplacementDuplicateController"><![CDATA[Dem Controller „{$urlControllerReplacementError}“ wurde bereits ein Alias zugewiesen.]]></item>
+               <item name="wcf.acp.option.error.controllerReplacementDuplicateUnknown"><![CDATA[Der Controller „{$urlControllerReplacementError}“ ist unbekannt.]]></item>
                <item name="wcf.acp.option.error.tooHigh"><![CDATA[Der angegebene Wert ist zu hoch.{if $option->maxvalue !== null} Der maximale Wert ist {#$option->maxvalue}.{/if}]]></item>
                <item name="wcf.acp.option.error.tooLow"><![CDATA[Der angegebene Wert ist zu gering.{if $option->minvalue !== null} Der minimale Wert ist {#$option->minvalue}.{/if}]]></item>
                <item name="wcf.acp.option.export"><![CDATA[Optionen sichern]]></item>
 Beispiele:<br />
 WBB=WoltLab Burning Board<br />
 GmbH=Gesellschaft mit beschränkter Haftung]]></item>
+               <item name="wcf.acp.option.url_controller_replacement"><![CDATA[Controller-Umbenennung]]></item>
+               <item name="wcf.acp.option.url_controller_replacement.description"><![CDATA[Sie können die Controller umbenennen und diesen so eine neue Bezeichnung zuweisen, pro Zeile können Sie einen Alias definieren. Die Angabe muss im Format „realer-name=neuer-name“ erfolgen und darf aus Kompatibilitätsgründen nur aus den Kleinbuchstaben a-z, den Zahlen 0-9 sowie Bindestrichen bestehen. Beispiele:
+<ul class="nativeList">
+       <li>„board-list=forums“, der Link „http://example.com/index.php?board-list/“ wird zu „http://example.com/index.php?forums/“</li>
+       <li>„members-list=profile“, der Link „http://example.com/index.php?members-list/“ wird zu „http://example.com/index.php?profile/“</li>
+</ul>]]></item>
                <item name="wcf.acp.option.users_online_record_no_guests"><![CDATA[Nur registrierte Benutzer im Benutzer-Online-Rekord zählen]]></item>
                <item name="wcf.acp.option.users_online_enable_legend"><![CDATA[Legende der Benutzergruppen anzeigen]]></item>
                <item name="wcf.acp.option.category.general.system.googleMaps"><![CDATA[Google Maps]]></item>
@@ -1005,15 +1015,13 @@ GmbH=Gesellschaft mit beschränkter Haftung]]></item>
                <item name="wcf.acp.option.module_cookie_policy_page"><![CDATA[Erklärung zum „Einsatz von Cookies“ aktivieren]]></item>
                <item name="wcf.acp.option.module_cookie_policy_page.description"><![CDATA[Weist Besucher beim ersten Aufruf der Seite gemäß EU-Richtlinie 2009/136/EG auf den Einsatz von Cookies hin.]]></item>
                <item name="wcf.acp.option.url_omit_index_php"><![CDATA[Link-Umschreibungen aktivieren]]></item>
-               <item name="wcf.acp.option.url_omit_index_php.description"><![CDATA[Wandelt Links in eine vereinfachte Form um, aus „http://example.com/?Thread/1-Dies-ist-ein-Test/“ wird „http://example.com/thread/1-dies-ist-ein-test/“ und vergleichbar. Achtung: Die Aktivierung der Link-Umschreibungen erfordert Rewrite-Unterstützung in Ihrem Webserver sowie eine entsprechende Konfiguration. Fehlerhafte Einstellungen können hier dazu führen, dass Links nicht mehr aufrufbar sind.]]></item>
+               <item name="wcf.acp.option.url_omit_index_php.description"><![CDATA[Wandelt Links in eine vereinfachte Form um, aus „http://example.com/index.php?thread/1-dies-ist-ein-test/“ wird „http://example.com/thread/1-dies-ist-ein-test/“ und vergleichbar. Achtung: Die Aktivierung der Link-Umschreibungen erfordert Rewrite-Unterstützung in Ihrem Webserver sowie eine entsprechende Konfiguration. Fehlerhafte Einstellungen können hier dazu führen, dass Links nicht mehr aufrufbar sind.]]></item>
                <item name="wcf.acp.option.url_legacy_mode"><![CDATA[Kompatibilitätsmodus für Links aktivieren]]></item>
                <item name="wcf.acp.option.url_legacy_mode.description"><![CDATA[Die Aktivierung dieses Modus erzwingt die URL-Struktur von WoltLab Community Framework 2.0 und sollte nur aus Kompatibilitätsgründen zur Beibehaltung der Link-Gültigkeit aktiviert werden:
 <ul class="nativeList">
        <li>WCF 2.0: „index.php/Thread/123-Title/“</li>
-       <li>WCF 2.1+: „?Thread/123-Title/“</li>
+       <li>WCF 2.1+: „index.php?thread/123-title/“</li>
 </ul>]]></item>
-               <item name="wcf.acp.option.url_to_lowercase"><![CDATA[Durchgehende Kleinschreibung in Links aktivieren]]></item>
-               <item name="wcf.acp.option.url_to_lowercase.description"><![CDATA[Links verwenden keine Großbuchstaben mehr, aus „Thread/1-Dies-ist-ein-Test/“ wird „thread/1-dies-ist-ein-test/“.]]></item>
                <item name="wcf.acp.option.module_wcf_ad"><![CDATA[Werbung]]></item>
                <item name="wcf.acp.option.module_wcf_ad.description"><![CDATA[Aktiviert die <a href="{link controller='AdList'}{/link}">Verwaltung von Werbe-Anzeigen</a>.]]></item>
                <item name="wcf.acp.option.captcha_type"><![CDATA[Captcha-Art]]></item>
index a15a776d1d0e283c6048173ad408e73f79b51bb6..f77a81687d2e59d15474f06a271f36525e7390e6 100644 (file)
@@ -736,6 +736,10 @@ Examples for medium ID detection:
                <item name="wcf.acp.option.cookie_path"><![CDATA[Cookie Path]]></item>
                <item name="wcf.acp.option.cookie_path.description"><![CDATA[Value should be absolute to Document Root, e.g. “/forum” for “http://www.woltlab.com/forum”.]]></item>
                <item name="wcf.acp.option.cookie_prefix"><![CDATA[Cookie Prefix]]></item>
+               <item name="wcf.acp.option.error.controllerReplacementCollision"><![CDATA[The alias “{$urlControllerReplacementError}” equals an existing controller and cannot be used.]]></item>
+               <item name="wcf.acp.option.error.controllerReplacementDuplicateAlias"><![CDATA[The alias “{$urlControllerReplacementError}” is already in use.]]></item>
+               <item name="wcf.acp.option.error.controllerReplacementDuplicateController"><![CDATA[The controller “{$urlControllerReplacementError}” has already been aliased.]]></item>
+               <item name="wcf.acp.option.error.controllerReplacementDuplicateUnknown"><![CDATA[The controller “{$urlControllerReplacementError}” is unknown.]]></item>
                <item name="wcf.acp.option.error.tooHigh"><![CDATA[Exceeds the maximum value{if $option->maxvalue !== null} of {#$option->maxvalue}{/if}.]]></item>
                <item name="wcf.acp.option.error.tooLow"><![CDATA[Below the minimum value{if $option->minvalue !== null} of {#$option->minvalue}{/if}.]]></item>
                <item name="wcf.acp.option.export"><![CDATA[Download Options]]></item>
@@ -981,6 +985,12 @@ Examples for medium ID detection:
 Examples:<br />
 WBB=WoltLab Burning Board<br />
 GmbH=Gesellschaft mit beschränkter Haftung]]></item>
+               <item name="wcf.acp.option.url_controller_replacement"><![CDATA[Controller Aliasing]]></item>
+               <item name="wcf.acp.option.url_controller_replacement.description"><![CDATA[You can rename controllers by assigning an alias to them, please provide only one alias per line. Aliases must be defined as “real-name=custom-name” and contain only lower-case a-z, the numbers 0-9 and the dash for compatibility reasons. Examples:
+<ul class="nativeList">
+       <li>“board-list=forums”, the link “http://example.com/index.php?board-list/” will turn into “http://example.com/index.php?forums/”</li>
+       <li>“members-list=profiles”, the link “http://example.com/index.php?members-list/” will turn into “http://example.com/index.php?profiles/”</li>
+</ul>]]></item>
                <item name="wcf.acp.option.users_online_enable_legend"><![CDATA[Display legend for “Users Online” list]]></item>
                <item name="wcf.acp.option.category.general.system.googleMaps"><![CDATA[Google Maps]]></item>
                <item name="wcf.acp.option.google_maps_zoom"><![CDATA[Map Zoom]]></item>
@@ -1004,15 +1014,13 @@ GmbH=Gesellschaft mit beschränkter Haftung]]></item>
                <item name="wcf.acp.option.module_cookie_policy_page"><![CDATA[Enable explanation on “Cookie Usage”]]></item>
                <item name="wcf.acp.option.module_cookie_policy_page.description"><![CDATA[Displays a notice on cookie usage according to EU Directive 2009/136/EG upon first visit.]]></item>
                <item name="wcf.acp.option.url_omit_index_php"><![CDATA[Enable URL-Rewrite]]></item>
-               <item name="wcf.acp.option.url_omit_index_php.description"><![CDATA[Rewrites URLs into a better readable representation, turning links like “http://example.com/?Thread/1-Hello-I-am-John-Doe/” into “http://example.com/thread/1-hello-i-am-john-doe/” and similar. Heads up! This option requires a rewrite module installed on your webserver and an appropriate configuration; It will not work without any prior configuration applied by you!]]></item>
+               <item name="wcf.acp.option.url_omit_index_php.description"><![CDATA[Rewrites URLs into a better readable representation, turning links like “http://example.com/index.php?thread/1-hello-i-am-john-doe/” into “http://example.com/thread/1-hello-i-am-john-doe/” and similar. Heads up! This option requires a rewrite module installed on your webserver and an appropriate configuration; It will not work without any prior configuration applied by you!]]></item>
                <item name="wcf.acp.option.url_legacy_mode"><![CDATA[Enable link compatibility mode]]></item>
                <item name="wcf.acp.option.url_legacy_mode.description"><![CDATA[Enabling this option forces the system to use WoltLab Community Framework 2.0-compilant URLs and should only be used to ensure compatibility with legacy URLs:
 <ul class="nativeList">
        <li>WCF 2.0: “index.php/Thread/123-Title/”</li>
-       <li>WCF 2.1+: “?Thread/123-Title/”</li>
+       <li>WCF 2.1+: “index.php?thread/123-title/”</li>
 </ul>]]></item>
-               <item name="wcf.acp.option.url_to_lowercase"><![CDATA[Force lower-case links]]></item>
-               <item name="wcf.acp.option.url_to_lowercase.description"><![CDATA[Links will no longer contain uppercase letters turning links like “Thread/1-Hello-I-am-John-Doe/” into “thread/1-hello-i-am-john-doe/”.]]></item>
                <item name="wcf.acp.option.module_wcf_ad"><![CDATA[Ads]]></item>
                <item name="wcf.acp.option.module_wcf_ad.description"><![CDATA[Enables the <a href="{link controller='AdList'}{/link}">advertisement management</a>.]]></item>
                <item name="wcf.acp.option.captcha_type"><![CDATA[Captcha Type]]></item>