diff --git a/build/psalm-baseline.xml b/build/psalm-baseline.xml
index 39eeb1e91d110..6a311383810b1 100644
--- a/build/psalm-baseline.xml
+++ b/build/psalm-baseline.xml
@@ -4335,14 +4335,6 @@
-
-
-
-
-
- collectionName]]>
-
-
request->server]]>
diff --git a/core/Command/Router/ListRoutes.php b/core/Command/Router/ListRoutes.php
new file mode 100644
index 0000000000000..458579ec81873
--- /dev/null
+++ b/core/Command/Router/ListRoutes.php
@@ -0,0 +1,99 @@
+setName('router:list')
+ ->setDescription('List registered routes')
+ ->addArgument('appId', InputArgument::OPTIONAL, 'Limit routes to specific app', '')
+ ->addOption('grouped', 'g', InputOption::VALUE_NONE, 'Group routes by app');
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ $this->input = $input;
+
+ $app = (string)$input->getArgument('appId');
+ $apps = $app === '' ? $this->appManager->getEnabledApps() : [$app];
+
+ $this->router->loadRoutes();
+ $allRoutes = [];
+ foreach ($apps as $appId) {
+ $routes = $this->router->getCollection($appId)->all();
+ $routes = array_merge($routes, $this->router->getCollection("$appId.ocs")->all());
+ $allRoutes = array_merge($allRoutes, $routes);
+
+ if (!empty($routes) && $input->getOption('grouped')) {
+ $output->writeln("\nRoutes of $appId:");
+ $rows = $this->formatRoutes($routes);
+ $this->writeTableInOutputFormat($input, $output, $rows);
+ }
+ }
+ if (!$input->getOption('grouped')) {
+ $rows = $this->formatRoutes($allRoutes);
+ $this->writeTableInOutputFormat($input, $output, $rows);
+ }
+ return 0;
+ }
+
+ /**
+ * @param Route[] $routes
+ */
+ private function formatRoutes(array $routes): array {
+ $rows = [];
+ foreach ($routes as $route) {
+ [$realApp, $controller, $function] = $route->getDefault('caller');
+ //print_r($route->__serialize());
+ $rows[] = [
+ 'app' => $realApp,
+ 'controller' => $controller,
+ 'function' => $function,
+ 'method' => $route->getMethods()[0],
+ 'path' => str_replace('/ocsapp', '/ocs/v2.php', $route->getPath()),
+ 'defaults' => $this->formatDefaults($route->getDefaults()),
+ 'requirements' => $this->formatArray($route->getRequirements()),
+ ];
+ }
+ return $rows;
+ }
+
+ private function formatDefaults(array $defaults): array|string {
+ $defaults = array_filter($defaults, fn (string $name) => !in_array($name, ['action', 'caller']), ARRAY_FILTER_USE_KEY);
+ return $this->formatArray($defaults);
+ }
+
+ private function formatArray(array $value): array|string {
+ if (str_starts_with($this->input->getOption('output'), self::OUTPUT_FORMAT_JSON)) {
+ return $value;
+ }
+ return empty($value) ? '' : json_encode($value);
+ }
+}
diff --git a/core/register_command.php b/core/register_command.php
index 72a4b70f059ca..60b6214d94c89 100644
--- a/core/register_command.php
+++ b/core/register_command.php
@@ -71,6 +71,7 @@
use OC\Core\Command\Memcache\RedisCommand;
use OC\Core\Command\Preview\Generate;
use OC\Core\Command\Preview\ResetRenderedTexts;
+use OC\Core\Command\Router\ListRoutes;
use OC\Core\Command\Security\BruteforceAttempts;
use OC\Core\Command\Security\BruteforceResetAttempts;
use OC\Core\Command\Security\ExportCertificates;
@@ -243,6 +244,8 @@
$application->add(Server::get(Statistics::class));
$application->add(Server::get(RedisCommand::class));
+
+ $application->add(Server::get(ListRoutes::class));
} else {
$application->add(Server::get(Command\Maintenance\Install::class));
}
diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php
index 36f64d970c3ab..b5d8abdf32725 100644
--- a/lib/composer/composer/autoload_classmap.php
+++ b/lib/composer/composer/autoload_classmap.php
@@ -1295,6 +1295,7 @@
'OC\\Core\\Command\\Preview\\Generate' => $baseDir . '/core/Command/Preview/Generate.php',
'OC\\Core\\Command\\Preview\\Repair' => $baseDir . '/core/Command/Preview/Repair.php',
'OC\\Core\\Command\\Preview\\ResetRenderedTexts' => $baseDir . '/core/Command/Preview/ResetRenderedTexts.php',
+ 'OC\\Core\\Command\\Router\\ListRoutes' => $baseDir . '/core/Command/Router/ListRoutes.php',
'OC\\Core\\Command\\Security\\BruteforceAttempts' => $baseDir . '/core/Command/Security/BruteforceAttempts.php',
'OC\\Core\\Command\\Security\\BruteforceResetAttempts' => $baseDir . '/core/Command/Security/BruteforceResetAttempts.php',
'OC\\Core\\Command\\Security\\ExportCertificates' => $baseDir . '/core/Command/Security/ExportCertificates.php',
diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php
index 327366ca889a0..8af5836003531 100644
--- a/lib/composer/composer/autoload_static.php
+++ b/lib/composer/composer/autoload_static.php
@@ -1336,6 +1336,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\Core\\Command\\Preview\\Generate' => __DIR__ . '/../../..' . '/core/Command/Preview/Generate.php',
'OC\\Core\\Command\\Preview\\Repair' => __DIR__ . '/../../..' . '/core/Command/Preview/Repair.php',
'OC\\Core\\Command\\Preview\\ResetRenderedTexts' => __DIR__ . '/../../..' . '/core/Command/Preview/ResetRenderedTexts.php',
+ 'OC\\Core\\Command\\Router\\ListRoutes' => __DIR__ . '/../../..' . '/core/Command/Router/ListRoutes.php',
'OC\\Core\\Command\\Security\\BruteforceAttempts' => __DIR__ . '/../../..' . '/core/Command/Security/BruteforceAttempts.php',
'OC\\Core\\Command\\Security\\BruteforceResetAttempts' => __DIR__ . '/../../..' . '/core/Command/Security/BruteforceResetAttempts.php',
'OC\\Core\\Command\\Security\\ExportCertificates' => __DIR__ . '/../../..' . '/core/Command/Security/ExportCertificates.php',
diff --git a/lib/private/Route/CachingRouter.php b/lib/private/Route/CachingRouter.php
index dbd5ef02603eb..314d4c22812b3 100644
--- a/lib/private/Route/CachingRouter.php
+++ b/lib/private/Route/CachingRouter.php
@@ -43,10 +43,12 @@ public function __construct(
*
* @param string $name Name of the route to use.
* @param array $parameters Parameters for the route
- * @param bool $absolute
- * @return string
*/
- public function generate($name, $parameters = [], $absolute = false) {
+ public function generate(
+ string $name,
+ array $parameters = [],
+ bool $absolute = false,
+ ): string {
asort($parameters);
$key = $this->context->getHost() . '#' . $this->context->getBaseUrl() . $name . sha1(json_encode($parameters)) . (int)$absolute;
$cachedKey = $this->cache->get($key);
@@ -115,7 +117,6 @@ 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');
@@ -143,7 +144,7 @@ protected function requireRouteFile(string $file, string $appName): void {
$this->legacyCreatedRoutes = [];
parent::requireRouteFile($file, $appName);
foreach ($this->legacyCreatedRoutes as $routeName) {
- $route = $this->collection?->get($routeName);
+ $route = $this->getCollection($appName)->get($routeName);
if ($route === null) {
/* Should never happen */
throw new \Exception("Could not find route $routeName");
diff --git a/lib/private/Route/Router.php b/lib/private/Route/Router.php
index 02f371e808a44..1f6cb438cb205 100644
--- a/lib/private/Route/Router.php
+++ b/lib/private/Route/Router.php
@@ -31,24 +31,19 @@
use Symfony\Component\Routing\RouteCollection;
class Router implements IRouter {
+ /** @var string[] */
+ protected ?array $routingFiles = null;
/** @var RouteCollection[] */
- protected $collections = [];
- /** @var null|RouteCollection */
- protected $collection = null;
- /** @var null|string */
- protected $collectionName = null;
- /** @var null|RouteCollection */
- protected $root = null;
- /** @var null|UrlGenerator */
- protected $generator = null;
- /** @var string[]|null */
- protected $routingFiles;
- /** @var bool */
- protected $loaded = false;
- /** @var array */
- protected $loadedApps = [];
- /** @var RequestContext */
- protected $context;
+ protected array $collections = [];
+ protected RouteCollection $root;
+ protected ?UrlGenerator $generator = null;
+ protected bool $loaded = false;
+ /** @var Array */
+ protected array $loadedApps = [];
+ protected RequestContext $context;
+
+ // for legacy reasons - todo: drop
+ protected string $collectionName = 'root';
public function __construct(
protected LoggerInterface $logger,
@@ -98,33 +93,35 @@ public function getRoutingFiles() {
}
/**
- * Loads the routes
- *
- * @param null|string $app
+ * Loads the routes.
*/
- public function loadRoutes($app = null) {
+ public function loadRoutes(?string $app = null): void {
if (is_string($app)) {
$app = $this->appManager->cleanAppId($app);
+ // skip loading if not enabled for user
+ if (!$this->appManager->isEnabledForUser($app)) {
+ return;
+ }
}
- $requestedApp = $app;
- if ($this->loaded) {
+ // already loaded so we skip
+ if ($this->loaded || ($app !== null && isset($this->loadedApps[$app]))) {
return;
}
+
+ $requestedApp = $app ?? 'all';
$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;
- }
+ $this->loadAttributeRoutes($app);
try {
$appPath = $this->appManager->getAppPath($app);
$file = $appPath . '/appinfo/routes.php';
@@ -136,59 +133,40 @@ public function loadRoutes($app = null) {
} catch (AppPathNotFoundException) {
$routingFiles = [];
}
-
- if ($this->appManager->isEnabledForUser($app)) {
- $this->loadAttributeRoutes($app);
- }
}
$this->eventLogger->start('route:load:files', 'Loading Routes from files');
- foreach ($routingFiles as $app => $file) {
- if (!isset($this->loadedApps[$app])) {
- if (!$this->appManager->isAppLoaded($app)) {
- // app MUST be loaded before app routes
- // try again next time loadRoutes() is called
- $this->loaded = false;
- continue;
- }
- $this->loadedApps[$app] = true;
- $this->useCollection($app);
- $this->requireRouteFile($file, $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);
+ foreach ($routingFiles as $appId => $file) {
+ if (isset($this->loadedApps[$appId])) {
+ continue;
+ }
+ if (!$this->appManager->isAppLoaded($appId)) {
+ // app MUST be loaded before app routes
+ // try again next time loadRoutes() is called
+ $this->loaded = false;
+ continue;
+ }
+ $this->requireRouteFile($file, $appId);
+ // mark fully loaded app as loaded (route file + annotation)
+ if ($app === null || $app === $appId) {
+ $this->loadedApps[$appId] = true;
}
}
$this->eventLogger->end('route:load:files');
if (!isset($this->loadedApps['core'])) {
- $this->loadedApps['core'] = true;
- $this->useCollection('root');
$this->setupRoutes($this->getAttributeRoutes('core'), 'core');
$this->requireRouteFile(__DIR__ . '/../../../core/routes.php', 'core');
-
- // Also add the OCS collection
- $collection = $this->getCollection('root.ocs');
- $collection->addPrefix('/ocsapp');
- $this->root->addCollection($collection);
- }
- if ($this->loaded) {
- $collection = $this->getCollection('ocs');
- $collection->addPrefix('/ocs');
- $this->root->addCollection($collection);
+ $this->loadedApps['core'] = true;
}
+
$this->eventLogger->end('route:load:' . $requestedApp);
}
/**
- * @param string $name
- * @return \Symfony\Component\Routing\RouteCollection
+ * Get the collection for the specified app
*/
- protected function getCollection($name) {
+ public function getCollection(string $name): RouteCollection {
if (!isset($this->collections[$name])) {
$this->collections[$name] = new RouteCollection();
}
@@ -202,7 +180,6 @@ protected function getCollection($name) {
* @return void
*/
public function useCollection($name) {
- $this->collection = $this->getCollection($name);
$this->collectionName = $name;
}
@@ -230,7 +207,9 @@ public function create($name,
array $defaults = [],
array $requirements = []) {
$route = new Route($pattern, $defaults, $requirements);
- $this->collection->add($name, $route);
+
+ $collection = $this->getCollection($this->collectionName);
+ $collection->add($name, $route);
return $route;
}
@@ -351,11 +330,8 @@ protected function callLegacyActionRoute(array $parameters): void {
/**
* Get the url generator
- *
- * @return \Symfony\Component\Routing\Generator\UrlGenerator
- *
*/
- public function getGenerator() {
+ public function getGenerator(): UrlGenerator {
if ($this->generator !== null) {
return $this->generator;
}
@@ -368,12 +344,13 @@ public function getGenerator() {
*
* @param string $name Name of the route to use.
* @param array $parameters Parameters for the route
- * @param bool $absolute
* @return string
*/
- public function generate($name,
- $parameters = [],
- $absolute = false) {
+ public function generate(
+ string $name,
+ array $parameters = [],
+ bool $absolute = false,
+ ): string {
$referenceType = UrlGenerator::ABSOLUTE_URL;
if ($absolute === false) {
$referenceType = UrlGenerator::ABSOLUTE_PATH;
@@ -445,16 +422,7 @@ private function loadAttributeRoutes(string $app): void {
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);
}
/**
@@ -513,32 +481,34 @@ private function getAttributeRoutes(string $app): array {
* @param string $appName
*/
protected function requireRouteFile(string $file, string $appName): void {
- $this->setupRoutes(include $file, $appName);
+ try {
+ $this->useCollection($appName);
+ $this->setupRoutes(include $file, $appName);
+ } catch (\TypeError) {
+ $this->logger->debug('Routes should only be registered by returning an array of routes from the routes.php');
+ }
}
/**
- * If a routes.php file returns an array, try to set up the application and
- * 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 initialized. This makes it optional to ship an
- * appinfo/application.php by using the built in query resolver
+ * Parse our custom route format and add the routes to the collection.
*
* @param array $routes the application routes
* @param string $appName the name of the app.
*/
- private function setupRoutes($routes, $appName) {
- if (is_array($routes)) {
- $routeParser = new RouteParser();
-
- $defaultRoutes = $routeParser->parseDefaultRoutes($routes, $appName);
- $ocsRoutes = $routeParser->parseOCSRoutes($routes, $appName);
-
- $this->root->addCollection($defaultRoutes);
- $ocsRoutes->addPrefix('/ocsapp');
- $this->root->addCollection($ocsRoutes);
- }
+ private function setupRoutes(array $routes, string $appName): void {
+ $routeParser = new RouteParser();
+
+ $defaultCollection = $this->getCollection($appName);
+ $defaultRoutes = $routeParser->parseDefaultRoutes($routes, $appName);
+ $defaultCollection->addCollection($defaultRoutes);
+ $this->root->addCollection($defaultCollection);
+
+ $ocsCollection = $this->getCollection("$appName.ocs");
+ $ocsRoutes = $routeParser->parseOCSRoutes($routes, $appName);
+ $ocsCollection->addCollection($ocsRoutes);
+ $ocsCollection->addPrefix('/ocsapp');
+ $this->root->addCollection($ocsCollection);
}
private function getApplicationClass(string $appName) {
diff --git a/tests/lib/Route/RouterTest.php b/tests/lib/Route/RouterTest.php
index f99ebe4767ffe..21ae2e9dfac20 100644
--- a/tests/lib/Route/RouterTest.php
+++ b/tests/lib/Route/RouterTest.php
@@ -66,6 +66,12 @@ public function testGenerateConsecutively(): void {
$this->appManager->expects(self::atLeastOnce())
->method('isAppLoaded')
->willReturn(true);
+ $this->appManager->expects(self::atLeastOnce())
+ ->method('isEnabledForUser')
+ ->willReturn(true);
+ $this->appManager->expects(self::exactly(2))
+ ->method('getEnabledApps')
+ ->willReturn(['files', 'dav']);
$this->assertEquals('/index.php/apps/files/', $this->router->generate('files.view.index'));