diff --git a/src/CoreBundle/Helpers/ThemeHelper.php b/src/CoreBundle/Helpers/ThemeHelper.php
index df981a4bde4..184bbabb34d 100644
--- a/src/CoreBundle/Helpers/ThemeHelper.php
+++ b/src/CoreBundle/Helpers/ThemeHelper.php
@@ -20,6 +20,10 @@
final class ThemeHelper
{
+ /**
+ * Absolute last resort if nothing else is configured.
+ * Kept for backward compatibility.
+ */
public const DEFAULT_THEME = 'chamilo';
public function __construct(
@@ -31,11 +35,19 @@ public function __construct(
private readonly RouterInterface $router,
#[Autowire(service: 'oneup_flysystem.themes_filesystem')]
private readonly FilesystemOperator $filesystem,
+ // Injected from services.yaml (.env -> THEME_FALLBACK)
+ #[Autowire(param: 'theme_fallback')]
+ private readonly string $themeFallback = '',
) {}
/**
- * Returns the name of the color theme configured to be applied on the current page.
- * The returned name depends on the platform, course or user settings.
+ * Returns the slug of the theme that should be applied on the current page.
+ * Precedence:
+ * 1) Active theme bound to current AccessUrl (DB relation)
+ * 2) User-selected theme (if enabled)
+ * 3) Course/LP theme (if enabled)
+ * 4) THEME_FALLBACK from .env
+ * 5) DEFAULT_THEME ('chamilo')
*/
public function getVisualTheme(): string
{
@@ -50,97 +62,125 @@ public function getVisualTheme(): string
$visualTheme = null;
$accessUrl = $this->accessUrlHelper->getCurrent();
+ // 1) Active theme bound to current AccessUrl (DB relation)
if ($accessUrl instanceof AccessUrl) {
$visualTheme = $accessUrl->getActiveColorTheme()?->getColorTheme()->getSlug();
}
- if ('true' == $this->settingsManager->getSetting('profile.user_selected_theme')) {
- $visualTheme = $this->userHelper->getCurrent()?->getTheme();
+ // 2) User-selected theme (if setting is enabled)
+ if ('true' === $this->settingsManager->getSetting('profile.user_selected_theme')) {
+ $visualTheme = $this->userHelper->getCurrent()?->getTheme() ?: $visualTheme;
}
- if ('true' == $this->settingsManager->getSetting('course.allow_course_theme')) {
+ // 3) Course theme / Learning path theme (if setting is enabled)
+ if ('true' === $this->settingsManager->getSetting('course.allow_course_theme')) {
$course = $this->cidReqHelper->getCourseEntity();
if ($course) {
$this->settingsCourseManager->setCourse($course);
- $visualTheme = $this->settingsCourseManager->getCourseSettingValue('course_theme');
+ $courseTheme = (string) $this->settingsCourseManager->getCourseSettingValue('course_theme');
+ if ($courseTheme !== '') {
+ $visualTheme = $courseTheme;
+ }
if (1 === (int) $this->settingsCourseManager->getCourseSettingValue('allow_learning_path_theme')) {
- $visualTheme = $lp_theme_css;
+ if (!empty($lp_theme_css)) {
+ $visualTheme = $lp_theme_css;
+ }
}
}
}
- if (empty($visualTheme)) {
- $visualTheme = self::DEFAULT_THEME;
+ // 4) .env fallback if still empty
+ if ($visualTheme === null || $visualTheme === '') {
+ $fallback = \trim((string) $this->themeFallback);
+ $visualTheme = $fallback !== '' ? $fallback : self::DEFAULT_THEME;
}
return $visualTheme;
}
/**
- * @throws FilesystemException
+ * Decide the theme in which the requested asset actually exists.
+ * This prevents 404 when the file is only present in DEFAULT_THEME.
*/
- public function getFileLocation(string $path): ?string
+ private function resolveAssetTheme(string $path): ?string
{
- $themeName = $this->getVisualTheme();
-
- $locations = [
- $themeName.DIRECTORY_SEPARATOR.$path,
- self::DEFAULT_THEME.DIRECTORY_SEPARATOR.$path,
- ];
+ $visual = $this->getVisualTheme();
- foreach ($locations as $location) {
- if ($this->filesystem->fileExists($location)) {
- return $location;
+ try {
+ if ($this->filesystem->fileExists($visual.DIRECTORY_SEPARATOR.$path)) {
+ return $visual;
+ }
+ if ($this->filesystem->fileExists(self::DEFAULT_THEME.DIRECTORY_SEPARATOR.$path)) {
+ return self::DEFAULT_THEME;
}
+ } catch (FilesystemException) {
+ return null;
}
return null;
}
+ /**
+ * Resolves a themed file location checking the selected theme first,
+ * then falling back to DEFAULT_THEME as a last resort.
+ */
+ public function getFileLocation(string $path): ?string
+ {
+ $assetTheme = $this->resolveAssetTheme($path);
+ if ($assetTheme === null) {
+ return null;
+ }
+
+ return $assetTheme.DIRECTORY_SEPARATOR.$path;
+ }
+
+ /**
+ * Build a URL for the theme asset, using the theme where the file actually exists.
+ */
public function getThemeAssetUrl(string $path, bool $absoluteUrl = false): string
{
- try {
- if (!$this->getFileLocation($path)) {
- return '';
- }
- } catch (FilesystemException) {
+ $assetTheme = $this->resolveAssetTheme($path);
+ if ($assetTheme === null) {
return '';
}
- $themeName = $this->getVisualTheme();
-
return $this->router->generate(
'theme_asset',
- ['name' => $themeName, 'path' => $path],
+ ['name' => $assetTheme, 'path' => $path],
$absoluteUrl ? UrlGeneratorInterface::ABSOLUTE_URL : UrlGeneratorInterface::ABSOLUTE_PATH
);
}
+ /**
+ * Convenience helper to emit a tag for a theme asset.
+ */
public function getThemeAssetLinkTag(string $path, bool $absoluteUrl = false): string
{
$url = $this->getThemeAssetUrl($path, $absoluteUrl);
-
- if (empty($url)) {
+ if ($url === '') {
return '';
}
return \sprintf('', $url);
}
+ /**
+ * Read raw contents from the themed filesystem.
+ */
public function getAssetContents(string $path): string
{
try {
- if ($fullPath = $this->getFileLocation($path)) {
+ $fullPath = $this->getFileLocation($path);
+ if ($fullPath) {
$stream = $this->filesystem->readStream($fullPath);
-
- $contents = stream_get_contents($stream);
-
- fclose($stream);
-
- return $contents;
+ $contents = \is_resource($stream) ? stream_get_contents($stream) : false;
+ if (\is_resource($stream)) {
+ fclose($stream);
+ }
+ return $contents !== false ? $contents : '';
}
} catch (FilesystemException) {
return '';
@@ -149,14 +189,21 @@ public function getAssetContents(string $path): string
return '';
}
+ /**
+ * Return a Base64-encoded data URI for the given themed asset.
+ */
public function getAssetBase64Encoded(string $path): string
{
try {
- if ($fullPath = $this->getFileLocation($path)) {
+ $fullPath = $this->getFileLocation($path);
+ if ($fullPath) {
$detector = new ExtensionMimeTypeDetector();
$mimeType = (string) $detector->detectMimeTypeFromFile($fullPath);
+ $data = $this->getAssetContents($path);
- return 'data:'.$mimeType.';base64,'.base64_encode($this->getAssetContents($path));
+ return $data !== ''
+ ? 'data:'.$mimeType.';base64,'.base64_encode($data)
+ : '';
}
} catch (FilesystemException) {
return '';
@@ -165,15 +212,19 @@ public function getAssetBase64Encoded(string $path): string
return '';
}
+ /**
+ * Return the preferred logo URL for current theme (header/email),
+ * falling back to DEFAULT_THEME if needed.
+ */
public function getPreferredLogoUrl(string $type = 'header', bool $absoluteUrl = false): string
{
- $candidates = 'email' === $type
+ $candidates = $type === 'email'
? ['images/email-logo.svg', 'images/email-logo.png']
: ['images/header-logo.svg', 'images/header-logo.png'];
foreach ($candidates as $relPath) {
$url = $this->getThemeAssetUrl($relPath, $absoluteUrl);
- if ('' !== $url) {
+ if ($url !== '') {
return $url;
}
}