Passed
Push — master ( ed7adc...d02aa6 )
by Yannick
09:57
created

ThemeController::sanitizeSvg()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 5
nc 1
nop 1
dl 0
loc 7
rs 10
c 0
b 0
f 0
1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
declare(strict_types=1);
6
7
namespace Chamilo\CoreBundle\Controller;
8
9
use Chamilo\CoreBundle\Helpers\ThemeHelper;
10
use League\Flysystem\FilesystemOperator;
11
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
12
use Symfony\Component\DependencyInjection\Attribute\Autowire;
13
use Symfony\Component\HttpFoundation\JsonResponse;
14
use Symfony\Component\HttpFoundation\Request;
15
use Symfony\Component\HttpFoundation\Response;
16
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
17
use Symfony\Component\HttpFoundation\StreamedResponse;
18
use Symfony\Component\Routing\Attribute\Route;
19
use Symfony\Component\Security\Http\Attribute\IsGranted;
20
21
#[Route('/themes')]
22
class ThemeController extends AbstractController
23
{
24
    public function __construct(
25
        private readonly ThemeHelper $themeHelper
26
    ) {}
27
28
    /**
29
     * Upload logos (SVG/PNG) for theme header/email.
30
     */
31
    #[Route(
32
        '/{slug}/logos',
33
        name: 'theme_logos_upload',
34
        methods: ['POST'],
35
        priority: 10
36
    )]
37
    #[IsGranted('ROLE_ADMIN')]
38
    public function uploadLogos(
39
        string $slug,
40
        Request $request,
41
        #[Autowire(service: 'oneup_flysystem.themes_filesystem')] FilesystemOperator $fs
42
    ): JsonResponse {
43
        $map = [
44
            'header_svg' => 'images/header-logo.svg',
45
            'header_png' => 'images/header-logo.png',
46
            'email_svg'  => 'images/email-logo.svg',
47
            'email_png'  => 'images/email-logo.png',
48
        ];
49
50
        if (!$fs->directoryExists($slug)) {
51
            $fs->createDirectory($slug);
52
        }
53
        if (!$fs->directoryExists("$slug/images")) {
54
            $fs->createDirectory("$slug/images");
55
        }
56
57
        $results = [];
58
59
        foreach ($map as $field => $relativePath) {
60
            $file = $request->files->get($field);
61
            if (!$file) { $results[$field] = 'skipped'; continue; }
62
63
            $ext  = strtolower((string) $file->getClientOriginalExtension());
64
            $mime = (string) $file->getMimeType();
65
66
            // SVG
67
            if (str_ends_with($field, '_svg')) {
68
                if ($mime !== 'image/svg+xml' && $ext !== 'svg') {
69
                    $results[$field] = 'invalid_mime'; continue;
70
                }
71
                $content = @file_get_contents($file->getPathname()) ?: '';
72
                $content = $this->sanitizeSvg($content);
73
                $this->ensureDir($fs, $slug.'/images');
74
                $fs->write($slug.'/'.$relativePath, $content);
75
                $results[$field] = 'uploaded'; continue;
76
            }
77
78
            // PNG
79
            if ($mime !== 'image/png' && $ext !== 'png') {
80
                $results[$field] = 'invalid_mime'; continue;
81
            }
82
            $info = @getimagesize($file->getPathname());
83
            if (!$info) { $results[$field] = 'invalid_image'; continue; }
84
            [$w, $h] = $info;
85
86
            if ($field === 'header_png' && ($w > 190 || $h > 60)) {
87
                $results[$field] = 'invalid_dimensions_header_png'; continue;
88
            }
89
90
            $this->ensureDir($fs, $slug.'/images');
91
            $stream = fopen($file->getPathname(), 'rb');
92
            $fs->writeStream($slug.'/'.$relativePath, $stream);
93
            if (is_resource($stream)) { fclose($stream); }
94
95
            $results[$field] = 'uploaded';
96
        }
97
98
        return $this->json(['status' => 'ok', 'results' => $results], Response::HTTP_CREATED);
99
    }
100
101
    /**
102
     * Delete a specific logo.
103
     */
104
    #[Route(
105
        '/{slug}/logos/{type}',
106
        name: 'theme_logos_delete',
107
        requirements: ['type' => 'header_svg|header_png|email_svg|email_png'],
108
        methods: ['DELETE'],
109
        priority: 10
