diff options
Diffstat (limited to 'lib/private/Route')
-rw-r--r-- | lib/private/Route/CachingRouter.php | 100 | ||||
-rw-r--r-- | lib/private/Route/Route.php | 10 | ||||
-rw-r--r-- | lib/private/Route/Router.php | 48 |
3 files changed, 136 insertions, 22 deletions
diff --git a/lib/private/Route/CachingRouter.php b/lib/private/Route/CachingRouter.php index 7dd26827d3c..becdb807f73 100644 --- a/lib/private/Route/CachingRouter.php +++ b/lib/private/Route/CachingRouter.php @@ -15,10 +15,16 @@ 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 { protected ICache $cache; + protected array $legacyCreatedRoutes = []; + public function __construct( ICacheFactory $cacheFactory, LoggerInterface $logger, @@ -54,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 ab5a1f6b59a..08231649e76 100644 --- a/lib/private/Route/Route.php +++ b/lib/private/Route/Route.php @@ -124,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 376852a1b6e..22dfb21d4f3 100644 --- a/lib/private/Route/Router.php +++ b/lib/private/Route/Router.php @@ -53,7 +53,7 @@ class Router implements IRouter { public function __construct( protected LoggerInterface $logger, IRequest $request, - private IConfig $config, + protected IConfig $config, protected IEventLogger $eventLogger, private ContainerInterface $container, protected IAppManager $appManager, @@ -82,7 +82,7 @@ class Router implements IRouter { public function getRoutingFiles() { if ($this->routingFiles === null) { $this->routingFiles = []; - foreach (\OC_APP::getEnabledApps() as $app) { + foreach ($this->appManager->getEnabledApps() as $app) { try { $appPath = $this->appManager->getAppPath($app); $file = $appPath . '/appinfo/routes.php'; @@ -117,7 +117,7 @@ class Router implements IRouter { $routingFiles = $this->getRoutingFiles(); $this->eventLogger->start('route:load:attributes', 'Loading Routes from attributes'); - foreach (\OC_App::getEnabledApps() as $enabledApp) { + foreach ($this->appManager->getEnabledApps() as $enabledApp) { $this->loadAttributeRoutes($enabledApp); } $this->eventLogger->end('route:load:attributes'); @@ -312,17 +312,11 @@ 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']); - $this->eventLogger->start('route:run:call', 'Run callable route'); - call_user_func($action, $parameters); - $this->eventLogger->end('route:run:call'); + $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'); } @@ -330,6 +324,32 @@ class Router implements IRouter { } /** + * @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'); + } + + /** * Get the url generator * * @return \Symfony\Component\Routing\Generator\UrlGenerator @@ -492,7 +512,7 @@ class Router implements IRouter { * @param string $file the route file location to include * @param string $appName */ - private function requireRouteFile($file, $appName) { + protected function requireRouteFile(string $file, string $appName): void { $this->setupRoutes(include $file, $appName); } |