Passed
Push — master ( 78e3fe...4914df )
by
unknown
11:31
created

ThemeHelper::resolveAssetTheme()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 9
c 0
b 0
f 0
nc 5
nop 1
dl 0
loc 16
rs 9.9666
1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
declare(strict_types=1);
6
7
namespace Chamilo\CoreBundle\Helpers;
8
9
use Chamilo\CoreBundle\Entity\AccessUrl;
10
use Chamilo\CoreBundle\Settings\SettingsManager;
11
use Chamilo\CourseBundle\Settings\SettingsCourseManager;
12
use League\Flysystem\FilesystemException;
13
use League\Flysystem\FilesystemOperator;
14
use League\MimeTypeDetection\ExtensionMimeTypeDetector;
15
use Symfony\Component\DependencyInjection\Attribute\Autowire;
16
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
17
use Symfony\Component\Routing\RouterInterface;
18
19
use const DIRECTORY_SEPARATOR;
20
21
final class ThemeHelper
22
{
23
    /**
24
     * Absolute last resort if nothing else is configured.
25
     * Kept for backward compatibility.
26
     */
27
    public const DEFAULT_THEME = 'chamilo';
28
29
    public function __construct(
30
        private readonly AccessUrlHelper $accessUrlHelper,
31
        private readonly SettingsManager $settingsManager,
32
        private readonly UserHelper $userHelper,
33
        private readonly CidReqHelper $cidReqHelper,
34
        private readonly SettingsCourseManager $settingsCourseManager,
35
        private readonly RouterInterface $router,
36
        #[Autowire(service: 'oneup_flysystem.themes_filesystem')]
37
        private readonly FilesystemOperator $filesystem,
38
        // Injected from services.yaml (.env -> THEME_FALLBACK)
39
        #[Autowire(param: 'theme_fallback')]
40
        private readonly string $themeFallback = '',
41
    ) {}
42
43
    /**
44
     * Returns the slug of the theme that should be applied on the current page.
45
     * Precedence:
46
     * 1) Active theme bound to current AccessUrl (DB relation)
47
     * 2) User-selected theme (if enabled)
48
     * 3) Course/LP theme (if enabled)
49
     * 4) THEME_FALLBACK from .env
50
     * 5) DEFAULT_THEME ('chamilo')
51
     */
52
    public function getVisualTheme(): string
53
    {
54
        static $visualTheme;
55
56
        global $lp_theme_css;
57
58
        if (isset($visualTheme)) {
59
            return $visualTheme;
60
        }
61
62
        $visualTheme = null;
63
        $accessUrl = $this->accessUrlHelper->getCurrent();
64
65
        // 1) Active theme bound to current AccessUrl (DB relation)
66
        if ($accessUrl instanceof AccessUrl) {
67
            $visualTheme = $accessUrl->getActiveColorTheme()?->getColorTheme()->getSlug();
68
        }
69
70
        // 2) User-selected theme (if setting is enabled)
71
        if ('true' === $this->settingsManager->getSetting('profile.user_selected_theme')) {
72
            $visualTheme = $this->userHelper->getCurrent()?->getTheme() ?: $visualTheme;
73
        }
74
75
        // 3) Course theme / Learning path theme (if setting is enabled)
76
        if ('true' === $this->settingsManager->getSetting('course.allow_course_theme')) {
77
            $course = $this->cidReqHelper->getCourseEntity();
78
79
            if ($course) {
80
                $this->settingsCourseManager->setCourse($course);
81
82
                $courseTheme = (string) $this->settingsCourseManager->getCourseSettingValue('course_theme');
83
                if ($courseTheme !== '') {
84
                    $visualTheme = $courseTheme;
85
                }
86
87
                if (1 === (int) $this->settingsCourseManager->getCourseSettingValue('allow_learning_path_theme')) {
88
                    if (!empty($lp_theme_css)) {
89
                        $visualTheme = $lp_theme_css;
90
                    }
91
                }
92
            }
93
        }
94
95
        // 4) .env fallback if still empty
96
        if ($visualTheme === null || $visualTheme === '') {
97
            $fallback = \trim((string) $this->themeFallback);
98
            $visualTheme = $fallback !== '' ? $fallback : self::DEFAULT_THEME;
99
        }
100
101
        return $visualTheme;
102
    }
103
104
    /**
105
     * Decide the theme in which the requested asset actually exists.
106
     * This prevents 404 when the file is only present in DEFAULT_THEME.
107
     */
108
    private function resolveAssetTheme(string $path): ?string
109
    {
110
        $visual = $this->getVisualTheme();
111
112
        try {
113
            if ($this->filesystem->fileExists($visual.DIRECTORY_SEPARATOR.$path)) {
114
                return $visual;
115
            }
116
            if ($this->filesystem->fileExists(self::DEFAULT_THEME.DIRECTORY_SEPARATOR.$path)) {
117
                return self::DEFAULT_THEME;
118
            }
119
        } catch (FilesystemException) {
120
            return null;
121
        }
122
123
        return null;
124
    }
125
126
    /**
127
     * Resolves a themed file location checking the selected theme first,
128
     * then falling back to DEFAULT_THEME as a last resort.
129
     */
130
    public function getFileLocation(string $path): ?string
131
    {
132
        $assetTheme = $this->resolveAssetTheme($path);
133
        if ($assetTheme === null) {
134
            return null;
135
        }
136
137
        return $assetTheme.DIRECTORY_SEPARATOR.$path;
138
    }
139
140
    /**
141
     * Build a URL for the theme asset, using the theme where the file actually exists.
142
     */
143
    public function getThemeAssetUrl(string $path, bool $absoluteUrl = false): string
144
    {
145
        $assetTheme = $this->resolveAssetTheme($path);
146
        if ($assetTheme === null) {
147
            return '';
148
        }
149
150
        return $this->router->generate(
151
            'theme_asset',
152
            ['name' => $assetTheme, 'path' => $path],
153
            $absoluteUrl ? UrlGeneratorInterface::ABSOLUTE_URL : UrlGeneratorInterface::ABSOLUTE_PATH
154
        );
155
    }
156
157
    /**
158
     * Convenience helper to emit a <link> tag for a theme asset.
159
     */
160
    public function getThemeAssetLinkTag(string $path, bool $absoluteUrl = false): string
161
    {
162
        $url = $this->getThemeAssetUrl($path, $absoluteUrl);
163
        if ($url === '') {
164
            return '';
165
        }
166
167
        return \sprintf('<link rel="stylesheet" href="%s">', $url);
168
    }
169
170
    /**
171
     * Read raw contents from the themed filesystem.
172
     */
173
    public function getAssetContents(string $path): string
174
    {
175
        try {
176
            $fullPath = $this->getFileLocation($path);
177
            if ($fullPath) {
178
                $stream = $this->filesystem->readStream($fullPath);
179
                $contents = \is_resource($stream) ? stream_get_contents($stream) : false;
180
                if (\is_resource($stream)) {
181
                    fclose($stream);
182
                }
183
                return $contents !== false ? $contents : '';
184
            }
185
        } catch (FilesystemException) {
186
            return '';
187
        }
188
189
        return '';
190
    }
191
192
    /**
193
     * Return a Base64-encoded data URI for the given themed asset.
194
     */
195
    public function getAssetBase64Encoded(string $path): string
196
    {
197
        try {
198
            $fullPath = $this->getFileLocation($path);
199
            if ($fullPath) {
200
                $detector = new ExtensionMimeTypeDetector();
201
                $mimeType = (string) $detector->detectMimeTypeFromFile($fullPath);
202
                $data = $this->getAssetContents($path);
203
204
                return $data !== ''
205
                    ? 'data:'.$mimeType.';base64,'.base64_encode($data)
206
                    : '';
207
            }
208
        } catch (FilesystemException) {
209
            return '';
210
        }
211
212
        return '';
213
    }
214
215
    /**
216
     * Return the preferred logo URL for current theme (header/email),
217
     * falling back to DEFAULT_THEME if needed.
218
     */
219
    public function getPreferredLogoUrl(string $type = 'header', bool $absoluteUrl = false): string
220
    {
221
        $candidates = $type === 'email'
222
            ? ['images/email-logo.svg', 'images/email-logo.png']
223
            : ['images/header-logo.svg', 'images/header-logo.png'];
224
225
        foreach ($candidates as $relPath) {
226
            $url = $this->getThemeAssetUrl($relPath, $absoluteUrl);
227
            if ($url !== '') {
228
                return $url;
229
            }
230
        }
231
232
        return '';
233
    }
234
}
235