diff options
Diffstat (limited to 'lib/private/Route')
-rw-r--r-- | lib/private/Route/CachingRouter.php | 154 | ||||
-rw-r--r-- | lib/private/Route/Route.php | 39 | ||||
-rw-r--r-- | lib/private/Route/Router.php | 311 |
3 files changed, 345 insertions, 159 deletions
diff --git a/lib/private/Route/CachingRouter.php b/lib/private/Route/CachingRouter.php index f65060e710b..becdb807f73 100644 --- a/lib/private/Route/CachingRouter.php +++ b/lib/private/Route/CachingRouter.php @@ -1,43 +1,41 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * - * @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 <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Route; +use OCP\App\IAppManager; +use OCP\Diagnostics\IEventLogger; +use OCP\ICache; +use OCP\ICacheFactory; +use OCP\IConfig; +use OCP\IRequest; +use Psr\Container\ContainerInterface; use Psr\Log\LoggerInterface; +use Symfony\Component\Routing\Exception\ResourceNotFoundException; +use Symfony\Component\Routing\Matcher\CompiledUrlMatcher; +use Symfony\Component\Routing\Matcher\Dumper\CompiledUrlMatcherDumper; +use Symfony\Component\Routing\RouteCollection; class CachingRouter extends Router { - /** - * @var \OCP\ICache - */ - protected $cache; + protected ICache $cache; - /** - * @param \OCP\ICache $cache - */ - public function __construct($cache, LoggerInterface $logger) { - $this->cache = $cache; - parent::__construct($logger); + protected array $legacyCreatedRoutes = []; + + public function __construct( + ICacheFactory $cacheFactory, + LoggerInterface $logger, + IRequest $request, + IConfig $config, + IEventLogger $eventLogger, + ContainerInterface $container, + IAppManager $appManager, + ) { + $this->cache = $cacheFactory->createLocal('route'); + parent::__construct($logger, $request, $config, $eventLogger, $container, $appManager); } /** @@ -62,4 +60,98 @@ class CachingRouter extends Router { return $url; } } + + private function serializeRouteCollection(RouteCollection $collection): array { + $dumper = new CompiledUrlMatcherDumper($collection); + return $dumper->getCompiledRoutes(); + } + + /** + * Find the route matching $url + * + * @param string $url The url to find + * @throws \Exception + * @return array + */ + public function findMatchingRoute(string $url): array { + $this->eventLogger->start('cacheroute:match', 'Match route'); + $key = $this->context->getHost() . '#' . $this->context->getBaseUrl() . '#rootCollection'; + $cachedRoutes = $this->cache->get($key); + if (!$cachedRoutes) { + parent::loadRoutes(); + $cachedRoutes = $this->serializeRouteCollection($this->root); + $this->cache->set($key, $cachedRoutes, ($this->config->getSystemValueBool('debug') ? 3 : 3600)); + } + $matcher = new CompiledUrlMatcher($cachedRoutes, $this->context); + $this->eventLogger->start('cacheroute:url:match', 'Symfony URL match call'); + try { + $parameters = $matcher->match($url); + } catch (ResourceNotFoundException $e) { + if (!str_ends_with($url, '/')) { + // We allow links to apps/files? for backwards compatibility reasons + // However, since Symfony does not allow empty route names, the route + // we need to match is '/', so we need to append the '/' here. + try { + $parameters = $matcher->match($url . '/'); + } catch (ResourceNotFoundException $newException) { + // If we still didn't match a route, we throw the original exception + throw $e; + } + } else { + throw $e; + } + } + $this->eventLogger->end('cacheroute:url:match'); + + $this->eventLogger->end('cacheroute:match'); + return $parameters; + } + + /** + * @param array{action:mixed, ...} $parameters + */ + protected function callLegacyActionRoute(array $parameters): void { + /* + * Closures cannot be serialized to cache, so for legacy routes calling an action we have to include the routes.php file again + */ + $app = $parameters['app']; + $this->useCollection($app); + parent::requireRouteFile($parameters['route-file'], $app); + $collection = $this->getCollection($app); + $parameters['action'] = $collection->get($parameters['_route'])?->getDefault('action'); + parent::callLegacyActionRoute($parameters); + } + + /** + * Create a \OC\Route\Route. + * Deprecated + * + * @param string $name Name of the route to create. + * @param string $pattern The pattern to match + * @param array $defaults An array of default parameter values + * @param array $requirements An array of requirements for parameters (regexes) + */ + public function create($name, $pattern, array $defaults = [], array $requirements = []): Route { + $this->legacyCreatedRoutes[] = $name; + return parent::create($name, $pattern, $defaults, $requirements); + } + + /** + * Require a routes.php file + */ + protected function requireRouteFile(string $file, string $appName): void { + $this->legacyCreatedRoutes = []; + parent::requireRouteFile($file, $appName); + foreach ($this->legacyCreatedRoutes as $routeName) { + $route = $this->collection?->get($routeName); + if ($route === null) { + /* Should never happen */ + throw new \Exception("Could not find route $routeName"); + } + if ($route->hasDefault('action')) { + $route->setDefault('route-file', $file); + $route->setDefault('app', $appName); + } + } + } } diff --git a/lib/private/Route/Route.php b/lib/private/Route/Route.php index ad440a001af..08231649e76 100644 --- a/lib/private/Route/Route.php +++ b/lib/private/Route/Route.php @@ -1,30 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Bart Visscher <bartv@thisnet.nl> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author David Prévot <taffit@debian.org> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Tanghus <thomas@tanghus.net> - * - * @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 <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Route; @@ -145,15 +124,9 @@ class Route extends SymfonyRoute implements IRoute { * The action to execute when this route matches, includes a file like * it is called directly * @param string $file - * @return void */ public function actionInclude($file) { - $function = function ($param) use ($file) { - unset($param["_route"]); - $_GET = array_merge($_GET, $param); - unset($param); - require_once "$file"; - } ; - $this->action($function); + $this->setDefault('file', $file); + return $this; } } diff --git a/lib/private/Route/Router.php b/lib/private/Route/Router.php index b957173cacc..90225212e9a 100644 --- a/lib/private/Route/Router.php +++ b/lib/private/Route/Router.php @@ -1,42 +1,28 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Bart Visscher <bartv@thisnet.nl> - * @author Bernhard Posselt <dev@bernhard-posselt.com> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Julius Härtl <jus@bitgrid.net> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @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 <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Route; +use DirectoryIterator; use OC\AppFramework\Routing\RouteParser; +use OCP\App\AppPathNotFoundException; +use OCP\App\IAppManager; use OCP\AppFramework\App; +use OCP\AppFramework\Http\Attribute\Route as RouteAttribute; +use OCP\Diagnostics\IEventLogger; +use OCP\IConfig; +use OCP\IRequest; use OCP\Route\IRouter; use OCP\Util; +use Psr\Container\ContainerInterface; use Psr\Log\LoggerInterface; +use ReflectionAttribute; +use ReflectionClass; +use ReflectionException; use Symfony\Component\Routing\Exception\ResourceNotFoundException; use Symfony\Component\Routing\Exception\RouteNotFoundException; use Symfony\Component\Routing\Generator\UrlGenerator; @@ -61,14 +47,19 @@ class Router implements IRouter { protected $loaded = false; /** @var array */ protected $loadedApps = []; - protected LoggerInterface $logger; /** @var RequestContext */ protected $context; - public function __construct(LoggerInterface $logger) { - $this->logger = $logger; + public function __construct( + protected LoggerInterface $logger, + IRequest $request, + protected IConfig $config, + protected IEventLogger $eventLogger, + private ContainerInterface $container, + protected IAppManager $appManager, + ) { $baseUrl = \OC::$WEBROOT; - if (!(\OC::$server->getConfig()->getSystemValue('htaccess.IgnoreFrontController', false) === true || getenv('front_controller_active') === 'true')) { + if (!($config->getSystemValue('htaccess.IgnoreFrontController', false) === true || getenv('front_controller_active') === 'true')) { $baseUrl .= '/index.php'; } if (!\OC::$CLI && isset($_SERVER['REQUEST_METHOD'])) { @@ -76,7 +67,6 @@ class Router implements IRouter { } else { $method = 'GET'; } - $request = \OC::$server->getRequest(); $host = $request->getServerHost(); $schema = $request->getServerProtocol(); $this->context = new RequestContext($baseUrl, $method, $host, $schema); @@ -84,6 +74,14 @@ class Router implements IRouter { $this->root = $this->getCollection('root'); } + public function setContext(RequestContext $context): void { + $this->context = $context; + } + + public function getRouteCollection() { + return $this->root; + } + /** * Get the files to load the routes from * @@ -92,13 +90,15 @@ class Router implements IRouter { public function getRoutingFiles() { if ($this->routingFiles === null) { $this->routingFiles = []; - foreach (\OC_APP::getEnabledApps() as $app) { - $appPath = \OC_App::getAppPath($app); - if ($appPath !== false) { + foreach ($this->appManager->getEnabledApps() as $app) { + try { + $appPath = $this->appManager->getAppPath($app); $file = $appPath . '/appinfo/routes.php'; if (file_exists($file)) { $this->routingFiles[$app] = $file; } + } catch (AppPathNotFoundException) { + /* ignore */ } } } @@ -110,34 +110,50 @@ class Router implements IRouter { * * @param null|string $app */ - public function loadRoutes($app = null) { + public function loadRoutes(?string $app = null, bool $skipLoadingCore = false): void { if (is_string($app)) { - $app = \OC_App::cleanAppId($app); + $app = $this->appManager->cleanAppId($app); } $requestedApp = $app; if ($this->loaded) { return; } + $this->eventLogger->start('route:load:' . $requestedApp, 'Loading Routes for ' . $requestedApp); if (is_null($app)) { $this->loaded = true; $routingFiles = $this->getRoutingFiles(); + + $this->eventLogger->start('route:load:attributes', 'Loading Routes from attributes'); + foreach ($this->appManager->getEnabledApps() as $enabledApp) { + $this->loadAttributeRoutes($enabledApp); + } + $this->eventLogger->end('route:load:attributes'); } else { if (isset($this->loadedApps[$app])) { return; } - $appPath = \OC_App::getAppPath($app); - $file = $appPath . '/appinfo/routes.php'; - if ($appPath !== false && file_exists($file)) { - $routingFiles = [$app => $file]; - } else { + try { + $appPath = $this->appManager->getAppPath($app); + $file = $appPath . '/appinfo/routes.php'; + if (file_exists($file)) { + $routingFiles = [$app => $file]; + } else { + $routingFiles = []; + } + } catch (AppPathNotFoundException) { $routingFiles = []; } + + if ($this->appManager->isEnabledForUser($app)) { + $this->loadAttributeRoutes($app); + } } - \OC::$server->getEventLogger()->start('loadroutes' . $requestedApp, 'Loading Routes'); + + $this->eventLogger->start('route:load:files', 'Loading Routes from files'); foreach ($routingFiles as $app => $file) { if (!isset($this->loadedApps[$app])) { - if (!\OC_App::isAppLoaded($app)) { + if (!$this->appManager->isAppLoaded($app)) { // app MUST be loaded before app routes // try again next time loadRoutes() is called $this->loaded = false; @@ -150,15 +166,18 @@ class Router implements IRouter { $this->root->addCollection($collection); // Also add the OCS collection - $collection = $this->getCollection($app.'.ocs'); + $collection = $this->getCollection($app . '.ocs'); $collection->addPrefix('/ocsapp'); $this->root->addCollection($collection); } } - if (!isset($this->loadedApps['core'])) { + $this->eventLogger->end('route:load:files'); + + if (!$skipLoadingCore && !isset($this->loadedApps['core'])) { $this->loadedApps['core'] = true; $this->useCollection('root'); - require_once __DIR__ . '/../../../core/routes.php'; + $this->setupRoutes($this->getAttributeRoutes('core'), 'core'); + $this->requireRouteFile(__DIR__ . '/../../../core/routes.php', 'core'); // Also add the OCS collection $collection = $this->getCollection('root.ocs'); @@ -170,7 +189,7 @@ class Router implements IRouter { $collection->addPrefix('/ocs'); $this->root->addCollection($collection); } - \OC::$server->getEventLogger()->end('loadroutes' . $requestedApp); + $this->eventLogger->end('route:load:' . $requestedApp); } /** @@ -215,9 +234,9 @@ class Router implements IRouter { * @return \OC\Route\Route */ public function create($name, - $pattern, - array $defaults = [], - array $requirements = []) { + $pattern, + array $defaults = [], + array $requirements = []) { $route = new Route($pattern, $defaults, $requirements); $this->collection->add($name, $route); return $route; @@ -231,37 +250,39 @@ class Router implements IRouter { * @return array */ public function findMatchingRoute(string $url): array { - if (substr($url, 0, 6) === '/apps/') { + $this->eventLogger->start('route:match', 'Match route'); + if (str_starts_with($url, '/apps/')) { // empty string / 'apps' / $app / rest of the route [, , $app,] = explode('/', $url, 4); - $app = \OC_App::cleanAppId($app); + $app = $this->appManager->cleanAppId($app); \OC::$REQUESTEDAPP = $app; $this->loadRoutes($app); - } elseif (substr($url, 0, 13) === '/ocsapp/apps/') { + } elseif (str_starts_with($url, '/ocsapp/apps/')) { // empty string / 'ocsapp' / 'apps' / $app / rest of the route [, , , $app,] = explode('/', $url, 5); - $app = \OC_App::cleanAppId($app); + $app = $this->appManager->cleanAppId($app); \OC::$REQUESTEDAPP = $app; $this->loadRoutes($app); - } elseif (substr($url, 0, 10) === '/settings/') { + } elseif (str_starts_with($url, '/settings/')) { $this->loadRoutes('settings'); - } elseif (substr($url, 0, 6) === '/core/') { + } elseif (str_starts_with($url, '/core/')) { \OC::$REQUESTEDAPP = $url; - if (!\OC::$server->getConfig()->getSystemValueBool('maintenance') && !Util::needUpgrade()) { - \OC_App::loadApps(); + if ($this->config->getSystemValueBool('installed', false) && !Util::needUpgrade()) { + $this->appManager->loadApps(); } $this->loadRoutes('core'); } else { $this->loadRoutes(); } + $this->eventLogger->start('route:url:match', 'Symfony url matcher call'); $matcher = new UrlMatcher($this->root, $this->context); try { $parameters = $matcher->match($url); } catch (ResourceNotFoundException $e) { - if (substr($url, -1) !== '/') { + if (!str_ends_with($url, '/')) { // We allow links to apps/files? for backwards compatibility reasons // However, since Symfony does not allow empty route names, the route // we need to match is '/', so we need to append the '/' here. @@ -275,7 +296,9 @@ class Router implements IRouter { throw $e; } } + $this->eventLogger->end('route:url:match'); + $this->eventLogger->end('route:match'); return $parameters; } @@ -289,7 +312,7 @@ class Router implements IRouter { public function match($url) { $parameters = $this->findMatchingRoute($url); - \OC::$server->getEventLogger()->start('run_route', 'Run route'); + $this->eventLogger->start('route:run', 'Run route'); if (isset($parameters['caller'])) { $caller = $parameters['caller']; unset($parameters['caller']); @@ -297,19 +320,41 @@ class Router implements IRouter { $application = $this->getApplicationClass($caller[0]); \OC\AppFramework\App::main($caller[1], $caller[2], $application->getContainer(), $parameters); } elseif (isset($parameters['action'])) { - $action = $parameters['action']; - if (!is_callable($action)) { - throw new \Exception('not a callable action'); - } - unset($parameters['action']); - unset($parameters['caller']); - call_user_func($action, $parameters); + $this->logger->warning('Deprecated action route used', ['parameters' => $parameters]); + $this->callLegacyActionRoute($parameters); } elseif (isset($parameters['file'])) { - include $parameters['file']; + $this->logger->debug('Deprecated file route used', ['parameters' => $parameters]); + $this->includeLegacyFileRoute($parameters); } else { throw new \Exception('no action available'); } - \OC::$server->getEventLogger()->end('run_route'); + $this->eventLogger->end('route:run'); + } + + /** + * @param array{file:mixed, ...} $parameters + */ + protected function includeLegacyFileRoute(array $parameters): void { + $param = $parameters; + unset($param['_route']); + $_GET = array_merge($_GET, $param); + unset($param); + require_once $parameters['file']; + } + + /** + * @param array{action:mixed, ...} $parameters + */ + protected function callLegacyActionRoute(array $parameters): void { + $action = $parameters['action']; + if (!is_callable($action)) { + throw new \Exception('not a callable action'); + } + unset($parameters['action']); + unset($parameters['caller']); + $this->eventLogger->start('route:run:call', 'Run callable route'); + call_user_func($action, $parameters); + $this->eventLogger->end('route:run:call'); } /** @@ -319,7 +364,7 @@ class Router implements IRouter { * */ public function getGenerator() { - if (null !== $this->generator) { + if ($this->generator !== null) { return $this->generator; } @@ -335,14 +380,21 @@ class Router implements IRouter { * @return string */ public function generate($name, - $parameters = [], - $absolute = false) { + $parameters = [], + $absolute = false) { $referenceType = UrlGenerator::ABSOLUTE_URL; if ($absolute === false) { $referenceType = UrlGenerator::ABSOLUTE_PATH; } + /* + * The route name has to be lowercase, for symfony to match it correctly. + * This is required because smyfony allows mixed casing for controller names in the routes. + * To avoid breaking all the existing route names, registering and matching will only use the lowercase names. + * This is also safe on the PHP side because class and method names collide regardless of the casing. + */ + $name = strtolower($name); $name = $this->fixLegacyRootName($name); - if (strpos($name, '.') !== false) { + if (str_contains($name, '.')) { [$appName, $other] = explode('.', $name, 3); // OCS routes are prefixed with "ocs." if ($appName === 'ocs') { @@ -366,41 +418,110 @@ class Router implements IRouter { } protected function fixLegacyRootName(string $routeName): string { - if ($routeName === 'files.viewcontroller.showFile') { - return 'files.View.showFile'; + if ($routeName === 'files.viewcontroller.showfile') { + return 'files.view.showfile'; } - if ($routeName === 'files_sharing.sharecontroller.showShare') { - return 'files_sharing.Share.showShare'; + if ($routeName === 'files_sharing.sharecontroller.showshare') { + return 'files_sharing.share.showshare'; } - if ($routeName === 'files_sharing.sharecontroller.showAuthenticate') { - return 'files_sharing.Share.showAuthenticate'; + if ($routeName === 'files_sharing.sharecontroller.showauthenticate') { + return 'files_sharing.share.showauthenticate'; } if ($routeName === 'files_sharing.sharecontroller.authenticate') { - return 'files_sharing.Share.authenticate'; + return 'files_sharing.share.authenticate'; + } + if ($routeName === 'files_sharing.sharecontroller.downloadshare') { + return 'files_sharing.share.downloadshare'; } - if ($routeName === 'files_sharing.sharecontroller.downloadShare') { - return 'files_sharing.Share.downloadShare'; + if ($routeName === 'files_sharing.publicpreview.directlink') { + return 'files_sharing.publicpreview.directlink'; } - if ($routeName === 'files_sharing.publicpreview.directLink') { - return 'files_sharing.PublicPreview.directLink'; + if ($routeName === 'cloud_federation_api.requesthandlercontroller.addshare') { + return 'cloud_federation_api.requesthandler.addshare'; } - if ($routeName === 'cloud_federation_api.requesthandlercontroller.addShare') { - return 'cloud_federation_api.RequestHandler.addShare'; + if ($routeName === 'cloud_federation_api.requesthandlercontroller.receivenotification') { + return 'cloud_federation_api.requesthandler.receivenotification'; } - if ($routeName === 'cloud_federation_api.requesthandlercontroller.receiveNotification') { - return 'cloud_federation_api.RequestHandler.receiveNotification'; + if ($routeName === 'core.ProfilePage.index') { + return 'profile.ProfilePage.index'; } return $routeName; } + private function loadAttributeRoutes(string $app): void { + $routes = $this->getAttributeRoutes($app); + if (count($routes) === 0) { + return; + } + + $this->useCollection($app); + $this->setupRoutes($routes, $app); + $collection = $this->getCollection($app); + $this->root->addCollection($collection); + + // Also add the OCS collection + $collection = $this->getCollection($app . '.ocs'); + $collection->addPrefix('/ocsapp'); + $this->root->addCollection($collection); + } + + /** + * @throws ReflectionException + */ + private function getAttributeRoutes(string $app): array { + $routes = []; + + if ($app === 'core') { + $appControllerPath = __DIR__ . '/../../../core/Controller'; + $appNameSpace = 'OC\\Core'; + } else { + try { + $appControllerPath = $this->appManager->getAppPath($app) . '/lib/Controller'; + } catch (AppPathNotFoundException) { + return []; + } + $appNameSpace = App::buildAppNamespace($app); + } + + if (!file_exists($appControllerPath)) { + return []; + } + + $dir = new DirectoryIterator($appControllerPath); + foreach ($dir as $file) { + if (!str_ends_with($file->getPathname(), 'Controller.php')) { + continue; + } + + $class = new ReflectionClass($appNameSpace . '\\Controller\\' . basename($file->getPathname(), '.php')); + + foreach ($class->getMethods() as $method) { + foreach ($method->getAttributes(RouteAttribute::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) { + $route = $attribute->newInstance(); + + $serializedRoute = $route->toArray(); + // Remove 'Controller' suffix + $serializedRoute['name'] = substr($class->getShortName(), 0, -10) . '#' . $method->getName(); + + $key = $route->getType(); + + $routes[$key] ??= []; + $routes[$key][] = $serializedRoute; + } + } + } + + return $routes; + } + /** * To isolate the variable scope used inside the $file it is required in it's own method * * @param string $file the route file location to include * @param string $appName */ - private function requireRouteFile($file, $appName) { - $this->setupRoutes(include_once $file, $appName); + protected function requireRouteFile(string $file, string $appName): void { + $this->setupRoutes(include $file, $appName); } @@ -409,7 +530,7 @@ class Router implements IRouter { * register the routes for the app. The application class will be chosen by * camelcasing the appname, e.g.: my_app will be turned into * \OCA\MyApp\AppInfo\Application. If that class does not exist, a default - * App will be intialized. This makes it optional to ship an + * App will be initialized. This makes it optional to ship an * appinfo/application.php by using the built in query resolver * * @param array $routes the application routes @@ -434,7 +555,7 @@ class Router implements IRouter { $applicationClassName = $appNameSpace . '\\AppInfo\\Application'; if (class_exists($applicationClassName)) { - $application = \OC::$server->query($applicationClassName); + $application = $this->container->get($applicationClassName); } else { $application = new App($appName); } |