Skip to content
Open
Show file tree
Hide file tree
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
10 changes: 8 additions & 2 deletions apps/theming/lib/Controller/ThemingController.php
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,13 @@ public function getImage(string $key, bool $useSvg = true) {
$csp->allowInlineStyle();
$response->setContentSecurityPolicy($csp);
$response->cacheFor(3600);
$response->addHeader('Content-Type', $file->getMimeType());
// The original stored file has no extension (e.g. "logo"), so getMimeType() returns
// application/octet-stream for it. Use the config-stored MIME type for the original
// file, and getMimeType() only for converted files which have a proper extension.
$mimeType = $file->getName() === $key
? $this->appConfig->getAppValueString($key . 'Mime', '')
: $file->getMimeType();
$response->addHeader('Content-Type', $mimeType);
$response->addHeader('Content-Disposition', 'attachment; filename="' . $key . '"');
return $response;
}
Expand Down Expand Up @@ -450,7 +456,7 @@ public function getThemeStylesheet(string $themeId, bool $plain = false, bool $w
#[BruteForceProtection(action: 'manifest')]
#[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
public function getManifest(string $app): JSONResponse {
$cacheBusterValue = $this->config->getAppValue('theming', 'cachebuster', '0');
$cacheBusterValue = $this->appConfig->getAppValueString('cachebuster', '0');
if ($app === 'core' || $app === 'settings') {
$name = $this->themingDefaults->getName();
$shortName = $this->themingDefaults->getName();
Expand Down
34 changes: 11 additions & 23 deletions apps/theming/lib/ImageManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,31 +85,14 @@ public function getImageUrlAbsolute(string $key): string {
public function getImage(string $key, bool $useSvg = true): ISimpleFile {
$mime = $this->config->getAppValue('theming', $key . 'Mime', '');
$folder = $this->getRootFolder()->getFolder('images');
$useSvg = $useSvg && $this->canConvert('SVG');

if ($mime === '' || !$folder->fileExists($key)) {
throw new NotFoundException();
}
// if SVG was requested and is supported
if ($useSvg) {
if (!$folder->fileExists($key . '.svg')) {
try {
$finalIconFile = new \Imagick();
$finalIconFile->setBackgroundColor('none');
$finalIconFile->readImageBlob($folder->getFile($key)->getContent());
$finalIconFile->setImageFormat('SVG');
$svgFile = $folder->newFile($key . '.svg');
$svgFile->putContent($finalIconFile->getImageBlob());
return $svgFile;
} catch (\ImagickException $e) {
$this->logger->info('The image was requested to be no SVG file, but converting it to SVG failed: ' . $e->getMessage());
}
} else {
return $folder->getFile($key . '.svg');
}
}
// if SVG was not requested, but PNG is supported
if (!$useSvg && $this->canConvert('PNG')) {
// only convert SVG originals to PNG when SVG output is not desired;
// converting raster images to SVG produces broken output and is not useful
$isOriginalSvg = ($mime === 'image/svg+xml' || $mime === 'image/svg');
if ($isOriginalSvg && !$useSvg && $this->canConvert('SVG') && $this->canConvert('PNG')) {
if (!$folder->fileExists($key . '.png')) {
try {
$finalIconFile = new \Imagick();
Expand All @@ -120,13 +103,12 @@ public function getImage(string $key, bool $useSvg = true): ISimpleFile {
$pngFile->putContent($finalIconFile->getImageBlob());
return $pngFile;
} catch (\ImagickException $e) {
$this->logger->info('The image was requested to be no SVG file, but converting it to PNG failed: ' . $e->getMessage());
$this->logger->info('Converting SVG to PNG failed: ' . $e->getMessage());
}
} else {
return $folder->getFile($key . '.png');
}
}
// fallback to the original file
return $folder->getFile($key);
}

Expand Down Expand Up @@ -214,6 +196,12 @@ public function delete(string $key): void {
} catch (NotFoundException $e) {
} catch (NotPermittedException $e) {
}
try {
$file = $this->getRootFolder()->getFolder('images')->getFile($key . '.svg');
$file->delete();
} catch (NotFoundException $e) {
} catch (NotPermittedException $e) {
}

if ($key === 'logo') {
$this->config->deleteAppValue('theming', 'logoDimensions');
Expand Down
29 changes: 26 additions & 3 deletions apps/theming/tests/Controller/ThemingControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -669,6 +669,29 @@ public function testGetLogo(): void {
}


public function testGetLogoOriginalFile(): void {
$file = $this->createMock(ISimpleFile::class);
$file->method('getName')->willReturn('logo');
$file->method('getMTime')->willReturn(42);
$this->imageManager->expects($this->once())
->method('getImage')
->willReturn($file);
$this->appConfig
->expects($this->once())
->method('getAppValueString')
->with('logoMime', '')
->willReturn('image/png');

@$expected = new FileDisplayResponse($file);
$expected->cacheFor(3600);
$expected->addHeader('Content-Type', 'image/png');
$expected->addHeader('Content-Disposition', 'attachment; filename="logo"');
$csp = new ContentSecurityPolicy();
$csp->allowInlineStyle();
$expected->setContentSecurityPolicy($csp);
@$this->assertEquals($expected, $this->themingController->getImage('logo', false));
}

public function testGetLoginBackgroundNotExistent(): void {
$this->imageManager->method('getImage')
->with($this->equalTo('background'))
Expand Down Expand Up @@ -711,10 +734,10 @@ public static function dataGetManifest(): array {

#[\PHPUnit\Framework\Attributes\DataProvider(methodName: 'dataGetManifest')]
public function testGetManifest(bool $standalone): void {
$this->config
$this->appConfig
->expects($this->once())
->method('getAppValue')
->with('theming', 'cachebuster', '0')
->method('getAppValueString')
->with('cachebuster', '0')
->willReturn('0');
$this->themingDefaults
->expects($this->any())
Expand Down
78 changes: 60 additions & 18 deletions apps/theming/tests/ImageManagerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,26 +79,14 @@ public function mockGetImage($key, $file) {
->with('logo')
->willThrowException(new NotFoundException());
} else {
$file->expects($this->once())
->method('getContent')
->willReturn(file_get_contents(__DIR__ . '/../../../tests/data/testimage.png'));
$folder->expects($this->exactly(2))
$folder->expects($this->once())
->method('fileExists')
->willReturnMap([
['logo', true],
['logo.png', false],
]);
->with('logo')
->willReturn(true);
$folder->expects($this->once())
->method('getFile')
->with('logo')
->willReturn($file);
$newFile = $this->createMock(ISimpleFile::class);
$folder->expects($this->once())
->method('newFile')
->with('logo.png')
->willReturn($newFile);
$newFile->expects($this->once())
->method('putContent');
$this->rootFolder->expects($this->once())
->method('getFolder')
->with('images')
Expand Down Expand Up @@ -149,15 +137,69 @@ public function testGetImageUrlAbsolute(): void {
}

public function testGetImage(): void {
$this->checkImagick();
$this->config->expects($this->once())
->method('getAppValue')->with('theming', 'logoMime', false)
->willReturn('png');
->method('getAppValue')->with('theming', 'logoMime', '')
->willReturn('image/png');
$file = $this->createMock(ISimpleFile::class);
$this->mockGetImage('logo', $file);
$this->assertEquals($file, $this->imageManager->getImage('logo', false));
}

public function testGetImageSvgToSvg(): void {
$this->config->expects($this->once())
->method('getAppValue')->with('theming', 'logoMime', '')
->willReturn('image/svg+xml');
$folder = $this->createMock(ISimpleFolder::class);
$file = $this->createMock(ISimpleFile::class);
$folder->expects($this->once())
->method('fileExists')
->with('logo')
->willReturn(true);
$folder->expects($this->once())
->method('getFile')
->with('logo')
->willReturn($file);
$this->rootFolder->expects($this->once())
->method('getFolder')
->with('images')
->willReturn($folder);
$this->assertEquals($file, $this->imageManager->getImage('logo', true));
}

public function testGetImageSvgToPng(): void {
$this->checkImagick();
$this->config->expects($this->once())
->method('getAppValue')->with('theming', 'logoMime', '')
->willReturn('image/svg+xml');
$folder = $this->createMock(ISimpleFolder::class);
$svgFile = $this->createMock(ISimpleFile::class);
$pngFile = $this->createMock(ISimpleFile::class);
$svgFile->expects($this->once())
->method('getContent')
->willReturn(file_get_contents(__DIR__ . '/../../../core/img/logo/logo.svg'));
$folder->expects($this->exactly(2))
->method('fileExists')
->willReturnMap([
['logo', true],
['logo.png', false],
]);
$folder->expects($this->once())
->method('getFile')
->with('logo')
->willReturn($svgFile);
$folder->expects($this->once())
->method('newFile')
->with('logo.png')
->willReturn($pngFile);
$pngFile->expects($this->once())
->method('putContent');
$this->rootFolder->expects($this->once())
->method('getFolder')
->with('images')
->willReturn($folder);
$this->assertEquals($pngFile, $this->imageManager->getImage('logo', false));
}


public function testGetImageUnset(): void {
$this->expectException(NotFoundException::class);
Expand Down
5 changes: 0 additions & 5 deletions build/psalm-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2583,11 +2583,6 @@
<code><![CDATA[getAppValue]]></code>
</DeprecatedMethod>
</file>
<file src="apps/theming/lib/Controller/ThemingController.php">
<DeprecatedMethod>
<code><![CDATA[getAppValue]]></code>
</DeprecatedMethod>
</file>
<file src="apps/theming/lib/Controller/UserThemeController.php">
<DeprecatedMethod>
<code><![CDATA[getUserValue]]></code>
Expand Down
Loading