Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
215 changes: 150 additions & 65 deletions lib/private/URLGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
* SPDX-License-Identifier: AGPL-3.0-only
*/

namespace OC;

use OC\Route\Router;
Expand Down Expand Up @@ -116,7 +117,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);
Expand Down Expand Up @@ -154,93 +158,174 @@ 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 is:
* - legacy filesystem theme assets first
* - then theming app overrides
* - then app and core fallback locations.
*
* If requested filename is unavailable, automatically tries to fallback to
* "BASENAME.{PNG/SVG}", if available.
*
* Returns the path to the image.
* @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()

// Search for image assets in a deterministic order.
$resolvedPath = $this->resolveLegacyThemeImagePath($appName, $file, $basename)
?? $this->getThemingImageOverridePath($appName, $file)
?? $this->resolveAppOrCoreImagePath($appName, $appPath, $file, $basename);

if ($resolvedPath !== null) {
$cache->set($cacheKey, $resolvedPath);
return $resolvedPath;
}

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";
throw new RuntimeException(
'image not found: image:' . $file . ' webroot:' . \OC::$WEBROOT . ' serverroot:' . \OC::$SERVERROOT
);
}

/**
* 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 resolveLegacyThemeImagePath(string $appName, string $file, string $basename): ?string {
$legacyThemeName = \OC_Util::getTheme();
if ($legacyThemeName === '') {
return null;
}

if ($path !== '') {
$cache->set($cacheKey, $path);
return $path;
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,
);
}

/**
* Resolve modern theming app override path, if available.
*/
private function getThemingImageOverridePath(string $appName, string $file): ?string {
$installed = $this->config->getSystemValueBool('installed', false);
if (!$installed) {
return null;
}

throw new RuntimeException('image not found: image:' . $file . ' webroot:' . \OC::$WEBROOT . ' serverroot:' . \OC::$SERVERROOT);
$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;
}

$themingImagePath = $themingDefaults->replaceImagePath($appName, $file);
return $themingImagePath ?: null;
}

/**
* 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 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)) {
return $webBasePath . $file;
}

$svgPath = $serverBasePath . $basename . '.svg';
$pngPath = $serverBasePath . $basename . '.png';
if (!file_exists($svgPath) && file_exists($pngPath)) {
return $webBasePath . $basename . '.png';
}

return null;
}

/**
* Makes an URL absolute
Expand Down