* @author Morris Jobke * @author Patrick Paysant * @author Robin Appelman * @author Robin McCorkell * @author Roeland Jago Douma * @author Thomas Müller * * @license AGPL-3.0 * * This code is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, version 3, * as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License, version 3, * along with this program. If not, see * */ namespace OC\AppFramework\Routing; use OC\AppFramework\DependencyInjection\DIContainer; use OCP\AppFramework\App; use OCP\Route\IRouter; /** * Class RouteConfig * @package OC\AppFramework\routing */ class RouteConfig { /** @var DIContainer */ private $container; /** @var IRouter */ private $router; /** @var array */ private $routes; /** @var string */ private $appName; /** @var string[] */ private $controllerNameCache = []; /** * @param \OC\AppFramework\DependencyInjection\DIContainer $container * @param \OCP\Route\IRouter $router * @param array $routes * @internal param $appName */ public function __construct(DIContainer $container, IRouter $router, $routes) { $this->routes = $routes; $this->container = $container; $this->router = $router; $this->appName = $container['AppName']; } /** * The routes and resource will be registered to the \OCP\Route\IRouter */ public function register() { // parse simple $this->processSimpleRoutes($this->routes); // parse resources $this->processResources($this->routes); /* * OCS routes go into a different collection */ $oldCollection = $this->router->getCurrentCollection(); $this->router->useCollection($oldCollection . '.ocs'); // parse ocs simple routes $this->processOCS($this->routes); // parse ocs simple routes $this->processOCSResources($this->routes); $this->router->useCollection($oldCollection); } private function processOCS(array $routes): void { $ocsRoutes = $routes['ocs'] ?? []; foreach ($ocsRoutes as $ocsRoute) { $name = $ocsRoute['name']; $postfix = $ocsRoute['postfix'] ?? ''; $root = $ocsRoute['root'] ?? '/apps/' . $this->appName; $url = $root . $ocsRoute['url']; $verb = strtoupper($ocsRoute['verb'] ?? 'GET'); $split = explode('#', $name, 2); if (count($split) !== 2) { throw new \UnexpectedValueException('Invalid route name'); } list($controller, $action) = $split; $controllerName = $this->buildControllerName($controller); $actionName = $this->buildActionName($action); $routeName = 'ocs.' . $this->appName . '.' . $controller . '.' . $action . $postfix; // register the route $handler = new RouteActionHandler($this->container, $controllerName, $actionName); $router = $this->router->create($routeName, $url) ->method($verb) ->action($handler); // optionally register requirements for route. This is used to // tell the route parser how url parameters should be matched if(array_key_exists('requirements', $ocsRoute)) { $router->requirements($ocsRoute['requirements']); } // optionally register defaults for route. This is used to // tell the route parser how url parameters should be default valued if(array_key_exists('defaults', $ocsRoute)) { $router->defaults($ocsRoute['defaults']); } } } /** * Creates one route base on the give configuration * @param array $routes * @throws \UnexpectedValueException */ private function processSimpleRoutes(array $routes): void { $simpleRoutes = $routes['routes'] ?? []; foreach ($simpleRoutes as $simpleRoute) { $name = $simpleRoute['name']; $postfix = $simpleRoute['postfix'] ?? ''; $url = $simpleRoute['url']; $verb = strtoupper($simpleRoute['verb'] ?? 'GET'); $split = explode('#', $name, 2); if (count($split) !== 2) { throw new \UnexpectedValueException('Invalid route name'); } list($controller, $action) = $split; $controllerName = $this->buildControllerName($controller); $actionName = $this->buildActionName($action); $appName = $simpleRoute['app'] ?? $this->appName; if (isset($simpleRoute['app'])) { // Legacy routes that need to be globally available while they are handled by an app // E.g. '/f/{id}', '/s/{token}', '/call/{token}', … $controllerName = str_replace('controllerController', 'Controller', $controllerName); if ($controllerName === 'PublicpreviewController') { $controllerName = 'PublicPreviewController'; } else if ($controllerName === 'RequesthandlerController') { $controllerName = 'RequestHandlerController'; } $controllerName = App::buildAppNamespace($appName) . '\\Controller\\' . $controllerName; } $routeName = $appName . '.' . $controller . '.' . $action . $postfix; // register the route $handler = new RouteActionHandler($this->container, $controllerName, $actionName); $router = $this->router->create($routeName, $url) ->method($verb) ->action($handler); // optionally register requirements for route. This is used to // tell the route parser how url parameters should be matched if(array_key_exists('requirements', $simpleRoute)) { $router->requirements($simpleRoute['requirements']); } // optionally register defaults for route. This is used to // tell the route parser how url parameters should be default valued if(array_key_exists('defaults', $simpleRoute)) { $router->defaults($simpleRoute['defaults']); } } } /** * For a given name and url restful OCS routes are created: * - index * - show * - create * - update * - destroy * * @param array $routes */ private function processOCSResources(array $routes): void { // declaration of all restful actions $actions = [ ['name' => 'index', 'verb' => 'GET', 'on-collection' => true], ['name' => 'show', 'verb' => 'GET'], ['name' => 'create', 'verb' => 'POST', 'on-collection' => true], ['name' => 'update', 'verb' => 'PUT'], ['name' => 'destroy', 'verb' => 'DELETE'], ]; $resources = $routes['ocs-resources'] ?? []; foreach ($resources as $resource => $config) { $root = $config['root'] ?? '/apps/' . $this->appName; // the url parameter used as id to the resource foreach($actions as $action) { $url = $root . $config['url']; $method = $action['name']; $verb = strtoupper($action['verb'] ?? 'GET'); $collectionAction = $action['on-collection'] ?? false; if (!$collectionAction) { $url .= '/{id}'; } if (isset($action['url-postfix'])) { $url .= '/' . $action['url-postfix']; } $controller = $resource; $controllerName = $this->buildControllerName($controller); $actionName = $this->buildActionName($method); $routeName = 'ocs.' . $this->appName . '.' . strtolower($resource) . '.' . strtolower($method); $this->router->create($routeName, $url)->method($verb)->action( new RouteActionHandler($this->container, $controllerName, $actionName) ); } } } /** * For a given name and url restful routes are created: * - index * - show * - create * - update * - destroy * * @param array $routes */ private function processResources(array $routes): void { // declaration of all restful actions $actions = [ ['name' => 'index', 'verb' => 'GET', 'on-collection' => true], ['name' => 'show', 'verb' => 'GET'], ['name' => 'create', 'verb' => 'POST', 'on-collection' => true], ['name' => 'update', 'verb' => 'PUT'], ['name' => 'destroy', 'verb' => 'DELETE'], ]; $resources = $routes['resources'] ?? []; foreach ($resources as $resource => $config) { // the url parameter used as id to the resource foreach($actions as $action) { $url = $config['url']; $method = $action['name']; $verb = strtoupper($action['verb'] ?? 'GET'); $collectionAction = $action['on-collection'] ?? false; if (!$collectionAction) { $url .= '/{id}'; } if (isset($action['url-postfix'])) { $url .= '/' . $action['url-postfix']; } $controller = $resource; $controllerName = $this->buildControllerName($controller); $actionName = $this->buildActionName($method); $routeName = $this->appName . '.' . strtolower($resource) . '.' . strtolower($method); $this->router->create($routeName, $url)->method($verb)->action( new RouteActionHandler($this->container, $controllerName, $actionName) ); } } } /** * Based on a given route name the controller name is generated * @param string $controller * @return string */ private function buildControllerName(string $controller): string { if (!isset($this->controllerNameCache[$controller])) { $this->controllerNameCache[$controller] = $this->underScoreToCamelCase(ucfirst($controller)) . 'Controller'; } return $this->controllerNameCache[$controller]; } /** * Based on the action part of the route name the controller method name is generated * @param string $action * @return string */ private function buildActionName(string $action): string { return $this->underScoreToCamelCase($action); } /** * Underscored strings are converted to camel case strings * @param string $str * @return string */ private function underScoreToCamelCase(string $str): string { $pattern = '/_[a-z]?/'; return preg_replace_callback( $pattern, function ($matches) { return strtoupper(ltrim($matches[0], '_')); }, $str); } }