110
    )]
111
    #[IsGranted('ROLE_ADMIN')]
112
    public function deleteLogo(
113
        string $slug,
114
        string $type,
115
        #[Autowire(service: 'oneup_flysystem.themes_filesystem')] FilesystemOperator $fs
116
    ): JsonResponse {
117
        $map = [
118
            'header_svg' => 'images/header-logo.svg',
119
            'header_png' => 'images/header-logo.png',
120
            'email_svg'  => 'images/email-logo.svg',
121
            'email_png'  => 'images/email-logo.png',
122
        ];
123
124
        $path = $slug.'/'.$map[$type];
125
        if ($fs->fileExists($path)) {
126
            $fs->delete($path);
127
        }
128
129
        return $this->json(['status' => 'deleted']);
130
    }
131
132
    /**
133
     * Serve an asset from the theme.
134
     * - If ?strict=1 → only serves {name}/{path}. If it doesn't exist, 404 (no fallback).
135
     * - If strict isn't available → tries {name}/{path} and then the default theme.
136
     */
137
    #[Route(
138
        '/{name}/{path}',
139
        name: 'theme_asset',
140
        requirements: ['path' => '.+'],
141
        methods: ['GET'],
142
        priority: -10
143
    )]
144
    public function index(
145
        string $name,
146
        string $path,
147
        Request $request,
148
        #[Autowire(service: 'oneup_flysystem.themes_filesystem')] FilesystemOperator $filesystem
149
    ): Response {
150
        $themeDir = basename($name);
151
        $strict   = $request->query->getBoolean('strict', false);
152
153
        if (!$filesystem->directoryExists($themeDir)) {
154
            throw $this->createNotFoundException('The folder name does not exist.');
155
        }
156
157
        $filePath = null;
158
159
        if ($strict) {
160
            $candidate = $themeDir.DIRECTORY_SEPARATOR.$path;
161
            if ($filesystem->fileExists($candidate)) {
162
                $filePath = $candidate;
163
            } else {
164
                throw $this->createNotFoundException('The requested file does not exist.');
165
            }
166
        } else {
167
            $candidates = [
168
                $themeDir.DIRECTORY_SEPARATOR.$path,
169
                ThemeHelper::DEFAULT_THEME.DIRECTORY_SEPARATOR.$path,
170
            ];
171
            foreach ($candidates as $c) {
172
                if ($filesystem->fileExists($c)) {
173
                    $filePath = $c;
174
                    break;
175
                }
176
            }
177
            if (!$filePath) {
178
                throw $this->createNotFoundException('The requested file does not exist.');
179
            }
180
        }
181
182
        $response = new StreamedResponse(function () use ($filesystem, $filePath): void {
183
            $out = fopen('php://output', 'wb');
184
            $in  = $filesystem->readStream($filePath);
185
            stream_copy_to_stream($in, $out);
186
            fclose($out);
187
            fclose($in);
188
        });
189
190
        $mimeType = $filesystem->mimeType($filePath) ?: 'application/octet-stream';
191
        if (str_ends_with(strtolower($filePath), '.svg')) {
192
            $mimeType = 'image/svg+xml';
193
        }
194
195
        $disposition = $response->headers->makeDisposition(
196
            ResponseHeaderBag::DISPOSITION_INLINE,
197
            basename($path)
198
        );
199
200
        $response->headers->set('Content-Disposition', $disposition);
201
        $response->headers->set('Content-Type', $mimeType);
202
        $response->headers->set('Cache-Control', 'no-store');
203
204
        return $response;
205
    }
206
207
    private function ensureDir(FilesystemOperator $fs, string $dir): void
208
    {
209
        if (!$fs->directoryExists($dir)) {
210
            $fs->createDirectory($dir);
211
        }
212
    }
213
214
    private function sanitizeSvg(string $svg): string
215
    {
216
        $svg = preg_replace('#<script[^>]*>.*?</script>#is', '', $svg) ?? $svg;
217
        $svg = preg_replace('/ on\w+="[^"]*"/i', '', $svg) ?? $svg;
218
        $svg = preg_replace("/ on\w+='[^']*'/i", '', $svg) ?? $svg;
219
        $svg = preg_replace('/xlink:href=["\']\s*javascript:[^"\']*["\']/i', 'xlink:href="#"', $svg) ?? $svg;
220
        return $svg;
221
    }
222
}
223