From 3d2838f240bb41498484e79aee65c43e9123dc1b Mon Sep 17 00:00:00 2001 From: Josh Date: Sat, 16 May 2026 13:58:48 -0400 Subject: [PATCH 1/4] refactor(URLGenerator): streamline imagePath for clarity/policy ordering Also fixes a couple presumed bugs Signed-off-by: Josh --- lib/private/URLGenerator.php | 228 +++++++++++++++++++++++++---------- 1 file changed, 164 insertions(+), 64 deletions(-) diff --git a/lib/private/URLGenerator.php b/lib/private/URLGenerator.php index e083886aae92c..0e0c9ad9c196c 100644 --- a/lib/private/URLGenerator.php +++ b/lib/private/URLGenerator.php @@ -154,93 +154,193 @@ public function linkTo(string $appName, string $file, array $args = []): string } /** - * Creates path to an image + * Resolves the web path for an image asset. * - * @param string $appName app - * @param string $file image name - * @throws \RuntimeException If the image does not exist - * @return string the url + * Lookup order prefers legacy filesystem theme assets first, then + * theming app overrides, then app and core fallback locations. * - * Returns the path to the image. + * At each lookup location, the requested filename is checked first as-is. + * If it is missing, a same-basename PNG may be returned, but only when a + * same-basename SVG is also missing at that location. If the requested + * file is missing and an SVG variant exists, lookup continues to the next + * location instead of falling back to PNG there. + * + * @param string $appName The app to resolve the image for. Empty string is treated as 'core'. + * @param string $file The requested image filename, including extension. + * @return string The resolved web path to the image asset. + * @throws \RuntimeException If no matching image can be found. */ #[\Override] public function imagePath(string $appName, string $file): string { + if ($appName === '') { + $appName = 'core'; + } + $cache = $this->cacheFactory->createDistributed('imagePath-' . md5($this->getBaseUrl()) . '-'); $cacheKey = $appName . '-' . $file; if ($key = $cache->get($cacheKey)) { return $key; } - // Read the selected theme from the config file - $theme = \OC_Util::getTheme(); - - //if a theme has a png but not an svg always use the png - $basename = substr(basename($file), 0, -4); - - try { - if ($appName === 'core' || $appName === '') { - $appName = 'core'; - $appPath = false; - } else { + if ($appName === 'core') { + $appPath = false; + } else { + try { $appPath = $this->getAppManager()->getAppPath($appName); + } catch (AppPathNotFoundException $e) { + throw new RuntimeException( + 'image asset not found for app ' . $appName . ' - requested image name: ' . $file . ' webroot: ' . \OC::$WEBROOT . ' serverroot: ' . \OC::$SERVERROOT + ); } - } catch (AppPathNotFoundException $e) { - throw new RuntimeException('image not found: image: ' . $file . ' webroot: ' . \OC::$WEBROOT . ' serverroot: ' . \OC::$SERVERROOT); } - // Check if the app is in the app folder - $path = ''; - $themingEnabled = $this->config->getSystemValueBool('installed', false) && $this->getAppManager()->isEnabledForUser('theming'); - $themingImagePath = false; - if ($themingEnabled) { - $themingDefaults = Server::get('ThemingDefaults'); - if ($themingDefaults instanceof ThemingDefaults) { - $themingImagePath = $themingDefaults->replaceImagePath($appName, $file); - } + // image filename without extension; used to check for SVG before PNG. + $basename = substr(basename($file), 0, -4); // FIXME: consider switching to pathinfo() + + $resolvedPath = null; + + // + // Search for image assets in a deterministic order. + // + // Split into stages to make prioritization clearer. + // + // FIXME: The PNG fallback behavior matches the current implementation, + // but the policy is odd and may deserve separate review. + // + + // 1. Legacy filesystem themed assets (if active) + $legacyThemeName = \OC_Util::getTheme(); + if ($legacyThemeName !== '') { + $resolvedPath = $this->resolveLegacyThemeAppsImagePath($legacyThemeName, $appName, $file, $basename) + ?? $this->resolveLegacyThemeAppImagePath($legacyThemeName, $appName, $file, $basename) + ?? $this->resolveLegacyThemeCoreImagePath($legacyThemeName, $file, $basename); + } + + // 2. Modern theming app overrides + if ($resolvedPath === null) { + $resolvedPath = $this->getThemingImageOverridePath($appName, $file); + } + + // 3. app and core fallback locations + if ($resolvedPath === null) { + $resolvedPath = $this->resolveAppImagePath($appName, $appPath, $file, $basename) + ?? $this->resolveLegacyAppImagePath($appName, $file, $basename) + ?? $this->resolveCoreImagePath($file, $basename); + } + + if ($resolvedPath !== null) { + $cache->set($cacheKey, $resolvedPath); + return $resolvedPath; + } + + throw new RuntimeException( + 'image not found: image:' . $file . ' webroot:' . \OC::$WEBROOT . ' serverroot:' . \OC::$SERVERROOT + ); + } + + /** + * Look for legacy themed assets: app specific image paths located in `/themes/$themeName/apps/$appName` + */ + private function resolveLegacyThemeAppsImagePath(string $legacyThemeName, string $appName, string $file, string $basename): ?string { + $serverBasePath = \OC::$SERVERROOT . "/themes/$legacyThemeName/apps/$appName/img/"; + $webBasePath = \OC::$WEBROOT . "/themes/$legacyThemeName/apps/$appName/img/"; + + return $this->resolveImagePathFromBases($serverBasePath, $webBasePath, $file, $basename); + } + + /** + * Look for legacy themed assets: app specific image paths located in `/themes/$themeName/$appName` + */ + private function resolveLegacyThemeAppImagePath(string $legacyThemeName, string $appName, string $file, string $basename): ?string { + if ($appName === '') { + return null; + } + + $serverBasePath = \OC::$SERVERROOT . "/themes/$legacyThemeName/$appName/img/"; + $webBasePath = \OC::$WEBROOT . "/themes/$legacyThemeName/$appName/img/"; + + return $this->resolveImagePathFromBases($serverBasePath, $webBasePath, $file, $basename); + } + + /** + * Look for legacy themed assets: core image paths located in `/themes/$themeName/core` + */ + private function resolveLegacyThemeCoreImagePath(string $legacyThemeName, string $file, string $basename): ?string { + $serverBasePath = \OC::$SERVERROOT . "/themes/$legacyThemeName/core/img/"; + $webBasePath = \OC::$WEBROOT . "/themes/$legacyThemeName/core/img/"; + + return $this->resolveImagePathFromBases($serverBasePath, $webBasePath, $file, $basename); + } + + /** + * Look for app provided image assets + */ + private function resolveAppImagePath(string $appName, string|false $appPath, string $file, string $basename): ?string { + if ($appPath === false) { + return null; + } + + $serverBasePath = $appPath . "/img/"; + $webBasePath = $this->getAppManager()->getAppWebPath($appName) . "/img/"; + + return $this->resolveImagePathFromBases($serverBasePath, $webBasePath, $file, $basename); + } + + /** + * Look up legacy app specific image assets located directly underneath $serverRoot + * FIXME: This may not be needed any longer. + */ + private function resolveLegacyAppImagePath(string $appName, string $file, string $basename): ?string { + if ($appName === '') { + return null; } - if (file_exists(\OC::$SERVERROOT . "/themes/$theme/apps/$appName/img/$file")) { - $path = \OC::$WEBROOT . "/themes/$theme/apps/$appName/img/$file"; - } elseif (!file_exists(\OC::$SERVERROOT . "/themes/$theme/apps/$appName/img/$basename.svg") - && file_exists(\OC::$SERVERROOT . "/themes/$theme/apps/$appName/img/$basename.png")) { - $path = \OC::$WEBROOT . "/themes/$theme/apps/$appName/img/$basename.png"; - } elseif (!empty($appName) && file_exists(\OC::$SERVERROOT . "/themes/$theme/$appName/img/$file")) { - $path = \OC::$WEBROOT . "/themes/$theme/$appName/img/$file"; - } elseif (!empty($appName) && (!file_exists(\OC::$SERVERROOT . "/themes/$theme/$appName/img/$basename.svg") - && file_exists(\OC::$SERVERROOT . "/themes/$theme/$appName/img/$basename.png"))) { - $path = \OC::$WEBROOT . "/themes/$theme/$appName/img/$basename.png"; - } elseif (file_exists(\OC::$SERVERROOT . "/themes/$theme/core/img/$file")) { - $path = \OC::$WEBROOT . "/themes/$theme/core/img/$file"; - } elseif (!file_exists(\OC::$SERVERROOT . "/themes/$theme/core/img/$basename.svg") - && file_exists(\OC::$SERVERROOT . "/themes/$theme/core/img/$basename.png")) { - $path = \OC::$WEBROOT . "/themes/$theme/core/img/$basename.png"; - } elseif ($themingEnabled && $themingImagePath) { - $path = $themingImagePath; - } elseif ($appPath && file_exists($appPath . "/img/$file")) { - $path = $this->getAppManager()->getAppWebPath($appName) . "/img/$file"; - } elseif ($appPath && !file_exists($appPath . "/img/$basename.svg") - && file_exists($appPath . "/img/$basename.png")) { - $path = $this->getAppManager()->getAppWebPath($appName) . "/img/$basename.png"; - } elseif (!empty($appName) && file_exists(\OC::$SERVERROOT . "/$appName/img/$file")) { - $path = \OC::$WEBROOT . "/$appName/img/$file"; - } elseif (!empty($appName) && (!file_exists(\OC::$SERVERROOT . "/$appName/img/$basename.svg") - && file_exists(\OC::$SERVERROOT . "/$appName/img/$basename.png"))) { - $path = \OC::$WEBROOT . "/$appName/img/$basename.png"; - } elseif (file_exists(\OC::$SERVERROOT . "/core/img/$file")) { - $path = \OC::$WEBROOT . "/core/img/$file"; - } elseif (!file_exists(\OC::$SERVERROOT . "/core/img/$basename.svg") - && file_exists(\OC::$SERVERROOT . "/core/img/$basename.png")) { - $path = \OC::$WEBROOT . "/themes/$theme/core/img/$basename.png"; + $serverBasePath = \OC::$SERVERROOT . "/" . $appName . "/img/"; + $webBasePath = \OC::$WEBROOT . "/" . $appName . "/img/"; + + return $this->resolveImagePathFromBases($serverBasePath, $webBasePath, $file, $basename); + } + + /** + * Look up core image assets + */ + private function resolveCoreImagePath(string $file, string $basename): ?string { + $serverBasePath = \OC::$SERVERROOT . "/core/img/"; + $webBasePath = \OC::$WEBROOT . "/core/img/"; + + return $this->resolveImagePathFromBases($serverBasePath, $webBasePath, $file, $basename); + } + + private function resolveImagePathFromBases(string $serverBasePath, string $webBasePath, string $file, string $basename): ?string { + $filePath = $serverBasePath . $file; + if (file_exists($filePath)) { + return $webBasePath . $file; } - if ($path !== '') { - $cache->set($cacheKey, $path); - return $path; + $svgPath = $serverBasePath . $basename . '.svg'; + $pngPath = $serverBasePath . $basename . '.png'; + if (!file_exists($svgPath) && file_exists($pngPath)) { + return $webBasePath . $basename . '.png'; } - throw new RuntimeException('image not found: image:' . $file . ' webroot:' . \OC::$WEBROOT . ' serverroot:' . \OC::$SERVERROOT); + return null; } + + private function getThemingImageOverridePath(string $appName, string $file): ?string { + $installed = $this->config->getSystemValueBool('installed', false); + if (!$installed) { + return null; + } + + $themingDefaults = Server::get('ThemingDefaults'); + if (!$themingDefaults instanceof ThemingDefaults) { + return null; + } + + $themingImagePath = $themingDefaults->replaceImagePath($appName, $file); + return $themingImagePath ?: null; + } /** * Makes an URL absolute From 6ab5e5b6cb61121f93b6cd4672cf5cdf495a5537 Mon Sep 17 00:00:00 2001 From: Josh Date: Mon, 1 Jun 2026 21:19:30 -0400 Subject: [PATCH 2/4] refactor(URLGenerator): collapse into three helpers + one shared for readability/ Signed-off-by: Josh --- lib/private/URLGenerator.php | 186 +++++++++++++++-------------------- 1 file changed, 81 insertions(+), 105 deletions(-) diff --git a/lib/private/URLGenerator.php b/lib/private/URLGenerator.php index 0e0c9ad9c196c..475b470b6b07b 100644 --- a/lib/private/URLGenerator.php +++ b/lib/private/URLGenerator.php @@ -156,14 +156,13 @@ public function linkTo(string $appName, string $file, array $args = []): string /** * Resolves the web path for an image asset. * - * Lookup order prefers legacy filesystem theme assets first, then - * theming app overrides, then app and core fallback locations. + * Lookup order is: + * - legacy filesystem theme assets first + * - then theming app overrides + * - then app and core fallback locations. * - * At each lookup location, the requested filename is checked first as-is. - * If it is missing, a same-basename PNG may be returned, but only when a - * same-basename SVG is also missing at that location. If the requested - * file is missing and an SVG variant exists, lookup continues to the next - * location instead of falling back to PNG there. + * If requested filename is unavailable, automatically tries to fallback to + * "BASENAME.{PNG/SVG}", if available. * * @param string $appName The app to resolve the image for. Empty string is treated as 'core'. * @param string $file The requested image filename, including extension. @@ -178,6 +177,7 @@ public function imagePath(string $appName, string $file): string { $cache = $this->cacheFactory->createDistributed('imagePath-' . md5($this->getBaseUrl()) . '-'); $cacheKey = $appName . '-' . $file; + if ($key = $cache->get($cacheKey)) { return $key; } @@ -189,44 +189,19 @@ public function imagePath(string $appName, string $file): string { $appPath = $this->getAppManager()->getAppPath($appName); } catch (AppPathNotFoundException $e) { throw new RuntimeException( - 'image asset not found for app ' . $appName . ' - requested image name: ' . $file . ' webroot: ' . \OC::$WEBROOT . ' serverroot: ' . \OC::$SERVERROOT + 'image asset not found for app ' . $appName . ' - requested image name: ' . $file . + ' webroot: ' . \OC::$WEBROOT . ' serverroot: ' . \OC::$SERVERROOT ); } } - // image filename without extension; used to check for SVG before PNG. + // Image filename without extension; used to check for SVG before PNG. $basename = substr(basename($file), 0, -4); // FIXME: consider switching to pathinfo() - $resolvedPath = null; - - // // Search for image assets in a deterministic order. - // - // Split into stages to make prioritization clearer. - // - // FIXME: The PNG fallback behavior matches the current implementation, - // but the policy is odd and may deserve separate review. - // - - // 1. Legacy filesystem themed assets (if active) - $legacyThemeName = \OC_Util::getTheme(); - if ($legacyThemeName !== '') { - $resolvedPath = $this->resolveLegacyThemeAppsImagePath($legacyThemeName, $appName, $file, $basename) - ?? $this->resolveLegacyThemeAppImagePath($legacyThemeName, $appName, $file, $basename) - ?? $this->resolveLegacyThemeCoreImagePath($legacyThemeName, $file, $basename); - } - - // 2. Modern theming app overrides - if ($resolvedPath === null) { - $resolvedPath = $this->getThemingImageOverridePath($appName, $file); - } - - // 3. app and core fallback locations - if ($resolvedPath === null) { - $resolvedPath = $this->resolveAppImagePath($appName, $appPath, $file, $basename) - ?? $this->resolveLegacyAppImagePath($appName, $file, $basename) - ?? $this->resolveCoreImagePath($file, $basename); - } + $resolvedPath = $this->resolveLegacyThemeImagePath($appName, $file, $basename) + ?? $this->getThemingImageOverridePath($appName, $file) + ?? $this->resolveAppOrCoreImagePath($appName, $appPath, $file, $basename); if ($resolvedPath !== null) { $cache->set($cacheKey, $resolvedPath); @@ -239,78 +214,95 @@ public function imagePath(string $appName, string $file): string { } /** - * Look for legacy themed assets: app specific image paths located in `/themes/$themeName/apps/$appName` - */ - private function resolveLegacyThemeAppsImagePath(string $legacyThemeName, string $appName, string $file, string $basename): ?string { - $serverBasePath = \OC::$SERVERROOT . "/themes/$legacyThemeName/apps/$appName/img/"; - $webBasePath = \OC::$WEBROOT . "/themes/$legacyThemeName/apps/$appName/img/"; - - return $this->resolveImagePathFromBases($serverBasePath, $webBasePath, $file, $basename); - } - - /** - * Look for legacy themed assets: app specific image paths located in `/themes/$themeName/$appName` + * Resolves image paths from the active legacy filesystem theme, if configured. + * + * Lookup order within the theme is: + * - /themes/$themeName/apps/$appName/img/ + * - /themes/$themeName/$appName/img/ + * - /themes/$themeName/core/img/ */ - private function resolveLegacyThemeAppImagePath(string $legacyThemeName, string $appName, string $file, string $basename): ?string { - if ($appName === '') { + private function resolveLegacyThemeImagePath(string $appName, string $file, string $basename): ?string { + $legacyThemeName = \OC_Util::getTheme(); + if ($legacyThemeName === '') { return null; } - $serverBasePath = \OC::$SERVERROOT . "/themes/$legacyThemeName/$appName/img/"; - $webBasePath = \OC::$WEBROOT . "/themes/$legacyThemeName/$appName/img/"; - - return $this->resolveImagePathFromBases($serverBasePath, $webBasePath, $file, $basename); - } - - /** - * Look for legacy themed assets: core image paths located in `/themes/$themeName/core` - */ - private function resolveLegacyThemeCoreImagePath(string $legacyThemeName, string $file, string $basename): ?string { - $serverBasePath = \OC::$SERVERROOT . "/themes/$legacyThemeName/core/img/"; - $webBasePath = \OC::$WEBROOT . "/themes/$legacyThemeName/core/img/"; - - return $this->resolveImagePathFromBases($serverBasePath, $webBasePath, $file, $basename); + return $this->resolveImagePathFromBases( + \OC::$SERVERROOT . "/themes/$legacyThemeName/apps/$appName/img/", + \OC::$WEBROOT . "/themes/$legacyThemeName/apps/$appName/img/", + $file, + $basename, + ) ?? ($appName === '' ? null : $this->resolveImagePathFromBases( + \OC::$SERVERROOT . "/themes/$legacyThemeName/$appName/img/", + \OC::$WEBROOT . "/themes/$legacyThemeName/$appName/img/", + $file, + $basename, + )) ?? $this->resolveImagePathFromBases( + \OC::$SERVERROOT . "/themes/$legacyThemeName/core/img/", + \OC::$WEBROOT . "/themes/$legacyThemeName/core/img/", + $file, + $basename, + ); } /** - * Look for app provided image assets + * Resolve modern theming app override path, if available. */ - private function resolveAppImagePath(string $appName, string|false $appPath, string $file, string $basename): ?string { - if ($appPath === false) { + private function getThemingImageOverridePath(string $appName, string $file): ?string { + $installed = $this->config->getSystemValueBool('installed', false); + if (!$installed) { return null; } - $serverBasePath = $appPath . "/img/"; - $webBasePath = $this->getAppManager()->getAppWebPath($appName) . "/img/"; - - return $this->resolveImagePathFromBases($serverBasePath, $webBasePath, $file, $basename); - } + $themingDefaults = Server::get('ThemingDefaults'); - /** - * Look up legacy app specific image assets located directly underneath $serverRoot - * FIXME: This may not be needed any longer. - */ - private function resolveLegacyAppImagePath(string $appName, string $file, string $basename): ?string { - if ($appName === '') { + if (!$themingDefaults instanceof ThemingDefaults) { return null; } - $serverBasePath = \OC::$SERVERROOT . "/" . $appName . "/img/"; - $webBasePath = \OC::$WEBROOT . "/" . $appName . "/img/"; - - return $this->resolveImagePathFromBases($serverBasePath, $webBasePath, $file, $basename); + $themingImagePath = $themingDefaults->replaceImagePath($appName, $file); + return $themingImagePath ?: null; } /** - * Look up core image assets + * Resolves image paths from app-provided and core fallback locations. + * + * Lookup order is: + * - app path: $appPath/img/ + * - legacy app path: /$appName/img/ + * - core path: /core/img/ + * + * FIXME: The legacy app path (located directly underneath $serverRoot) may not be needed any longer. */ - private function resolveCoreImagePath(string $file, string $basename): ?string { - $serverBasePath = \OC::$SERVERROOT . "/core/img/"; - $webBasePath = \OC::$WEBROOT . "/core/img/"; - - return $this->resolveImagePathFromBases($serverBasePath, $webBasePath, $file, $basename); + private function resolveAppOrCoreImagePath(string $appName, string|false $appPath, string $file, string $basename): ?string { + return ($appPath === false ? null : $this->resolveImagePathFromBases( + $appPath . '/img/', + $this->getAppManager()->getAppWebPath($appName) . '/img/', + $file, + $basename, + )) ?? ($appName === '' ? null : $this->resolveImagePathFromBases( + \OC::$SERVERROOT . "/$appName/img/", + \OC::$WEBROOT . "/$appName/img/", + $file, + $basename, + )) ?? $this->resolveImagePathFromBases( + \OC::$SERVERROOT . '/core/img/', + \OC::$WEBROOT . '/core/img/', + $file, + $basename, + ); } + /** + * At each lookup location, the requested filename is checked first as-is. + * If it is missing, a same-basename PNG may be returned, but only when a + * same-basename SVG is also missing at that location. If the requested + * file is missing and an SVG variant exists, lookup continues to the next + * location instead of falling back to PNG there. + * + * FIXME: The PNG fallback behavior matches the current implementation, + * but the policy is odd and may deserve separate review. + */ private function resolveImagePathFromBases(string $serverBasePath, string $webBasePath, string $file, string $basename): ?string { $filePath = $serverBasePath . $file; if (file_exists($filePath)) { @@ -325,22 +317,6 @@ private function resolveImagePathFromBases(string $serverBasePath, string $webBa return null; } - - private function getThemingImageOverridePath(string $appName, string $file): ?string { - $installed = $this->config->getSystemValueBool('installed', false); - if (!$installed) { - return null; - } - - $themingDefaults = Server::get('ThemingDefaults'); - - if (!$themingDefaults instanceof ThemingDefaults) { - return null; - } - - $themingImagePath = $themingDefaults->replaceImagePath($appName, $file); - return $themingImagePath ?: null; - } /** * Makes an URL absolute From c94f7ddbd3f5ad19f8e6a09b6a3419d808849926 Mon Sep 17 00:00:00 2001 From: Josh Date: Mon, 1 Jun 2026 21:35:57 -0400 Subject: [PATCH 3/4] chore(URLGenerator): keep theming enabled check to match existing behavior Signed-off-by: Josh --- lib/private/URLGenerator.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/private/URLGenerator.php b/lib/private/URLGenerator.php index 475b470b6b07b..d95d66d6ed766 100644 --- a/lib/private/URLGenerator.php +++ b/lib/private/URLGenerator.php @@ -116,7 +116,10 @@ public function linkToOCSRouteAbsolute(string $routeName, array $arguments = []) */ #[\Override] public function linkTo(string $appName, string $file, array $args = []): string { - $frontControllerActive = ($this->config->getSystemValueBool('htaccess.IgnoreFrontController', false) || getenv('front_controller_active') === 'true'); + $frontControllerActive = ( + $this->config->getSystemValueBool('htaccess.IgnoreFrontController', false) + || getenv('front_controller_active') === 'true' + ); if ($appName !== '') { $app_path = $this->getAppManager()->getAppPath($appName); @@ -254,8 +257,13 @@ private function getThemingImageOverridePath(string $appName, string $file): ?st return null; } - $themingDefaults = Server::get('ThemingDefaults'); + $themingEnabled = $this->getAppManager()->isEnabledForUser('theming'); + // TODO: Confirm this is really (still?) needed; matches current policy. + if (!$themingEnabled) { + return null; + } + $themingDefaults = Server::get('ThemingDefaults'); if (!$themingDefaults instanceof ThemingDefaults) { return null; } From 0c73423e9961fddb9bcb78112d8407f5c4abbf3b Mon Sep 17 00:00:00 2001 From: Josh Date: Mon, 1 Jun 2026 21:41:37 -0400 Subject: [PATCH 4/4] chore(URLGenerator): fixup conflict Signed-off-by: Josh --- lib/private/URLGenerator.php | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/private/URLGenerator.php b/lib/private/URLGenerator.php index d95d66d6ed766..217b3725d2bcf 100644 --- a/lib/private/URLGenerator.php +++ b/lib/private/URLGenerator.php @@ -6,6 +6,7 @@ * SPDX-FileCopyrightText: 2016 ownCloud, Inc. * SPDX-License-Identifier: AGPL-3.0-only */ + namespace OC; use OC\Route\Router;