diff options
-rw-r--r-- | lib/composer/composer/autoload_classmap.php | 3 | ||||
-rw-r--r-- | lib/composer/composer/autoload_static.php | 3 | ||||
-rw-r--r-- | lib/private/Route/Router.php | 68 | ||||
-rw-r--r-- | lib/public/AppFramework/Http/Attribute/ApiRoute.php | 63 | ||||
-rw-r--r-- | lib/public/AppFramework/Http/Attribute/FrontpageRoute.php | 63 | ||||
-rw-r--r-- | lib/public/AppFramework/Http/Attribute/Route.php | 161 |
6 files changed, 361 insertions, 0 deletions
diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 9c3bf0f7e9a..84dcb562a8d 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -38,15 +38,18 @@ return array( 'OCP\\AppFramework\\Http' => $baseDir . '/lib/public/AppFramework/Http.php', 'OCP\\AppFramework\\Http\\Attribute\\ARateLimit' => $baseDir . '/lib/public/AppFramework/Http/Attribute/ARateLimit.php', 'OCP\\AppFramework\\Http\\Attribute\\AnonRateLimit' => $baseDir . '/lib/public/AppFramework/Http/Attribute/AnonRateLimit.php', + 'OCP\\AppFramework\\Http\\Attribute\\ApiRoute' => $baseDir . '/lib/public/AppFramework/Http/Attribute/ApiRoute.php', 'OCP\\AppFramework\\Http\\Attribute\\AuthorizedAdminSetting' => $baseDir . '/lib/public/AppFramework/Http/Attribute/AuthorizedAdminSetting.php', 'OCP\\AppFramework\\Http\\Attribute\\BruteForceProtection' => $baseDir . '/lib/public/AppFramework/Http/Attribute/BruteForceProtection.php', 'OCP\\AppFramework\\Http\\Attribute\\CORS' => $baseDir . '/lib/public/AppFramework/Http/Attribute/CORS.php', + 'OCP\\AppFramework\\Http\\Attribute\\FrontpageRoute' => $baseDir . '/lib/public/AppFramework/Http/Attribute/FrontpageRoute.php', 'OCP\\AppFramework\\Http\\Attribute\\IgnoreOpenAPI' => $baseDir . '/lib/public/AppFramework/Http/Attribute/IgnoreOpenAPI.php', 'OCP\\AppFramework\\Http\\Attribute\\NoAdminRequired' => $baseDir . '/lib/public/AppFramework/Http/Attribute/NoAdminRequired.php', 'OCP\\AppFramework\\Http\\Attribute\\NoCSRFRequired' => $baseDir . '/lib/public/AppFramework/Http/Attribute/NoCSRFRequired.php', 'OCP\\AppFramework\\Http\\Attribute\\OpenAPI' => $baseDir . '/lib/public/AppFramework/Http/Attribute/OpenAPI.php', 'OCP\\AppFramework\\Http\\Attribute\\PasswordConfirmationRequired' => $baseDir . '/lib/public/AppFramework/Http/Attribute/PasswordConfirmationRequired.php', 'OCP\\AppFramework\\Http\\Attribute\\PublicPage' => $baseDir . '/lib/public/AppFramework/Http/Attribute/PublicPage.php', + 'OCP\\AppFramework\\Http\\Attribute\\Route' => $baseDir . '/lib/public/AppFramework/Http/Attribute/Route.php', 'OCP\\AppFramework\\Http\\Attribute\\StrictCookiesRequired' => $baseDir . '/lib/public/AppFramework/Http/Attribute/StrictCookiesRequired.php', 'OCP\\AppFramework\\Http\\Attribute\\SubAdminRequired' => $baseDir . '/lib/public/AppFramework/Http/Attribute/SubAdminRequired.php', 'OCP\\AppFramework\\Http\\Attribute\\UseSession' => $baseDir . '/lib/public/AppFramework/Http/Attribute/UseSession.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index b222594af75..d04b9ce23c5 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -71,15 +71,18 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OCP\\AppFramework\\Http' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http.php', 'OCP\\AppFramework\\Http\\Attribute\\ARateLimit' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/Attribute/ARateLimit.php', 'OCP\\AppFramework\\Http\\Attribute\\AnonRateLimit' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/Attribute/AnonRateLimit.php', + 'OCP\\AppFramework\\Http\\Attribute\\ApiRoute' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/Attribute/ApiRoute.php', 'OCP\\AppFramework\\Http\\Attribute\\AuthorizedAdminSetting' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/Attribute/AuthorizedAdminSetting.php', 'OCP\\AppFramework\\Http\\Attribute\\BruteForceProtection' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/Attribute/BruteForceProtection.php', 'OCP\\AppFramework\\Http\\Attribute\\CORS' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/Attribute/CORS.php', + 'OCP\\AppFramework\\Http\\Attribute\\FrontpageRoute' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/Attribute/FrontpageRoute.php', 'OCP\\AppFramework\\Http\\Attribute\\IgnoreOpenAPI' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/Attribute/IgnoreOpenAPI.php', 'OCP\\AppFramework\\Http\\Attribute\\NoAdminRequired' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/Attribute/NoAdminRequired.php', 'OCP\\AppFramework\\Http\\Attribute\\NoCSRFRequired' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/Attribute/NoCSRFRequired.php', 'OCP\\AppFramework\\Http\\Attribute\\OpenAPI' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/Attribute/OpenAPI.php', 'OCP\\AppFramework\\Http\\Attribute\\PasswordConfirmationRequired' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/Attribute/PasswordConfirmationRequired.php', 'OCP\\AppFramework\\Http\\Attribute\\PublicPage' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/Attribute/PublicPage.php', + 'OCP\\AppFramework\\Http\\Attribute\\Route' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/Attribute/Route.php', 'OCP\\AppFramework\\Http\\Attribute\\StrictCookiesRequired' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/Attribute/StrictCookiesRequired.php', 'OCP\\AppFramework\\Http\\Attribute\\SubAdminRequired' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/Attribute/SubAdminRequired.php', 'OCP\\AppFramework\\Http\\Attribute\\UseSession' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/Attribute/UseSession.php', diff --git a/lib/private/Route/Router.php b/lib/private/Route/Router.php index 9cf12f00185..e7e2a9f0e49 100644 --- a/lib/private/Route/Router.php +++ b/lib/private/Route/Router.php @@ -14,6 +14,7 @@ * @author Robin McCorkell <robin@mccorkell.me.uk> * @author Roeland Jago Douma <roeland@famdouma.nl> * @author Thomas Müller <thomas.mueller@tmit.eu> + * @author Kate Döen <kate.doeen@nextcloud.com> * * @license AGPL-3.0 * @@ -32,8 +33,10 @@ */ namespace OC\Route; +use DirectoryIterator; use OC\AppFramework\Routing\RouteParser; use OCP\AppFramework\App; +use OCP\AppFramework\Http\Attribute\Route as RouteAttribute; use OCP\Diagnostics\IEventLogger; use OCP\IConfig; use OCP\IRequest; @@ -41,6 +44,9 @@ 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; @@ -150,6 +156,22 @@ class Router implements IRouter { } } $this->eventLogger->start('route:load:' . $requestedApp, 'Loading Routes for ' . $requestedApp); + + if ($requestedApp !== null) { + $routes = $this->getAttributeRoutes($requestedApp); + if (count($routes) > 0) { + $this->useCollection($requestedApp); + $this->setupRoutes($routes, $requestedApp); + $collection = $this->getCollection($requestedApp); + $this->root->addCollection($collection); + + // Also add the OCS collection + $collection = $this->getCollection($requestedApp . '.ocs'); + $collection->addPrefix('/ocsapp'); + $this->root->addCollection($collection); + } + } + foreach ($routingFiles as $app => $file) { if (!isset($this->loadedApps[$app])) { if (!\OC_App::isAppLoaded($app)) { @@ -173,6 +195,7 @@ class Router implements IRouter { if (!isset($this->loadedApps['core'])) { $this->loadedApps['core'] = true; $this->useCollection('root'); + $this->setupRoutes($this->getAttributeRoutes('core'), 'core'); require_once __DIR__ . '/../../../core/routes.php'; // Also add the OCS collection @@ -420,6 +443,51 @@ class Router implements IRouter { } /** + * @throws ReflectionException + */ + private function getAttributeRoutes(string $app): array { + $routes = []; + + if ($app === 'core') { + $appControllerPath = __DIR__ . '/../../../core/Controller'; + $appNameSpace = 'OC\\Core'; + } else { + $appControllerPath = \OC_App::getAppPath($app) . '/lib/Controller'; + $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 diff --git a/lib/public/AppFramework/Http/Attribute/ApiRoute.php b/lib/public/AppFramework/Http/Attribute/ApiRoute.php new file mode 100644 index 00000000000..0b566082521 --- /dev/null +++ b/lib/public/AppFramework/Http/Attribute/ApiRoute.php @@ -0,0 +1,63 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2024 Kate Döen <kate.doeen@nextcloud.com> + * + * @author Kate Döen <kate.doeen@nextcloud.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OCP\AppFramework\Http\Attribute; + +use Attribute; + +/** + * This attribute can be used to define API routes on controller methods. + * + * It works in addition to the traditional routes.php method and has the same parameters + * (except for the `name` parameter which is not needed). + * + * @since 29.0.0 + */ +#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] +class ApiRoute extends Route { + /** + * @inheritDoc + * + * @since 29.0.0 + */ + public function __construct( + protected string $verb, + protected string $url, + protected ?array $requirements = null, + protected ?array $defaults = null, + protected ?string $root = null, + protected ?string $postfix = null, + ) { + parent::__construct( + Route::TYPE_API, + $verb, + $url, + $requirements, + $defaults, + $root, + $postfix, + ); + } +} diff --git a/lib/public/AppFramework/Http/Attribute/FrontpageRoute.php b/lib/public/AppFramework/Http/Attribute/FrontpageRoute.php new file mode 100644 index 00000000000..ac08513a2a9 --- /dev/null +++ b/lib/public/AppFramework/Http/Attribute/FrontpageRoute.php @@ -0,0 +1,63 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2024 Kate Döen <kate.doeen@nextcloud.com> + * + * @author Kate Döen <kate.doeen@nextcloud.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OCP\AppFramework\Http\Attribute; + +use Attribute; + +/** + * This attribute can be used to define Frontpage routes on controller methods. + * + * It works in addition to the traditional routes.php method and has the same parameters + * (except for the `name` parameter which is not needed). + * + * @since 29.0.0 + */ +#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] +class FrontpageRoute extends Route { + /** + * @inheritDoc + * + * @since 29.0.0 + */ + public function __construct( + protected string $verb, + protected string $url, + protected ?array $requirements = null, + protected ?array $defaults = null, + protected ?string $root = null, + protected ?string $postfix = null, + ) { + parent::__construct( + Route::TYPE_FRONTPAGE, + $verb, + $url, + $requirements, + $defaults, + $root, + $postfix, + ); + } +} diff --git a/lib/public/AppFramework/Http/Attribute/Route.php b/lib/public/AppFramework/Http/Attribute/Route.php new file mode 100644 index 00000000000..58579c1f956 --- /dev/null +++ b/lib/public/AppFramework/Http/Attribute/Route.php @@ -0,0 +1,161 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2024 Kate Döen <kate.doeen@nextcloud.com> + * + * @author Kate Döen <kate.doeen@nextcloud.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OCP\AppFramework\Http\Attribute; + +use Attribute; + +/** + * This attribute can be used to define routes on controller methods. + * + * It works in addition to the traditional routes.php method and has the same parameters + * (except for the `name` parameter which is not needed). + * + * @since 29.0.0 + */ +#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] +class Route { + + /** + * Corresponds to the `ocs` key in routes.php + * + * @see ApiRoute + * @since 29.0.0 + */ + public const TYPE_API = 'ocs'; + + /** + * Corresponds to the `routes` key in routes.php + * + * @see FrontpageRoute + * @since 29.0.0 + */ + public const TYPE_FRONTPAGE = 'routes'; + + /** + * @param string $type Either Route::TYPE_API or Route::TYPE_FRONTPAGE. + * @psalm-param Route::TYPE_* $type + * @param string $verb HTTP method of the route. + * @psalm-param 'GET'|'HEAD'|'POST'|'PUT'|'DELETE'|'OPTIONS'|'PATCH' $verb + * @param string $url The path of the route. + * @param ?array<string, string> $requirements Array of regexes mapped to the path parameters. + * @param ?array<string, mixed> $defaults Array of default values mapped to the path parameters. + * @param ?string $root Custom root. For OCS all apps are allowed, but for index.php only some can use it. + * @param ?string $postfix Postfix for the route name. + * @since 29.0.0 + */ + public function __construct( + protected string $type, + protected string $verb, + protected string $url, + protected ?array $requirements = null, + protected ?array $defaults = null, + protected ?string $root = null, + protected ?string $postfix = null, + ) { + } + + /** + * @return array{ + * verb: string, + * url: string, + * requirements?: array<string, string>, + * defaults?: array<string, mixed>, + * root?: string, + * postfix?: string, + * } + * @since 29.0.0 + */ + public function toArray() { + $route = [ + 'verb' => $this->verb, + 'url' => $this->url, + ]; + + if ($this->requirements !== null) { + $route['requirements'] = $this->requirements; + } + if ($this->defaults !== null) { + $route['defaults'] = $this->defaults; + } + if ($this->root !== null) { + $route['root'] = $this->root; + } + if ($this->postfix !== null) { + $route['postfix'] = $this->postfix; + } + + return $route; + } + + /** + * @since 29.0.0 + */ + public function getType(): string { + return $this->type; + } + + /** + * @since 29.0.0 + */ + public function getVerb(): string { + return $this->verb; + } + + /** + * @since 29.0.0 + */ + public function getUrl(): string { + return $this->url; + } + + /** + * @since 29.0.0 + */ + public function getRequirements(): ?array { + return $this->requirements; + } + + /** + * @since 29.0.0 + */ + public function getDefaults(): ?array { + return $this->defaults; + } + + /** + * @since 29.0.0 + */ + public function getRoot(): ?string { + return $this->root; + } + + /** + * @since 29.0.0 + */ + public function getPostfix(): ?string { + return $this->postfix; + } +} |