Completed
Push — master ( 92d9ca...929e16 )
by
unknown
38:00
created
apps/theming/tests/Controller/IconControllerTest.php 2 patches
Indentation   +171 added lines, -171 removed lines patch added patch discarded remove patch
@@ -26,175 +26,175 @@
 block discarded – undo
26 26
 use Test\TestCase;
27 27
 
28 28
 class IconControllerTest extends TestCase {
29
-	private IRequest&MockObject $request;
30
-	private ThemingDefaults&MockObject $themingDefaults;
31
-	private ITimeFactory&MockObject $timeFactory;
32
-	private IconBuilder&MockObject $iconBuilder;
33
-	private FileAccessHelper&MockObject $fileAccessHelper;
34
-	private IAppManager&MockObject $appManager;
35
-	private ImageManager&MockObject $imageManager;
36
-	private IconController $iconController;
37
-	private IConfig&MockObject $config;
38
-
39
-	protected function setUp(): void {
40
-		$this->request = $this->createMock(IRequest::class);
41
-		$this->themingDefaults = $this->createMock(ThemingDefaults::class);
42
-		$this->iconBuilder = $this->createMock(IconBuilder::class);
43
-		$this->imageManager = $this->createMock(ImageManager::class);
44
-		$this->fileAccessHelper = $this->createMock(FileAccessHelper::class);
45
-		$this->appManager = $this->createMock(IAppManager::class);
46
-		$this->config = $this->createMock(IConfig::class);
47
-
48
-		$this->timeFactory = $this->createMock(ITimeFactory::class);
49
-		$this->timeFactory->expects($this->any())
50
-			->method('getTime')
51
-			->willReturn(123);
52
-
53
-		$this->overwriteService(ITimeFactory::class, $this->timeFactory);
54
-
55
-		$this->iconController = new IconController(
56
-			'theming',
57
-			$this->request,
58
-			$this->config,
59
-			$this->themingDefaults,
60
-			$this->iconBuilder,
61
-			$this->imageManager,
62
-			$this->fileAccessHelper,
63
-			$this->appManager,
64
-		);
65
-
66
-		parent::setUp();
67
-	}
68
-
69
-	private function iconFileMock($filename, $data): SimpleFile {
70
-		$icon = $this->createMock(File::class);
71
-		$icon->expects($this->any())->method('getContent')->willReturn($data);
72
-		$icon->expects($this->any())->method('getMimeType')->willReturn('image type');
73
-		$icon->expects($this->any())->method('getEtag')->willReturn('my etag');
74
-		$icon->expects($this->any())->method('getName')->willReturn('my name');
75
-		$icon->expects($this->any())->method('getMTime')->willReturn(42);
76
-		$icon->method('getName')->willReturn($filename);
77
-		return new SimpleFile($icon);
78
-	}
79
-
80
-	public function testGetThemedIcon(): void {
81
-		$file = $this->iconFileMock('icon-core-filetypes_folder.svg', 'filecontent');
82
-		$this->imageManager->expects($this->once())
83
-			->method('getCachedImage')
84
-			->with('icon-core-filetypes_folder.svg')
85
-			->willReturn($file);
86
-		$expected = new FileDisplayResponse($file, Http::STATUS_OK, ['Content-Type' => 'image/svg+xml']);
87
-		$expected->cacheFor(86400, false, true);
88
-		$this->assertEquals($expected, $this->iconController->getThemedIcon('core', 'filetypes/folder.svg'));
89
-	}
90
-
91
-	public function testGetFaviconThemed(): void {
92
-		if (!extension_loaded('imagick')) {
93
-			$this->markTestSkipped('Imagemagick is required for dynamic icon generation.');
94
-		}
95
-		$checkImagick = new \Imagick();
96
-		if (count($checkImagick->queryFormats('SVG')) < 1) {
97
-			$this->markTestSkipped('No SVG provider present.');
98
-		}
99
-		$file = $this->iconFileMock('filename', 'filecontent');
100
-		$this->imageManager->expects($this->once())
101
-			->method('getImage', false)
102
-			->with('favicon')
103
-			->willThrowException(new NotFoundException());
104
-		$this->imageManager->expects($this->any())
105
-			->method('canConvert')
106
-			->willReturnMap([
107
-				['SVG', true],
108
-				['PNG', true],
109
-				['ICO', true],
110
-			]);
111
-		$this->imageManager->expects($this->once())
112
-			->method('getCachedImage')
113
-			->willThrowException(new NotFoundException());
114
-		$this->iconBuilder->expects($this->once())
115
-			->method('getFavicon')
116
-			->with('core')
117
-			->willReturn('filecontent');
118
-		$this->imageManager->expects($this->once())
119
-			->method('setCachedImage')
120
-			->willReturn($file);
121
-
122
-		$expected = new FileDisplayResponse($file, Http::STATUS_OK, ['Content-Type' => 'image/x-icon']);
123
-		$expected->cacheFor(86400);
124
-		$this->assertEquals($expected, $this->iconController->getFavicon());
125
-	}
126
-
127
-	public function testGetFaviconDefault(): void {
128
-		$this->imageManager->expects($this->once())
129
-			->method('getImage')
130
-			->with('favicon', false)
131
-			->willThrowException(new NotFoundException());
132
-		$this->imageManager->expects($this->any())
133
-			->method('canConvert')
134
-			->willReturnMap([
135
-				['SVG', false],
136
-				['PNG', false],
137
-				['ICO', false],
138
-			]);
139
-		$fallbackLogo = \OC::$SERVERROOT . '/core/img/favicon.png';
140
-		$this->fileAccessHelper->expects($this->once())
141
-			->method('file_get_contents')
142
-			->with($fallbackLogo)
143
-			->willReturn(file_get_contents($fallbackLogo));
144
-		$expected = new DataDisplayResponse(file_get_contents($fallbackLogo), Http::STATUS_OK, ['Content-Type' => 'image/png']);
145
-		$expected->cacheFor(86400);
146
-		$this->assertEquals($expected, $this->iconController->getFavicon());
147
-	}
148
-
149
-	public function testGetTouchIconDefault(): void {
150
-		if (!extension_loaded('imagick')) {
151
-			$this->markTestSkipped('Imagemagick is required for dynamic icon generation.');
152
-		}
153
-		$checkImagick = new \Imagick();
154
-		if (count($checkImagick->queryFormats('SVG')) < 1) {
155
-			$this->markTestSkipped('No SVG provider present.');
156
-		}
157
-
158
-		$this->imageManager->expects($this->once())
159
-			->method('getImage')
160
-			->willThrowException(new NotFoundException());
161
-		$this->imageManager->expects($this->any())
162
-			->method('canConvert')
163
-			->with('PNG')
164
-			->willReturn(true);
165
-		$this->iconBuilder->expects($this->once())
166
-			->method('getTouchIcon')
167
-			->with('core')
168
-			->willReturn('filecontent');
169
-		$file = $this->iconFileMock('filename', 'filecontent');
170
-		$this->imageManager->expects($this->once())
171
-			->method('getCachedImage')
172
-			->willThrowException(new NotFoundException());
173
-		$this->imageManager->expects($this->once())
174
-			->method('setCachedImage')
175
-			->willReturn($file);
176
-
177
-		$expected = new FileDisplayResponse($file, Http::STATUS_OK, ['Content-Type' => 'image/png']);
178
-		$expected->cacheFor(86400);
179
-		$this->assertEquals($expected, $this->iconController->getTouchIcon());
180
-	}
181
-
182
-	public function testGetTouchIconFail(): void {
183
-		$this->imageManager->expects($this->once())
184
-			->method('getImage')
185
-			->with('favicon')
186
-			->willThrowException(new NotFoundException());
187
-		$this->imageManager->expects($this->any())
188
-			->method('canConvert')
189
-			->with('PNG')
190
-			->willReturn(false);
191
-		$fallbackLogo = \OC::$SERVERROOT . '/core/img/favicon-touch.png';
192
-		$this->fileAccessHelper->expects($this->once())
193
-			->method('file_get_contents')
194
-			->with($fallbackLogo)
195
-			->willReturn(file_get_contents($fallbackLogo));
196
-		$expected = new DataDisplayResponse(file_get_contents($fallbackLogo), Http::STATUS_OK, ['Content-Type' => 'image/png']);
197
-		$expected->cacheFor(86400);
198
-		$this->assertEquals($expected, $this->iconController->getTouchIcon());
199
-	}
29
+    private IRequest&MockObject $request;
30
+    private ThemingDefaults&MockObject $themingDefaults;
31
+    private ITimeFactory&MockObject $timeFactory;
32
+    private IconBuilder&MockObject $iconBuilder;
33
+    private FileAccessHelper&MockObject $fileAccessHelper;
34
+    private IAppManager&MockObject $appManager;
35
+    private ImageManager&MockObject $imageManager;
36
+    private IconController $iconController;
37
+    private IConfig&MockObject $config;
38
+
39
+    protected function setUp(): void {
40
+        $this->request = $this->createMock(IRequest::class);
41
+        $this->themingDefaults = $this->createMock(ThemingDefaults::class);
42
+        $this->iconBuilder = $this->createMock(IconBuilder::class);
43
+        $this->imageManager = $this->createMock(ImageManager::class);
44
+        $this->fileAccessHelper = $this->createMock(FileAccessHelper::class);
45
+        $this->appManager = $this->createMock(IAppManager::class);
46
+        $this->config = $this->createMock(IConfig::class);
47
+
48
+        $this->timeFactory = $this->createMock(ITimeFactory::class);
49
+        $this->timeFactory->expects($this->any())
50
+            ->method('getTime')
51
+            ->willReturn(123);
52
+
53
+        $this->overwriteService(ITimeFactory::class, $this->timeFactory);
54
+
55
+        $this->iconController = new IconController(
56
+            'theming',
57
+            $this->request,
58
+            $this->config,
59
+            $this->themingDefaults,
60
+            $this->iconBuilder,
61
+            $this->imageManager,
62
+            $this->fileAccessHelper,
63
+            $this->appManager,
64
+        );
65
+
66
+        parent::setUp();
67
+    }
68
+
69
+    private function iconFileMock($filename, $data): SimpleFile {
70
+        $icon = $this->createMock(File::class);
71
+        $icon->expects($this->any())->method('getContent')->willReturn($data);
72
+        $icon->expects($this->any())->method('getMimeType')->willReturn('image type');
73
+        $icon->expects($this->any())->method('getEtag')->willReturn('my etag');
74
+        $icon->expects($this->any())->method('getName')->willReturn('my name');
75
+        $icon->expects($this->any())->method('getMTime')->willReturn(42);
76
+        $icon->method('getName')->willReturn($filename);
77
+        return new SimpleFile($icon);
78
+    }
79
+
80
+    public function testGetThemedIcon(): void {
81
+        $file = $this->iconFileMock('icon-core-filetypes_folder.svg', 'filecontent');
82
+        $this->imageManager->expects($this->once())
83
+            ->method('getCachedImage')
84
+            ->with('icon-core-filetypes_folder.svg')
85
+            ->willReturn($file);
86
+        $expected = new FileDisplayResponse($file, Http::STATUS_OK, ['Content-Type' => 'image/svg+xml']);
87
+        $expected->cacheFor(86400, false, true);
88
+        $this->assertEquals($expected, $this->iconController->getThemedIcon('core', 'filetypes/folder.svg'));
89
+    }
90
+
91
+    public function testGetFaviconThemed(): void {
92
+        if (!extension_loaded('imagick')) {
93
+            $this->markTestSkipped('Imagemagick is required for dynamic icon generation.');
94
+        }
95
+        $checkImagick = new \Imagick();
96
+        if (count($checkImagick->queryFormats('SVG')) < 1) {
97
+            $this->markTestSkipped('No SVG provider present.');
98
+        }
99
+        $file = $this->iconFileMock('filename', 'filecontent');
100
+        $this->imageManager->expects($this->once())
101
+            ->method('getImage', false)
102
+            ->with('favicon')
103
+            ->willThrowException(new NotFoundException());
104
+        $this->imageManager->expects($this->any())
105
+            ->method('canConvert')
106
+            ->willReturnMap([
107
+                ['SVG', true],
108
+                ['PNG', true],
109
+                ['ICO', true],
110
+            ]);
111
+        $this->imageManager->expects($this->once())
112
+            ->method('getCachedImage')
113
+            ->willThrowException(new NotFoundException());
114
+        $this->iconBuilder->expects($this->once())
115
+            ->method('getFavicon')
116
+            ->with('core')
117
+            ->willReturn('filecontent');
118
+        $this->imageManager->expects($this->once())
119
+            ->method('setCachedImage')
120
+            ->willReturn($file);
121
+
122
+        $expected = new FileDisplayResponse($file, Http::STATUS_OK, ['Content-Type' => 'image/x-icon']);
123
+        $expected->cacheFor(86400);
124
+        $this->assertEquals($expected, $this->iconController->getFavicon());
125
+    }
126
+
127
+    public function testGetFaviconDefault(): void {
128
+        $this->imageManager->expects($this->once())
129
+            ->method('getImage')
130
+            ->with('favicon', false)
131
+            ->willThrowException(new NotFoundException());
132
+        $this->imageManager->expects($this->any())
133
+            ->method('canConvert')
134
+            ->willReturnMap([
135
+                ['SVG', false],
136
+                ['PNG', false],
137
+                ['ICO', false],
138
+            ]);
139
+        $fallbackLogo = \OC::$SERVERROOT . '/core/img/favicon.png';
140
+        $this->fileAccessHelper->expects($this->once())
141
+            ->method('file_get_contents')
142
+            ->with($fallbackLogo)
143
+            ->willReturn(file_get_contents($fallbackLogo));
144
+        $expected = new DataDisplayResponse(file_get_contents($fallbackLogo), Http::STATUS_OK, ['Content-Type' => 'image/png']);
145
+        $expected->cacheFor(86400);
146
+        $this->assertEquals($expected, $this->iconController->getFavicon());
147
+    }
148
+
149
+    public function testGetTouchIconDefault(): void {
150
+        if (!extension_loaded('imagick')) {
151
+            $this->markTestSkipped('Imagemagick is required for dynamic icon generation.');
152
+        }
153
+        $checkImagick = new \Imagick();
154
+        if (count($checkImagick->queryFormats('SVG')) < 1) {
155
+            $this->markTestSkipped('No SVG provider present.');
156
+        }
157
+
158
+        $this->imageManager->expects($this->once())
159
+            ->method('getImage')
160
+            ->willThrowException(new NotFoundException());
161
+        $this->imageManager->expects($this->any())
162
+            ->method('canConvert')
163
+            ->with('PNG')
164
+            ->willReturn(true);
165
+        $this->iconBuilder->expects($this->once())
166
+            ->method('getTouchIcon')
167
+            ->with('core')
168
+            ->willReturn('filecontent');
169
+        $file = $this->iconFileMock('filename', 'filecontent');
170
+        $this->imageManager->expects($this->once())
171
+            ->method('getCachedImage')
172
+            ->willThrowException(new NotFoundException());
173
+        $this->imageManager->expects($this->once())
174
+            ->method('setCachedImage')
175
+            ->willReturn($file);
176
+
177
+        $expected = new FileDisplayResponse($file, Http::STATUS_OK, ['Content-Type' => 'image/png']);
178
+        $expected->cacheFor(86400);
179
+        $this->assertEquals($expected, $this->iconController->getTouchIcon());
180
+    }
181
+
182
+    public function testGetTouchIconFail(): void {
183
+        $this->imageManager->expects($this->once())
184
+            ->method('getImage')
185
+            ->with('favicon')
186
+            ->willThrowException(new NotFoundException());
187
+        $this->imageManager->expects($this->any())
188
+            ->method('canConvert')
189
+            ->with('PNG')
190
+            ->willReturn(false);
191
+        $fallbackLogo = \OC::$SERVERROOT . '/core/img/favicon-touch.png';
192
+        $this->fileAccessHelper->expects($this->once())
193
+            ->method('file_get_contents')
194
+            ->with($fallbackLogo)
195
+            ->willReturn(file_get_contents($fallbackLogo));
196
+        $expected = new DataDisplayResponse(file_get_contents($fallbackLogo), Http::STATUS_OK, ['Content-Type' => 'image/png']);
197
+        $expected->cacheFor(86400);
198
+        $this->assertEquals($expected, $this->iconController->getTouchIcon());
199
+    }
200 200
 }
Please login to merge, or discard this patch.
Spacing   +2 added lines, -2 removed lines patch added patch discarded remove patch
@@ -136,7 +136,7 @@  discard block
 block discarded – undo
136 136
 				['PNG', false],
137 137
 				['ICO', false],
138 138
 			]);
139
-		$fallbackLogo = \OC::$SERVERROOT . '/core/img/favicon.png';
139
+		$fallbackLogo = \OC::$SERVERROOT.'/core/img/favicon.png';
140 140
 		$this->fileAccessHelper->expects($this->once())
141 141
 			->method('file_get_contents')
142 142
 			->with($fallbackLogo)
@@ -188,7 +188,7 @@  discard block
 block discarded – undo
188 188
 			->method('canConvert')
189 189
 			->with('PNG')
190 190
 			->willReturn(false);
191
-		$fallbackLogo = \OC::$SERVERROOT . '/core/img/favicon-touch.png';
191
+		$fallbackLogo = \OC::$SERVERROOT.'/core/img/favicon-touch.png';
192 192
 		$this->fileAccessHelper->expects($this->once())
193 193
 			->method('file_get_contents')
194 194
 			->with($fallbackLogo)
Please login to merge, or discard this patch.
apps/theming/tests/Controller/ThemingControllerTest.php 1 patch
Indentation   +727 added lines, -727 removed lines patch added patch discarded remove patch
@@ -35,731 +35,731 @@
 block discarded – undo
35 35
 
36 36
 class ThemingControllerTest extends TestCase {
37 37
 
38
-	private IRequest&MockObject $request;
39
-	private IConfig&MockObject $config;
40
-	private IAppConfig&MockObject $appConfig;
41
-	private ThemingDefaults&MockObject $themingDefaults;
42
-	private IL10N&MockObject $l10n;
43
-	private IAppManager&MockObject $appManager;
44
-	private ImageManager&MockObject $imageManager;
45
-	private IURLGenerator&MockObject $urlGenerator;
46
-	private ThemesService&MockObject $themesService;
47
-	private INavigationManager&MockObject $navigationManager;
48
-
49
-	private ThemingController $themingController;
50
-
51
-	protected function setUp(): void {
52
-		$this->request = $this->createMock(IRequest::class);
53
-		$this->config = $this->createMock(IConfig::class);
54
-		$this->appConfig = $this->createMock(IAppConfig::class);
55
-		$this->themingDefaults = $this->createMock(ThemingDefaults::class);
56
-		$this->l10n = $this->createMock(L10N::class);
57
-		$this->appManager = $this->createMock(IAppManager::class);
58
-		$this->urlGenerator = $this->createMock(IURLGenerator::class);
59
-		$this->imageManager = $this->createMock(ImageManager::class);
60
-		$this->themesService = $this->createMock(ThemesService::class);
61
-		$this->navigationManager = $this->createMock(INavigationManager::class);
62
-
63
-		$timeFactory = $this->createMock(ITimeFactory::class);
64
-		$timeFactory->expects($this->any())
65
-			->method('getTime')
66
-			->willReturn(123);
67
-
68
-		$this->overwriteService(ITimeFactory::class, $timeFactory);
69
-
70
-		$this->themingController = new ThemingController(
71
-			'theming',
72
-			$this->request,
73
-			$this->config,
74
-			$this->appConfig,
75
-			$this->themingDefaults,
76
-			$this->l10n,
77
-			$this->urlGenerator,
78
-			$this->appManager,
79
-			$this->imageManager,
80
-			$this->themesService,
81
-			$this->navigationManager,
82
-		);
83
-
84
-		parent::setUp();
85
-	}
86
-
87
-	public static function dataUpdateStylesheetSuccess(): array {
88
-		return [
89
-			['name', str_repeat('a', 250), 'Saved'],
90
-			['url', 'https://nextcloud.com/' . str_repeat('a', 478), 'Saved'],
91
-			['slogan', str_repeat('a', 500), 'Saved'],
92
-			['primaryColor', '#0082c9', 'Saved', 'primary_color'],
93
-			['primary_color', '#0082C9', 'Saved'],
94
-			['backgroundColor', '#0082C9', 'Saved', 'background_color'],
95
-			['background_color', '#0082C9', 'Saved'],
96
-			['imprintUrl', 'https://nextcloud.com/' . str_repeat('a', 478), 'Saved'],
97
-			['privacyUrl', 'https://nextcloud.com/' . str_repeat('a', 478), 'Saved'],
98
-		];
99
-	}
100
-
101
-	#[\PHPUnit\Framework\Attributes\DataProvider(methodName: 'dataUpdateStylesheetSuccess')]
102
-	public function testUpdateStylesheetSuccess(string $setting, string $value, string $message, ?string $realSetting = null): void {
103
-		$this->themingDefaults
104
-			->expects($this->once())
105
-			->method('set')
106
-			->with($realSetting ?? $setting, $value);
107
-		$this->l10n
108
-			->expects($this->once())
109
-			->method('t')
110
-			->willReturnCallback(function ($str) {
111
-				return $str;
112
-			});
113
-
114
-		$expected = new DataResponse(
115
-			[
116
-				'data'
117
-					=> [
118
-						'message' => $message,
119
-					],
120
-				'status' => 'success',
121
-			]
122
-		);
123
-		$this->assertEquals($expected, $this->themingController->updateStylesheet($setting, $value));
124
-	}
125
-
126
-	public static function dataUpdateStylesheetError(): array {
127
-		$urls = [
128
-			'url' => 'web address',
129
-			'imprintUrl' => 'legal notice address',
130
-			'privacyUrl' => 'privacy policy address',
131
-		];
132
-
133
-		$urlTests = [];
134
-		foreach ($urls as $urlKey => $urlName) {
135
-			// Check length limit
136
-			$urlTests[] = [$urlKey, 'http://example.com/' . str_repeat('a', 501), "The given {$urlName} is too long"];
137
-			// Check potential evil javascript
138
-			$urlTests[] = [$urlKey, 'javascript:alert(1)', "The given {$urlName} is not a valid URL"];
139
-			// Check XSS
140
-			$urlTests[] = [$urlKey, 'https://example.com/"><script/src="alert(\'1\')"><a/href/="', "The given {$urlName} is not a valid URL"];
141
-		}
142
-
143
-		return [
144
-			['name', str_repeat('a', 251), 'The given name is too long'],
145
-			['slogan', str_repeat('a', 501), 'The given slogan is too long'],
146
-			['primary_color', '0082C9', 'The given color is invalid'],
147
-			['primary_color', '#0082Z9', 'The given color is invalid'],
148
-			['primary_color', 'Nextcloud', 'The given color is invalid'],
149
-			['background_color', '0082C9', 'The given color is invalid'],
150
-			['background_color', '#0082Z9', 'The given color is invalid'],
151
-			['background_color', 'Nextcloud', 'The given color is invalid'],
152
-
153
-			['doesnotexist', 'value', 'Invalid setting key'],
154
-
155
-			...$urlTests,
156
-		];
157
-	}
158
-
159
-	#[\PHPUnit\Framework\Attributes\DataProvider(methodName: 'dataUpdateStylesheetError')]
160
-	public function testUpdateStylesheetError(string $setting, string $value, string $message): void {
161
-		$this->themingDefaults
162
-			->expects($this->never())
163
-			->method('set')
164
-			->with($setting, $value);
165
-		$this->l10n
166
-			->expects($this->any())
167
-			->method('t')
168
-			->willReturnCallback(function ($str) {
169
-				return $str;
170
-			});
171
-
172
-		$expected = new DataResponse(
173
-			[
174
-				'data'
175
-					=> [
176
-						'message' => $message,
177
-					],
178
-				'status' => 'error',
179
-			],
180
-			Http::STATUS_BAD_REQUEST
181
-		);
182
-		$this->assertEquals($expected, $this->themingController->updateStylesheet($setting, $value));
183
-	}
184
-
185
-	public function testUpdateLogoNoData(): void {
186
-		$this->request
187
-			->expects($this->once())
188
-			->method('getParam')
189
-			->with('key')
190
-			->willReturn('logo');
191
-		$this->request
192
-			->expects($this->once())
193
-			->method('getUploadedFile')
194
-			->with('image')
195
-			->willReturn(null);
196
-		$this->l10n
197
-			->expects($this->any())
198
-			->method('t')
199
-			->willReturnCallback(function ($str) {
200
-				return $str;
201
-			});
202
-
203
-		$expected = new DataResponse(
204
-			[
205
-				'data'
206
-					=> [
207
-						'message' => 'No file uploaded',
208
-					],
209
-				'status' => 'failure',
210
-			],
211
-			Http::STATUS_UNPROCESSABLE_ENTITY
212
-		);
213
-
214
-		$this->assertEquals($expected, $this->themingController->uploadImage());
215
-	}
216
-
217
-	public function testUploadInvalidUploadKey(): void {
218
-		$this->request
219
-			->expects($this->once())
220
-			->method('getParam')
221
-			->with('key')
222
-			->willReturn('invalid');
223
-		$this->request
224
-			->expects($this->never())
225
-			->method('getUploadedFile');
226
-		$this->l10n
227
-			->expects($this->any())
228
-			->method('t')
229
-			->willReturnCallback(function ($str) {
230
-				return $str;
231
-			});
232
-
233
-		$expected = new DataResponse(
234
-			[
235
-				'data'
236
-					=> [
237
-						'message' => 'Invalid key',
238
-					],
239
-				'status' => 'failure',
240
-			],
241
-			Http::STATUS_BAD_REQUEST
242
-		);
243
-
244
-		$this->assertEquals($expected, $this->themingController->uploadImage());
245
-	}
246
-
247
-	/**
248
-	 * Checks that trying to upload an SVG favicon without imagemagick
249
-	 * results in an unsupported media type response.
250
-	 */
251
-	public function testUploadSVGFaviconWithoutImagemagick(): void {
252
-		$this->imageManager
253
-			->method('shouldReplaceIcons')
254
-			->willReturn(false);
255
-
256
-		$this->request
257
-			->expects($this->once())
258
-			->method('getParam')
259
-			->with('key')
260
-			->willReturn('favicon');
261
-		$this->request
262
-			->expects($this->once())
263
-			->method('getUploadedFile')
264
-			->with('image')
265
-			->willReturn([
266
-				'tmp_name' => __DIR__ . '/../../../../tests/data/testimagelarge.svg',
267
-				'type' => 'image/svg',
268
-				'name' => 'testimagelarge.svg',
269
-				'error' => 0,
270
-			]);
271
-		$this->l10n
272
-			->expects($this->any())
273
-			->method('t')
274
-			->willReturnCallback(function ($str) {
275
-				return $str;
276
-			});
277
-
278
-		$this->imageManager->expects($this->once())
279
-			->method('updateImage')
280
-			->willThrowException(new \Exception('Unsupported image type'));
281
-
282
-		$expected = new DataResponse(
283
-			[
284
-				'data'
285
-					=> [
286
-						'message' => 'Unsupported image type',
287
-					],
288
-				'status' => 'failure'
289
-			],
290
-			Http::STATUS_UNPROCESSABLE_ENTITY
291
-		);
292
-
293
-		$this->assertEquals($expected, $this->themingController->uploadImage());
294
-	}
295
-
296
-	public function testUpdateLogoInvalidMimeType(): void {
297
-		$this->request
298
-			->expects($this->once())
299
-			->method('getParam')
300
-			->with('key')
301
-			->willReturn('logo');
302
-		$this->request
303
-			->expects($this->once())
304
-			->method('getUploadedFile')
305
-			->with('image')
306
-			->willReturn([
307
-				'tmp_name' => __DIR__ . '/../../../../tests/data/lorem.txt',
308
-				'type' => 'application/pdf',
309
-				'name' => 'logo.pdf',
310
-				'error' => 0,
311
-			]);
312
-		$this->l10n
313
-			->expects($this->any())
314
-			->method('t')
315
-			->willReturnCallback(function ($str) {
316
-				return $str;
317
-			});
318
-
319
-		$this->imageManager->expects($this->once())
320
-			->method('updateImage')
321
-			->willThrowException(new \Exception('Unsupported image type'));
322
-
323
-		$expected = new DataResponse(
324
-			[
325
-				'data'
326
-					=> [
327
-						'message' => 'Unsupported image type',
328
-					],
329
-				'status' => 'failure'
330
-			],
331
-			Http::STATUS_UNPROCESSABLE_ENTITY
332
-		);
333
-
334
-		$this->assertEquals($expected, $this->themingController->uploadImage());
335
-	}
336
-
337
-	public static function dataUpdateImages(): array {
338
-		return [
339
-			['image/jpeg', false],
340
-			['image/jpeg', true],
341
-			['image/gif'],
342
-			['image/png'],
343
-			['image/svg+xml'],
344
-			['image/svg']
345
-		];
346
-	}
347
-
348
-	#[\PHPUnit\Framework\Attributes\DataProvider(methodName: 'dataUpdateImages')]
349
-	public function testUpdateLogoNormalLogoUpload(string $mimeType, bool $folderExists = true): void {
350
-		$tmpLogo = Server::get(ITempManager::class)->getTemporaryFolder() . '/logo.svg';
351
-		$destination = Server::get(ITempManager::class)->getTemporaryFolder();
352
-
353
-		touch($tmpLogo);
354
-		copy(__DIR__ . '/../../../../tests/data/testimage.png', $tmpLogo);
355
-		$this->request
356
-			->expects($this->once())
357
-			->method('getParam')
358
-			->with('key')
359
-			->willReturn('logo');
360
-		$this->request
361
-			->expects($this->once())
362
-			->method('getUploadedFile')
363
-			->with('image')
364
-			->willReturn([
365
-				'tmp_name' => $tmpLogo,
366
-				'type' => $mimeType,
367
-				'name' => 'logo.svg',
368
-				'error' => 0,
369
-			]);
370
-		$this->l10n
371
-			->expects($this->any())
372
-			->method('t')
373
-			->willReturnCallback(function ($str) {
374
-				return $str;
375
-			});
376
-
377
-		$this->imageManager->expects($this->once())
378
-			->method('getImageUrl')
379
-			->with('logo')
380
-			->willReturn('imageUrl');
381
-
382
-		$this->imageManager->expects($this->once())
383
-			->method('updateImage');
384
-
385
-		$expected = new DataResponse(
386
-			[
387
-				'data'
388
-					=> [
389
-						'name' => 'logo.svg',
390
-						'message' => 'Saved',
391
-						'url' => 'imageUrl',
392
-					],
393
-				'status' => 'success'
394
-			]
395
-		);
396
-
397
-		$this->assertEquals($expected, $this->themingController->uploadImage());
398
-	}
399
-
400
-	public function testUpdateLogoLoginScreenUpload(): void {
401
-		$tmpLogo = Server::get(ITempManager::class)->getTemporaryFolder() . 'logo.png';
402
-
403
-		touch($tmpLogo);
404
-		copy(__DIR__ . '/../../../../tests/data/desktopapp.png', $tmpLogo);
405
-		$this->request
406
-			->expects($this->once())
407
-			->method('getParam')
408
-			->with('key')
409
-			->willReturn('background');
410
-		$this->request
411
-			->expects($this->once())
412
-			->method('getUploadedFile')
413
-			->with('image')
414
-			->willReturn([
415
-				'tmp_name' => $tmpLogo,
416
-				'type' => 'image/svg+xml',
417
-				'name' => 'logo.svg',
418
-				'error' => 0,
419
-			]);
420
-		$this->l10n
421
-			->expects($this->any())
422
-			->method('t')
423
-			->willReturnCallback(function ($str) {
424
-				return $str;
425
-			});
426
-
427
-		$this->imageManager->expects($this->once())
428
-			->method('updateImage');
429
-
430
-		$this->imageManager->expects($this->once())
431
-			->method('getImageUrl')
432
-			->with('background')
433
-			->willReturn('imageUrl');
434
-		$expected = new DataResponse(
435
-			[
436
-				'data'
437
-					=> [
438
-						'name' => 'logo.svg',
439
-						'message' => 'Saved',
440
-						'url' => 'imageUrl',
441
-					],
442
-				'status' => 'success'
443
-			]
444
-		);
445
-		$this->assertEquals($expected, $this->themingController->uploadImage());
446
-	}
447
-
448
-	public function testUpdateLogoLoginScreenUploadWithInvalidImage(): void {
449
-		$tmpLogo = Server::get(ITempManager::class)->getTemporaryFolder() . '/logo.svg';
450
-
451
-		touch($tmpLogo);
452
-		file_put_contents($tmpLogo, file_get_contents(__DIR__ . '/../../../../tests/data/data.zip'));
453
-		$this->request
454
-			->expects($this->once())
455
-			->method('getParam')
456
-			->with('key')
457
-			->willReturn('logo');
458
-		$this->request
459
-			->expects($this->once())
460
-			->method('getUploadedFile')
461
-			->with('image')
462
-			->willReturn([
463
-				'tmp_name' => $tmpLogo,
464
-				'type' => 'foobar',
465
-				'name' => 'logo.svg',
466
-				'error' => 0,
467
-			]);
468
-		$this->l10n
469
-			->expects($this->any())
470
-			->method('t')
471
-			->willReturnCallback(function ($str) {
472
-				return $str;
473
-			});
474
-
475
-		$this->imageManager->expects($this->once())
476
-			->method('updateImage')
477
-			->willThrowException(new \Exception('Unsupported image type'));
478
-
479
-		$expected = new DataResponse(
480
-			[
481
-				'data'
482
-					=> [
483
-						'message' => 'Unsupported image type',
484
-					],
485
-				'status' => 'failure'
486
-			],
487
-			Http::STATUS_UNPROCESSABLE_ENTITY
488
-		);
489
-		$this->assertEquals($expected, $this->themingController->uploadImage());
490
-	}
491
-
492
-	public static function dataPhpUploadErrors(): array {
493
-		return [
494
-			[UPLOAD_ERR_INI_SIZE, 'The uploaded file exceeds the upload_max_filesize directive in php.ini'],
495
-			[UPLOAD_ERR_FORM_SIZE, 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'],
496
-			[UPLOAD_ERR_PARTIAL, 'The file was only partially uploaded'],
497
-			[UPLOAD_ERR_NO_FILE, 'No file was uploaded'],
498
-			[UPLOAD_ERR_NO_TMP_DIR, 'Missing a temporary folder'],
499
-			[UPLOAD_ERR_CANT_WRITE, 'Could not write file to disk'],
500
-			[UPLOAD_ERR_EXTENSION, 'A PHP extension stopped the file upload'],
501
-		];
502
-	}
503
-
504
-	#[\PHPUnit\Framework\Attributes\DataProvider(methodName: 'dataPhpUploadErrors')]
505
-	public function testUpdateLogoLoginScreenUploadWithInvalidImageUpload(int $error, string $expectedErrorMessage): void {
506
-		$this->request
507
-			->expects($this->once())
508
-			->method('getParam')
509
-			->with('key')
510
-			->willReturn('background');
511
-		$this->request
512
-			->expects($this->once())
513
-			->method('getUploadedFile')
514
-			->with('image')
515
-			->willReturn([
516
-				'tmp_name' => '',
517
-				'type' => 'image/svg+xml',
518
-				'name' => 'logo.svg',
519
-				'error' => $error,
520
-			]);
521
-		$this->l10n
522
-			->expects($this->any())
523
-			->method('t')
524
-			->willReturnCallback(function ($str) {
525
-				return $str;
526
-			});
527
-
528
-		$expected = new DataResponse(
529
-			[
530
-				'data'
531
-					=> [
532
-						'message' => $expectedErrorMessage,
533
-					],
534
-				'status' => 'failure'
535
-			],
536
-			Http::STATUS_UNPROCESSABLE_ENTITY
537
-		);
538
-		$this->assertEquals($expected, $this->themingController->uploadImage());
539
-	}
540
-
541
-	#[\PHPUnit\Framework\Attributes\DataProvider(methodName: 'dataPhpUploadErrors')]
542
-	public function testUpdateLogoUploadWithInvalidImageUpload($error, $expectedErrorMessage): void {
543
-		$this->request
544
-			->expects($this->once())
545
-			->method('getParam')
546
-			->with('key')
547
-			->willReturn('background');
548
-		$this->request
549
-			->expects($this->once())
550
-			->method('getUploadedFile')
551
-			->with('image')
552
-			->willReturn([
553
-				'tmp_name' => '',
554
-				'type' => 'text/svg',
555
-				'name' => 'logo.svg',
556
-				'error' => $error,
557
-			]);
558
-		$this->l10n
559
-			->expects($this->any())
560
-			->method('t')
561
-			->willReturnCallback(function ($str) {
562
-				return $str;
563
-			});
564
-
565
-		$expected = new DataResponse(
566
-			[
567
-				'data'
568
-					=> [
569
-						'message' => $expectedErrorMessage
570
-					],
571
-				'status' => 'failure'
572
-			],
573
-			Http::STATUS_UNPROCESSABLE_ENTITY
574
-		);
575
-		$this->assertEquals($expected, $this->themingController->uploadImage());
576
-	}
577
-
578
-	public function testUndo(): void {
579
-		$this->l10n
580
-			->expects($this->once())
581
-			->method('t')
582
-			->with('Saved')
583
-			->willReturn('Saved');
584
-		$this->themingDefaults
585
-			->expects($this->once())
586
-			->method('undo')
587
-			->with('MySetting')
588
-			->willReturn('MyValue');
589
-
590
-		$expected = new DataResponse(
591
-			[
592
-				'data'
593
-					=> [
594
-						'value' => 'MyValue',
595
-						'message' => 'Saved'
596
-					],
597
-				'status' => 'success'
598
-			]
599
-		);
600
-		$this->assertEquals($expected, $this->themingController->undo('MySetting'));
601
-	}
602
-
603
-	public static function dataUndoDelete(): array {
604
-		return [
605
-			[ 'backgroundMime', 'background' ],
606
-			[ 'logoMime', 'logo' ]
607
-		];
608
-	}
609
-
610
-	#[\PHPUnit\Framework\Attributes\DataProvider(methodName: 'dataUndoDelete')]
611
-	public function testUndoDelete(string $value, string $filename): void {
612
-		$this->l10n
613
-			->expects($this->once())
614
-			->method('t')
615
-			->with('Saved')
616
-			->willReturn('Saved');
617
-		$this->themingDefaults
618
-			->expects($this->once())
619
-			->method('undo')
620
-			->with($value)
621
-			->willReturn($value);
622
-
623
-		$expected = new DataResponse(
624
-			[
625
-				'data'
626
-					=> [
627
-						'value' => $value,
628
-						'message' => 'Saved',
629
-					],
630
-				'status' => 'success'
631
-			]
632
-		);
633
-		$this->assertEquals($expected, $this->themingController->undo($value));
634
-	}
635
-
636
-
637
-
638
-	public function testGetLogoNotExistent(): void {
639
-		$this->imageManager->method('getImage')
640
-			->with($this->equalTo('logo'))
641
-			->willThrowException(new NotFoundException());
642
-
643
-		$expected = new NotFoundResponse();
644
-		$this->assertEquals($expected, $this->themingController->getImage('logo'));
645
-	}
646
-
647
-	public function testGetLogo(): void {
648
-		$file = $this->createMock(ISimpleFile::class);
649
-		$file->method('getName')->willReturn('logo.svg');
650
-		$file->method('getMTime')->willReturn(42);
651
-		$file->method('getMimeType')->willReturn('text/svg');
652
-		$this->imageManager->expects($this->once())
653
-			->method('getImage')
654
-			->willReturn($file);
655
-		$this->config
656
-			->expects($this->any())
657
-			->method('getAppValue')
658
-			->with('theming', 'logoMime', '')
659
-			->willReturn('text/svg');
660
-
661
-		@$expected = new FileDisplayResponse($file);
662
-		$expected->cacheFor(3600);
663
-		$expected->addHeader('Content-Type', 'text/svg');
664
-		$expected->addHeader('Content-Disposition', 'attachment; filename="logo"');
665
-		$csp = new ContentSecurityPolicy();
666
-		$csp->allowInlineStyle();
667
-		$expected->setContentSecurityPolicy($csp);
668
-		@$this->assertEquals($expected, $this->themingController->getImage('logo', true));
669
-	}
670
-
671
-
672
-	public function testGetLoginBackgroundNotExistent(): void {
673
-		$this->imageManager->method('getImage')
674
-			->with($this->equalTo('background'))
675
-			->willThrowException(new NotFoundException());
676
-		$expected = new NotFoundResponse();
677
-		$this->assertEquals($expected, $this->themingController->getImage('background'));
678
-	}
679
-
680
-	public function testGetLoginBackground(): void {
681
-		$file = $this->createMock(ISimpleFile::class);
682
-		$file->method('getName')->willReturn('background.png');
683
-		$file->method('getMTime')->willReturn(42);
684
-		$file->method('getMimeType')->willReturn('image/png');
685
-		$this->imageManager->expects($this->once())
686
-			->method('getImage')
687
-			->willReturn($file);
688
-
689
-		$this->config
690
-			->expects($this->any())
691
-			->method('getAppValue')
692
-			->with('theming', 'backgroundMime', '')
693
-			->willReturn('image/png');
694
-
695
-		@$expected = new FileDisplayResponse($file);
696
-		$expected->cacheFor(3600);
697
-		$expected->addHeader('Content-Type', 'image/png');
698
-		$expected->addHeader('Content-Disposition', 'attachment; filename="background"');
699
-		$csp = new ContentSecurityPolicy();
700
-		$csp->allowInlineStyle();
701
-		$expected->setContentSecurityPolicy($csp);
702
-		@$this->assertEquals($expected, $this->themingController->getImage('background'));
703
-	}
704
-
705
-	public static function dataGetManifest(): array {
706
-		return [
707
-			[true],
708
-			[false],
709
-		];
710
-	}
711
-
712
-	#[\PHPUnit\Framework\Attributes\DataProvider(methodName: 'dataGetManifest')]
713
-	public function testGetManifest(bool $standalone): void {
714
-		$this->config
715
-			->expects($this->once())
716
-			->method('getAppValue')
717
-			->with('theming', 'cachebuster', '0')
718
-			->willReturn('0');
719
-		$this->themingDefaults
720
-			->expects($this->any())
721
-			->method('getName')
722
-			->willReturn('Nextcloud');
723
-		$this->urlGenerator
724
-			->expects($this->once())
725
-			->method('getBaseUrl')
726
-			->willReturn('localhost');
727
-		$this->urlGenerator
728
-			->expects($this->exactly(2))
729
-			->method('linkToRoute')
730
-			->willReturnMap([
731
-				['theming.Icon.getTouchIcon', ['app' => 'core'], 'touchicon'],
732
-				['theming.Icon.getFavicon', ['app' => 'core'], 'favicon'],
733
-			]);
734
-		$this->config
735
-			->expects($this->exactly(2))
736
-			->method('getSystemValueBool')
737
-			->with('theming.standalone_window.enabled', true)
738
-			->willReturn($standalone);
739
-		$response = new JSONResponse([
740
-			'name' => 'Nextcloud',
741
-			'start_url' => 'localhost',
742
-			'icons'
743
-				=> [
744
-					[
745
-						'src' => 'touchicon?v=0',
746
-						'type' => 'image/png',
747
-						'sizes' => '512x512'
748
-					],
749
-					[
750
-						'src' => 'favicon?v=0',
751
-						'type' => 'image/svg+xml',
752
-						'sizes' => '16x16'
753
-					]
754
-				],
755
-			'display_override' => [$standalone ? 'minimal-ui' : ''],
756
-			'display' => $standalone ? 'standalone' : 'browser',
757
-			'short_name' => 'Nextcloud',
758
-			'theme_color' => null,
759
-			'background_color' => null,
760
-			'description' => null
761
-		]);
762
-		$response->cacheFor(3600);
763
-		$this->assertEquals($response, $this->themingController->getManifest('core'));
764
-	}
38
+    private IRequest&MockObject $request;
39
+    private IConfig&MockObject $config;
40
+    private IAppConfig&MockObject $appConfig;
41
+    private ThemingDefaults&MockObject $themingDefaults;
42
+    private IL10N&MockObject $l10n;
43
+    private IAppManager&MockObject $appManager;
44
+    private ImageManager&MockObject $imageManager;
45
+    private IURLGenerator&MockObject $urlGenerator;
46
+    private ThemesService&MockObject $themesService;
47
+    private INavigationManager&MockObject $navigationManager;
48
+
49
+    private ThemingController $themingController;
50
+
51
+    protected function setUp(): void {
52
+        $this->request = $this->createMock(IRequest::class);
53
+        $this->config = $this->createMock(IConfig::class);
54
+        $this->appConfig = $this->createMock(IAppConfig::class);
55
+        $this->themingDefaults = $this->createMock(ThemingDefaults::class);
56
+        $this->l10n = $this->createMock(L10N::class);
57
+        $this->appManager = $this->createMock(IAppManager::class);
58
+        $this->urlGenerator = $this->createMock(IURLGenerator::class);
59
+        $this->imageManager = $this->createMock(ImageManager::class);
60
+        $this->themesService = $this->createMock(ThemesService::class);
61
+        $this->navigationManager = $this->createMock(INavigationManager::class);
62
+
63
+        $timeFactory = $this->createMock(ITimeFactory::class);
64
+        $timeFactory->expects($this->any())
65
+            ->method('getTime')
66
+            ->willReturn(123);
67
+
68
+        $this->overwriteService(ITimeFactory::class, $timeFactory);
69
+
70
+        $this->themingController = new ThemingController(
71
+            'theming',
72
+            $this->request,
73
+            $this->config,
74
+            $this->appConfig,
75
+            $this->themingDefaults,
76
+            $this->l10n,
77
+            $this->urlGenerator,
78
+            $this->appManager,
79
+            $this->imageManager,
80
+            $this->themesService,
81
+            $this->navigationManager,
82
+        );
83
+
84
+        parent::setUp();
85
+    }
86
+
87
+    public static function dataUpdateStylesheetSuccess(): array {
88
+        return [
89
+            ['name', str_repeat('a', 250), 'Saved'],
90
+            ['url', 'https://nextcloud.com/' . str_repeat('a', 478), 'Saved'],
91
+            ['slogan', str_repeat('a', 500), 'Saved'],
92
+            ['primaryColor', '#0082c9', 'Saved', 'primary_color'],
93
+            ['primary_color', '#0082C9', 'Saved'],
94
+            ['backgroundColor', '#0082C9', 'Saved', 'background_color'],
95
+            ['background_color', '#0082C9', 'Saved'],
96
+            ['imprintUrl', 'https://nextcloud.com/' . str_repeat('a', 478), 'Saved'],
97
+            ['privacyUrl', 'https://nextcloud.com/' . str_repeat('a', 478), 'Saved'],
98
+        ];
99
+    }
100
+
101
+    #[\PHPUnit\Framework\Attributes\DataProvider(methodName: 'dataUpdateStylesheetSuccess')]
102
+    public function testUpdateStylesheetSuccess(string $setting, string $value, string $message, ?string $realSetting = null): void {
103
+        $this->themingDefaults
104
+            ->expects($this->once())
105
+            ->method('set')
106
+            ->with($realSetting ?? $setting, $value);
107
+        $this->l10n
108
+            ->expects($this->once())
109
+            ->method('t')
110
+            ->willReturnCallback(function ($str) {
111
+                return $str;
112
+            });
113
+
114
+        $expected = new DataResponse(
115
+            [
116
+                'data'
117
+                    => [
118
+                        'message' => $message,
119
+                    ],
120
+                'status' => 'success',
121
+            ]
122
+        );
123
+        $this->assertEquals($expected, $this->themingController->updateStylesheet($setting, $value));
124
+    }
125
+
126
+    public static function dataUpdateStylesheetError(): array {
127
+        $urls = [
128
+            'url' => 'web address',
129
+            'imprintUrl' => 'legal notice address',
130
+            'privacyUrl' => 'privacy policy address',
131
+        ];
132
+
133
+        $urlTests = [];
134
+        foreach ($urls as $urlKey => $urlName) {
135
+            // Check length limit
136
+            $urlTests[] = [$urlKey, 'http://example.com/' . str_repeat('a', 501), "The given {$urlName} is too long"];
137
+            // Check potential evil javascript
138
+            $urlTests[] = [$urlKey, 'javascript:alert(1)', "The given {$urlName} is not a valid URL"];
139
+            // Check XSS
140
+            $urlTests[] = [$urlKey, 'https://example.com/"><script/src="alert(\'1\')"><a/href/="', "The given {$urlName} is not a valid URL"];
141
+        }
142
+
143
+        return [
144
+            ['name', str_repeat('a', 251), 'The given name is too long'],
145
+            ['slogan', str_repeat('a', 501), 'The given slogan is too long'],
146
+            ['primary_color', '0082C9', 'The given color is invalid'],
147
+            ['primary_color', '#0082Z9', 'The given color is invalid'],
148
+            ['primary_color', 'Nextcloud', 'The given color is invalid'],
149
+            ['background_color', '0082C9', 'The given color is invalid'],
150
+            ['background_color', '#0082Z9', 'The given color is invalid'],
151
+            ['background_color', 'Nextcloud', 'The given color is invalid'],
152
+
153
+            ['doesnotexist', 'value', 'Invalid setting key'],
154
+
155
+            ...$urlTests,
156
+        ];
157
+    }
158
+
159
+    #[\PHPUnit\Framework\Attributes\DataProvider(methodName: 'dataUpdateStylesheetError')]
160
+    public function testUpdateStylesheetError(string $setting, string $value, string $message): void {
161
+        $this->themingDefaults
162
+            ->expects($this->never())
163
+            ->method('set')
164
+            ->with($setting, $value);
165
+        $this->l10n
166
+            ->expects($this->any())
167
+            ->method('t')
168
+            ->willReturnCallback(function ($str) {
169
+                return $str;
170
+            });
171
+
172
+        $expected = new DataResponse(
173
+            [
174
+                'data'
175
+                    => [
176
+                        'message' => $message,
177
+                    ],
178
+                'status' => 'error',
179
+            ],
180
+            Http::STATUS_BAD_REQUEST
181
+        );
182
+        $this->assertEquals($expected, $this->themingController->updateStylesheet($setting, $value));
183
+    }
184
+
185
+    public function testUpdateLogoNoData(): void {
186
+        $this->request
187
+            ->expects($this->once())
188
+            ->method('getParam')
189
+            ->with('key')
190
+            ->willReturn('logo');
191
+        $this->request
192
+            ->expects($this->once())
193
+            ->method('getUploadedFile')
194
+            ->with('image')
195
+            ->willReturn(null);
196
+        $this->l10n
197
+            ->expects($this->any())
198
+            ->method('t')
199
+            ->willReturnCallback(function ($str) {
200
+                return $str;
201
+            });
202
+
203
+        $expected = new DataResponse(
204
+            [
205
+                'data'
206
+                    => [
207
+                        'message' => 'No file uploaded',
208
+                    ],
209
+                'status' => 'failure',
210
+            ],
211
+            Http::STATUS_UNPROCESSABLE_ENTITY
212
+        );
213
+
214
+        $this->assertEquals($expected, $this->themingController->uploadImage());
215
+    }
216
+
217
+    public function testUploadInvalidUploadKey(): void {
218
+        $this->request
219
+            ->expects($this->once())
220
+            ->method('getParam')
221
+            ->with('key')
222
+            ->willReturn('invalid');
223
+        $this->request
224
+            ->expects($this->never())
225
+            ->method('getUploadedFile');
226
+        $this->l10n
227
+            ->expects($this->any())
228
+            ->method('t')
229
+            ->willReturnCallback(function ($str) {
230
+                return $str;
231
+            });
232
+
233
+        $expected = new DataResponse(
234
+            [
235
+                'data'
236
+                    => [
237
+                        'message' => 'Invalid key',
238
+                    ],
239
+                'status' => 'failure',
240
+            ],
241
+            Http::STATUS_BAD_REQUEST
242
+        );
243
+
244
+        $this->assertEquals($expected, $this->themingController->uploadImage());
245
+    }
246
+
247
+    /**
248
+     * Checks that trying to upload an SVG favicon without imagemagick
249
+     * results in an unsupported media type response.
250
+     */
251
+    public function testUploadSVGFaviconWithoutImagemagick(): void {
252
+        $this->imageManager
253
+            ->method('shouldReplaceIcons')
254
+            ->willReturn(false);
255
+
256
+        $this->request
257
+            ->expects($this->once())
258
+            ->method('getParam')
259
+            ->with('key')
260
+            ->willReturn('favicon');
261
+        $this->request
262
+            ->expects($this->once())
263
+            ->method('getUploadedFile')
264
+            ->with('image')
265
+            ->willReturn([
266
+                'tmp_name' => __DIR__ . '/../../../../tests/data/testimagelarge.svg',
267
+                'type' => 'image/svg',
268
+                'name' => 'testimagelarge.svg',
269
+                'error' => 0,
270
+            ]);
271
+        $this->l10n
272
+            ->expects($this->any())
273
+            ->method('t')
274
+            ->willReturnCallback(function ($str) {
275
+                return $str;
276
+            });
277
+
278
+        $this->imageManager->expects($this->once())
279
+            ->method('updateImage')
280
+            ->willThrowException(new \Exception('Unsupported image type'));
281
+
282
+        $expected = new DataResponse(
283
+            [
284
+                'data'
285
+                    => [
286
+                        'message' => 'Unsupported image type',
287
+                    ],
288
+                'status' => 'failure'
289
+            ],
290
+            Http::STATUS_UNPROCESSABLE_ENTITY
291
+        );
292
+
293
+        $this->assertEquals($expected, $this->themingController->uploadImage());
294
+    }
295
+
296
+    public function testUpdateLogoInvalidMimeType(): void {
297
+        $this->request
298
+            ->expects($this->once())
299
+            ->method('getParam')
300
+            ->with('key')
301
+            ->willReturn('logo');
302
+        $this->request
303
+            ->expects($this->once())
304
+            ->method('getUploadedFile')
305
+            ->with('image')
306
+            ->willReturn([
307
+                'tmp_name' => __DIR__ . '/../../../../tests/data/lorem.txt',
308
+                'type' => 'application/pdf',
309
+                'name' => 'logo.pdf',
310
+                'error' => 0,
311
+            ]);
312
+        $this->l10n
313
+            ->expects($this->any())
314
+            ->method('t')
315
+            ->willReturnCallback(function ($str) {
316
+                return $str;
317
+            });
318
+
319
+        $this->imageManager->expects($this->once())
320
+            ->method('updateImage')
321
+            ->willThrowException(new \Exception('Unsupported image type'));
322
+
323
+        $expected = new DataResponse(
324
+            [
325
+                'data'
326
+                    => [
327
+                        'message' => 'Unsupported image type',
328
+                    ],
329
+                'status' => 'failure'
330
+            ],
331
+            Http::STATUS_UNPROCESSABLE_ENTITY
332
+        );
333
+
334
+        $this->assertEquals($expected, $this->themingController->uploadImage());
335
+    }
336
+
337
+    public static function dataUpdateImages(): array {
338
+        return [
339
+            ['image/jpeg', false],
340
+            ['image/jpeg', true],
341
+            ['image/gif'],
342
+            ['image/png'],
343
+            ['image/svg+xml'],
344
+            ['image/svg']
345
+        ];
346
+    }
347
+
348
+    #[\PHPUnit\Framework\Attributes\DataProvider(methodName: 'dataUpdateImages')]
349
+    public function testUpdateLogoNormalLogoUpload(string $mimeType, bool $folderExists = true): void {
350
+        $tmpLogo = Server::get(ITempManager::class)->getTemporaryFolder() . '/logo.svg';
351
+        $destination = Server::get(ITempManager::class)->getTemporaryFolder();
352
+
353
+        touch($tmpLogo);
354
+        copy(__DIR__ . '/../../../../tests/data/testimage.png', $tmpLogo);
355
+        $this->request
356
+            ->expects($this->once())
357
+            ->method('getParam')
358
+            ->with('key')
359
+            ->willReturn('logo');
360
+        $this->request
361
+            ->expects($this->once())
362
+            ->method('getUploadedFile')
363
+            ->with('image')
364
+            ->willReturn([
365
+                'tmp_name' => $tmpLogo,
366
+                'type' => $mimeType,
367
+                'name' => 'logo.svg',
368
+                'error' => 0,
369
+            ]);
370
+        $this->l10n
371
+            ->expects($this->any())
372
+            ->method('t')
373
+            ->willReturnCallback(function ($str) {
374
+                return $str;
375
+            });
376
+
377
+        $this->imageManager->expects($this->once())
378
+            ->method('getImageUrl')
379
+            ->with('logo')
380
+            ->willReturn('imageUrl');
381
+
382
+        $this->imageManager->expects($this->once())
383
+            ->method('updateImage');
384
+
385
+        $expected = new DataResponse(
386
+            [
387
+                'data'
388
+                    => [
389
+                        'name' => 'logo.svg',
390
+                        'message' => 'Saved',
391
+                        'url' => 'imageUrl',
392
+                    ],
393
+                'status' => 'success'
394
+            ]
395
+        );
396
+
397
+        $this->assertEquals($expected, $this->themingController->uploadImage());
398
+    }
399
+
400
+    public function testUpdateLogoLoginScreenUpload(): void {
401
+        $tmpLogo = Server::get(ITempManager::class)->getTemporaryFolder() . 'logo.png';
402
+
403
+        touch($tmpLogo);
404
+        copy(__DIR__ . '/../../../../tests/data/desktopapp.png', $tmpLogo);
405
+        $this->request
406
+            ->expects($this->once())
407
+            ->method('getParam')
408
+            ->with('key')
409
+            ->willReturn('background');
410
+        $this->request
411
+            ->expects($this->once())
412
+            ->method('getUploadedFile')
413
+            ->with('image')
414
+            ->willReturn([
415
+                'tmp_name' => $tmpLogo,
416
+                'type' => 'image/svg+xml',
417
+                'name' => 'logo.svg',
418
+                'error' => 0,
419
+            ]);
420
+        $this->l10n
421
+            ->expects($this->any())
422
+            ->method('t')
423
+            ->willReturnCallback(function ($str) {
424
+                return $str;
425
+            });
426
+
427
+        $this->imageManager->expects($this->once())
428
+            ->method('updateImage');
429
+
430
+        $this->imageManager->expects($this->once())
431
+            ->method('getImageUrl')
432
+            ->with('background')
433
+            ->willReturn('imageUrl');
434
+        $expected = new DataResponse(
435
+            [
436
+                'data'
437
+                    => [
438
+                        'name' => 'logo.svg',
439
+                        'message' => 'Saved',
440
+                        'url' => 'imageUrl',
441
+                    ],
442
+                'status' => 'success'
443
+            ]
444
+        );
445
+        $this->assertEquals($expected, $this->themingController->uploadImage());
446
+    }
447
+
448
+    public function testUpdateLogoLoginScreenUploadWithInvalidImage(): void {
449
+        $tmpLogo = Server::get(ITempManager::class)->getTemporaryFolder() . '/logo.svg';
450
+
451
+        touch($tmpLogo);
452
+        file_put_contents($tmpLogo, file_get_contents(__DIR__ . '/../../../../tests/data/data.zip'));
453
+        $this->request
454
+            ->expects($this->once())
455
+            ->method('getParam')
456
+            ->with('key')
457
+            ->willReturn('logo');
458
+        $this->request
459
+            ->expects($this->once())
460
+            ->method('getUploadedFile')
461
+            ->with('image')
462
+            ->willReturn([
463
+                'tmp_name' => $tmpLogo,
464
+                'type' => 'foobar',
465
+                'name' => 'logo.svg',
466
+                'error' => 0,
467
+            ]);
468
+        $this->l10n
469
+            ->expects($this->any())
470
+            ->method('t')
471
+            ->willReturnCallback(function ($str) {
472
+                return $str;
473
+            });
474
+
475
+        $this->imageManager->expects($this->once())
476
+            ->method('updateImage')
477
+            ->willThrowException(new \Exception('Unsupported image type'));
478
+
479
+        $expected = new DataResponse(
480
+            [
481
+                'data'
482
+                    => [
483
+                        'message' => 'Unsupported image type',
484
+                    ],
485
+                'status' => 'failure'
486
+            ],
487
+            Http::STATUS_UNPROCESSABLE_ENTITY
488
+        );
489
+        $this->assertEquals($expected, $this->themingController->uploadImage());
490
+    }
491
+
492
+    public static function dataPhpUploadErrors(): array {
493
+        return [
494
+            [UPLOAD_ERR_INI_SIZE, 'The uploaded file exceeds the upload_max_filesize directive in php.ini'],
495
+            [UPLOAD_ERR_FORM_SIZE, 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'],
496
+            [UPLOAD_ERR_PARTIAL, 'The file was only partially uploaded'],
497
+            [UPLOAD_ERR_NO_FILE, 'No file was uploaded'],
498
+            [UPLOAD_ERR_NO_TMP_DIR, 'Missing a temporary folder'],
499
+            [UPLOAD_ERR_CANT_WRITE, 'Could not write file to disk'],
500
+            [UPLOAD_ERR_EXTENSION, 'A PHP extension stopped the file upload'],
501
+        ];
502
+    }
503
+
504
+    #[\PHPUnit\Framework\Attributes\DataProvider(methodName: 'dataPhpUploadErrors')]
505
+    public function testUpdateLogoLoginScreenUploadWithInvalidImageUpload(int $error, string $expectedErrorMessage): void {
506
+        $this->request
507
+            ->expects($this->once())
508
+            ->method('getParam')
509
+            ->with('key')
510
+            ->willReturn('background');
511
+        $this->request
512
+            ->expects($this->once())
513
+            ->method('getUploadedFile')
514
+            ->with('image')
515
+            ->willReturn([
516
+                'tmp_name' => '',
517
+                'type' => 'image/svg+xml',
518
+                'name' => 'logo.svg',
519
+                'error' => $error,
520
+            ]);
521
+        $this->l10n
522
+            ->expects($this->any())
523
+            ->method('t')
524
+            ->willReturnCallback(function ($str) {
525
+                return $str;
526
+            });
527
+
528
+        $expected = new DataResponse(
529
+            [
530
+                'data'
531
+                    => [
532
+                        'message' => $expectedErrorMessage,
533
+                    ],
534
+                'status' => 'failure'
535
+            ],
536
+            Http::STATUS_UNPROCESSABLE_ENTITY
537
+        );
538
+        $this->assertEquals($expected, $this->themingController->uploadImage());
539
+    }
540
+
541
+    #[\PHPUnit\Framework\Attributes\DataProvider(methodName: 'dataPhpUploadErrors')]
542
+    public function testUpdateLogoUploadWithInvalidImageUpload($error, $expectedErrorMessage): void {
543
+        $this->request
544
+            ->expects($this->once())
545
+            ->method('getParam')
546
+            ->with('key')
547
+            ->willReturn('background');
548
+        $this->request
549
+            ->expects($this->once())
550
+            ->method('getUploadedFile')
551
+            ->with('image')
552
+            ->willReturn([
553
+                'tmp_name' => '',
554
+                'type' => 'text/svg',
555
+                'name' => 'logo.svg',
556
+                'error' => $error,
557
+            ]);
558
+        $this->l10n
559
+            ->expects($this->any())
560
+            ->method('t')
561
+            ->willReturnCallback(function ($str) {
562
+                return $str;
563
+            });
564
+
565
+        $expected = new DataResponse(
566
+            [
567
+                'data'
568
+                    => [
569
+                        'message' => $expectedErrorMessage
570
+                    ],
571
+                'status' => 'failure'
572
+            ],
573
+            Http::STATUS_UNPROCESSABLE_ENTITY
574
+        );
575
+        $this->assertEquals($expected, $this->themingController->uploadImage());
576
+    }
577
+
578
+    public function testUndo(): void {
579
+        $this->l10n
580
+            ->expects($this->once())
581
+            ->method('t')
582
+            ->with('Saved')
583
+            ->willReturn('Saved');
584
+        $this->themingDefaults
585
+            ->expects($this->once())
586
+            ->method('undo')
587
+            ->with('MySetting')
588
+            ->willReturn('MyValue');
589
+
590
+        $expected = new DataResponse(
591
+            [
592
+                'data'
593
+                    => [
594
+                        'value' => 'MyValue',
595
+                        'message' => 'Saved'
596
+                    ],
597
+                'status' => 'success'
598
+            ]
599
+        );
600
+        $this->assertEquals($expected, $this->themingController->undo('MySetting'));
601
+    }
602
+
603
+    public static function dataUndoDelete(): array {
604
+        return [
605
+            [ 'backgroundMime', 'background' ],
606
+            [ 'logoMime', 'logo' ]
607
+        ];
608
+    }
609
+
610
+    #[\PHPUnit\Framework\Attributes\DataProvider(methodName: 'dataUndoDelete')]
611
+    public function testUndoDelete(string $value, string $filename): void {
612
+        $this->l10n
613
+            ->expects($this->once())
614
+            ->method('t')
615
+            ->with('Saved')
616
+            ->willReturn('Saved');
617
+        $this->themingDefaults
618
+            ->expects($this->once())
619
+            ->method('undo')
620
+            ->with($value)
621
+            ->willReturn($value);
622
+
623
+        $expected = new DataResponse(
624
+            [
625
+                'data'
626
+                    => [
627
+                        'value' => $value,
628
+                        'message' => 'Saved',
629
+                    ],
630
+                'status' => 'success'
631
+            ]
632
+        );
633
+        $this->assertEquals($expected, $this->themingController->undo($value));
634
+    }
635
+
636
+
637
+
638
+    public function testGetLogoNotExistent(): void {
639
+        $this->imageManager->method('getImage')
640
+            ->with($this->equalTo('logo'))
641
+            ->willThrowException(new NotFoundException());
642
+
643
+        $expected = new NotFoundResponse();
644
+        $this->assertEquals($expected, $this->themingController->getImage('logo'));
645
+    }
646
+
647
+    public function testGetLogo(): void {
648
+        $file = $this->createMock(ISimpleFile::class);
649
+        $file->method('getName')->willReturn('logo.svg');
650
+        $file->method('getMTime')->willReturn(42);
651
+        $file->method('getMimeType')->willReturn('text/svg');
652
+        $this->imageManager->expects($this->once())
653
+            ->method('getImage')
654
+            ->willReturn($file);
655
+        $this->config
656
+            ->expects($this->any())
657
+            ->method('getAppValue')
658
+            ->with('theming', 'logoMime', '')
659
+            ->willReturn('text/svg');
660
+
661
+        @$expected = new FileDisplayResponse($file);
662
+        $expected->cacheFor(3600);
663
+        $expected->addHeader('Content-Type', 'text/svg');
664
+        $expected->addHeader('Content-Disposition', 'attachment; filename="logo"');
665
+        $csp = new ContentSecurityPolicy();
666
+        $csp->allowInlineStyle();
667
+        $expected->setContentSecurityPolicy($csp);
668
+        @$this->assertEquals($expected, $this->themingController->getImage('logo', true));
669
+    }
670
+
671
+
672
+    public function testGetLoginBackgroundNotExistent(): void {
673
+        $this->imageManager->method('getImage')
674
+            ->with($this->equalTo('background'))
675
+            ->willThrowException(new NotFoundException());
676
+        $expected = new NotFoundResponse();
677
+        $this->assertEquals($expected, $this->themingController->getImage('background'));
678
+    }
679
+
680
+    public function testGetLoginBackground(): void {
681
+        $file = $this->createMock(ISimpleFile::class);
682
+        $file->method('getName')->willReturn('background.png');
683
+        $file->method('getMTime')->willReturn(42);
684
+        $file->method('getMimeType')->willReturn('image/png');
685
+        $this->imageManager->expects($this->once())
686
+            ->method('getImage')
687
+            ->willReturn($file);
688
+
689
+        $this->config
690
+            ->expects($this->any())
691
+            ->method('getAppValue')
692
+            ->with('theming', 'backgroundMime', '')
693
+            ->willReturn('image/png');
694
+
695
+        @$expected = new FileDisplayResponse($file);
696
+        $expected->cacheFor(3600);
697
+        $expected->addHeader('Content-Type', 'image/png');
698
+        $expected->addHeader('Content-Disposition', 'attachment; filename="background"');
699
+        $csp = new ContentSecurityPolicy();
700
+        $csp->allowInlineStyle();
701
+        $expected->setContentSecurityPolicy($csp);
702
+        @$this->assertEquals($expected, $this->themingController->getImage('background'));
703
+    }
704
+
705
+    public static function dataGetManifest(): array {
706
+        return [
707
+            [true],
708
+            [false],
709
+        ];
710
+    }
711
+
712
+    #[\PHPUnit\Framework\Attributes\DataProvider(methodName: 'dataGetManifest')]
713
+    public function testGetManifest(bool $standalone): void {
714
+        $this->config
715
+            ->expects($this->once())
716
+            ->method('getAppValue')
717
+            ->with('theming', 'cachebuster', '0')
718
+            ->willReturn('0');
719
+        $this->themingDefaults
720
+            ->expects($this->any())
721
+            ->method('getName')
722
+            ->willReturn('Nextcloud');
723
+        $this->urlGenerator
724
+            ->expects($this->once())
725
+            ->method('getBaseUrl')
726
+            ->willReturn('localhost');
727
+        $this->urlGenerator
728
+            ->expects($this->exactly(2))
729
+            ->method('linkToRoute')
730
+            ->willReturnMap([
731
+                ['theming.Icon.getTouchIcon', ['app' => 'core'], 'touchicon'],
732
+                ['theming.Icon.getFavicon', ['app' => 'core'], 'favicon'],
733
+            ]);
734
+        $this->config
735
+            ->expects($this->exactly(2))
736
+            ->method('getSystemValueBool')
737
+            ->with('theming.standalone_window.enabled', true)
738
+            ->willReturn($standalone);
739
+        $response = new JSONResponse([
740
+            'name' => 'Nextcloud',
741
+            'start_url' => 'localhost',
742
+            'icons'
743
+                => [
744
+                    [
745
+                        'src' => 'touchicon?v=0',
746
+                        'type' => 'image/png',
747
+                        'sizes' => '512x512'
748
+                    ],
749
+                    [
750
+                        'src' => 'favicon?v=0',
751
+                        'type' => 'image/svg+xml',
752
+                        'sizes' => '16x16'
753
+                    ]
754
+                ],
755
+            'display_override' => [$standalone ? 'minimal-ui' : ''],
756
+            'display' => $standalone ? 'standalone' : 'browser',
757
+            'short_name' => 'Nextcloud',
758
+            'theme_color' => null,
759
+            'background_color' => null,
760
+            'description' => null
761
+        ]);
762
+        $response->cacheFor(3600);
763
+        $this->assertEquals($response, $this->themingController->getManifest('core'));
764
+    }
765 765
 }
Please login to merge, or discard this patch.
apps/theming/tests/IconBuilderTest.php 2 patches
Indentation   +323 added lines, -323 removed lines patch added patch discarded remove patch
@@ -18,344 +18,344 @@
 block discarded – undo
18 18
 use Test\TestCase;
19 19
 
20 20
 class IconBuilderTest extends TestCase {
21
-	protected IConfig&MockObject $config;
22
-	protected AppData&MockObject $appData;
23
-	protected ThemingDefaults&MockObject $themingDefaults;
24
-	protected ImageManager&MockObject $imageManager;
25
-	protected IAppManager&MockObject $appManager;
26
-	protected Util&MockObject $util;
27
-	protected IconBuilder $iconBuilder;
21
+    protected IConfig&MockObject $config;
22
+    protected AppData&MockObject $appData;
23
+    protected ThemingDefaults&MockObject $themingDefaults;
24
+    protected ImageManager&MockObject $imageManager;
25
+    protected IAppManager&MockObject $appManager;
26
+    protected Util&MockObject $util;
27
+    protected IconBuilder $iconBuilder;
28 28
 
29
-	protected function setUp(): void {
30
-		parent::setUp();
29
+    protected function setUp(): void {
30
+        parent::setUp();
31 31
 
32
-		$this->config = $this->createMock(IConfig::class);
33
-		$this->appData = $this->createMock(AppData::class);
34
-		$this->themingDefaults = $this->createMock(ThemingDefaults::class);
35
-		$this->appManager = $this->createMock(IAppManager::class);
36
-		$this->imageManager = $this->createMock(ImageManager::class);
37
-		$this->util = $this->createMock(Util::class);
38
-		$this->iconBuilder = new IconBuilder($this->themingDefaults, $this->util, $this->imageManager);
39
-	}
32
+        $this->config = $this->createMock(IConfig::class);
33
+        $this->appData = $this->createMock(AppData::class);
34
+        $this->themingDefaults = $this->createMock(ThemingDefaults::class);
35
+        $this->appManager = $this->createMock(IAppManager::class);
36
+        $this->imageManager = $this->createMock(ImageManager::class);
37
+        $this->util = $this->createMock(Util::class);
38
+        $this->iconBuilder = new IconBuilder($this->themingDefaults, $this->util, $this->imageManager);
39
+    }
40 40
 
41
-	/**
42
-	 * Checks if Imagick and the required format are available.
43
-	 * If provider is null, only checks for Imagick extension.
44
-	 */
45
-	private function checkImagick(?string $provider = null) {
46
-		if (!extension_loaded('imagick')) {
47
-			$this->markTestSkipped('Imagemagick is required for dynamic icon generation.');
48
-		}
49
-		if ($provider !== null) {
50
-			$checkImagick = new \Imagick();
51
-			if (count($checkImagick->queryFormats($provider)) < 1) {
52
-				$this->markTestSkipped('Imagemagick ' . $provider . ' support is required for this icon generation test.');
53
-			}
54
-		}
55
-	}
41
+    /**
42
+     * Checks if Imagick and the required format are available.
43
+     * If provider is null, only checks for Imagick extension.
44
+     */
45
+    private function checkImagick(?string $provider = null) {
46
+        if (!extension_loaded('imagick')) {
47
+            $this->markTestSkipped('Imagemagick is required for dynamic icon generation.');
48
+        }
49
+        if ($provider !== null) {
50
+            $checkImagick = new \Imagick();
51
+            if (count($checkImagick->queryFormats($provider)) < 1) {
52
+                $this->markTestSkipped('Imagemagick ' . $provider . ' support is required for this icon generation test.');
53
+            }
54
+        }
55
+    }
56 56
 
57
-	/**
58
-	 * Data provider for app icon rendering tests (SVG only).
59
-	 */
60
-	public static function dataRenderAppIconSvg(): array {
61
-		return [
62
-			['logo', '#0082c9', 'logo.svg'],
63
-			['settings', '#FF0000', 'settings.svg'],
64
-		];
65
-	}
57
+    /**
58
+     * Data provider for app icon rendering tests (SVG only).
59
+     */
60
+    public static function dataRenderAppIconSvg(): array {
61
+        return [
62
+            ['logo', '#0082c9', 'logo.svg'],
63
+            ['settings', '#FF0000', 'settings.svg'],
64
+        ];
65
+    }
66 66
 
67
-	/**
68
-	 * Data provider for app icon rendering tests (PNG only).
69
-	 */
70
-	public static function dataRenderAppIconPng(): array {
71
-		return [
72
-			['logo', '#0082c9', 'logo.png'],
73
-			['settings', '#FF0000', 'settings.png'],
74
-		];
75
-	}
67
+    /**
68
+     * Data provider for app icon rendering tests (PNG only).
69
+     */
70
+    public static function dataRenderAppIconPng(): array {
71
+        return [
72
+            ['logo', '#0082c9', 'logo.png'],
73
+            ['settings', '#FF0000', 'settings.png'],
74
+        ];
75
+    }
76 76
 
77
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataRenderAppIconSvg')]
78
-	public function testRenderAppIconSvg(string $app, string $color, string $file): void {
79
-		$this->checkImagick('SVG');
80
-		// mock required methods
81
-		$this->imageManager->expects($this->any())
82
-			->method('canConvert')
83
-			->willReturnMap([
84
-				['SVG', true],
85
-				['PNG', true]
86
-			]);
87
-		$this->util->expects($this->once())
88
-			->method('getAppIcon')
89
-			->with($app, true)
90
-			->willReturn(__DIR__ . '/data/' . $file);
91
-		$this->themingDefaults->expects($this->any())
92
-			->method('getColorPrimary')
93
-			->willReturn($color);
94
-		// generate expected output from source file
95
-		$expectedIcon = $this->generateTestIcon($file, 'SVG', 512, $color);
96
-		// run test
97
-		$icon = $this->iconBuilder->renderAppIcon($app, 512);
98
-		$this->assertEquals(true, $icon->valid());
99
-		$this->assertEquals(512, $icon->getImageWidth());
100
-		$this->assertEquals(512, $icon->getImageHeight());
101
-		$icon->setImageFormat('SVG');
102
-		$expectedIcon->setImageFormat('SVG');
103
-		$this->assertEquals($expectedIcon->getImageBlob(), $icon->getImageBlob(), 'Generated icon differs from expected');
104
-		$icon->destroy();
105
-		$expectedIcon->destroy();
106
-	}
77
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataRenderAppIconSvg')]
78
+    public function testRenderAppIconSvg(string $app, string $color, string $file): void {
79
+        $this->checkImagick('SVG');
80
+        // mock required methods
81
+        $this->imageManager->expects($this->any())
82
+            ->method('canConvert')
83
+            ->willReturnMap([
84
+                ['SVG', true],
85
+                ['PNG', true]
86
+            ]);
87
+        $this->util->expects($this->once())
88
+            ->method('getAppIcon')
89
+            ->with($app, true)
90
+            ->willReturn(__DIR__ . '/data/' . $file);
91
+        $this->themingDefaults->expects($this->any())
92
+            ->method('getColorPrimary')
93
+            ->willReturn($color);
94
+        // generate expected output from source file
95
+        $expectedIcon = $this->generateTestIcon($file, 'SVG', 512, $color);
96
+        // run test
97
+        $icon = $this->iconBuilder->renderAppIcon($app, 512);
98
+        $this->assertEquals(true, $icon->valid());
99
+        $this->assertEquals(512, $icon->getImageWidth());
100
+        $this->assertEquals(512, $icon->getImageHeight());
101
+        $icon->setImageFormat('SVG');
102
+        $expectedIcon->setImageFormat('SVG');
103
+        $this->assertEquals($expectedIcon->getImageBlob(), $icon->getImageBlob(), 'Generated icon differs from expected');
104
+        $icon->destroy();
105
+        $expectedIcon->destroy();
106
+    }
107 107
 
108
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataRenderAppIconPng')]
109
-	public function testRenderAppIconPng(string $app, string $color, string $file): void {
110
-		$this->checkImagick('PNG');
111
-		// mock required methods
112
-		$this->imageManager->expects($this->any())
113
-			->method('canConvert')
114
-			->willReturnMap([
115
-				['SVG', false],
116
-				['PNG', true]
117
-			]);
118
-		$this->util->expects($this->once())
119
-			->method('getAppIcon')
120
-			->with($app, false)
121
-			->willReturn(__DIR__ . '/data/' . $file);
122
-		$this->themingDefaults->expects($this->any())
123
-			->method('getColorPrimary')
124
-			->willReturn($color);
125
-		// generate expected output from source file
126
-		$expectedIcon = $this->generateTestIcon($file, 'PNG', 512, $color);
127
-		// run test
128
-		$icon = $this->iconBuilder->renderAppIcon($app, 512);
129
-		$this->assertEquals(true, $icon->valid());
130
-		$this->assertEquals(512, $icon->getImageWidth());
131
-		$this->assertEquals(512, $icon->getImageHeight());
132
-		$icon->setImageFormat('PNG');
133
-		$expectedIcon->setImageFormat('PNG');
134
-		$this->assertEquals($expectedIcon->getImageBlob(), $icon->getImageBlob(), 'Generated icon differs from expected');
135
-		$icon->destroy();
136
-		$expectedIcon->destroy();
137
-	}
108
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataRenderAppIconPng')]
109
+    public function testRenderAppIconPng(string $app, string $color, string $file): void {
110
+        $this->checkImagick('PNG');
111
+        // mock required methods
112
+        $this->imageManager->expects($this->any())
113
+            ->method('canConvert')
114
+            ->willReturnMap([
115
+                ['SVG', false],
116
+                ['PNG', true]
117
+            ]);
118
+        $this->util->expects($this->once())
119
+            ->method('getAppIcon')
120
+            ->with($app, false)
121
+            ->willReturn(__DIR__ . '/data/' . $file);
122
+        $this->themingDefaults->expects($this->any())
123
+            ->method('getColorPrimary')
124
+            ->willReturn($color);
125
+        // generate expected output from source file
126
+        $expectedIcon = $this->generateTestIcon($file, 'PNG', 512, $color);
127
+        // run test
128
+        $icon = $this->iconBuilder->renderAppIcon($app, 512);
129
+        $this->assertEquals(true, $icon->valid());
130
+        $this->assertEquals(512, $icon->getImageWidth());
131
+        $this->assertEquals(512, $icon->getImageHeight());
132
+        $icon->setImageFormat('PNG');
133
+        $expectedIcon->setImageFormat('PNG');
134
+        $this->assertEquals($expectedIcon->getImageBlob(), $icon->getImageBlob(), 'Generated icon differs from expected');
135
+        $icon->destroy();
136
+        $expectedIcon->destroy();
137
+    }
138 138
 
139
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataRenderAppIconSvg')]
140
-	public function testGetTouchIconSvg(string $app, string $color, string $file): void {
141
-		$this->checkImagick('SVG');
142
-		// mock required methods
143
-		$this->imageManager->expects($this->any())
144
-			->method('canConvert')
145
-			->willReturnMap([
146
-				['SVG', true],
147
-				['PNG', true]
148
-			]);
149
-		$this->util->expects($this->once())
150
-			->method('getAppIcon')
151
-			->with($app, true)
152
-			->willReturn(__DIR__ . '/data/' . $file);
153
-		$this->themingDefaults->expects($this->any())
154
-			->method('getColorPrimary')
155
-			->willReturn($color);
156
-		// generate expected output from source file
157
-		$expectedIcon = $this->generateTestIcon($file, 'SVG', 512, $color);
158
-		$expectedIcon->setImageFormat('PNG32');
159
-		// run test
160
-		$result = $this->iconBuilder->getTouchIcon($app);
161
-		$this->assertIsString($result, 'Touch icon generation should return a PNG blob');
162
-		$this->assertEquals($expectedIcon->getImageBlob(), $result, 'Generated touch icon differs from expected');
163
-		$expectedIcon->destroy();
164
-	}
139
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataRenderAppIconSvg')]
140
+    public function testGetTouchIconSvg(string $app, string $color, string $file): void {
141
+        $this->checkImagick('SVG');
142
+        // mock required methods
143
+        $this->imageManager->expects($this->any())
144
+            ->method('canConvert')
145
+            ->willReturnMap([
146
+                ['SVG', true],
147
+                ['PNG', true]
148
+            ]);
149
+        $this->util->expects($this->once())
150
+            ->method('getAppIcon')
151
+            ->with($app, true)
152
+            ->willReturn(__DIR__ . '/data/' . $file);
153
+        $this->themingDefaults->expects($this->any())
154
+            ->method('getColorPrimary')
155
+            ->willReturn($color);
156
+        // generate expected output from source file
157
+        $expectedIcon = $this->generateTestIcon($file, 'SVG', 512, $color);
158
+        $expectedIcon->setImageFormat('PNG32');
159
+        // run test
160
+        $result = $this->iconBuilder->getTouchIcon($app);
161
+        $this->assertIsString($result, 'Touch icon generation should return a PNG blob');
162
+        $this->assertEquals($expectedIcon->getImageBlob(), $result, 'Generated touch icon differs from expected');
163
+        $expectedIcon->destroy();
164
+    }
165 165
 
166
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataRenderAppIconPng')]
167
-	public function testGetTouchIconPng(string $app, string $color, string $file): void {
168
-		$this->checkImagick('PNG');
169
-		// mock required methods
170
-		$this->imageManager->expects($this->any())
171
-			->method('canConvert')
172
-			->willReturnMap([
173
-				['SVG', false],
174
-				['PNG', true]
175
-			]);
176
-		$this->util->expects($this->once())
177
-			->method('getAppIcon')
178
-			->with($app, false)
179
-			->willReturn(__DIR__ . '/data/' . $file);
180
-		$this->themingDefaults->expects($this->any())
181
-			->method('getColorPrimary')
182
-			->willReturn($color);
183
-		// generate expected output from source file
184
-		$expectedIcon = $this->generateTestIcon($file, 'PNG', 512, $color);
185
-		$expectedIcon->setImageFormat('PNG32');
186
-		// run test
187
-		$result = $this->iconBuilder->getTouchIcon($app);
188
-		$this->assertIsString($result, 'Touch icon generation should return a PNG blob');
189
-		$this->assertEquals($expectedIcon->getImageBlob(), $result, 'Generated touch icon differs from expected');
190
-		$expectedIcon->destroy();
191
-	}
166
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataRenderAppIconPng')]
167
+    public function testGetTouchIconPng(string $app, string $color, string $file): void {
168
+        $this->checkImagick('PNG');
169
+        // mock required methods
170
+        $this->imageManager->expects($this->any())
171
+            ->method('canConvert')
172
+            ->willReturnMap([
173
+                ['SVG', false],
174
+                ['PNG', true]
175
+            ]);
176
+        $this->util->expects($this->once())
177
+            ->method('getAppIcon')
178
+            ->with($app, false)
179
+            ->willReturn(__DIR__ . '/data/' . $file);
180
+        $this->themingDefaults->expects($this->any())
181
+            ->method('getColorPrimary')
182
+            ->willReturn($color);
183
+        // generate expected output from source file
184
+        $expectedIcon = $this->generateTestIcon($file, 'PNG', 512, $color);
185
+        $expectedIcon->setImageFormat('PNG32');
186
+        // run test
187
+        $result = $this->iconBuilder->getTouchIcon($app);
188
+        $this->assertIsString($result, 'Touch icon generation should return a PNG blob');
189
+        $this->assertEquals($expectedIcon->getImageBlob(), $result, 'Generated touch icon differs from expected');
190
+        $expectedIcon->destroy();
191
+    }
192 192
 
193
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataRenderAppIconSvg')]
194
-	public function testGetFavIconSvg(string $app, string $color, string $file): void {
195
-		$this->checkImagick('SVG');
196
-		// mock required methods
197
-		$this->imageManager->expects($this->any())
198
-			->method('canConvert')
199
-			->willReturnMap([
200
-				['ICO', true],
201
-				['SVG', true],
202
-				['PNG', true]
203
-			]);
204
-		$this->util->expects($this->once())
205
-			->method('getAppIcon')
206
-			->with($app, true)
207
-			->willReturn(__DIR__ . '/data/' . $file);
208
-		$this->themingDefaults->expects($this->any())
209
-			->method('getColorPrimary')
210
-			->willReturn($color);
211
-		// generate expected output from source file
212
-		$expectedIcon = $this->generateTestFavIcon($file, 'SVG', $color);
213
-		// run test
214
-		$result = $this->iconBuilder->getFavicon($app);
215
-		$this->assertIsString($result, 'Favicon generation should return a ICO blob');
216
-		$this->assertEquals($expectedIcon->getImagesBlob(), $result, 'Generated favicon differs from expected');
217
-		$expectedIcon->destroy();
218
-	}
193
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataRenderAppIconSvg')]
194
+    public function testGetFavIconSvg(string $app, string $color, string $file): void {
195
+        $this->checkImagick('SVG');
196
+        // mock required methods
197
+        $this->imageManager->expects($this->any())
198
+            ->method('canConvert')
199
+            ->willReturnMap([
200
+                ['ICO', true],
201
+                ['SVG', true],
202
+                ['PNG', true]
203
+            ]);
204
+        $this->util->expects($this->once())
205
+            ->method('getAppIcon')
206
+            ->with($app, true)
207
+            ->willReturn(__DIR__ . '/data/' . $file);
208
+        $this->themingDefaults->expects($this->any())
209
+            ->method('getColorPrimary')
210
+            ->willReturn($color);
211
+        // generate expected output from source file
212
+        $expectedIcon = $this->generateTestFavIcon($file, 'SVG', $color);
213
+        // run test
214
+        $result = $this->iconBuilder->getFavicon($app);
215
+        $this->assertIsString($result, 'Favicon generation should return a ICO blob');
216
+        $this->assertEquals($expectedIcon->getImagesBlob(), $result, 'Generated favicon differs from expected');
217
+        $expectedIcon->destroy();
218
+    }
219 219
 
220
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataRenderAppIconPng')]
221
-	public function testGetFaviconPng(string $app, string $color, string $file): void {
222
-		$this->checkImagick('PNG');
223
-		// mock required methods
224
-		$this->imageManager->expects($this->any())
225
-			->method('canConvert')
226
-			->willReturnMap([
227
-				['ICO', true],
228
-				['SVG', false],
229
-				['PNG', true]
230
-			]);
231
-		$this->util->expects($this->once())
232
-			->method('getAppIcon')
233
-			->with($app, false)
234
-			->willReturn(__DIR__ . '/data/' . $file);
235
-		$this->themingDefaults->expects($this->any())
236
-			->method('getColorPrimary')
237
-			->willReturn($color);
238
-		// generate expected output from source file
239
-		$expectedIcon = $this->generateTestFavIcon($file, 'PNG', $color);
240
-		// run test
241
-		$result = $this->iconBuilder->getFavicon($app);
242
-		$this->assertIsString($result, 'Favicon generation should return a ICO blob');
243
-		$this->assertEquals($expectedIcon->getImagesBlob(), $result, 'Generated favicon differs from expected');
244
-		$expectedIcon->destroy();
245
-	}
220
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataRenderAppIconPng')]
221
+    public function testGetFaviconPng(string $app, string $color, string $file): void {
222
+        $this->checkImagick('PNG');
223
+        // mock required methods
224
+        $this->imageManager->expects($this->any())
225
+            ->method('canConvert')
226
+            ->willReturnMap([
227
+                ['ICO', true],
228
+                ['SVG', false],
229
+                ['PNG', true]
230
+            ]);
231
+        $this->util->expects($this->once())
232
+            ->method('getAppIcon')
233
+            ->with($app, false)
234
+            ->willReturn(__DIR__ . '/data/' . $file);
235
+        $this->themingDefaults->expects($this->any())
236
+            ->method('getColorPrimary')
237
+            ->willReturn($color);
238
+        // generate expected output from source file
239
+        $expectedIcon = $this->generateTestFavIcon($file, 'PNG', $color);
240
+        // run test
241
+        $result = $this->iconBuilder->getFavicon($app);
242
+        $this->assertIsString($result, 'Favicon generation should return a ICO blob');
243
+        $this->assertEquals($expectedIcon->getImagesBlob(), $result, 'Generated favicon differs from expected');
244
+        $expectedIcon->destroy();
245
+    }
246 246
 
247
-	public function testGetFaviconNotFound(): void {
248
-		$this->checkImagick('ICO');
249
-		$util = $this->createMock(Util::class);
250
-		$iconBuilder = new IconBuilder($this->themingDefaults, $util, $this->imageManager);
251
-		$this->imageManager->expects($this->any())
252
-			->method('canConvert')
253
-			->willReturn(true);
254
-		$util->expects($this->once())
255
-			->method('getAppIcon')
256
-			->willReturn('notexistingfile');
257
-		$result = $iconBuilder->getFavicon('noapp');
258
-		$this->assertFalse($result, 'Favicon generation should fail for missing file');
259
-	}
247
+    public function testGetFaviconNotFound(): void {
248
+        $this->checkImagick('ICO');
249
+        $util = $this->createMock(Util::class);
250
+        $iconBuilder = new IconBuilder($this->themingDefaults, $util, $this->imageManager);
251
+        $this->imageManager->expects($this->any())
252
+            ->method('canConvert')
253
+            ->willReturn(true);
254
+        $util->expects($this->once())
255
+            ->method('getAppIcon')
256
+            ->willReturn('notexistingfile');
257
+        $result = $iconBuilder->getFavicon('noapp');
258
+        $this->assertFalse($result, 'Favicon generation should fail for missing file');
259
+    }
260 260
 
261
-	public function testGetTouchIconNotFound(): void {
262
-		$this->checkImagick();
263
-		$util = $this->createMock(Util::class);
264
-		$iconBuilder = new IconBuilder($this->themingDefaults, $util, $this->imageManager);
265
-		$util->expects($this->once())
266
-			->method('getAppIcon')
267
-			->willReturn('notexistingfile');
268
-		$this->assertFalse($iconBuilder->getTouchIcon('noapp'));
269
-	}
261
+    public function testGetTouchIconNotFound(): void {
262
+        $this->checkImagick();
263
+        $util = $this->createMock(Util::class);
264
+        $iconBuilder = new IconBuilder($this->themingDefaults, $util, $this->imageManager);
265
+        $util->expects($this->once())
266
+            ->method('getAppIcon')
267
+            ->willReturn('notexistingfile');
268
+        $this->assertFalse($iconBuilder->getTouchIcon('noapp'));
269
+    }
270 270
 
271
-	public function testColorSvgNotFound(): void {
272
-		$this->checkImagick();
273
-		$util = $this->createMock(Util::class);
274
-		$iconBuilder = new IconBuilder($this->themingDefaults, $util, $this->imageManager);
275
-		$util->expects($this->once())
276
-			->method('getAppImage')
277
-			->willReturn('notexistingfile');
278
-		$this->assertFalse($iconBuilder->colorSvg('noapp', 'noimage'));
279
-	}
271
+    public function testColorSvgNotFound(): void {
272
+        $this->checkImagick();
273
+        $util = $this->createMock(Util::class);
274
+        $iconBuilder = new IconBuilder($this->themingDefaults, $util, $this->imageManager);
275
+        $util->expects($this->once())
276
+            ->method('getAppImage')
277
+            ->willReturn('notexistingfile');
278
+        $this->assertFalse($iconBuilder->colorSvg('noapp', 'noimage'));
279
+    }
280 280
 
281
-	/**
282
-	 * Helper to generate expected icon from source file for tests.
283
-	 */
284
-	private function generateTestIcon(string $file, string $format, int $size, string $color): \Imagick {
285
-		$filePath = realpath(__DIR__ . '/data/' . $file);
286
-		$appIconFile = new \Imagick();
287
-		if ($format === 'SVG') {
288
-			$svgContent = file_get_contents($filePath);
289
-			if (substr($svgContent, 0, 5) !== '<?xml') {
290
-				$svgContent = '<?xml version="1.0"?>' . $svgContent;
291
-			}
292
-			// get dimensions for resolution calculation
293
-			$tmp = new \Imagick();
294
-			$tmp->setBackgroundColor(new \ImagickPixel('transparent'));
295
-			$tmp->setResolution(72, 72);
296
-			$tmp->readImageBlob($svgContent);
297
-			$x = $tmp->getImageWidth();
298
-			$y = $tmp->getImageHeight();
299
-			$tmp->destroy();
300
-			// set resolution for proper scaling
301
-			$resX = (int)(72 * $size / $x);
302
-			$resY = (int)(72 * $size / $y);
303
-			$appIconFile->setBackgroundColor(new \ImagickPixel('transparent'));
304
-			$appIconFile->setResolution($resX, $resY);
305
-			$appIconFile->readImageBlob($svgContent);
306
-		} else {
307
-			$appIconFile->readImage($filePath);
308
-		}
309
-		$padding = 0.85;
310
-		$original_w = $appIconFile->getImageWidth();
311
-		$original_h = $appIconFile->getImageHeight();
312
-		$contentSize = (int)floor($size * $padding);
313
-		$scale = min($contentSize / $original_w, $contentSize / $original_h);
314
-		$new_w = max(1, (int)floor($original_w * $scale));
315
-		$new_h = max(1, (int)floor($original_h * $scale));
316
-		$offset_w = (int)floor(($size - $new_w) / 2);
317
-		$offset_h = (int)floor(($size - $new_h) / 2);
318
-		$cornerRadius = 0.2 * $size;
319
-		$appIconFile->resizeImage($new_w, $new_h, \Imagick::FILTER_LANCZOS, 1);
320
-		$finalIconFile = new \Imagick();
321
-		$finalIconFile->setBackgroundColor(new \ImagickPixel('transparent'));
322
-		$finalIconFile->newImage($size, $size, new \ImagickPixel('transparent'));
323
-		$draw = new \ImagickDraw();
324
-		$draw->setFillColor($color);
325
-		$draw->roundRectangle(0, 0, $size - 1, $size - 1, $cornerRadius, $cornerRadius);
326
-		$finalIconFile->drawImage($draw);
327
-		$draw->destroy();
328
-		$finalIconFile->setImageVirtualPixelMethod(\Imagick::VIRTUALPIXELMETHOD_TRANSPARENT);
329
-		$finalIconFile->setImageArtifact('compose:args', '1,0,-0.5,0.5');
330
-		$finalIconFile->compositeImage($appIconFile, \Imagick::COMPOSITE_ATOP, $offset_w, $offset_h);
331
-		$finalIconFile->setImageFormat('PNG32');
332
-		if (defined('Imagick::INTERPOLATE_BICUBIC') === true) {
333
-			$filter = \Imagick::INTERPOLATE_BICUBIC;
334
-		} else {
335
-			$filter = \Imagick::FILTER_LANCZOS;
336
-		}
337
-		$finalIconFile->resizeImage($size, $size, $filter, 1, false);
338
-		$finalIconFile->setImageFormat('png');
339
-		$appIconFile->destroy();
340
-		return $finalIconFile;
341
-	}
281
+    /**
282
+     * Helper to generate expected icon from source file for tests.
283
+     */
284
+    private function generateTestIcon(string $file, string $format, int $size, string $color): \Imagick {
285
+        $filePath = realpath(__DIR__ . '/data/' . $file);
286
+        $appIconFile = new \Imagick();
287
+        if ($format === 'SVG') {
288
+            $svgContent = file_get_contents($filePath);
289
+            if (substr($svgContent, 0, 5) !== '<?xml') {
290
+                $svgContent = '<?xml version="1.0"?>' . $svgContent;
291
+            }
292
+            // get dimensions for resolution calculation
293
+            $tmp = new \Imagick();
294
+            $tmp->setBackgroundColor(new \ImagickPixel('transparent'));
295
+            $tmp->setResolution(72, 72);
296
+            $tmp->readImageBlob($svgContent);
297
+            $x = $tmp->getImageWidth();
298
+            $y = $tmp->getImageHeight();
299
+            $tmp->destroy();
300
+            // set resolution for proper scaling
301
+            $resX = (int)(72 * $size / $x);
302
+            $resY = (int)(72 * $size / $y);
303
+            $appIconFile->setBackgroundColor(new \ImagickPixel('transparent'));
304
+            $appIconFile->setResolution($resX, $resY);
305
+            $appIconFile->readImageBlob($svgContent);
306
+        } else {
307
+            $appIconFile->readImage($filePath);
308
+        }
309
+        $padding = 0.85;
310
+        $original_w = $appIconFile->getImageWidth();
311
+        $original_h = $appIconFile->getImageHeight();
312
+        $contentSize = (int)floor($size * $padding);
313
+        $scale = min($contentSize / $original_w, $contentSize / $original_h);
314
+        $new_w = max(1, (int)floor($original_w * $scale));
315
+        $new_h = max(1, (int)floor($original_h * $scale));
316
+        $offset_w = (int)floor(($size - $new_w) / 2);
317
+        $offset_h = (int)floor(($size - $new_h) / 2);
318
+        $cornerRadius = 0.2 * $size;
319
+        $appIconFile->resizeImage($new_w, $new_h, \Imagick::FILTER_LANCZOS, 1);
320
+        $finalIconFile = new \Imagick();
321
+        $finalIconFile->setBackgroundColor(new \ImagickPixel('transparent'));
322
+        $finalIconFile->newImage($size, $size, new \ImagickPixel('transparent'));
323
+        $draw = new \ImagickDraw();
324
+        $draw->setFillColor($color);
325
+        $draw->roundRectangle(0, 0, $size - 1, $size - 1, $cornerRadius, $cornerRadius);
326
+        $finalIconFile->drawImage($draw);
327
+        $draw->destroy();
328
+        $finalIconFile->setImageVirtualPixelMethod(\Imagick::VIRTUALPIXELMETHOD_TRANSPARENT);
329
+        $finalIconFile->setImageArtifact('compose:args', '1,0,-0.5,0.5');
330
+        $finalIconFile->compositeImage($appIconFile, \Imagick::COMPOSITE_ATOP, $offset_w, $offset_h);
331
+        $finalIconFile->setImageFormat('PNG32');
332
+        if (defined('Imagick::INTERPOLATE_BICUBIC') === true) {
333
+            $filter = \Imagick::INTERPOLATE_BICUBIC;
334
+        } else {
335
+            $filter = \Imagick::FILTER_LANCZOS;
336
+        }
337
+        $finalIconFile->resizeImage($size, $size, $filter, 1, false);
338
+        $finalIconFile->setImageFormat('png');
339
+        $appIconFile->destroy();
340
+        return $finalIconFile;
341
+    }
342 342
 
343
-	/**
344
-	 * Helper to generate expected favicon from source file for tests.
345
-	 */
346
-	private function generateTestFavIcon(string $file, string $format, string $color): \Imagick {
347
-		$baseIcon = $this->generateTestIcon($file, $format, 128, $color);
348
-		$baseIcon->setImageFormat('PNG32');
343
+    /**
344
+     * Helper to generate expected favicon from source file for tests.
345
+     */
346
+    private function generateTestFavIcon(string $file, string $format, string $color): \Imagick {
347
+        $baseIcon = $this->generateTestIcon($file, $format, 128, $color);
348
+        $baseIcon->setImageFormat('PNG32');
349 349
 
350
-		$testIcon = new \Imagick();
351
-		$testIcon->setFormat('ICO');
352
-		foreach ([16, 32, 64, 128] as $size) {
353
-			$clone = clone $baseIcon;
354
-			$clone->scaleImage($size, 0);
355
-			$testIcon->addImage($clone);
356
-			$clone->destroy();
357
-		}
358
-		$baseIcon->destroy();
359
-		return $testIcon;
360
-	}
350
+        $testIcon = new \Imagick();
351
+        $testIcon->setFormat('ICO');
352
+        foreach ([16, 32, 64, 128] as $size) {
353
+            $clone = clone $baseIcon;
354
+            $clone->scaleImage($size, 0);
355
+            $testIcon->addImage($clone);
356
+            $clone->destroy();
357
+        }
358
+        $baseIcon->destroy();
359
+        return $testIcon;
360
+    }
361 361
 }
Please login to merge, or discard this patch.
Spacing   +16 added lines, -16 removed lines patch added patch discarded remove patch
@@ -49,7 +49,7 @@  discard block
 block discarded – undo
49 49
 		if ($provider !== null) {
50 50
 			$checkImagick = new \Imagick();
51 51
 			if (count($checkImagick->queryFormats($provider)) < 1) {
52
-				$this->markTestSkipped('Imagemagick ' . $provider . ' support is required for this icon generation test.');
52
+				$this->markTestSkipped('Imagemagick '.$provider.' support is required for this icon generation test.');
53 53
 			}
54 54
 		}
55 55
 	}
@@ -87,7 +87,7 @@  discard block
 block discarded – undo
87 87
 		$this->util->expects($this->once())
88 88
 			->method('getAppIcon')
89 89
 			->with($app, true)
90
-			->willReturn(__DIR__ . '/data/' . $file);
90
+			->willReturn(__DIR__.'/data/'.$file);
91 91
 		$this->themingDefaults->expects($this->any())
92 92
 			->method('getColorPrimary')
93 93
 			->willReturn($color);
@@ -118,7 +118,7 @@  discard block
 block discarded – undo
118 118
 		$this->util->expects($this->once())
119 119
 			->method('getAppIcon')
120 120
 			->with($app, false)
121
-			->willReturn(__DIR__ . '/data/' . $file);
121
+			->willReturn(__DIR__.'/data/'.$file);
122 122
 		$this->themingDefaults->expects($this->any())
123 123
 			->method('getColorPrimary')
124 124
 			->willReturn($color);
@@ -149,7 +149,7 @@  discard block
 block discarded – undo
149 149
 		$this->util->expects($this->once())
150 150
 			->method('getAppIcon')
151 151
 			->with($app, true)
152
-			->willReturn(__DIR__ . '/data/' . $file);
152
+			->willReturn(__DIR__.'/data/'.$file);
153 153
 		$this->themingDefaults->expects($this->any())
154 154
 			->method('getColorPrimary')
155 155
 			->willReturn($color);
@@ -176,7 +176,7 @@  discard block
 block discarded – undo
176 176
 		$this->util->expects($this->once())
177 177
 			->method('getAppIcon')
178 178
 			->with($app, false)
179
-			->willReturn(__DIR__ . '/data/' . $file);
179
+			->willReturn(__DIR__.'/data/'.$file);
180 180
 		$this->themingDefaults->expects($this->any())
181 181
 			->method('getColorPrimary')
182 182
 			->willReturn($color);
@@ -204,7 +204,7 @@  discard block
 block discarded – undo
204 204
 		$this->util->expects($this->once())
205 205
 			->method('getAppIcon')
206 206
 			->with($app, true)
207
-			->willReturn(__DIR__ . '/data/' . $file);
207
+			->willReturn(__DIR__.'/data/'.$file);
208 208
 		$this->themingDefaults->expects($this->any())
209 209
 			->method('getColorPrimary')
210 210
 			->willReturn($color);
@@ -231,7 +231,7 @@  discard block
 block discarded – undo
231 231
 		$this->util->expects($this->once())
232 232
 			->method('getAppIcon')
233 233
 			->with($app, false)
234
-			->willReturn(__DIR__ . '/data/' . $file);
234
+			->willReturn(__DIR__.'/data/'.$file);
235 235
 		$this->themingDefaults->expects($this->any())
236 236
 			->method('getColorPrimary')
237 237
 			->willReturn($color);
@@ -282,12 +282,12 @@  discard block
 block discarded – undo
282 282
 	 * Helper to generate expected icon from source file for tests.
283 283
 	 */
284 284
 	private function generateTestIcon(string $file, string $format, int $size, string $color): \Imagick {
285
-		$filePath = realpath(__DIR__ . '/data/' . $file);
285
+		$filePath = realpath(__DIR__.'/data/'.$file);
286 286
 		$appIconFile = new \Imagick();
287 287
 		if ($format === 'SVG') {
288 288
 			$svgContent = file_get_contents($filePath);
289 289
 			if (substr($svgContent, 0, 5) !== '<?xml') {
290
-				$svgContent = '<?xml version="1.0"?>' . $svgContent;
290
+				$svgContent = '<?xml version="1.0"?>'.$svgContent;
291 291
 			}
292 292
 			// get dimensions for resolution calculation
293 293
 			$tmp = new \Imagick();
@@ -298,8 +298,8 @@  discard block
 block discarded – undo
298 298
 			$y = $tmp->getImageHeight();
299 299
 			$tmp->destroy();
300 300
 			// set resolution for proper scaling
301
-			$resX = (int)(72 * $size / $x);
302
-			$resY = (int)(72 * $size / $y);
301
+			$resX = (int) (72 * $size / $x);
302
+			$resY = (int) (72 * $size / $y);
303 303
 			$appIconFile->setBackgroundColor(new \ImagickPixel('transparent'));
304 304
 			$appIconFile->setResolution($resX, $resY);
305 305
 			$appIconFile->readImageBlob($svgContent);
@@ -309,12 +309,12 @@  discard block
 block discarded – undo
309 309
 		$padding = 0.85;
310 310
 		$original_w = $appIconFile->getImageWidth();
311 311
 		$original_h = $appIconFile->getImageHeight();
312
-		$contentSize = (int)floor($size * $padding);
312
+		$contentSize = (int) floor($size * $padding);
313 313
 		$scale = min($contentSize / $original_w, $contentSize / $original_h);
314
-		$new_w = max(1, (int)floor($original_w * $scale));
315
-		$new_h = max(1, (int)floor($original_h * $scale));
316
-		$offset_w = (int)floor(($size - $new_w) / 2);
317
-		$offset_h = (int)floor(($size - $new_h) / 2);
314
+		$new_w = max(1, (int) floor($original_w * $scale));
315
+		$new_h = max(1, (int) floor($original_h * $scale));
316
+		$offset_w = (int) floor(($size - $new_w) / 2);
317
+		$offset_h = (int) floor(($size - $new_h) / 2);
318 318
 		$cornerRadius = 0.2 * $size;
319 319
 		$appIconFile->resizeImage($new_w, $new_h, \Imagick::FILTER_LANCZOS, 1);
320 320
 		$finalIconFile = new \Imagick();
Please login to merge, or discard this patch.
apps/theming/lib/Controller/ThemingController.php 2 patches
Indentation   +444 added lines, -444 removed lines patch added patch discarded remove patch
@@ -42,474 +42,474 @@
 block discarded – undo
42 42
  * @package OCA\Theming\Controller
43 43
  */
44 44
 class ThemingController extends Controller {
45
-	public const VALID_UPLOAD_KEYS = ['header', 'logo', 'logoheader', 'background', 'favicon'];
45
+    public const VALID_UPLOAD_KEYS = ['header', 'logo', 'logoheader', 'background', 'favicon'];
46 46
 
47
-	public function __construct(
48
-		string $appName,
49
-		IRequest $request,
50
-		private IConfig $config,
51
-		private IAppConfig $appConfig,
52
-		private ThemingDefaults $themingDefaults,
53
-		private IL10N $l10n,
54
-		private IURLGenerator $urlGenerator,
55
-		private IAppManager $appManager,
56
-		private ImageManager $imageManager,
57
-		private ThemesService $themesService,
58
-		private INavigationManager $navigationManager,
59
-	) {
60
-		parent::__construct($appName, $request);
61
-	}
47
+    public function __construct(
48
+        string $appName,
49
+        IRequest $request,
50
+        private IConfig $config,
51
+        private IAppConfig $appConfig,
52
+        private ThemingDefaults $themingDefaults,
53
+        private IL10N $l10n,
54
+        private IURLGenerator $urlGenerator,
55
+        private IAppManager $appManager,
56
+        private ImageManager $imageManager,
57
+        private ThemesService $themesService,
58
+        private INavigationManager $navigationManager,
59
+    ) {
60
+        parent::__construct($appName, $request);
61
+    }
62 62
 
63
-	/**
64
-	 * @param string $setting
65
-	 * @param string $value
66
-	 * @return DataResponse
67
-	 * @throws NotPermittedException
68
-	 */
69
-	#[AuthorizedAdminSetting(settings: Admin::class)]
70
-	public function updateStylesheet($setting, $value) {
71
-		$value = trim($value);
72
-		$error = null;
73
-		$saved = false;
74
-		switch ($setting) {
75
-			case 'name':
76
-				if (strlen($value) > 250) {
77
-					$error = $this->l10n->t('The given name is too long');
78
-				}
79
-				break;
80
-			case 'url':
81
-				if (strlen($value) > 500) {
82
-					$error = $this->l10n->t('The given web address is too long');
83
-				}
84
-				if ($value !== '' && !$this->isValidUrl($value)) {
85
-					$error = $this->l10n->t('The given web address is not a valid URL');
86
-				}
87
-				break;
88
-			case 'legalNoticeUrl':
89
-				$setting = 'imprintUrl';
90
-				// no break
91
-			case 'imprintUrl':
92
-				if (strlen($value) > 500) {
93
-					$error = $this->l10n->t('The given legal notice address is too long');
94
-				}
95
-				if ($value !== '' && !$this->isValidUrl($value)) {
96
-					$error = $this->l10n->t('The given legal notice address is not a valid URL');
97
-				}
98
-				break;
99
-			case 'privacyPolicyUrl':
100
-				$setting = 'privacyUrl';
101
-				// no break
102
-			case 'privacyUrl':
103
-				if (strlen($value) > 500) {
104
-					$error = $this->l10n->t('The given privacy policy address is too long');
105
-				}
106
-				if ($value !== '' && !$this->isValidUrl($value)) {
107
-					$error = $this->l10n->t('The given privacy policy address is not a valid URL');
108
-				}
109
-				break;
110
-			case 'slogan':
111
-				if (strlen($value) > 500) {
112
-					$error = $this->l10n->t('The given slogan is too long');
113
-				}
114
-				break;
115
-			case 'primaryColor':
116
-				$setting = 'primary_color';
117
-				// no break
118
-			case 'primary_color':
119
-				if (!preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $value)) {
120
-					$error = $this->l10n->t('The given color is invalid');
121
-				}
122
-				break;
123
-			case 'backgroundColor':
124
-				$setting = 'background_color';
125
-				// no break
126
-			case 'background_color':
127
-				if (!preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $value)) {
128
-					$error = $this->l10n->t('The given color is invalid');
129
-				}
130
-				break;
131
-			case 'disableUserTheming':
132
-			case 'disable-user-theming':
133
-				if (!in_array($value, ['yes', 'true', 'no', 'false'])) {
134
-					$error = $this->l10n->t('%1$s should be true or false', ['disable-user-theming']);
135
-				} else {
136
-					$this->appConfig->setAppValueBool('disable-user-theming', $value === 'yes' || $value === 'true');
137
-					$saved = true;
138
-				}
139
-				break;
140
-			case 'backgroundMime':
141
-				if ($value !== 'backgroundColor') {
142
-					$error = $this->l10n->t('%1$s can only be set to %2$s through the API', ['backgroundMime', 'backgroundColor']);
143
-				}
144
-				break;
145
-			default:
146
-				$error = $this->l10n->t('Invalid setting key');
147
-		}
148
-		if ($error !== null) {
149
-			return new DataResponse([
150
-				'data' => [
151
-					'message' => $error,
152
-				],
153
-				'status' => 'error'
154
-			], Http::STATUS_BAD_REQUEST);
155
-		}
63
+    /**
64
+     * @param string $setting
65
+     * @param string $value
66
+     * @return DataResponse
67
+     * @throws NotPermittedException
68
+     */
69
+    #[AuthorizedAdminSetting(settings: Admin::class)]
70
+    public function updateStylesheet($setting, $value) {
71
+        $value = trim($value);
72
+        $error = null;
73
+        $saved = false;
74
+        switch ($setting) {
75
+            case 'name':
76
+                if (strlen($value) > 250) {
77
+                    $error = $this->l10n->t('The given name is too long');
78
+                }
79
+                break;
80
+            case 'url':
81
+                if (strlen($value) > 500) {
82
+                    $error = $this->l10n->t('The given web address is too long');
83
+                }
84
+                if ($value !== '' && !$this->isValidUrl($value)) {
85
+                    $error = $this->l10n->t('The given web address is not a valid URL');
86
+                }
87
+                break;
88
+            case 'legalNoticeUrl':
89
+                $setting = 'imprintUrl';
90
+                // no break
91
+            case 'imprintUrl':
92
+                if (strlen($value) > 500) {
93
+                    $error = $this->l10n->t('The given legal notice address is too long');
94
+                }
95
+                if ($value !== '' && !$this->isValidUrl($value)) {
96
+                    $error = $this->l10n->t('The given legal notice address is not a valid URL');
97
+                }
98
+                break;
99
+            case 'privacyPolicyUrl':
100
+                $setting = 'privacyUrl';
101
+                // no break
102
+            case 'privacyUrl':
103
+                if (strlen($value) > 500) {
104
+                    $error = $this->l10n->t('The given privacy policy address is too long');
105
+                }
106
+                if ($value !== '' && !$this->isValidUrl($value)) {
107
+                    $error = $this->l10n->t('The given privacy policy address is not a valid URL');
108
+                }
109
+                break;
110
+            case 'slogan':
111
+                if (strlen($value) > 500) {
112
+                    $error = $this->l10n->t('The given slogan is too long');
113
+                }
114
+                break;
115
+            case 'primaryColor':
116
+                $setting = 'primary_color';
117
+                // no break
118
+            case 'primary_color':
119
+                if (!preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $value)) {
120
+                    $error = $this->l10n->t('The given color is invalid');
121
+                }
122
+                break;
123
+            case 'backgroundColor':
124
+                $setting = 'background_color';
125
+                // no break
126
+            case 'background_color':
127
+                if (!preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $value)) {
128
+                    $error = $this->l10n->t('The given color is invalid');
129
+                }
130
+                break;
131
+            case 'disableUserTheming':
132
+            case 'disable-user-theming':
133
+                if (!in_array($value, ['yes', 'true', 'no', 'false'])) {
134
+                    $error = $this->l10n->t('%1$s should be true or false', ['disable-user-theming']);
135
+                } else {
136
+                    $this->appConfig->setAppValueBool('disable-user-theming', $value === 'yes' || $value === 'true');
137
+                    $saved = true;
138
+                }
139
+                break;
140
+            case 'backgroundMime':
141
+                if ($value !== 'backgroundColor') {
142
+                    $error = $this->l10n->t('%1$s can only be set to %2$s through the API', ['backgroundMime', 'backgroundColor']);
143
+                }
144
+                break;
145
+            default:
146
+                $error = $this->l10n->t('Invalid setting key');
147
+        }
148
+        if ($error !== null) {
149
+            return new DataResponse([
150
+                'data' => [
151
+                    'message' => $error,
152
+                ],
153
+                'status' => 'error'
154
+            ], Http::STATUS_BAD_REQUEST);
155
+        }
156 156
 
157
-		if (!$saved) {
158
-			$this->themingDefaults->set($setting, $value);
159
-		}
157
+        if (!$saved) {
158
+            $this->themingDefaults->set($setting, $value);
159
+        }
160 160
 
161
-		return new DataResponse([
162
-			'data' => [
163
-				'message' => $this->l10n->t('Saved'),
164
-			],
165
-			'status' => 'success'
166
-		]);
167
-	}
161
+        return new DataResponse([
162
+            'data' => [
163
+                'message' => $this->l10n->t('Saved'),
164
+            ],
165
+            'status' => 'success'
166
+        ]);
167
+    }
168 168
 
169
-	/**
170
-	 * @param string $setting
171
-	 * @param mixed $value
172
-	 * @return DataResponse
173
-	 * @throws NotPermittedException
174
-	 */
175
-	#[AuthorizedAdminSetting(settings: Admin::class)]
176
-	public function updateAppMenu($setting, $value) {
177
-		$error = null;
178
-		switch ($setting) {
179
-			case 'defaultApps':
180
-				if (is_array($value)) {
181
-					try {
182
-						$this->navigationManager->setDefaultEntryIds($value);
183
-					} catch (InvalidArgumentException $e) {
184
-						$error = $this->l10n->t('Invalid app given');
185
-					}
186
-				} else {
187
-					$error = $this->l10n->t('Invalid type for setting "defaultApp" given');
188
-				}
189
-				break;
190
-			default:
191
-				$error = $this->l10n->t('Invalid setting key');
192
-		}
193
-		if ($error !== null) {
194
-			return new DataResponse([
195
-				'data' => [
196
-					'message' => $error,
197
-				],
198
-				'status' => 'error'
199
-			], Http::STATUS_BAD_REQUEST);
200
-		}
169
+    /**
170
+     * @param string $setting
171
+     * @param mixed $value
172
+     * @return DataResponse
173
+     * @throws NotPermittedException
174
+     */
175
+    #[AuthorizedAdminSetting(settings: Admin::class)]
176
+    public function updateAppMenu($setting, $value) {
177
+        $error = null;
178
+        switch ($setting) {
179
+            case 'defaultApps':
180
+                if (is_array($value)) {
181
+                    try {
182
+                        $this->navigationManager->setDefaultEntryIds($value);
183
+                    } catch (InvalidArgumentException $e) {
184
+                        $error = $this->l10n->t('Invalid app given');
185
+                    }
186
+                } else {
187
+                    $error = $this->l10n->t('Invalid type for setting "defaultApp" given');
188
+                }
189
+                break;
190
+            default:
191
+                $error = $this->l10n->t('Invalid setting key');
192
+        }
193
+        if ($error !== null) {
194
+            return new DataResponse([
195
+                'data' => [
196
+                    'message' => $error,
197
+                ],
198
+                'status' => 'error'
199
+            ], Http::STATUS_BAD_REQUEST);
200
+        }
201 201
 
202
-		return new DataResponse([
203
-			'data' => [
204
-				'message' => $this->l10n->t('Saved'),
205
-			],
206
-			'status' => 'success'
207
-		]);
208
-	}
202
+        return new DataResponse([
203
+            'data' => [
204
+                'message' => $this->l10n->t('Saved'),
205
+            ],
206
+            'status' => 'success'
207
+        ]);
208
+    }
209 209
 
210
-	/**
211
-	 * Check that a string is a valid http/https url.
212
-	 * Also validates that there is no way for XSS through HTML
213
-	 */
214
-	private function isValidUrl(string $url): bool {
215
-		return ((str_starts_with($url, 'http://') || str_starts_with($url, 'https://'))
216
-			&& filter_var($url, FILTER_VALIDATE_URL) !== false)
217
-			&& !str_contains($url, '"');
218
-	}
210
+    /**
211
+     * Check that a string is a valid http/https url.
212
+     * Also validates that there is no way for XSS through HTML
213
+     */
214
+    private function isValidUrl(string $url): bool {
215
+        return ((str_starts_with($url, 'http://') || str_starts_with($url, 'https://'))
216
+            && filter_var($url, FILTER_VALIDATE_URL) !== false)
217
+            && !str_contains($url, '"');
218
+    }
219 219
 
220
-	/**
221
-	 * @return DataResponse
222
-	 * @throws NotPermittedException
223
-	 */
224
-	#[AuthorizedAdminSetting(settings: Admin::class)]
225
-	public function uploadImage(): DataResponse {
226
-		$key = $this->request->getParam('key');
227
-		if (!in_array($key, self::VALID_UPLOAD_KEYS, true)) {
228
-			return new DataResponse(
229
-				[
230
-					'data' => [
231
-						'message' => 'Invalid key'
232
-					],
233
-					'status' => 'failure',
234
-				],
235
-				Http::STATUS_BAD_REQUEST
236
-			);
237
-		}
238
-		$image = $this->request->getUploadedFile('image');
239
-		$error = null;
240
-		$phpFileUploadErrors = [
241
-			UPLOAD_ERR_OK => $this->l10n->t('The file was uploaded'),
242
-			UPLOAD_ERR_INI_SIZE => $this->l10n->t('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
243
-			UPLOAD_ERR_FORM_SIZE => $this->l10n->t('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
244
-			UPLOAD_ERR_PARTIAL => $this->l10n->t('The file was only partially uploaded'),
245
-			UPLOAD_ERR_NO_FILE => $this->l10n->t('No file was uploaded'),
246
-			UPLOAD_ERR_NO_TMP_DIR => $this->l10n->t('Missing a temporary folder'),
247
-			UPLOAD_ERR_CANT_WRITE => $this->l10n->t('Could not write file to disk'),
248
-			UPLOAD_ERR_EXTENSION => $this->l10n->t('A PHP extension stopped the file upload'),
249
-		];
250
-		if (empty($image)) {
251
-			$error = $this->l10n->t('No file uploaded');
252
-		}
253
-		if (!empty($image) && array_key_exists('error', $image) && $image['error'] !== UPLOAD_ERR_OK) {
254
-			$error = $phpFileUploadErrors[$image['error']];
255
-		}
220
+    /**
221
+     * @return DataResponse
222
+     * @throws NotPermittedException
223
+     */
224
+    #[AuthorizedAdminSetting(settings: Admin::class)]
225
+    public function uploadImage(): DataResponse {
226
+        $key = $this->request->getParam('key');
227
+        if (!in_array($key, self::VALID_UPLOAD_KEYS, true)) {
228
+            return new DataResponse(
229
+                [
230
+                    'data' => [
231
+                        'message' => 'Invalid key'
232
+                    ],
233
+                    'status' => 'failure',
234
+                ],
235
+                Http::STATUS_BAD_REQUEST
236
+            );
237
+        }
238
+        $image = $this->request->getUploadedFile('image');
239
+        $error = null;
240
+        $phpFileUploadErrors = [
241
+            UPLOAD_ERR_OK => $this->l10n->t('The file was uploaded'),
242
+            UPLOAD_ERR_INI_SIZE => $this->l10n->t('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
243
+            UPLOAD_ERR_FORM_SIZE => $this->l10n->t('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
244
+            UPLOAD_ERR_PARTIAL => $this->l10n->t('The file was only partially uploaded'),
245
+            UPLOAD_ERR_NO_FILE => $this->l10n->t('No file was uploaded'),
246
+            UPLOAD_ERR_NO_TMP_DIR => $this->l10n->t('Missing a temporary folder'),
247
+            UPLOAD_ERR_CANT_WRITE => $this->l10n->t('Could not write file to disk'),
248
+            UPLOAD_ERR_EXTENSION => $this->l10n->t('A PHP extension stopped the file upload'),
249
+        ];
250
+        if (empty($image)) {
251
+            $error = $this->l10n->t('No file uploaded');
252
+        }
253
+        if (!empty($image) && array_key_exists('error', $image) && $image['error'] !== UPLOAD_ERR_OK) {
254
+            $error = $phpFileUploadErrors[$image['error']];
255
+        }
256 256
 
257
-		if ($error !== null) {
258
-			return new DataResponse(
259
-				[
260
-					'data' => [
261
-						'message' => $error
262
-					],
263
-					'status' => 'failure',
264
-				],
265
-				Http::STATUS_UNPROCESSABLE_ENTITY
266
-			);
267
-		}
257
+        if ($error !== null) {
258
+            return new DataResponse(
259
+                [
260
+                    'data' => [
261
+                        'message' => $error
262
+                    ],
263
+                    'status' => 'failure',
264
+                ],
265
+                Http::STATUS_UNPROCESSABLE_ENTITY
266
+            );
267
+        }
268 268
 
269
-		try {
270
-			$mime = $this->imageManager->updateImage($key, $image['tmp_name']);
271
-			$this->themingDefaults->set($key . 'Mime', $mime);
272
-		} catch (\Exception $e) {
273
-			return new DataResponse(
274
-				[
275
-					'data' => [
276
-						'message' => $e->getMessage()
277
-					],
278
-					'status' => 'failure',
279
-				],
280
-				Http::STATUS_UNPROCESSABLE_ENTITY
281
-			);
282
-		}
269
+        try {
270
+            $mime = $this->imageManager->updateImage($key, $image['tmp_name']);
271
+            $this->themingDefaults->set($key . 'Mime', $mime);
272
+        } catch (\Exception $e) {
273
+            return new DataResponse(
274
+                [
275
+                    'data' => [
276
+                        'message' => $e->getMessage()
277
+                    ],
278
+                    'status' => 'failure',
279
+                ],
280
+                Http::STATUS_UNPROCESSABLE_ENTITY
281
+            );
282
+        }
283 283
 
284
-		$name = $image['name'];
284
+        $name = $image['name'];
285 285
 
286
-		return new DataResponse(
287
-			[
288
-				'data'
289
-					=> [
290
-						'name' => $name,
291
-						'url' => $this->imageManager->getImageUrl($key),
292
-						'message' => $this->l10n->t('Saved'),
293
-					],
294
-				'status' => 'success'
295
-			]
296
-		);
297
-	}
286
+        return new DataResponse(
287
+            [
288
+                'data'
289
+                    => [
290
+                        'name' => $name,
291
+                        'url' => $this->imageManager->getImageUrl($key),
292
+                        'message' => $this->l10n->t('Saved'),
293
+                    ],
294
+                'status' => 'success'
295
+            ]
296
+        );
297
+    }
298 298
 
299
-	/**
300
-	 * Revert setting to default value
301
-	 *
302
-	 * @param string $setting setting which should be reverted
303
-	 * @return DataResponse
304
-	 * @throws NotPermittedException
305
-	 */
306
-	#[AuthorizedAdminSetting(settings: Admin::class)]
307
-	public function undo(string $setting): DataResponse {
308
-		$setting = match ($setting) {
309
-			'primaryColor' => 'primary_color',
310
-			'backgroundColor' => 'background_color',
311
-			'legalNoticeUrl' => 'imprintUrl',
312
-			'privacyPolicyUrl' => 'privacyUrl',
313
-			default => $setting,
314
-		};
315
-		$value = $this->themingDefaults->undo($setting);
299
+    /**
300
+     * Revert setting to default value
301
+     *
302
+     * @param string $setting setting which should be reverted
303
+     * @return DataResponse
304
+     * @throws NotPermittedException
305
+     */
306
+    #[AuthorizedAdminSetting(settings: Admin::class)]
307
+    public function undo(string $setting): DataResponse {
308
+        $setting = match ($setting) {
309
+            'primaryColor' => 'primary_color',
310
+            'backgroundColor' => 'background_color',
311
+            'legalNoticeUrl' => 'imprintUrl',
312
+            'privacyPolicyUrl' => 'privacyUrl',
313
+            default => $setting,
314
+        };
315
+        $value = $this->themingDefaults->undo($setting);
316 316
 
317
-		return new DataResponse(
318
-			[
319
-				'data'
320
-					=> [
321
-						'value' => $value,
322
-						'message' => $this->l10n->t('Saved'),
323
-					],
324
-				'status' => 'success'
325
-			]
326
-		);
327
-	}
317
+        return new DataResponse(
318
+            [
319
+                'data'
320
+                    => [
321
+                        'value' => $value,
322
+                        'message' => $this->l10n->t('Saved'),
323
+                    ],
324
+                'status' => 'success'
325
+            ]
326
+        );
327
+    }
328 328
 
329
-	/**
330
-	 * Revert all theming settings to their default values
331
-	 *
332
-	 * @return DataResponse
333
-	 * @throws NotPermittedException
334
-	 */
335
-	#[AuthorizedAdminSetting(settings: Admin::class)]
336
-	public function undoAll(): DataResponse {
337
-		$this->themingDefaults->undoAll();
338
-		$this->navigationManager->setDefaultEntryIds([]);
329
+    /**
330
+     * Revert all theming settings to their default values
331
+     *
332
+     * @return DataResponse
333
+     * @throws NotPermittedException
334
+     */
335
+    #[AuthorizedAdminSetting(settings: Admin::class)]
336
+    public function undoAll(): DataResponse {
337
+        $this->themingDefaults->undoAll();
338
+        $this->navigationManager->setDefaultEntryIds([]);
339 339
 
340
-		return new DataResponse(
341
-			[
342
-				'data'
343
-					=> [
344
-						'message' => $this->l10n->t('Saved'),
345
-					],
346
-				'status' => 'success'
347
-			]
348
-		);
349
-	}
340
+        return new DataResponse(
341
+            [
342
+                'data'
343
+                    => [
344
+                        'message' => $this->l10n->t('Saved'),
345
+                    ],
346
+                'status' => 'success'
347
+            ]
348
+        );
349
+    }
350 350
 
351
-	/**
352
-	 * @NoSameSiteCookieRequired
353
-	 *
354
-	 * Get an image
355
-	 *
356
-	 * @param string $key Key of the image
357
-	 * @param bool $useSvg Return image as SVG
358
-	 * @return FileDisplayResponse<Http::STATUS_OK, array{}>|NotFoundResponse<Http::STATUS_NOT_FOUND, array{}>
359
-	 * @throws NotPermittedException
360
-	 *
361
-	 * 200: Image returned
362
-	 * 404: Image not found
363
-	 */
364
-	#[PublicPage]
365
-	#[NoCSRFRequired]
366
-	#[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
367
-	public function getImage(string $key, bool $useSvg = true) {
368
-		try {
369
-			$useSvg = $useSvg && $this->imageManager->canConvert('SVG');
370
-			$file = $this->imageManager->getImage($key, $useSvg);
371
-		} catch (NotFoundException $e) {
372
-			return new NotFoundResponse();
373
-		}
351
+    /**
352
+     * @NoSameSiteCookieRequired
353
+     *
354
+     * Get an image
355
+     *
356
+     * @param string $key Key of the image
357
+     * @param bool $useSvg Return image as SVG
358
+     * @return FileDisplayResponse<Http::STATUS_OK, array{}>|NotFoundResponse<Http::STATUS_NOT_FOUND, array{}>
359
+     * @throws NotPermittedException
360
+     *
361
+     * 200: Image returned
362
+     * 404: Image not found
363
+     */
364
+    #[PublicPage]
365
+    #[NoCSRFRequired]
366
+    #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
367
+    public function getImage(string $key, bool $useSvg = true) {
368
+        try {
369
+            $useSvg = $useSvg && $this->imageManager->canConvert('SVG');
370
+            $file = $this->imageManager->getImage($key, $useSvg);
371
+        } catch (NotFoundException $e) {
372
+            return new NotFoundResponse();
373
+        }
374 374
 
375
-		$response = new FileDisplayResponse($file);
376
-		$csp = new ContentSecurityPolicy();
377
-		$csp->allowInlineStyle();
378
-		$response->setContentSecurityPolicy($csp);
379
-		$response->cacheFor(3600);
380
-		$response->addHeader('Content-Type', $file->getMimeType());
381
-		$response->addHeader('Content-Disposition', 'attachment; filename="' . $key . '"');
382
-		return $response;
383
-	}
375
+        $response = new FileDisplayResponse($file);
376
+        $csp = new ContentSecurityPolicy();
377
+        $csp->allowInlineStyle();
378
+        $response->setContentSecurityPolicy($csp);
379
+        $response->cacheFor(3600);
380
+        $response->addHeader('Content-Type', $file->getMimeType());
381
+        $response->addHeader('Content-Disposition', 'attachment; filename="' . $key . '"');
382
+        return $response;
383
+    }
384 384
 
385
-	/**
386
-	 * @NoSameSiteCookieRequired
387
-	 * @NoTwoFactorRequired
388
-	 *
389
-	 * Get the CSS stylesheet for a theme
390
-	 *
391
-	 * @param string $themeId ID of the theme
392
-	 * @param bool $plain Let the browser decide the CSS priority
393
-	 * @param bool $withCustomCss Include custom CSS
394
-	 * @return DataDisplayResponse<Http::STATUS_OK, array{Content-Type: 'text/css'}>|NotFoundResponse<Http::STATUS_NOT_FOUND, array{}>
395
-	 *
396
-	 * 200: Stylesheet returned
397
-	 * 404: Theme not found
398
-	 */
399
-	#[PublicPage]
400
-	#[NoCSRFRequired]
401
-	#[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
402
-	public function getThemeStylesheet(string $themeId, bool $plain = false, bool $withCustomCss = false) {
403
-		$themes = $this->themesService->getThemes();
404
-		if (!in_array($themeId, array_keys($themes))) {
405
-			return new NotFoundResponse();
406
-		}
385
+    /**
386
+     * @NoSameSiteCookieRequired
387
+     * @NoTwoFactorRequired
388
+     *
389
+     * Get the CSS stylesheet for a theme
390
+     *
391
+     * @param string $themeId ID of the theme
392
+     * @param bool $plain Let the browser decide the CSS priority
393
+     * @param bool $withCustomCss Include custom CSS
394
+     * @return DataDisplayResponse<Http::STATUS_OK, array{Content-Type: 'text/css'}>|NotFoundResponse<Http::STATUS_NOT_FOUND, array{}>
395
+     *
396
+     * 200: Stylesheet returned
397
+     * 404: Theme not found
398
+     */
399
+    #[PublicPage]
400
+    #[NoCSRFRequired]
401
+    #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
402
+    public function getThemeStylesheet(string $themeId, bool $plain = false, bool $withCustomCss = false) {
403
+        $themes = $this->themesService->getThemes();
404
+        if (!in_array($themeId, array_keys($themes))) {
405
+            return new NotFoundResponse();
406
+        }
407 407
 
408
-		$theme = $themes[$themeId];
409
-		$customCss = $theme->getCustomCss();
408
+        $theme = $themes[$themeId];
409
+        $customCss = $theme->getCustomCss();
410 410
 
411
-		// Generate variables
412
-		$variables = '';
413
-		foreach ($theme->getCSSVariables() as $variable => $value) {
414
-			$variables .= "$variable:$value; ";
415
-		};
411
+        // Generate variables
412
+        $variables = '';
413
+        foreach ($theme->getCSSVariables() as $variable => $value) {
414
+            $variables .= "$variable:$value; ";
415
+        };
416 416
 
417
-		// If plain is set, the browser decides of the css priority
418
-		if ($plain) {
419
-			$css = ":root { $variables } " . $customCss;
420
-		} else {
421
-			// If not set, we'll rely on the body class
422
-			// We need to separate @-rules from normal selectors, as they can't be nested
423
-			// This is a replacement for the SCSS compiler that did this automatically before f1448fcf0777db7d4254cb0a3ef94d63be9f7a24
424
-			// We need a better way to handle this, but for now we just remove comments and split the at-rules
425
-			// from the rest of the CSS.
426
-			$customCssWithoutComments = preg_replace('!/\*.*?\*/!s', '', $customCss);
427
-			$customCssWithoutComments = preg_replace('!//.*!', '', $customCssWithoutComments);
428
-			preg_match_all('/(@[^{]+{(?:[^{}]*|(?R))*})/', $customCssWithoutComments, $atRules);
429
-			$atRulesCss = implode('', $atRules[0]);
430
-			$scopedCss = preg_replace('/(@[^{]+{(?:[^{}]*|(?R))*})/', '', $customCssWithoutComments);
417
+        // If plain is set, the browser decides of the css priority
418
+        if ($plain) {
419
+            $css = ":root { $variables } " . $customCss;
420
+        } else {
421
+            // If not set, we'll rely on the body class
422
+            // We need to separate @-rules from normal selectors, as they can't be nested
423
+            // This is a replacement for the SCSS compiler that did this automatically before f1448fcf0777db7d4254cb0a3ef94d63be9f7a24
424
+            // We need a better way to handle this, but for now we just remove comments and split the at-rules
425
+            // from the rest of the CSS.
426
+            $customCssWithoutComments = preg_replace('!/\*.*?\*/!s', '', $customCss);
427
+            $customCssWithoutComments = preg_replace('!//.*!', '', $customCssWithoutComments);
428
+            preg_match_all('/(@[^{]+{(?:[^{}]*|(?R))*})/', $customCssWithoutComments, $atRules);
429
+            $atRulesCss = implode('', $atRules[0]);
430
+            $scopedCss = preg_replace('/(@[^{]+{(?:[^{}]*|(?R))*})/', '', $customCssWithoutComments);
431 431
 
432
-			$css = "$atRulesCss [data-theme-$themeId] { $variables $scopedCss }";
433
-		}
432
+            $css = "$atRulesCss [data-theme-$themeId] { $variables $scopedCss }";
433
+        }
434 434
 
435
-		try {
436
-			$response = new DataDisplayResponse($css, Http::STATUS_OK, ['Content-Type' => 'text/css']);
437
-			$response->cacheFor(86400);
438
-			return $response;
439
-		} catch (NotFoundException $e) {
440
-			return new NotFoundResponse();
441
-		}
442
-	}
435
+        try {
436
+            $response = new DataDisplayResponse($css, Http::STATUS_OK, ['Content-Type' => 'text/css']);
437
+            $response->cacheFor(86400);
438
+            return $response;
439
+        } catch (NotFoundException $e) {
440
+            return new NotFoundResponse();
441
+        }
442
+    }
443 443
 
444
-	/**
445
-	 * Get the manifest for an app
446
-	 *
447
-	 * @param string $app ID of the app
448
-	 * @psalm-suppress LessSpecificReturnStatement The content of the Manifest doesn't need to be described in the return type
449
-	 * @return JSONResponse<Http::STATUS_OK, array{name: string, short_name: string, start_url: string, theme_color: string, background_color: string, description: string, icons: list<array{src: non-empty-string, type: string, sizes: string}>, display_override: list<string>, display: string}, array{}>|JSONResponse<Http::STATUS_NOT_FOUND, list<empty>, array{}>
450
-	 *
451
-	 * 200: Manifest returned
452
-	 * 404: App not found
453
-	 */
454
-	#[PublicPage]
455
-	#[NoCSRFRequired]
456
-	#[BruteForceProtection(action: 'manifest')]
457
-	#[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
458
-	public function getManifest(string $app): JSONResponse {
459
-		$cacheBusterValue = $this->config->getAppValue('theming', 'cachebuster', '0');
460
-		if ($app === 'core' || $app === 'settings') {
461
-			$name = $this->themingDefaults->getName();
462
-			$shortName = $this->themingDefaults->getName();
463
-			$startUrl = $this->urlGenerator->getBaseUrl();
464
-			$description = $this->themingDefaults->getSlogan();
465
-		} else {
466
-			if (!$this->appManager->isEnabledForUser($app)) {
467
-				$response = new JSONResponse([], Http::STATUS_NOT_FOUND);
468
-				$response->throttle(['action' => 'manifest', 'app' => $app]);
469
-				return $response;
470
-			}
444
+    /**
445
+     * Get the manifest for an app
446
+     *
447
+     * @param string $app ID of the app
448
+     * @psalm-suppress LessSpecificReturnStatement The content of the Manifest doesn't need to be described in the return type
449
+     * @return JSONResponse<Http::STATUS_OK, array{name: string, short_name: string, start_url: string, theme_color: string, background_color: string, description: string, icons: list<array{src: non-empty-string, type: string, sizes: string}>, display_override: list<string>, display: string}, array{}>|JSONResponse<Http::STATUS_NOT_FOUND, list<empty>, array{}>
450
+     *
451
+     * 200: Manifest returned
452
+     * 404: App not found
453
+     */
454
+    #[PublicPage]
455
+    #[NoCSRFRequired]
456
+    #[BruteForceProtection(action: 'manifest')]
457
+    #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
458
+    public function getManifest(string $app): JSONResponse {
459
+        $cacheBusterValue = $this->config->getAppValue('theming', 'cachebuster', '0');
460
+        if ($app === 'core' || $app === 'settings') {
461
+            $name = $this->themingDefaults->getName();
462
+            $shortName = $this->themingDefaults->getName();
463
+            $startUrl = $this->urlGenerator->getBaseUrl();
464
+            $description = $this->themingDefaults->getSlogan();
465
+        } else {
466
+            if (!$this->appManager->isEnabledForUser($app)) {
467
+                $response = new JSONResponse([], Http::STATUS_NOT_FOUND);
468
+                $response->throttle(['action' => 'manifest', 'app' => $app]);
469
+                return $response;
470
+            }
471 471
 
472
-			$info = $this->appManager->getAppInfo($app, false, $this->l10n->getLanguageCode());
473
-			$name = $info['name'] . ' - ' . $this->themingDefaults->getName();
474
-			$shortName = $info['name'];
475
-			if (str_contains($this->request->getRequestUri(), '/index.php/')) {
476
-				$startUrl = $this->urlGenerator->getBaseUrl() . '/index.php/apps/' . $app . '/';
477
-			} else {
478
-				$startUrl = $this->urlGenerator->getBaseUrl() . '/apps/' . $app . '/';
479
-			}
480
-			$description = $info['summary'] ?? '';
481
-		}
482
-		/**
483
-		 * @var string $description
484
-		 * @var string $shortName
485
-		 */
486
-		$responseJS = [
487
-			'name' => $name,
488
-			'short_name' => $shortName,
489
-			'start_url' => $startUrl,
490
-			'theme_color' => $this->themingDefaults->getColorPrimary(),
491
-			'background_color' => $this->themingDefaults->getColorPrimary(),
492
-			'description' => $description,
493
-			'icons'
494
-				=> [
495
-					[
496
-						'src' => $this->urlGenerator->linkToRoute('theming.Icon.getTouchIcon',
497
-							['app' => $app]) . '?v=' . $cacheBusterValue,
498
-						'type' => 'image/png',
499
-						'sizes' => '512x512'
500
-					],
501
-					[
502
-						'src' => $this->urlGenerator->linkToRoute('theming.Icon.getFavicon',
503
-							['app' => $app]) . '?v=' . $cacheBusterValue,
504
-						'type' => 'image/svg+xml',
505
-						'sizes' => '16x16'
506
-					]
507
-				],
508
-			'display_override' => [$this->config->getSystemValueBool('theming.standalone_window.enabled', true) ? 'minimal-ui' : ''],
509
-			'display' => $this->config->getSystemValueBool('theming.standalone_window.enabled', true) ? 'standalone' : 'browser'
510
-		];
511
-		$response = new JSONResponse($responseJS);
512
-		$response->cacheFor(3600);
513
-		return $response;
514
-	}
472
+            $info = $this->appManager->getAppInfo($app, false, $this->l10n->getLanguageCode());
473
+            $name = $info['name'] . ' - ' . $this->themingDefaults->getName();
474
+            $shortName = $info['name'];
475
+            if (str_contains($this->request->getRequestUri(), '/index.php/')) {
476
+                $startUrl = $this->urlGenerator->getBaseUrl() . '/index.php/apps/' . $app . '/';
477
+            } else {
478
+                $startUrl = $this->urlGenerator->getBaseUrl() . '/apps/' . $app . '/';
479
+            }
480
+            $description = $info['summary'] ?? '';
481
+        }
482
+        /**
483
+         * @var string $description
484
+         * @var string $shortName
485
+         */
486
+        $responseJS = [
487
+            'name' => $name,
488
+            'short_name' => $shortName,
489
+            'start_url' => $startUrl,
490
+            'theme_color' => $this->themingDefaults->getColorPrimary(),
491
+            'background_color' => $this->themingDefaults->getColorPrimary(),
492
+            'description' => $description,
493
+            'icons'
494
+                => [
495
+                    [
496
+                        'src' => $this->urlGenerator->linkToRoute('theming.Icon.getTouchIcon',
497
+                            ['app' => $app]) . '?v=' . $cacheBusterValue,
498
+                        'type' => 'image/png',
499
+                        'sizes' => '512x512'
500
+                    ],
501
+                    [
502
+                        'src' => $this->urlGenerator->linkToRoute('theming.Icon.getFavicon',
503
+                            ['app' => $app]) . '?v=' . $cacheBusterValue,
504
+                        'type' => 'image/svg+xml',
505
+                        'sizes' => '16x16'
506
+                    ]
507
+                ],
508
+            'display_override' => [$this->config->getSystemValueBool('theming.standalone_window.enabled', true) ? 'minimal-ui' : ''],
509
+            'display' => $this->config->getSystemValueBool('theming.standalone_window.enabled', true) ? 'standalone' : 'browser'
510
+        ];
511
+        $response = new JSONResponse($responseJS);
512
+        $response->cacheFor(3600);
513
+        return $response;
514
+    }
515 515
 }
Please login to merge, or discard this patch.
Spacing   +8 added lines, -8 removed lines patch added patch discarded remove patch
@@ -268,7 +268,7 @@  discard block
 block discarded – undo
268 268
 
269 269
 		try {
270 270
 			$mime = $this->imageManager->updateImage($key, $image['tmp_name']);
271
-			$this->themingDefaults->set($key . 'Mime', $mime);
271
+			$this->themingDefaults->set($key.'Mime', $mime);
272 272
 		} catch (\Exception $e) {
273 273
 			return new DataResponse(
274 274
 				[
@@ -378,7 +378,7 @@  discard block
 block discarded – undo
378 378
 		$response->setContentSecurityPolicy($csp);
379 379
 		$response->cacheFor(3600);
380 380
 		$response->addHeader('Content-Type', $file->getMimeType());
381
-		$response->addHeader('Content-Disposition', 'attachment; filename="' . $key . '"');
381
+		$response->addHeader('Content-Disposition', 'attachment; filename="'.$key.'"');
382 382
 		return $response;
383 383
 	}
384 384
 
@@ -416,7 +416,7 @@  discard block
 block discarded – undo
416 416
 
417 417
 		// If plain is set, the browser decides of the css priority
418 418
 		if ($plain) {
419
-			$css = ":root { $variables } " . $customCss;
419
+			$css = ":root { $variables } ".$customCss;
420 420
 		} else {
421 421
 			// If not set, we'll rely on the body class
422 422
 			// We need to separate @-rules from normal selectors, as they can't be nested
@@ -470,12 +470,12 @@  discard block
 block discarded – undo
470 470
 			}
471 471
 
472 472
 			$info = $this->appManager->getAppInfo($app, false, $this->l10n->getLanguageCode());
473
-			$name = $info['name'] . ' - ' . $this->themingDefaults->getName();
473
+			$name = $info['name'].' - '.$this->themingDefaults->getName();
474 474
 			$shortName = $info['name'];
475 475
 			if (str_contains($this->request->getRequestUri(), '/index.php/')) {
476
-				$startUrl = $this->urlGenerator->getBaseUrl() . '/index.php/apps/' . $app . '/';
476
+				$startUrl = $this->urlGenerator->getBaseUrl().'/index.php/apps/'.$app.'/';
477 477
 			} else {
478
-				$startUrl = $this->urlGenerator->getBaseUrl() . '/apps/' . $app . '/';
478
+				$startUrl = $this->urlGenerator->getBaseUrl().'/apps/'.$app.'/';
479 479
 			}
480 480
 			$description = $info['summary'] ?? '';
481 481
 		}
@@ -494,13 +494,13 @@  discard block
 block discarded – undo
494 494
 				=> [
495 495
 					[
496 496
 						'src' => $this->urlGenerator->linkToRoute('theming.Icon.getTouchIcon',
497
-							['app' => $app]) . '?v=' . $cacheBusterValue,
497
+							['app' => $app]).'?v='.$cacheBusterValue,
498 498
 						'type' => 'image/png',
499 499
 						'sizes' => '512x512'
500 500
 					],
501 501
 					[
502 502
 						'src' => $this->urlGenerator->linkToRoute('theming.Icon.getFavicon',
503
-							['app' => $app]) . '?v=' . $cacheBusterValue,
503
+							['app' => $app]).'?v='.$cacheBusterValue,
504 504
 						'type' => 'image/svg+xml',
505 505
 						'sizes' => '16x16'
506 506
 					]
Please login to merge, or discard this patch.
apps/theming/lib/Controller/IconController.php 2 patches
Indentation   +141 added lines, -141 removed lines patch added patch discarded remove patch
@@ -25,152 +25,152 @@
 block discarded – undo
25 25
 use OCP\IRequest;
26 26
 
27 27
 class IconController extends Controller {
28
-	/** @var FileAccessHelper */
29
-	private $fileAccessHelper;
28
+    /** @var FileAccessHelper */
29
+    private $fileAccessHelper;
30 30
 
31
-	public function __construct(
32
-		$appName,
33
-		IRequest $request,
34
-		private IConfig $config,
35
-		private ThemingDefaults $themingDefaults,
36
-		private IconBuilder $iconBuilder,
37
-		private ImageManager $imageManager,
38
-		FileAccessHelper $fileAccessHelper,
39
-		private IAppManager $appManager,
40
-	) {
41
-		parent::__construct($appName, $request);
42
-		$this->fileAccessHelper = $fileAccessHelper;
43
-	}
31
+    public function __construct(
32
+        $appName,
33
+        IRequest $request,
34
+        private IConfig $config,
35
+        private ThemingDefaults $themingDefaults,
36
+        private IconBuilder $iconBuilder,
37
+        private ImageManager $imageManager,
38
+        FileAccessHelper $fileAccessHelper,
39
+        private IAppManager $appManager,
40
+    ) {
41
+        parent::__construct($appName, $request);
42
+        $this->fileAccessHelper = $fileAccessHelper;
43
+    }
44 44
 
45
-	/**
46
-	 * Get a themed icon
47
-	 *
48
-	 * @param string $app ID of the app
49
-	 * @param string $image image file name (svg required)
50
-	 * @return FileDisplayResponse<Http::STATUS_OK, array{Content-Type: 'image/svg+xml'}>|NotFoundResponse<Http::STATUS_NOT_FOUND, array{}>
51
-	 * @throws \Exception
52
-	 *
53
-	 * 200: Themed icon returned
54
-	 * 404: Themed icon not found
55
-	 */
56
-	#[PublicPage]
57
-	#[NoCSRFRequired]
58
-	#[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
59
-	public function getThemedIcon(string $app, string $image): Response {
60
-		if ($app !== 'core' && !$this->appManager->isEnabledForUser($app)) {
61
-			$app = 'core';
62
-			$image = 'favicon.png';
63
-		}
45
+    /**
46
+     * Get a themed icon
47
+     *
48
+     * @param string $app ID of the app
49
+     * @param string $image image file name (svg required)
50
+     * @return FileDisplayResponse<Http::STATUS_OK, array{Content-Type: 'image/svg+xml'}>|NotFoundResponse<Http::STATUS_NOT_FOUND, array{}>
51
+     * @throws \Exception
52
+     *
53
+     * 200: Themed icon returned
54
+     * 404: Themed icon not found
55
+     */
56
+    #[PublicPage]
57
+    #[NoCSRFRequired]
58
+    #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
59
+    public function getThemedIcon(string $app, string $image): Response {
60
+        if ($app !== 'core' && !$this->appManager->isEnabledForUser($app)) {
61
+            $app = 'core';
62
+            $image = 'favicon.png';
63
+        }
64 64
 
65
-		$color = $this->themingDefaults->getColorPrimary();
66
-		try {
67
-			$iconFileName = $this->imageManager->getCachedImage('icon-' . $app . '-' . $color . str_replace('/', '_', $image));
68
-		} catch (NotFoundException $exception) {
69
-			$icon = $this->iconBuilder->colorSvg($app, $image);
70
-			if ($icon === false || $icon === '') {
71
-				return new NotFoundResponse();
72
-			}
73
-			$iconFileName = $this->imageManager->setCachedImage('icon-' . $app . '-' . $color . str_replace('/', '_', $image), $icon);
74
-		}
75
-		$response = new FileDisplayResponse($iconFileName, Http::STATUS_OK, ['Content-Type' => 'image/svg+xml']);
76
-		$response->cacheFor(86400, false, true);
77
-		return $response;
78
-	}
65
+        $color = $this->themingDefaults->getColorPrimary();
66
+        try {
67
+            $iconFileName = $this->imageManager->getCachedImage('icon-' . $app . '-' . $color . str_replace('/', '_', $image));
68
+        } catch (NotFoundException $exception) {
69
+            $icon = $this->iconBuilder->colorSvg($app, $image);
70
+            if ($icon === false || $icon === '') {
71
+                return new NotFoundResponse();
72
+            }
73
+            $iconFileName = $this->imageManager->setCachedImage('icon-' . $app . '-' . $color . str_replace('/', '_', $image), $icon);
74
+        }
75
+        $response = new FileDisplayResponse($iconFileName, Http::STATUS_OK, ['Content-Type' => 'image/svg+xml']);
76
+        $response->cacheFor(86400, false, true);
77
+        return $response;
78
+    }
79 79
 
80
-	/**
81
-	 * Return a 32x32 favicon as png
82
-	 *
83
-	 * @param string $app ID of the app
84
-	 * @return DataDisplayResponse<Http::STATUS_OK, array{Content-Type: 'image/png'}>|FileDisplayResponse<Http::STATUS_OK, array{Content-Type: 'image/x-icon'}>|NotFoundResponse<Http::STATUS_NOT_FOUND, array{}>
85
-	 * @throws \Exception
86
-	 *
87
-	 * 200: Favicon returned
88
-	 * 404: Favicon not found
89
-	 */
90
-	#[PublicPage]
91
-	#[NoCSRFRequired]
92
-	#[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
93
-	public function getFavicon(string $app = 'core'): Response {
94
-		if ($app !== 'core' && !$this->appManager->isEnabledForUser($app)) {
95
-			$app = 'core';
96
-		}
80
+    /**
81
+     * Return a 32x32 favicon as png
82
+     *
83
+     * @param string $app ID of the app
84
+     * @return DataDisplayResponse<Http::STATUS_OK, array{Content-Type: 'image/png'}>|FileDisplayResponse<Http::STATUS_OK, array{Content-Type: 'image/x-icon'}>|NotFoundResponse<Http::STATUS_NOT_FOUND, array{}>
85
+     * @throws \Exception
86
+     *
87
+     * 200: Favicon returned
88
+     * 404: Favicon not found
89
+     */
90
+    #[PublicPage]
91
+    #[NoCSRFRequired]
92
+    #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
93
+    public function getFavicon(string $app = 'core'): Response {
94
+        if ($app !== 'core' && !$this->appManager->isEnabledForUser($app)) {
95
+            $app = 'core';
96
+        }
97 97
 
98
-		$response = null;
99
-		$iconFile = null;
100
-		// retrieve instance favicon
101
-		try {
102
-			$iconFile = $this->imageManager->getImage('favicon', false);
103
-			$response = new FileDisplayResponse($iconFile, Http::STATUS_OK, ['Content-Type' => 'image/x-icon']);
104
-		} catch (NotFoundException $e) {
105
-		}
106
-		// retrieve or generate app specific favicon
107
-		if (($this->imageManager->canConvert('PNG') || $this->imageManager->canConvert('SVG')) && $this->imageManager->canConvert('ICO')) {
108
-			$color = $this->themingDefaults->getColorPrimary();
109
-			try {
110
-				$iconFile = $this->imageManager->getCachedImage('favIcon-' . $app . $color);
111
-			} catch (NotFoundException $exception) {
112
-				$icon = $this->iconBuilder->getFavicon($app);
113
-				if ($icon === false || $icon === '') {
114
-					return new NotFoundResponse();
115
-				}
116
-				$iconFile = $this->imageManager->setCachedImage('favIcon-' . $app . $color, $icon);
117
-			}
118
-			$response = new FileDisplayResponse($iconFile, Http::STATUS_OK, ['Content-Type' => 'image/x-icon']);
119
-		}
120
-		// fallback to core favicon
121
-		if ($response === null) {
122
-			$fallbackLogo = \OC::$SERVERROOT . '/core/img/favicon.png';
123
-			$response = new DataDisplayResponse($this->fileAccessHelper->file_get_contents($fallbackLogo), Http::STATUS_OK, ['Content-Type' => 'image/png']);
124
-		}
125
-		$response->cacheFor(86400);
126
-		return $response;
127
-	}
98
+        $response = null;
99
+        $iconFile = null;
100
+        // retrieve instance favicon
101
+        try {
102
+            $iconFile = $this->imageManager->getImage('favicon', false);
103
+            $response = new FileDisplayResponse($iconFile, Http::STATUS_OK, ['Content-Type' => 'image/x-icon']);
104
+        } catch (NotFoundException $e) {
105
+        }
106
+        // retrieve or generate app specific favicon
107
+        if (($this->imageManager->canConvert('PNG') || $this->imageManager->canConvert('SVG')) && $this->imageManager->canConvert('ICO')) {
108
+            $color = $this->themingDefaults->getColorPrimary();
109
+            try {
110
+                $iconFile = $this->imageManager->getCachedImage('favIcon-' . $app . $color);
111
+            } catch (NotFoundException $exception) {
112
+                $icon = $this->iconBuilder->getFavicon($app);
113
+                if ($icon === false || $icon === '') {
114
+                    return new NotFoundResponse();
115
+                }
116
+                $iconFile = $this->imageManager->setCachedImage('favIcon-' . $app . $color, $icon);
117
+            }
118
+            $response = new FileDisplayResponse($iconFile, Http::STATUS_OK, ['Content-Type' => 'image/x-icon']);
119
+        }
120
+        // fallback to core favicon
121
+        if ($response === null) {
122
+            $fallbackLogo = \OC::$SERVERROOT . '/core/img/favicon.png';
123
+            $response = new DataDisplayResponse($this->fileAccessHelper->file_get_contents($fallbackLogo), Http::STATUS_OK, ['Content-Type' => 'image/png']);
124
+        }
125
+        $response->cacheFor(86400);
126
+        return $response;
127
+    }
128 128
 
129
-	/**
130
-	 * Return a 512x512 icon for touch devices
131
-	 *
132
-	 * @param string $app ID of the app
133
-	 * @return DataDisplayResponse<Http::STATUS_OK, array{Content-Type: 'image/png'}>|FileDisplayResponse<Http::STATUS_OK, array{Content-Type: string}>|NotFoundResponse<Http::STATUS_NOT_FOUND, array{}>
134
-	 * @throws \Exception
135
-	 *
136
-	 * 200: Touch icon returned
137
-	 * 404: Touch icon not found
138
-	 */
139
-	#[PublicPage]
140
-	#[NoCSRFRequired]
141
-	#[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
142
-	public function getTouchIcon(string $app = 'core'): Response {
143
-		if ($app !== 'core' && !$this->appManager->isEnabledForUser($app)) {
144
-			$app = 'core';
145
-		}
129
+    /**
130
+     * Return a 512x512 icon for touch devices
131
+     *
132
+     * @param string $app ID of the app
133
+     * @return DataDisplayResponse<Http::STATUS_OK, array{Content-Type: 'image/png'}>|FileDisplayResponse<Http::STATUS_OK, array{Content-Type: string}>|NotFoundResponse<Http::STATUS_NOT_FOUND, array{}>
134
+     * @throws \Exception
135
+     *
136
+     * 200: Touch icon returned
137
+     * 404: Touch icon not found
138
+     */
139
+    #[PublicPage]
140
+    #[NoCSRFRequired]
141
+    #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
142
+    public function getTouchIcon(string $app = 'core'): Response {
143
+        if ($app !== 'core' && !$this->appManager->isEnabledForUser($app)) {
144
+            $app = 'core';
145
+        }
146 146
 
147
-		$response = null;
148
-		// retrieve instance favicon
149
-		try {
150
-			$iconFile = $this->imageManager->getImage('favicon');
151
-			$response = new FileDisplayResponse($iconFile, Http::STATUS_OK, ['Content-Type' => $iconFile->getMimeType()]);
152
-		} catch (NotFoundException $e) {
153
-		}
154
-		// retrieve or generate app specific touch icon
155
-		if ($this->imageManager->canConvert('PNG')) {
156
-			$color = $this->themingDefaults->getColorPrimary();
157
-			try {
158
-				$iconFile = $this->imageManager->getCachedImage('touchIcon-' . $app . $color);
159
-			} catch (NotFoundException $exception) {
160
-				$icon = $this->iconBuilder->getTouchIcon($app);
161
-				if ($icon === false || $icon === '') {
162
-					return new NotFoundResponse();
163
-				}
164
-				$iconFile = $this->imageManager->setCachedImage('touchIcon-' . $app . $color, $icon);
165
-			}
166
-			$response = new FileDisplayResponse($iconFile, Http::STATUS_OK, ['Content-Type' => 'image/png']);
167
-		}
168
-		// fallback to core touch icon
169
-		if ($response === null) {
170
-			$fallbackLogo = \OC::$SERVERROOT . '/core/img/favicon-touch.png';
171
-			$response = new DataDisplayResponse($this->fileAccessHelper->file_get_contents($fallbackLogo), Http::STATUS_OK, ['Content-Type' => 'image/png']);
172
-		}
173
-		$response->cacheFor(86400);
174
-		return $response;
175
-	}
147
+        $response = null;
148
+        // retrieve instance favicon
149
+        try {
150
+            $iconFile = $this->imageManager->getImage('favicon');
151
+            $response = new FileDisplayResponse($iconFile, Http::STATUS_OK, ['Content-Type' => $iconFile->getMimeType()]);
152
+        } catch (NotFoundException $e) {
153
+        }
154
+        // retrieve or generate app specific touch icon
155
+        if ($this->imageManager->canConvert('PNG')) {
156
+            $color = $this->themingDefaults->getColorPrimary();
157
+            try {
158
+                $iconFile = $this->imageManager->getCachedImage('touchIcon-' . $app . $color);
159
+            } catch (NotFoundException $exception) {
160
+                $icon = $this->iconBuilder->getTouchIcon($app);
161
+                if ($icon === false || $icon === '') {
162
+                    return new NotFoundResponse();
163
+                }
164
+                $iconFile = $this->imageManager->setCachedImage('touchIcon-' . $app . $color, $icon);
165
+            }
166
+            $response = new FileDisplayResponse($iconFile, Http::STATUS_OK, ['Content-Type' => 'image/png']);
167
+        }
168
+        // fallback to core touch icon
169
+        if ($response === null) {
170
+            $fallbackLogo = \OC::$SERVERROOT . '/core/img/favicon-touch.png';
171
+            $response = new DataDisplayResponse($this->fileAccessHelper->file_get_contents($fallbackLogo), Http::STATUS_OK, ['Content-Type' => 'image/png']);
172
+        }
173
+        $response->cacheFor(86400);
174
+        return $response;
175
+    }
176 176
 }
Please login to merge, or discard this patch.
Spacing   +8 added lines, -8 removed lines patch added patch discarded remove patch
@@ -64,13 +64,13 @@  discard block
 block discarded – undo
64 64
 
65 65
 		$color = $this->themingDefaults->getColorPrimary();
66 66
 		try {
67
-			$iconFileName = $this->imageManager->getCachedImage('icon-' . $app . '-' . $color . str_replace('/', '_', $image));
67
+			$iconFileName = $this->imageManager->getCachedImage('icon-'.$app.'-'.$color.str_replace('/', '_', $image));
68 68
 		} catch (NotFoundException $exception) {
69 69
 			$icon = $this->iconBuilder->colorSvg($app, $image);
70 70
 			if ($icon === false || $icon === '') {
71 71
 				return new NotFoundResponse();
72 72
 			}
73
-			$iconFileName = $this->imageManager->setCachedImage('icon-' . $app . '-' . $color . str_replace('/', '_', $image), $icon);
73
+			$iconFileName = $this->imageManager->setCachedImage('icon-'.$app.'-'.$color.str_replace('/', '_', $image), $icon);
74 74
 		}
75 75
 		$response = new FileDisplayResponse($iconFileName, Http::STATUS_OK, ['Content-Type' => 'image/svg+xml']);
76 76
 		$response->cacheFor(86400, false, true);
@@ -107,19 +107,19 @@  discard block
 block discarded – undo
107 107
 		if (($this->imageManager->canConvert('PNG') || $this->imageManager->canConvert('SVG')) && $this->imageManager->canConvert('ICO')) {
108 108
 			$color = $this->themingDefaults->getColorPrimary();
109 109
 			try {
110
-				$iconFile = $this->imageManager->getCachedImage('favIcon-' . $app . $color);
110
+				$iconFile = $this->imageManager->getCachedImage('favIcon-'.$app.$color);
111 111
 			} catch (NotFoundException $exception) {
112 112
 				$icon = $this->iconBuilder->getFavicon($app);
113 113
 				if ($icon === false || $icon === '') {
114 114
 					return new NotFoundResponse();
115 115
 				}
116
-				$iconFile = $this->imageManager->setCachedImage('favIcon-' . $app . $color, $icon);
116
+				$iconFile = $this->imageManager->setCachedImage('favIcon-'.$app.$color, $icon);
117 117
 			}
118 118
 			$response = new FileDisplayResponse($iconFile, Http::STATUS_OK, ['Content-Type' => 'image/x-icon']);
119 119
 		}
120 120
 		// fallback to core favicon
121 121
 		if ($response === null) {
122
-			$fallbackLogo = \OC::$SERVERROOT . '/core/img/favicon.png';
122
+			$fallbackLogo = \OC::$SERVERROOT.'/core/img/favicon.png';
123 123
 			$response = new DataDisplayResponse($this->fileAccessHelper->file_get_contents($fallbackLogo), Http::STATUS_OK, ['Content-Type' => 'image/png']);
124 124
 		}
125 125
 		$response->cacheFor(86400);
@@ -155,19 +155,19 @@  discard block
 block discarded – undo
155 155
 		if ($this->imageManager->canConvert('PNG')) {
156 156
 			$color = $this->themingDefaults->getColorPrimary();
157 157
 			try {
158
-				$iconFile = $this->imageManager->getCachedImage('touchIcon-' . $app . $color);
158
+				$iconFile = $this->imageManager->getCachedImage('touchIcon-'.$app.$color);
159 159
 			} catch (NotFoundException $exception) {
160 160
 				$icon = $this->iconBuilder->getTouchIcon($app);
161 161
 				if ($icon === false || $icon === '') {
162 162
 					return new NotFoundResponse();
163 163
 				}
164
-				$iconFile = $this->imageManager->setCachedImage('touchIcon-' . $app . $color, $icon);
164
+				$iconFile = $this->imageManager->setCachedImage('touchIcon-'.$app.$color, $icon);
165 165
 			}
166 166
 			$response = new FileDisplayResponse($iconFile, Http::STATUS_OK, ['Content-Type' => 'image/png']);
167 167
 		}
168 168
 		// fallback to core touch icon
169 169
 		if ($response === null) {
170
-			$fallbackLogo = \OC::$SERVERROOT . '/core/img/favicon-touch.png';
170
+			$fallbackLogo = \OC::$SERVERROOT.'/core/img/favicon-touch.png';
171 171
 			$response = new DataDisplayResponse($this->fileAccessHelper->file_get_contents($fallbackLogo), Http::STATUS_OK, ['Content-Type' => 'image/png']);
172 172
 		}
173 173
 		$response->cacheFor(86400);
Please login to merge, or discard this patch.
apps/theming/lib/ImageManager.php 2 patches
Indentation   +393 added lines, -393 removed lines patch added patch discarded remove patch
@@ -20,397 +20,397 @@
 block discarded – undo
20 20
 use Psr\Log\LoggerInterface;
21 21
 
22 22
 class ImageManager {
23
-	public const SUPPORTED_IMAGE_KEYS = ['background', 'logo', 'logoheader', 'favicon'];
24
-
25
-	public function __construct(
26
-		private IConfig $config,
27
-		private IAppData $appData,
28
-		private IURLGenerator $urlGenerator,
29
-		private ICacheFactory $cacheFactory,
30
-		private LoggerInterface $logger,
31
-		private ITempManager $tempManager,
32
-		private BackgroundService $backgroundService,
33
-	) {
34
-	}
35
-
36
-	/**
37
-	 * Get a globally defined image (admin theming settings)
38
-	 *
39
-	 * @param string $key the image key
40
-	 * @return string the image url
41
-	 */
42
-	public function getImageUrl(string $key): string {
43
-		$cacheBusterCounter = $this->config->getAppValue(Application::APP_ID, 'cachebuster', '0');
44
-		if ($this->hasImage($key)) {
45
-			return $this->urlGenerator->linkToRoute('theming.Theming.getImage', [ 'key' => $key ]) . '?v=' . $cacheBusterCounter;
46
-		} elseif ($key === 'backgroundDark' && $this->hasImage('background')) {
47
-			// Fall back to light variant
48
-			return $this->urlGenerator->linkToRoute('theming.Theming.getImage', [ 'key' => 'background' ]) . '?v=' . $cacheBusterCounter;
49
-		}
50
-
51
-		switch ($key) {
52
-			case 'logo':
53
-			case 'logoheader':
54
-			case 'favicon':
55
-				return $this->urlGenerator->imagePath('core', 'logo/logo.png') . '?v=' . $cacheBusterCounter;
56
-			case 'backgroundDark':
57
-			case 'background':
58
-				// Removing the background defines its mime as 'backgroundColor'
59
-				$mimeSetting = $this->config->getAppValue('theming', 'backgroundMime', '');
60
-				if ($mimeSetting !== 'backgroundColor') {
61
-					$image = BackgroundService::DEFAULT_BACKGROUND_IMAGE;
62
-					if ($key === 'backgroundDark') {
63
-						$image = BackgroundService::SHIPPED_BACKGROUNDS[$image]['dark_variant'] ?? $image;
64
-					}
65
-					return $this->urlGenerator->linkTo(Application::APP_ID, "img/background/$image");
66
-				}
67
-		}
68
-		return '';
69
-	}
70
-
71
-	/**
72
-	 * Get the absolute url. See getImageUrl
73
-	 */
74
-	public function getImageUrlAbsolute(string $key): string {
75
-		return $this->urlGenerator->getAbsoluteURL($this->getImageUrl($key));
76
-	}
77
-
78
-	/**
79
-	 * @param string $key
80
-	 * @param bool $useSvg
81
-	 * @return ISimpleFile
82
-	 * @throws NotFoundException
83
-	 * @throws NotPermittedException
84
-	 */
85
-	public function getImage(string $key, bool $useSvg = true): ISimpleFile {
86
-		$mime = $this->config->getAppValue('theming', $key . 'Mime', '');
87
-		$folder = $this->getRootFolder()->getFolder('images');
88
-		$useSvg = $useSvg && $this->canConvert('SVG');
89
-
90
-		if ($mime === '' || !$folder->fileExists($key)) {
91
-			throw new NotFoundException();
92
-		}
93
-		// if SVG was requested and is supported
94
-		if ($useSvg) {
95
-			if (!$folder->fileExists($key . '.svg')) {
96
-				try {
97
-					$finalIconFile = new \Imagick();
98
-					$finalIconFile->setBackgroundColor('none');
99
-					$finalIconFile->readImageBlob($folder->getFile($key)->getContent());
100
-					$finalIconFile->setImageFormat('SVG');
101
-					$svgFile = $folder->newFile($key . '.svg');
102
-					$svgFile->putContent($finalIconFile->getImageBlob());
103
-					return $svgFile;
104
-				} catch (\ImagickException $e) {
105
-					$this->logger->info('The image was requested to be no SVG file, but converting it to SVG failed: ' . $e->getMessage());
106
-				}
107
-			} else {
108
-				return $folder->getFile($key . '.svg');
109
-			}
110
-		}
111
-		// if SVG was not requested, but PNG is supported
112
-		if (!$useSvg && $this->canConvert('PNG')) {
113
-			if (!$folder->fileExists($key . '.png')) {
114
-				try {
115
-					$finalIconFile = new \Imagick();
116
-					$finalIconFile->setBackgroundColor('none');
117
-					$finalIconFile->readImageBlob($folder->getFile($key)->getContent());
118
-					$finalIconFile->setImageFormat('PNG32');
119
-					$pngFile = $folder->newFile($key . '.png');
120
-					$pngFile->putContent($finalIconFile->getImageBlob());
121
-					return $pngFile;
122
-				} catch (\ImagickException $e) {
123
-					$this->logger->info('The image was requested to be no SVG file, but converting it to PNG failed: ' . $e->getMessage());
124
-				}
125
-			} else {
126
-				return $folder->getFile($key . '.png');
127
-			}
128
-		}
129
-		// fallback to the original file
130
-		return $folder->getFile($key);
131
-	}
132
-
133
-	public function hasImage(string $key): bool {
134
-		$mimeSetting = $this->config->getAppValue('theming', $key . 'Mime', '');
135
-		// Removing the background defines its mime as 'backgroundColor'
136
-		return $mimeSetting !== '' && $mimeSetting !== 'backgroundColor';
137
-	}
138
-
139
-	/**
140
-	 * @return array<string, array{mime: string, url: string}>
141
-	 */
142
-	public function getCustomImages(): array {
143
-		$images = [];
144
-		foreach (self::SUPPORTED_IMAGE_KEYS as $key) {
145
-			$images[$key] = [
146
-				'mime' => $this->config->getAppValue('theming', $key . 'Mime', ''),
147
-				'url' => $this->getImageUrl($key),
148
-			];
149
-		}
150
-		return $images;
151
-	}
152
-
153
-	/**
154
-	 * Get folder for current theming files
155
-	 *
156
-	 * @return ISimpleFolder
157
-	 * @throws NotPermittedException
158
-	 */
159
-	public function getCacheFolder(): ISimpleFolder {
160
-		$cacheBusterValue = $this->config->getAppValue('theming', 'cachebuster', '0');
161
-		try {
162
-			$folder = $this->getRootFolder()->getFolder($cacheBusterValue);
163
-		} catch (NotFoundException $e) {
164
-			$folder = $this->getRootFolder()->newFolder($cacheBusterValue);
165
-			$this->cleanup();
166
-		}
167
-		return $folder;
168
-	}
169
-
170
-	/**
171
-	 * Get a file from AppData
172
-	 *
173
-	 * @param string $filename
174
-	 * @throws NotFoundException
175
-	 * @return ISimpleFile
176
-	 * @throws NotPermittedException
177
-	 */
178
-	public function getCachedImage(string $filename): ISimpleFile {
179
-		$currentFolder = $this->getCacheFolder();
180
-		return $currentFolder->getFile($filename);
181
-	}
182
-
183
-	/**
184
-	 * Store a file for theming in AppData
185
-	 *
186
-	 * @param string $filename
187
-	 * @param string $data
188
-	 * @return ISimpleFile
189
-	 * @throws NotFoundException
190
-	 * @throws NotPermittedException
191
-	 */
192
-	public function setCachedImage(string $filename, string $data): ISimpleFile {
193
-		$currentFolder = $this->getCacheFolder();
194
-		if ($currentFolder->fileExists($filename)) {
195
-			$file = $currentFolder->getFile($filename);
196
-		} else {
197
-			$file = $currentFolder->newFile($filename);
198
-		}
199
-		$file->putContent($data);
200
-		return $file;
201
-	}
202
-
203
-	public function delete(string $key): void {
204
-		/* ignore exceptions, since we don't want to fail hard if something goes wrong during cleanup */
205
-		try {
206
-			$file = $this->getRootFolder()->getFolder('images')->getFile($key);
207
-			$file->delete();
208
-		} catch (NotFoundException $e) {
209
-		} catch (NotPermittedException $e) {
210
-		}
211
-		try {
212
-			$file = $this->getRootFolder()->getFolder('images')->getFile($key . '.png');
213
-			$file->delete();
214
-		} catch (NotFoundException $e) {
215
-		} catch (NotPermittedException $e) {
216
-		}
217
-
218
-		if ($key === 'logo') {
219
-			$this->config->deleteAppValue('theming', 'logoDimensions');
220
-		}
221
-	}
222
-
223
-	public function updateImage(string $key, string $tmpFile): string {
224
-		$this->delete($key);
225
-
226
-		try {
227
-			$folder = $this->getRootFolder()->getFolder('images');
228
-		} catch (NotFoundException $e) {
229
-			$folder = $this->getRootFolder()->newFolder('images');
230
-		}
231
-
232
-		$target = $folder->newFile($key);
233
-		$supportedFormats = $this->getSupportedUploadImageFormats($key);
234
-		$detectedMimeType = mime_content_type($tmpFile);
235
-		if (!in_array($detectedMimeType, $supportedFormats, true)) {
236
-			throw new \Exception('Unsupported image type: ' . $detectedMimeType);
237
-		}
238
-
239
-		if ($key === 'background') {
240
-			if ($this->shouldOptimizeBackgroundImage($detectedMimeType, filesize($tmpFile))) {
241
-				try {
242
-					// Optimize the image since some people may upload images that will be
243
-					// either to big or are not progressive rendering.
244
-					$newImage = @imagecreatefromstring(file_get_contents($tmpFile));
245
-					if ($newImage === false) {
246
-						throw new \Exception('Could not read background image, possibly corrupted.');
247
-					}
248
-
249
-					// Preserve transparency
250
-					imagesavealpha($newImage, true);
251
-					imagealphablending($newImage, true);
252
-
253
-					$imageWidth = imagesx($newImage);
254
-					$imageHeight = imagesy($newImage);
255
-
256
-					/** @var int */
257
-					$newWidth = min(4096, $imageWidth);
258
-					$newHeight = intval($imageHeight / ($imageWidth / $newWidth));
259
-					$outputImage = imagescale($newImage, $newWidth, $newHeight);
260
-					if ($outputImage === false) {
261
-						throw new \Exception('Could not scale uploaded background image.');
262
-					}
263
-
264
-					$newTmpFile = $this->tempManager->getTemporaryFile();
265
-					imageinterlace($outputImage, true);
266
-					// Keep jpeg images encoded as jpeg
267
-					if (str_contains($detectedMimeType, 'image/jpeg')) {
268
-						if (!imagejpeg($outputImage, $newTmpFile, 90)) {
269
-							throw new \Exception('Could not recompress background image as JPEG');
270
-						}
271
-					} else {
272
-						if (!imagepng($outputImage, $newTmpFile, 8)) {
273
-							throw new \Exception('Could not recompress background image as PNG');
274
-						}
275
-					}
276
-					$tmpFile = $newTmpFile;
277
-					imagedestroy($outputImage);
278
-				} catch (\Exception $e) {
279
-					if (isset($outputImage) && is_resource($outputImage) || $outputImage instanceof \GdImage) {
280
-						imagedestroy($outputImage);
281
-					}
282
-
283
-					$this->logger->debug($e->getMessage());
284
-				}
285
-			}
286
-
287
-			// For background images we need to announce it
288
-			$this->backgroundService->setGlobalBackground($tmpFile);
289
-		}
290
-
291
-		$target->putContent(file_get_contents($tmpFile));
292
-
293
-		if ($key === 'logo') {
294
-			$content = file_get_contents($tmpFile);
295
-			$newImage = @imagecreatefromstring($content);
296
-			if ($newImage !== false) {
297
-				$this->config->setAppValue('theming', 'logoDimensions', imagesx($newImage) . 'x' . imagesy($newImage));
298
-			} elseif (str_starts_with($detectedMimeType, 'image/svg')) {
299
-				$matched = preg_match('/viewbox=["\']\d* \d* (\d*\.?\d*) (\d*\.?\d*)["\']/i', $content, $matches);
300
-				if ($matched) {
301
-					$this->config->setAppValue('theming', 'logoDimensions', $matches[1] . 'x' . $matches[2]);
302
-				} else {
303
-					$this->logger->warning('Could not read logo image dimensions to optimize for mail header');
304
-					$this->config->deleteAppValue('theming', 'logoDimensions');
305
-				}
306
-			} else {
307
-				$this->logger->warning('Could not read logo image dimensions to optimize for mail header');
308
-				$this->config->deleteAppValue('theming', 'logoDimensions');
309
-			}
310
-		}
311
-
312
-		return $detectedMimeType;
313
-	}
314
-
315
-	/**
316
-	 * Decide whether an image benefits from shrinking and reconverting
317
-	 *
318
-	 * @param string $mimeType the mime type of the image
319
-	 * @param int $contentSize size of the image file
320
-	 * @return bool
321
-	 */
322
-	private function shouldOptimizeBackgroundImage(string $mimeType, int $contentSize): bool {
323
-		// Do not touch SVGs
324
-		if (str_contains($mimeType, 'image/svg')) {
325
-			return false;
326
-		}
327
-		// GIF does not benefit from converting
328
-		if (str_contains($mimeType, 'image/gif')) {
329
-			return false;
330
-		}
331
-		// WebP also does not benefit from converting
332
-		// We could possibly try to convert to progressive image, but normally webP images are quite small
333
-		if (str_contains($mimeType, 'image/webp')) {
334
-			return false;
335
-		}
336
-		// As a rule of thumb background images should be max. 150-300 KiB, small images do not benefit from converting
337
-		return $contentSize > 150000;
338
-	}
339
-
340
-	/**
341
-	 * Returns a list of supported mime types for image uploads.
342
-	 * "favicon" images are only allowed to be SVG when imagemagick with SVG support is available.
343
-	 *
344
-	 * @param string $key The image key, e.g. "favicon"
345
-	 * @return string[]
346
-	 */
347
-	public function getSupportedUploadImageFormats(string $key): array {
348
-		$supportedFormats = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
349
-
350
-		if ($key !== 'favicon' || $this->canConvert('SVG') === true) {
351
-			$supportedFormats[] = 'image/svg+xml';
352
-			$supportedFormats[] = 'image/svg';
353
-		}
354
-
355
-		if ($key === 'favicon') {
356
-			$supportedFormats[] = 'image/x-icon';
357
-			$supportedFormats[] = 'image/vnd.microsoft.icon';
358
-		}
359
-
360
-		return $supportedFormats;
361
-	}
362
-
363
-	/**
364
-	 * remove cached files that are not required any longer
365
-	 *
366
-	 * @throws NotPermittedException
367
-	 * @throws NotFoundException
368
-	 */
369
-	public function cleanup() {
370
-		$currentFolder = $this->getCacheFolder();
371
-		$folders = $this->getRootFolder()->getDirectoryListing();
372
-		foreach ($folders as $folder) {
373
-			if ($folder->getName() !== 'images' && $folder->getName() !== $currentFolder->getName()) {
374
-				$folder->delete();
375
-			}
376
-		}
377
-	}
378
-
379
-	/**
380
-	 * Check if Imagemagick is enabled and if SVG is supported
381
-	 * otherwise we can't render custom icons
382
-	 *
383
-	 * @return bool
384
-	 */
385
-	public function shouldReplaceIcons() {
386
-		return $this->canConvert('SVG');
387
-	}
388
-
389
-	/**
390
-	 * Check if Imagemagick is enabled and if format is supported
391
-	 *
392
-	 * @return bool
393
-	 */
394
-	public function canConvert(string $format): bool {
395
-		$cache = $this->cacheFactory->createDistributed('theming-' . $this->urlGenerator->getBaseUrl());
396
-		if ($value = $cache->get('convert-' . $format)) {
397
-			return (bool)$value;
398
-		}
399
-		$value = false;
400
-		if (extension_loaded('imagick')) {
401
-			if (count(\Imagick::queryFormats($format)) >= 1) {
402
-				$value = true;
403
-			}
404
-		}
405
-		$cache->set('convert-' . $format, $value);
406
-		return $value;
407
-	}
408
-
409
-	private function getRootFolder(): ISimpleFolder {
410
-		try {
411
-			return $this->appData->getFolder('global');
412
-		} catch (NotFoundException $e) {
413
-			return $this->appData->newFolder('global');
414
-		}
415
-	}
23
+    public const SUPPORTED_IMAGE_KEYS = ['background', 'logo', 'logoheader', 'favicon'];
24
+
25
+    public function __construct(
26
+        private IConfig $config,
27
+        private IAppData $appData,
28
+        private IURLGenerator $urlGenerator,
29
+        private ICacheFactory $cacheFactory,
30
+        private LoggerInterface $logger,
31
+        private ITempManager $tempManager,
32
+        private BackgroundService $backgroundService,
33
+    ) {
34
+    }
35
+
36
+    /**
37
+     * Get a globally defined image (admin theming settings)
38
+     *
39
+     * @param string $key the image key
40
+     * @return string the image url
41
+     */
42
+    public function getImageUrl(string $key): string {
43
+        $cacheBusterCounter = $this->config->getAppValue(Application::APP_ID, 'cachebuster', '0');
44
+        if ($this->hasImage($key)) {
45
+            return $this->urlGenerator->linkToRoute('theming.Theming.getImage', [ 'key' => $key ]) . '?v=' . $cacheBusterCounter;
46
+        } elseif ($key === 'backgroundDark' && $this->hasImage('background')) {
47
+            // Fall back to light variant
48
+            return $this->urlGenerator->linkToRoute('theming.Theming.getImage', [ 'key' => 'background' ]) . '?v=' . $cacheBusterCounter;
49
+        }
50
+
51
+        switch ($key) {
52
+            case 'logo':
53
+            case 'logoheader':
54
+            case 'favicon':
55
+                return $this->urlGenerator->imagePath('core', 'logo/logo.png') . '?v=' . $cacheBusterCounter;
56
+            case 'backgroundDark':
57
+            case 'background':
58
+                // Removing the background defines its mime as 'backgroundColor'
59
+                $mimeSetting = $this->config->getAppValue('theming', 'backgroundMime', '');
60
+                if ($mimeSetting !== 'backgroundColor') {
61
+                    $image = BackgroundService::DEFAULT_BACKGROUND_IMAGE;
62
+                    if ($key === 'backgroundDark') {
63
+                        $image = BackgroundService::SHIPPED_BACKGROUNDS[$image]['dark_variant'] ?? $image;
64
+                    }
65
+                    return $this->urlGenerator->linkTo(Application::APP_ID, "img/background/$image");
66
+                }
67
+        }
68
+        return '';
69
+    }
70
+
71
+    /**
72
+     * Get the absolute url. See getImageUrl
73
+     */
74
+    public function getImageUrlAbsolute(string $key): string {
75
+        return $this->urlGenerator->getAbsoluteURL($this->getImageUrl($key));
76
+    }
77
+
78
+    /**
79
+     * @param string $key
80
+     * @param bool $useSvg
81
+     * @return ISimpleFile
82
+     * @throws NotFoundException
83
+     * @throws NotPermittedException
84
+     */
85
+    public function getImage(string $key, bool $useSvg = true): ISimpleFile {
86
+        $mime = $this->config->getAppValue('theming', $key . 'Mime', '');
87
+        $folder = $this->getRootFolder()->getFolder('images');
88
+        $useSvg = $useSvg && $this->canConvert('SVG');
89
+
90
+        if ($mime === '' || !$folder->fileExists($key)) {
91
+            throw new NotFoundException();
92
+        }
93
+        // if SVG was requested and is supported
94
+        if ($useSvg) {
95
+            if (!$folder->fileExists($key . '.svg')) {
96
+                try {
97
+                    $finalIconFile = new \Imagick();
98
+                    $finalIconFile->setBackgroundColor('none');
99
+                    $finalIconFile->readImageBlob($folder->getFile($key)->getContent());
100
+                    $finalIconFile->setImageFormat('SVG');
101
+                    $svgFile = $folder->newFile($key . '.svg');
102
+                    $svgFile->putContent($finalIconFile->getImageBlob());
103
+                    return $svgFile;
104
+                } catch (\ImagickException $e) {
105
+                    $this->logger->info('The image was requested to be no SVG file, but converting it to SVG failed: ' . $e->getMessage());
106
+                }
107
+            } else {
108
+                return $folder->getFile($key . '.svg');
109
+            }
110
+        }
111
+        // if SVG was not requested, but PNG is supported
112
+        if (!$useSvg && $this->canConvert('PNG')) {
113
+            if (!$folder->fileExists($key . '.png')) {
114
+                try {
115
+                    $finalIconFile = new \Imagick();
116
+                    $finalIconFile->setBackgroundColor('none');
117
+                    $finalIconFile->readImageBlob($folder->getFile($key)->getContent());
118
+                    $finalIconFile->setImageFormat('PNG32');
119
+                    $pngFile = $folder->newFile($key . '.png');
120
+                    $pngFile->putContent($finalIconFile->getImageBlob());
121
+                    return $pngFile;
122
+                } catch (\ImagickException $e) {
123
+                    $this->logger->info('The image was requested to be no SVG file, but converting it to PNG failed: ' . $e->getMessage());
124
+                }
125
+            } else {
126
+                return $folder->getFile($key . '.png');
127
+            }
128
+        }
129
+        // fallback to the original file
130
+        return $folder->getFile($key);
131
+    }
132
+
133
+    public function hasImage(string $key): bool {
134
+        $mimeSetting = $this->config->getAppValue('theming', $key . 'Mime', '');
135
+        // Removing the background defines its mime as 'backgroundColor'
136
+        return $mimeSetting !== '' && $mimeSetting !== 'backgroundColor';
137
+    }
138
+
139
+    /**
140
+     * @return array<string, array{mime: string, url: string}>
141
+     */
142
+    public function getCustomImages(): array {
143
+        $images = [];
144
+        foreach (self::SUPPORTED_IMAGE_KEYS as $key) {
145
+            $images[$key] = [
146
+                'mime' => $this->config->getAppValue('theming', $key . 'Mime', ''),
147
+                'url' => $this->getImageUrl($key),
148
+            ];
149
+        }
150
+        return $images;
151
+    }
152
+
153
+    /**
154
+     * Get folder for current theming files
155
+     *
156
+     * @return ISimpleFolder
157
+     * @throws NotPermittedException
158
+     */
159
+    public function getCacheFolder(): ISimpleFolder {
160
+        $cacheBusterValue = $this->config->getAppValue('theming', 'cachebuster', '0');
161
+        try {
162
+            $folder = $this->getRootFolder()->getFolder($cacheBusterValue);
163
+        } catch (NotFoundException $e) {
164
+            $folder = $this->getRootFolder()->newFolder($cacheBusterValue);
165
+            $this->cleanup();
166
+        }
167
+        return $folder;
168
+    }
169
+
170
+    /**
171
+     * Get a file from AppData
172
+     *
173
+     * @param string $filename
174
+     * @throws NotFoundException
175
+     * @return ISimpleFile
176
+     * @throws NotPermittedException
177
+     */
178
+    public function getCachedImage(string $filename): ISimpleFile {
179
+        $currentFolder = $this->getCacheFolder();
180
+        return $currentFolder->getFile($filename);
181
+    }
182
+
183
+    /**
184
+     * Store a file for theming in AppData
185
+     *
186
+     * @param string $filename
187
+     * @param string $data
188
+     * @return ISimpleFile
189
+     * @throws NotFoundException
190
+     * @throws NotPermittedException
191
+     */
192
+    public function setCachedImage(string $filename, string $data): ISimpleFile {
193
+        $currentFolder = $this->getCacheFolder();
194
+        if ($currentFolder->fileExists($filename)) {
195
+            $file = $currentFolder->getFile($filename);
196
+        } else {
197
+            $file = $currentFolder->newFile($filename);
198
+        }
199
+        $file->putContent($data);
200
+        return $file;
201
+    }
202
+
203
+    public function delete(string $key): void {
204
+        /* ignore exceptions, since we don't want to fail hard if something goes wrong during cleanup */
205
+        try {
206
+            $file = $this->getRootFolder()->getFolder('images')->getFile($key);
207
+            $file->delete();
208
+        } catch (NotFoundException $e) {
209
+        } catch (NotPermittedException $e) {
210
+        }
211
+        try {
212
+            $file = $this->getRootFolder()->getFolder('images')->getFile($key . '.png');
213
+            $file->delete();
214
+        } catch (NotFoundException $e) {
215
+        } catch (NotPermittedException $e) {
216
+        }
217
+
218
+        if ($key === 'logo') {
219
+            $this->config->deleteAppValue('theming', 'logoDimensions');
220
+        }
221
+    }
222
+
223
+    public function updateImage(string $key, string $tmpFile): string {
224
+        $this->delete($key);
225
+
226
+        try {
227
+            $folder = $this->getRootFolder()->getFolder('images');
228
+        } catch (NotFoundException $e) {
229
+            $folder = $this->getRootFolder()->newFolder('images');
230
+        }
231
+
232
+        $target = $folder->newFile($key);
233
+        $supportedFormats = $this->getSupportedUploadImageFormats($key);
234
+        $detectedMimeType = mime_content_type($tmpFile);
235
+        if (!in_array($detectedMimeType, $supportedFormats, true)) {
236
+            throw new \Exception('Unsupported image type: ' . $detectedMimeType);
237
+        }
238
+
239
+        if ($key === 'background') {
240
+            if ($this->shouldOptimizeBackgroundImage($detectedMimeType, filesize($tmpFile))) {
241
+                try {
242
+                    // Optimize the image since some people may upload images that will be
243
+                    // either to big or are not progressive rendering.
244
+                    $newImage = @imagecreatefromstring(file_get_contents($tmpFile));
245
+                    if ($newImage === false) {
246
+                        throw new \Exception('Could not read background image, possibly corrupted.');
247
+                    }
248
+
249
+                    // Preserve transparency
250
+                    imagesavealpha($newImage, true);
251
+                    imagealphablending($newImage, true);
252
+
253
+                    $imageWidth = imagesx($newImage);
254
+                    $imageHeight = imagesy($newImage);
255
+
256
+                    /** @var int */
257
+                    $newWidth = min(4096, $imageWidth);
258
+                    $newHeight = intval($imageHeight / ($imageWidth / $newWidth));
259
+                    $outputImage = imagescale($newImage, $newWidth, $newHeight);
260
+                    if ($outputImage === false) {
261
+                        throw new \Exception('Could not scale uploaded background image.');
262
+                    }
263
+
264
+                    $newTmpFile = $this->tempManager->getTemporaryFile();
265
+                    imageinterlace($outputImage, true);
266
+                    // Keep jpeg images encoded as jpeg
267
+                    if (str_contains($detectedMimeType, 'image/jpeg')) {
268
+                        if (!imagejpeg($outputImage, $newTmpFile, 90)) {
269
+                            throw new \Exception('Could not recompress background image as JPEG');
270
+                        }
271
+                    } else {
272
+                        if (!imagepng($outputImage, $newTmpFile, 8)) {
273
+                            throw new \Exception('Could not recompress background image as PNG');
274
+                        }
275
+                    }
276
+                    $tmpFile = $newTmpFile;
277
+                    imagedestroy($outputImage);
278
+                } catch (\Exception $e) {
279
+                    if (isset($outputImage) && is_resource($outputImage) || $outputImage instanceof \GdImage) {
280
+                        imagedestroy($outputImage);
281
+                    }
282
+
283
+                    $this->logger->debug($e->getMessage());
284
+                }
285
+            }
286
+
287
+            // For background images we need to announce it
288
+            $this->backgroundService->setGlobalBackground($tmpFile);
289
+        }
290
+
291
+        $target->putContent(file_get_contents($tmpFile));
292
+
293
+        if ($key === 'logo') {
294
+            $content = file_get_contents($tmpFile);
295
+            $newImage = @imagecreatefromstring($content);
296
+            if ($newImage !== false) {
297
+                $this->config->setAppValue('theming', 'logoDimensions', imagesx($newImage) . 'x' . imagesy($newImage));
298
+            } elseif (str_starts_with($detectedMimeType, 'image/svg')) {
299
+                $matched = preg_match('/viewbox=["\']\d* \d* (\d*\.?\d*) (\d*\.?\d*)["\']/i', $content, $matches);
300
+                if ($matched) {
301
+                    $this->config->setAppValue('theming', 'logoDimensions', $matches[1] . 'x' . $matches[2]);
302
+                } else {
303
+                    $this->logger->warning('Could not read logo image dimensions to optimize for mail header');
304
+                    $this->config->deleteAppValue('theming', 'logoDimensions');
305
+                }
306
+            } else {
307
+                $this->logger->warning('Could not read logo image dimensions to optimize for mail header');
308
+                $this->config->deleteAppValue('theming', 'logoDimensions');
309
+            }
310
+        }
311
+
312
+        return $detectedMimeType;
313
+    }
314
+
315
+    /**
316
+     * Decide whether an image benefits from shrinking and reconverting
317
+     *
318
+     * @param string $mimeType the mime type of the image
319
+     * @param int $contentSize size of the image file
320
+     * @return bool
321
+     */
322
+    private function shouldOptimizeBackgroundImage(string $mimeType, int $contentSize): bool {
323
+        // Do not touch SVGs
324
+        if (str_contains($mimeType, 'image/svg')) {
325
+            return false;
326
+        }
327
+        // GIF does not benefit from converting
328
+        if (str_contains($mimeType, 'image/gif')) {
329
+            return false;
330
+        }
331
+        // WebP also does not benefit from converting
332
+        // We could possibly try to convert to progressive image, but normally webP images are quite small
333
+        if (str_contains($mimeType, 'image/webp')) {
334
+            return false;
335
+        }
336
+        // As a rule of thumb background images should be max. 150-300 KiB, small images do not benefit from converting
337
+        return $contentSize > 150000;
338
+    }
339
+
340
+    /**
341
+     * Returns a list of supported mime types for image uploads.
342
+     * "favicon" images are only allowed to be SVG when imagemagick with SVG support is available.
343
+     *
344
+     * @param string $key The image key, e.g. "favicon"
345
+     * @return string[]
346
+     */
347
+    public function getSupportedUploadImageFormats(string $key): array {
348
+        $supportedFormats = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
349
+
350
+        if ($key !== 'favicon' || $this->canConvert('SVG') === true) {
351
+            $supportedFormats[] = 'image/svg+xml';
352
+            $supportedFormats[] = 'image/svg';
353
+        }
354
+
355
+        if ($key === 'favicon') {
356
+            $supportedFormats[] = 'image/x-icon';
357
+            $supportedFormats[] = 'image/vnd.microsoft.icon';
358
+        }
359
+
360
+        return $supportedFormats;
361
+    }
362
+
363
+    /**
364
+     * remove cached files that are not required any longer
365
+     *
366
+     * @throws NotPermittedException
367
+     * @throws NotFoundException
368
+     */
369
+    public function cleanup() {
370
+        $currentFolder = $this->getCacheFolder();
371
+        $folders = $this->getRootFolder()->getDirectoryListing();
372
+        foreach ($folders as $folder) {
373
+            if ($folder->getName() !== 'images' && $folder->getName() !== $currentFolder->getName()) {
374
+                $folder->delete();
375
+            }
376
+        }
377
+    }
378
+
379
+    /**
380
+     * Check if Imagemagick is enabled and if SVG is supported
381
+     * otherwise we can't render custom icons
382
+     *
383
+     * @return bool
384
+     */
385
+    public function shouldReplaceIcons() {
386
+        return $this->canConvert('SVG');
387
+    }
388
+
389
+    /**
390
+     * Check if Imagemagick is enabled and if format is supported
391
+     *
392
+     * @return bool
393
+     */
394
+    public function canConvert(string $format): bool {
395
+        $cache = $this->cacheFactory->createDistributed('theming-' . $this->urlGenerator->getBaseUrl());
396
+        if ($value = $cache->get('convert-' . $format)) {
397
+            return (bool)$value;
398
+        }
399
+        $value = false;
400
+        if (extension_loaded('imagick')) {
401
+            if (count(\Imagick::queryFormats($format)) >= 1) {
402
+                $value = true;
403
+            }
404
+        }
405
+        $cache->set('convert-' . $format, $value);
406
+        return $value;
407
+    }
408
+
409
+    private function getRootFolder(): ISimpleFolder {
410
+        try {
411
+            return $this->appData->getFolder('global');
412
+        } catch (NotFoundException $e) {
413
+            return $this->appData->newFolder('global');
414
+        }
415
+    }
416 416
 }
Please login to merge, or discard this patch.
Spacing   +22 added lines, -22 removed lines patch added patch discarded remove patch
@@ -42,17 +42,17 @@  discard block
 block discarded – undo
42 42
 	public function getImageUrl(string $key): string {
43 43
 		$cacheBusterCounter = $this->config->getAppValue(Application::APP_ID, 'cachebuster', '0');
44 44
 		if ($this->hasImage($key)) {
45
-			return $this->urlGenerator->linkToRoute('theming.Theming.getImage', [ 'key' => $key ]) . '?v=' . $cacheBusterCounter;
45
+			return $this->urlGenerator->linkToRoute('theming.Theming.getImage', ['key' => $key]).'?v='.$cacheBusterCounter;
46 46
 		} elseif ($key === 'backgroundDark' && $this->hasImage('background')) {
47 47
 			// Fall back to light variant
48
-			return $this->urlGenerator->linkToRoute('theming.Theming.getImage', [ 'key' => 'background' ]) . '?v=' . $cacheBusterCounter;
48
+			return $this->urlGenerator->linkToRoute('theming.Theming.getImage', ['key' => 'background']).'?v='.$cacheBusterCounter;
49 49
 		}
50 50
 
51 51
 		switch ($key) {
52 52
 			case 'logo':
53 53
 			case 'logoheader':
54 54
 			case 'favicon':
55
-				return $this->urlGenerator->imagePath('core', 'logo/logo.png') . '?v=' . $cacheBusterCounter;
55
+				return $this->urlGenerator->imagePath('core', 'logo/logo.png').'?v='.$cacheBusterCounter;
56 56
 			case 'backgroundDark':
57 57
 			case 'background':
58 58
 				// Removing the background defines its mime as 'backgroundColor'
@@ -83,7 +83,7 @@  discard block
 block discarded – undo
83 83
 	 * @throws NotPermittedException
84 84
 	 */
85 85
 	public function getImage(string $key, bool $useSvg = true): ISimpleFile {
86
-		$mime = $this->config->getAppValue('theming', $key . 'Mime', '');
86
+		$mime = $this->config->getAppValue('theming', $key.'Mime', '');
87 87
 		$folder = $this->getRootFolder()->getFolder('images');
88 88
 		$useSvg = $useSvg && $this->canConvert('SVG');
89 89
 
@@ -92,38 +92,38 @@  discard block
 block discarded – undo
92 92
 		}
93 93
 		// if SVG was requested and is supported
94 94
 		if ($useSvg) {
95
-			if (!$folder->fileExists($key . '.svg')) {
95
+			if (!$folder->fileExists($key.'.svg')) {
96 96
 				try {
97 97
 					$finalIconFile = new \Imagick();
98 98
 					$finalIconFile->setBackgroundColor('none');
99 99
 					$finalIconFile->readImageBlob($folder->getFile($key)->getContent());
100 100
 					$finalIconFile->setImageFormat('SVG');
101
-					$svgFile = $folder->newFile($key . '.svg');
101
+					$svgFile = $folder->newFile($key.'.svg');
102 102
 					$svgFile->putContent($finalIconFile->getImageBlob());
103 103
 					return $svgFile;
104 104
 				} catch (\ImagickException $e) {
105
-					$this->logger->info('The image was requested to be no SVG file, but converting it to SVG failed: ' . $e->getMessage());
105
+					$this->logger->info('The image was requested to be no SVG file, but converting it to SVG failed: '.$e->getMessage());
106 106
 				}
107 107
 			} else {
108
-				return $folder->getFile($key . '.svg');
108
+				return $folder->getFile($key.'.svg');
109 109
 			}
110 110
 		}
111 111
 		// if SVG was not requested, but PNG is supported
112 112
 		if (!$useSvg && $this->canConvert('PNG')) {
113
-			if (!$folder->fileExists($key . '.png')) {
113
+			if (!$folder->fileExists($key.'.png')) {
114 114
 				try {
115 115
 					$finalIconFile = new \Imagick();
116 116
 					$finalIconFile->setBackgroundColor('none');
117 117
 					$finalIconFile->readImageBlob($folder->getFile($key)->getContent());
118 118
 					$finalIconFile->setImageFormat('PNG32');
119
-					$pngFile = $folder->newFile($key . '.png');
119
+					$pngFile = $folder->newFile($key.'.png');
120 120
 					$pngFile->putContent($finalIconFile->getImageBlob());
121 121
 					return $pngFile;
122 122
 				} catch (\ImagickException $e) {
123
-					$this->logger->info('The image was requested to be no SVG file, but converting it to PNG failed: ' . $e->getMessage());
123
+					$this->logger->info('The image was requested to be no SVG file, but converting it to PNG failed: '.$e->getMessage());
124 124
 				}
125 125
 			} else {
126
-				return $folder->getFile($key . '.png');
126
+				return $folder->getFile($key.'.png');
127 127
 			}
128 128
 		}
129 129
 		// fallback to the original file
@@ -131,7 +131,7 @@  discard block
 block discarded – undo
131 131
 	}
132 132
 
133 133
 	public function hasImage(string $key): bool {
134
-		$mimeSetting = $this->config->getAppValue('theming', $key . 'Mime', '');
134
+		$mimeSetting = $this->config->getAppValue('theming', $key.'Mime', '');
135 135
 		// Removing the background defines its mime as 'backgroundColor'
136 136
 		return $mimeSetting !== '' && $mimeSetting !== 'backgroundColor';
137 137
 	}
@@ -143,7 +143,7 @@  discard block
 block discarded – undo
143 143
 		$images = [];
144 144
 		foreach (self::SUPPORTED_IMAGE_KEYS as $key) {
145 145
 			$images[$key] = [
146
-				'mime' => $this->config->getAppValue('theming', $key . 'Mime', ''),
146
+				'mime' => $this->config->getAppValue('theming', $key.'Mime', ''),
147 147
 				'url' => $this->getImageUrl($key),
148 148
 			];
149 149
 		}
@@ -209,7 +209,7 @@  discard block
 block discarded – undo
209 209
 		} catch (NotPermittedException $e) {
210 210
 		}
211 211
 		try {
212
-			$file = $this->getRootFolder()->getFolder('images')->getFile($key . '.png');
212
+			$file = $this->getRootFolder()->getFolder('images')->getFile($key.'.png');
213 213
 			$file->delete();
214 214
 		} catch (NotFoundException $e) {
215 215
 		} catch (NotPermittedException $e) {
@@ -233,7 +233,7 @@  discard block
 block discarded – undo
233 233
 		$supportedFormats = $this->getSupportedUploadImageFormats($key);
234 234
 		$detectedMimeType = mime_content_type($tmpFile);
235 235
 		if (!in_array($detectedMimeType, $supportedFormats, true)) {
236
-			throw new \Exception('Unsupported image type: ' . $detectedMimeType);
236
+			throw new \Exception('Unsupported image type: '.$detectedMimeType);
237 237
 		}
238 238
 
239 239
 		if ($key === 'background') {
@@ -294,11 +294,11 @@  discard block
 block discarded – undo
294 294
 			$content = file_get_contents($tmpFile);
295 295
 			$newImage = @imagecreatefromstring($content);
296 296
 			if ($newImage !== false) {
297
-				$this->config->setAppValue('theming', 'logoDimensions', imagesx($newImage) . 'x' . imagesy($newImage));
297
+				$this->config->setAppValue('theming', 'logoDimensions', imagesx($newImage).'x'.imagesy($newImage));
298 298
 			} elseif (str_starts_with($detectedMimeType, 'image/svg')) {
299 299
 				$matched = preg_match('/viewbox=["\']\d* \d* (\d*\.?\d*) (\d*\.?\d*)["\']/i', $content, $matches);
300 300
 				if ($matched) {
301
-					$this->config->setAppValue('theming', 'logoDimensions', $matches[1] . 'x' . $matches[2]);
301
+					$this->config->setAppValue('theming', 'logoDimensions', $matches[1].'x'.$matches[2]);
302 302
 				} else {
303 303
 					$this->logger->warning('Could not read logo image dimensions to optimize for mail header');
304 304
 					$this->config->deleteAppValue('theming', 'logoDimensions');
@@ -392,9 +392,9 @@  discard block
 block discarded – undo
392 392
 	 * @return bool
393 393
 	 */
394 394
 	public function canConvert(string $format): bool {
395
-		$cache = $this->cacheFactory->createDistributed('theming-' . $this->urlGenerator->getBaseUrl());
396
-		if ($value = $cache->get('convert-' . $format)) {
397
-			return (bool)$value;
395
+		$cache = $this->cacheFactory->createDistributed('theming-'.$this->urlGenerator->getBaseUrl());
396
+		if ($value = $cache->get('convert-'.$format)) {
397
+			return (bool) $value;
398 398
 		}
399 399
 		$value = false;
400 400
 		if (extension_loaded('imagick')) {
@@ -402,7 +402,7 @@  discard block
 block discarded – undo
402 402
 				$value = true;
403 403
 			}
404 404
 		}
405
-		$cache->set('convert-' . $format, $value);
405
+		$cache->set('convert-'.$format, $value);
406 406
 		return $value;
407 407
 	}
408 408
 
Please login to merge, or discard this patch.
apps/theming/lib/Util.php 2 patches
Indentation   +322 added lines, -322 removed lines patch added patch discarded remove patch
@@ -18,326 +18,326 @@
 block discarded – undo
18 18
 use OCP\ServerVersion;
19 19
 
20 20
 class Util {
21
-	public function __construct(
22
-		private ServerVersion $serverVersion,
23
-		private IConfig $config,
24
-		private IAppManager $appManager,
25
-		private IAppData $appData,
26
-		private ImageManager $imageManager,
27
-	) {
28
-	}
29
-
30
-	/**
31
-	 * Should we invert the text on this background color?
32
-	 * @param string $color rgb color value
33
-	 * @return bool
34
-	 */
35
-	public function invertTextColor(string $color): bool {
36
-		return $this->colorContrast($color, '#ffffff') < 4.5;
37
-	}
38
-
39
-	/**
40
-	 * Get the best text color contrast-wise for the given color.
41
-	 *
42
-	 * @since 32.0.0
43
-	 */
44
-	public function getTextColor(string $color): string {
45
-		return $this->invertTextColor($color) ? '#000000' : '#ffffff';
46
-	}
47
-
48
-	/**
49
-	 * Is this color too bright ?
50
-	 * @param string $color rgb color value
51
-	 * @return bool
52
-	 */
53
-	public function isBrightColor(string $color): bool {
54
-		$l = $this->calculateLuma($color);
55
-		if ($l > 0.6) {
56
-			return true;
57
-		} else {
58
-			return false;
59
-		}
60
-	}
61
-
62
-	/**
63
-	 * get color for on-page elements:
64
-	 * theme color by default, grey if theme color is to bright
65
-	 * @param string $color
66
-	 * @param ?bool $brightBackground
67
-	 * @return string
68
-	 */
69
-	public function elementColor($color, ?bool $brightBackground = null, ?string $backgroundColor = null, bool $highContrast = false) {
70
-		if ($backgroundColor !== null) {
71
-			$brightBackground = $brightBackground ?? $this->isBrightColor($backgroundColor);
72
-			// Minimal amount that is possible to change the luminance
73
-			$epsilon = 1.0 / 255.0;
74
-			// Current iteration to prevent infinite loops
75
-			$iteration = 0;
76
-			// We need to keep blurred backgrounds in mind which might be mixed with the background
77
-			$blurredBackground = $this->mix($backgroundColor, $brightBackground ? $color : '#ffffff', 66);
78
-			$contrast = $this->colorContrast($color, $blurredBackground);
79
-
80
-			// Min. element contrast is 3:1 but we need to keep hover states in mind -> min 3.2:1
81
-			$minContrast = $highContrast ? 5.6 : 3.2;
82
-
83
-			while ($contrast < $minContrast && $iteration++ < 100) {
84
-				$hsl = Color::hexToHsl($color);
85
-				$hsl['L'] = max(0, min(1, $hsl['L'] + ($brightBackground ? -$epsilon : $epsilon)));
86
-				$color = '#' . Color::hslToHex($hsl);
87
-				$contrast = $this->colorContrast($color, $blurredBackground);
88
-			}
89
-			return $color;
90
-		}
91
-
92
-		// Fallback for legacy calling
93
-		$luminance = $this->calculateLuminance($color);
94
-
95
-		if ($brightBackground !== false && $luminance > 0.8) {
96
-			// If the color is too bright in bright mode, we fall back to a darkened color
97
-			return $this->darken($color, 30);
98
-		}
99
-
100
-		if ($brightBackground !== true && $luminance < 0.2) {
101
-			// If the color is too dark in dark mode, we fall back to a brightened color
102
-			return $this->lighten($color, 30);
103
-		}
104
-
105
-		return $color;
106
-	}
107
-
108
-	public function mix(string $color1, string $color2, int $factor): string {
109
-		$color = new Color($color1);
110
-		return '#' . $color->mix($color2, $factor);
111
-	}
112
-
113
-	public function lighten(string $color, int $factor): string {
114
-		$color = new Color($color);
115
-		return '#' . $color->lighten($factor);
116
-	}
117
-
118
-	public function darken(string $color, int $factor): string {
119
-		$color = new Color($color);
120
-		return '#' . $color->darken($factor);
121
-	}
122
-
123
-	/**
124
-	 * Convert RGB to HSL
125
-	 *
126
-	 * Copied from cssphp, copyright Leaf Corcoran, licensed under MIT
127
-	 *
128
-	 * @param int $red
129
-	 * @param int $green
130
-	 * @param int $blue
131
-	 *
132
-	 * @return float[]
133
-	 */
134
-	public function toHSL(int $red, int $green, int $blue): array {
135
-		$color = new Color(Color::rgbToHex(['R' => $red, 'G' => $green, 'B' => $blue]));
136
-		return array_values($color->getHsl());
137
-	}
138
-
139
-	/**
140
-	 * @param string $color rgb color value
141
-	 * @return float
142
-	 */
143
-	public function calculateLuminance(string $color): float {
144
-		[$red, $green, $blue] = $this->hexToRGB($color);
145
-		$hsl = $this->toHSL($red, $green, $blue);
146
-		return $hsl[2];
147
-	}
148
-
149
-	/**
150
-	 * Calculate the Luma according to WCAG 2
151
-	 * http://www.w3.org/TR/WCAG20/#relativeluminancedef
152
-	 * @param string $color rgb color value
153
-	 * @return float
154
-	 */
155
-	public function calculateLuma(string $color): float {
156
-		$rgb = $this->hexToRGB($color);
157
-
158
-		// Normalize the values by converting to float and applying the rules from WCAG2.0
159
-		$rgb = array_map(function (int $color) {
160
-			$color = $color / 255.0;
161
-			if ($color <= 0.03928) {
162
-				return $color / 12.92;
163
-			} else {
164
-				return pow((($color + 0.055) / 1.055), 2.4);
165
-			}
166
-		}, $rgb);
167
-
168
-		[$red, $green, $blue] = $rgb;
169
-		return (0.2126 * $red + 0.7152 * $green + 0.0722 * $blue);
170
-	}
171
-
172
-	/**
173
-	 * Calculat the color contrast according to WCAG 2
174
-	 * http://www.w3.org/TR/WCAG20/#contrast-ratiodef
175
-	 * @param string $color1 The first color
176
-	 * @param string $color2 The second color
177
-	 */
178
-	public function colorContrast(string $color1, string $color2): float {
179
-		$luminance1 = $this->calculateLuma($color1) + 0.05;
180
-		$luminance2 = $this->calculateLuma($color2) + 0.05;
181
-		return max($luminance1, $luminance2) / min($luminance1, $luminance2);
182
-	}
183
-
184
-	/**
185
-	 * @param string $color rgb color value
186
-	 * @return int[]
187
-	 * @psalm-return array{0: int, 1: int, 2: int}
188
-	 */
189
-	public function hexToRGB(string $color): array {
190
-		$color = new Color($color);
191
-		return array_values($color->getRgb());
192
-	}
193
-
194
-	/**
195
-	 * @param $color
196
-	 * @return string base64 encoded radio button svg
197
-	 */
198
-	public function generateRadioButton($color) {
199
-		$radioButtonIcon = '<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16">'
200
-			. '<path d="M8 1a7 7 0 0 0-7 7 7 7 0 0 0 7 7 7 7 0 0 0 7-7 7 7 0 0 0-7-7zm0 1a6 6 0 0 1 6 6 6 6 0 0 1-6 6 6 6 0 0 1-6-6 6 6 0 0 1 6-6zm0 2a4 4 0 1 0 0 8 4 4 0 0 0 0-8z" fill="' . $color . '"/></svg>';
201
-		return base64_encode($radioButtonIcon);
202
-	}
203
-
204
-
205
-	/**
206
-	 * @param string $app app name
207
-	 * @return string|ISimpleFile path to app icon / file of logo
208
-	 */
209
-	public function getAppIcon($app, $useSvg = true) {
210
-		$app = $this->appManager->cleanAppId($app);
211
-		try {
212
-			// find app specific icon
213
-			$appPath = $this->appManager->getAppPath($app);
214
-			$extension = ($useSvg ? '.svg' : '.png');
215
-
216
-			$icon = $appPath . '/img/' . $app . $extension;
217
-			if (file_exists($icon)) {
218
-				return $icon;
219
-			}
220
-
221
-			$icon = $appPath . '/img/app' . $extension;
222
-			if (file_exists($icon)) {
223
-				return $icon;
224
-			}
225
-		} catch (AppPathNotFoundException $e) {
226
-		}
227
-		// fallback to custom instance logo
228
-		if ($this->config->getAppValue('theming', 'logoMime', '') !== '') {
229
-			try {
230
-				$folder = $this->appData->getFolder('global/images');
231
-				return $folder->getFile('logo');
232
-			} catch (NotFoundException $e) {
233
-			}
234
-		}
235
-		// fallback to core logo
236
-		if ($useSvg) {
237
-			return \OC::$SERVERROOT . '/core/img/logo/logo.svg';
238
-		} else {
239
-			return \OC::$SERVERROOT . '/core/img/logo/logo.png';
240
-		}
241
-	}
242
-
243
-	/**
244
-	 * @param string $app app name
245
-	 * @param string $image relative path to image in app folder
246
-	 * @return string|false absolute path to image
247
-	 */
248
-	public function getAppImage($app, $image) {
249
-		$app = $this->appManager->cleanAppId($app);
250
-		/**
251
-		 * @psalm-taint-escape file
252
-		 */
253
-		$image = str_replace(['\0', '\\', '..'], '', $image);
254
-		if ($app === 'core') {
255
-			$icon = \OC::$SERVERROOT . '/core/img/' . $image;
256
-			if (file_exists($icon)) {
257
-				return $icon;
258
-			}
259
-		}
260
-
261
-		try {
262
-			$appPath = $this->appManager->getAppPath($app);
263
-		} catch (AppPathNotFoundException $e) {
264
-			return false;
265
-		}
266
-
267
-		$icon = $appPath . '/img/' . $image;
268
-		if (file_exists($icon)) {
269
-			return $icon;
270
-		}
271
-		$icon = $appPath . '/img/' . $image . '.svg';
272
-		if (file_exists($icon)) {
273
-			return $icon;
274
-		}
275
-		$icon = $appPath . '/img/' . $image . '.png';
276
-		if (file_exists($icon)) {
277
-			return $icon;
278
-		}
279
-		$icon = $appPath . '/img/' . $image . '.gif';
280
-		if (file_exists($icon)) {
281
-			return $icon;
282
-		}
283
-		$icon = $appPath . '/img/' . $image . '.jpg';
284
-		if (file_exists($icon)) {
285
-			return $icon;
286
-		}
287
-
288
-		return false;
289
-	}
290
-
291
-	/**
292
-	 * replace default color with a custom one
293
-	 *
294
-	 * @param string $svg content of a svg file
295
-	 * @param string $color color to match
296
-	 * @return string
297
-	 */
298
-	public function colorizeSvg($svg, $color) {
299
-		$svg = preg_replace('/#0082c9/i', $color, $svg);
300
-		return $svg;
301
-	}
302
-
303
-	/**
304
-	 * Check if a custom theme is set in the server configuration
305
-	 *
306
-	 * @return bool
307
-	 */
308
-	public function isAlreadyThemed() {
309
-		$theme = $this->config->getSystemValue('theme', '');
310
-		if ($theme !== '') {
311
-			return true;
312
-		}
313
-		return false;
314
-	}
315
-
316
-	public function isBackgroundThemed() {
317
-		$backgroundLogo = $this->config->getAppValue('theming', 'backgroundMime', '');
318
-		return $backgroundLogo !== '' && $backgroundLogo !== 'backgroundColor';
319
-	}
320
-
321
-	public function isLogoThemed() {
322
-		return $this->imageManager->hasImage('logo')
323
-			|| $this->imageManager->hasImage('logoheader');
324
-	}
325
-
326
-	public function getCacheBuster(): string {
327
-		$userSession = Server::get(IUserSession::class);
328
-		$userId = '';
329
-		$user = $userSession->getUser();
330
-		if (!is_null($user)) {
331
-			$userId = $user->getUID();
332
-		}
333
-		$serverVersion = $this->serverVersion->getVersionString();
334
-		$themingAppVersion = $this->appManager->getAppVersion('theming');
335
-		$userCacheBuster = '';
336
-		if ($userId) {
337
-			$userCacheBusterValue = (int)$this->config->getUserValue($userId, 'theming', 'userCacheBuster', '0');
338
-			$userCacheBuster = $userId . '_' . $userCacheBusterValue;
339
-		}
340
-		$systemCacheBuster = $this->config->getAppValue('theming', 'cachebuster', '0');
341
-		return substr(sha1($serverVersion . $themingAppVersion . $userCacheBuster . $systemCacheBuster), 0, 8);
342
-	}
21
+    public function __construct(
22
+        private ServerVersion $serverVersion,
23
+        private IConfig $config,
24
+        private IAppManager $appManager,
25
+        private IAppData $appData,
26
+        private ImageManager $imageManager,
27
+    ) {
28
+    }
29
+
30
+    /**
31
+     * Should we invert the text on this background color?
32
+     * @param string $color rgb color value
33
+     * @return bool
34
+     */
35
+    public function invertTextColor(string $color): bool {
36
+        return $this->colorContrast($color, '#ffffff') < 4.5;
37
+    }
38
+
39
+    /**
40
+     * Get the best text color contrast-wise for the given color.
41
+     *
42
+     * @since 32.0.0
43
+     */
44
+    public function getTextColor(string $color): string {
45
+        return $this->invertTextColor($color) ? '#000000' : '#ffffff';
46
+    }
47
+
48
+    /**
49
+     * Is this color too bright ?
50
+     * @param string $color rgb color value
51
+     * @return bool
52
+     */
53
+    public function isBrightColor(string $color): bool {
54
+        $l = $this->calculateLuma($color);
55
+        if ($l > 0.6) {
56
+            return true;
57
+        } else {
58
+            return false;
59
+        }
60
+    }
61
+
62
+    /**
63
+     * get color for on-page elements:
64
+     * theme color by default, grey if theme color is to bright
65
+     * @param string $color
66
+     * @param ?bool $brightBackground
67
+     * @return string
68
+     */
69
+    public function elementColor($color, ?bool $brightBackground = null, ?string $backgroundColor = null, bool $highContrast = false) {
70
+        if ($backgroundColor !== null) {
71
+            $brightBackground = $brightBackground ?? $this->isBrightColor($backgroundColor);
72
+            // Minimal amount that is possible to change the luminance
73
+            $epsilon = 1.0 / 255.0;
74
+            // Current iteration to prevent infinite loops
75
+            $iteration = 0;
76
+            // We need to keep blurred backgrounds in mind which might be mixed with the background
77
+            $blurredBackground = $this->mix($backgroundColor, $brightBackground ? $color : '#ffffff', 66);
78
+            $contrast = $this->colorContrast($color, $blurredBackground);
79
+
80
+            // Min. element contrast is 3:1 but we need to keep hover states in mind -> min 3.2:1
81
+            $minContrast = $highContrast ? 5.6 : 3.2;
82
+
83
+            while ($contrast < $minContrast && $iteration++ < 100) {
84
+                $hsl = Color::hexToHsl($color);
85
+                $hsl['L'] = max(0, min(1, $hsl['L'] + ($brightBackground ? -$epsilon : $epsilon)));
86
+                $color = '#' . Color::hslToHex($hsl);
87
+                $contrast = $this->colorContrast($color, $blurredBackground);
88
+            }
89
+            return $color;
90
+        }
91
+
92
+        // Fallback for legacy calling
93
+        $luminance = $this->calculateLuminance($color);
94
+
95
+        if ($brightBackground !== false && $luminance > 0.8) {
96
+            // If the color is too bright in bright mode, we fall back to a darkened color
97
+            return $this->darken($color, 30);
98
+        }
99
+
100
+        if ($brightBackground !== true && $luminance < 0.2) {
101
+            // If the color is too dark in dark mode, we fall back to a brightened color
102
+            return $this->lighten($color, 30);
103
+        }
104
+
105
+        return $color;
106
+    }
107
+
108
+    public function mix(string $color1, string $color2, int $factor): string {
109
+        $color = new Color($color1);
110
+        return '#' . $color->mix($color2, $factor);
111
+    }
112
+
113
+    public function lighten(string $color, int $factor): string {
114
+        $color = new Color($color);
115
+        return '#' . $color->lighten($factor);
116
+    }
117
+
118
+    public function darken(string $color, int $factor): string {
119
+        $color = new Color($color);
120
+        return '#' . $color->darken($factor);
121
+    }
122
+
123
+    /**
124
+     * Convert RGB to HSL
125
+     *
126
+     * Copied from cssphp, copyright Leaf Corcoran, licensed under MIT
127
+     *
128
+     * @param int $red
129
+     * @param int $green
130
+     * @param int $blue
131
+     *
132
+     * @return float[]
133
+     */
134
+    public function toHSL(int $red, int $green, int $blue): array {
135
+        $color = new Color(Color::rgbToHex(['R' => $red, 'G' => $green, 'B' => $blue]));
136
+        return array_values($color->getHsl());
137
+    }
138
+
139
+    /**
140
+     * @param string $color rgb color value
141
+     * @return float
142
+     */
143
+    public function calculateLuminance(string $color): float {
144
+        [$red, $green, $blue] = $this->hexToRGB($color);
145
+        $hsl = $this->toHSL($red, $green, $blue);
146
+        return $hsl[2];
147
+    }
148
+
149
+    /**
150
+     * Calculate the Luma according to WCAG 2
151
+     * http://www.w3.org/TR/WCAG20/#relativeluminancedef
152
+     * @param string $color rgb color value
153
+     * @return float
154
+     */
155
+    public function calculateLuma(string $color): float {
156
+        $rgb = $this->hexToRGB($color);
157
+
158
+        // Normalize the values by converting to float and applying the rules from WCAG2.0
159
+        $rgb = array_map(function (int $color) {
160
+            $color = $color / 255.0;
161
+            if ($color <= 0.03928) {
162
+                return $color / 12.92;
163
+            } else {
164
+                return pow((($color + 0.055) / 1.055), 2.4);
165
+            }
166
+        }, $rgb);
167
+
168
+        [$red, $green, $blue] = $rgb;
169
+        return (0.2126 * $red + 0.7152 * $green + 0.0722 * $blue);
170
+    }
171
+
172
+    /**
173
+     * Calculat the color contrast according to WCAG 2
174
+     * http://www.w3.org/TR/WCAG20/#contrast-ratiodef
175
+     * @param string $color1 The first color
176
+     * @param string $color2 The second color
177
+     */
178
+    public function colorContrast(string $color1, string $color2): float {
179
+        $luminance1 = $this->calculateLuma($color1) + 0.05;
180
+        $luminance2 = $this->calculateLuma($color2) + 0.05;
181
+        return max($luminance1, $luminance2) / min($luminance1, $luminance2);
182
+    }
183
+
184
+    /**
185
+     * @param string $color rgb color value
186
+     * @return int[]
187
+     * @psalm-return array{0: int, 1: int, 2: int}
188
+     */
189
+    public function hexToRGB(string $color): array {
190
+        $color = new Color($color);
191
+        return array_values($color->getRgb());
192
+    }
193
+
194
+    /**
195
+     * @param $color
196
+     * @return string base64 encoded radio button svg
197
+     */
198
+    public function generateRadioButton($color) {
199
+        $radioButtonIcon = '<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16">'
200
+            . '<path d="M8 1a7 7 0 0 0-7 7 7 7 0 0 0 7 7 7 7 0 0 0 7-7 7 7 0 0 0-7-7zm0 1a6 6 0 0 1 6 6 6 6 0 0 1-6 6 6 6 0 0 1-6-6 6 6 0 0 1 6-6zm0 2a4 4 0 1 0 0 8 4 4 0 0 0 0-8z" fill="' . $color . '"/></svg>';
201
+        return base64_encode($radioButtonIcon);
202
+    }
203
+
204
+
205
+    /**
206
+     * @param string $app app name
207
+     * @return string|ISimpleFile path to app icon / file of logo
208
+     */
209
+    public function getAppIcon($app, $useSvg = true) {
210
+        $app = $this->appManager->cleanAppId($app);
211
+        try {
212
+            // find app specific icon
213
+            $appPath = $this->appManager->getAppPath($app);
214
+            $extension = ($useSvg ? '.svg' : '.png');
215
+
216
+            $icon = $appPath . '/img/' . $app . $extension;
217
+            if (file_exists($icon)) {
218
+                return $icon;
219
+            }
220
+
221
+            $icon = $appPath . '/img/app' . $extension;
222
+            if (file_exists($icon)) {
223
+                return $icon;
224
+            }
225
+        } catch (AppPathNotFoundException $e) {
226
+        }
227
+        // fallback to custom instance logo
228
+        if ($this->config->getAppValue('theming', 'logoMime', '') !== '') {
229
+            try {
230
+                $folder = $this->appData->getFolder('global/images');
231
+                return $folder->getFile('logo');
232
+            } catch (NotFoundException $e) {
233
+            }
234
+        }
235
+        // fallback to core logo
236
+        if ($useSvg) {
237
+            return \OC::$SERVERROOT . '/core/img/logo/logo.svg';
238
+        } else {
239
+            return \OC::$SERVERROOT . '/core/img/logo/logo.png';
240
+        }
241
+    }
242
+
243
+    /**
244
+     * @param string $app app name
245
+     * @param string $image relative path to image in app folder
246
+     * @return string|false absolute path to image
247
+     */
248
+    public function getAppImage($app, $image) {
249
+        $app = $this->appManager->cleanAppId($app);
250
+        /**
251
+         * @psalm-taint-escape file
252
+         */
253
+        $image = str_replace(['\0', '\\', '..'], '', $image);
254
+        if ($app === 'core') {
255
+            $icon = \OC::$SERVERROOT . '/core/img/' . $image;
256
+            if (file_exists($icon)) {
257
+                return $icon;
258
+            }
259
+        }
260
+
261
+        try {
262
+            $appPath = $this->appManager->getAppPath($app);
263
+        } catch (AppPathNotFoundException $e) {
264
+            return false;
265
+        }
266
+
267
+        $icon = $appPath . '/img/' . $image;
268
+        if (file_exists($icon)) {
269
+            return $icon;
270
+        }
271
+        $icon = $appPath . '/img/' . $image . '.svg';
272
+        if (file_exists($icon)) {
273
+            return $icon;
274
+        }
275
+        $icon = $appPath . '/img/' . $image . '.png';
276
+        if (file_exists($icon)) {
277
+            return $icon;
278
+        }
279
+        $icon = $appPath . '/img/' . $image . '.gif';
280
+        if (file_exists($icon)) {
281
+            return $icon;
282
+        }
283
+        $icon = $appPath . '/img/' . $image . '.jpg';
284
+        if (file_exists($icon)) {
285
+            return $icon;
286
+        }
287
+
288
+        return false;
289
+    }
290
+
291
+    /**
292
+     * replace default color with a custom one
293
+     *
294
+     * @param string $svg content of a svg file
295
+     * @param string $color color to match
296
+     * @return string
297
+     */
298
+    public function colorizeSvg($svg, $color) {
299
+        $svg = preg_replace('/#0082c9/i', $color, $svg);
300
+        return $svg;
301
+    }
302
+
303
+    /**
304
+     * Check if a custom theme is set in the server configuration
305
+     *
306
+     * @return bool
307
+     */
308
+    public function isAlreadyThemed() {
309
+        $theme = $this->config->getSystemValue('theme', '');
310
+        if ($theme !== '') {
311
+            return true;
312
+        }
313
+        return false;
314
+    }
315
+
316
+    public function isBackgroundThemed() {
317
+        $backgroundLogo = $this->config->getAppValue('theming', 'backgroundMime', '');
318
+        return $backgroundLogo !== '' && $backgroundLogo !== 'backgroundColor';
319
+    }
320
+
321
+    public function isLogoThemed() {
322
+        return $this->imageManager->hasImage('logo')
323
+            || $this->imageManager->hasImage('logoheader');
324
+    }
325
+
326
+    public function getCacheBuster(): string {
327
+        $userSession = Server::get(IUserSession::class);
328
+        $userId = '';
329
+        $user = $userSession->getUser();
330
+        if (!is_null($user)) {
331
+            $userId = $user->getUID();
332
+        }
333
+        $serverVersion = $this->serverVersion->getVersionString();
334
+        $themingAppVersion = $this->appManager->getAppVersion('theming');
335
+        $userCacheBuster = '';
336
+        if ($userId) {
337
+            $userCacheBusterValue = (int)$this->config->getUserValue($userId, 'theming', 'userCacheBuster', '0');
338
+            $userCacheBuster = $userId . '_' . $userCacheBusterValue;
339
+        }
340
+        $systemCacheBuster = $this->config->getAppValue('theming', 'cachebuster', '0');
341
+        return substr(sha1($serverVersion . $themingAppVersion . $userCacheBuster . $systemCacheBuster), 0, 8);
342
+    }
343 343
 }
Please login to merge, or discard this patch.
Spacing   +19 added lines, -19 removed lines patch added patch discarded remove patch
@@ -83,7 +83,7 @@  discard block
 block discarded – undo
83 83
 			while ($contrast < $minContrast && $iteration++ < 100) {
84 84
 				$hsl = Color::hexToHsl($color);
85 85
 				$hsl['L'] = max(0, min(1, $hsl['L'] + ($brightBackground ? -$epsilon : $epsilon)));
86
-				$color = '#' . Color::hslToHex($hsl);
86
+				$color = '#'.Color::hslToHex($hsl);
87 87
 				$contrast = $this->colorContrast($color, $blurredBackground);
88 88
 			}
89 89
 			return $color;
@@ -107,17 +107,17 @@  discard block
 block discarded – undo
107 107
 
108 108
 	public function mix(string $color1, string $color2, int $factor): string {
109 109
 		$color = new Color($color1);
110
-		return '#' . $color->mix($color2, $factor);
110
+		return '#'.$color->mix($color2, $factor);
111 111
 	}
112 112
 
113 113
 	public function lighten(string $color, int $factor): string {
114 114
 		$color = new Color($color);
115
-		return '#' . $color->lighten($factor);
115
+		return '#'.$color->lighten($factor);
116 116
 	}
117 117
 
118 118
 	public function darken(string $color, int $factor): string {
119 119
 		$color = new Color($color);
120
-		return '#' . $color->darken($factor);
120
+		return '#'.$color->darken($factor);
121 121
 	}
122 122
 
123 123
 	/**
@@ -156,7 +156,7 @@  discard block
 block discarded – undo
156 156
 		$rgb = $this->hexToRGB($color);
157 157
 
158 158
 		// Normalize the values by converting to float and applying the rules from WCAG2.0
159
-		$rgb = array_map(function (int $color) {
159
+		$rgb = array_map(function(int $color) {
160 160
 			$color = $color / 255.0;
161 161
 			if ($color <= 0.03928) {
162 162
 				return $color / 12.92;
@@ -197,7 +197,7 @@  discard block
 block discarded – undo
197 197
 	 */
198 198
 	public function generateRadioButton($color) {
199 199
 		$radioButtonIcon = '<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16">'
200
-			. '<path d="M8 1a7 7 0 0 0-7 7 7 7 0 0 0 7 7 7 7 0 0 0 7-7 7 7 0 0 0-7-7zm0 1a6 6 0 0 1 6 6 6 6 0 0 1-6 6 6 6 0 0 1-6-6 6 6 0 0 1 6-6zm0 2a4 4 0 1 0 0 8 4 4 0 0 0 0-8z" fill="' . $color . '"/></svg>';
200
+			. '<path d="M8 1a7 7 0 0 0-7 7 7 7 0 0 0 7 7 7 7 0 0 0 7-7 7 7 0 0 0-7-7zm0 1a6 6 0 0 1 6 6 6 6 0 0 1-6 6 6 6 0 0 1-6-6 6 6 0 0 1 6-6zm0 2a4 4 0 1 0 0 8 4 4 0 0 0 0-8z" fill="'.$color.'"/></svg>';
201 201
 		return base64_encode($radioButtonIcon);
202 202
 	}
203 203
 
@@ -213,12 +213,12 @@  discard block
 block discarded – undo
213 213
 			$appPath = $this->appManager->getAppPath($app);
214 214
 			$extension = ($useSvg ? '.svg' : '.png');
215 215
 
216
-			$icon = $appPath . '/img/' . $app . $extension;
216
+			$icon = $appPath.'/img/'.$app.$extension;
217 217
 			if (file_exists($icon)) {
218 218
 				return $icon;
219 219
 			}
220 220
 
221
-			$icon = $appPath . '/img/app' . $extension;
221
+			$icon = $appPath.'/img/app'.$extension;
222 222
 			if (file_exists($icon)) {
223 223
 				return $icon;
224 224
 			}
@@ -234,9 +234,9 @@  discard block
 block discarded – undo
234 234
 		}
235 235
 		// fallback to core logo
236 236
 		if ($useSvg) {
237
-			return \OC::$SERVERROOT . '/core/img/logo/logo.svg';
237
+			return \OC::$SERVERROOT.'/core/img/logo/logo.svg';
238 238
 		} else {
239
-			return \OC::$SERVERROOT . '/core/img/logo/logo.png';
239
+			return \OC::$SERVERROOT.'/core/img/logo/logo.png';
240 240
 		}
241 241
 	}
242 242
 
@@ -252,7 +252,7 @@  discard block
 block discarded – undo
252 252
 		 */
253 253
 		$image = str_replace(['\0', '\\', '..'], '', $image);
254 254
 		if ($app === 'core') {
255
-			$icon = \OC::$SERVERROOT . '/core/img/' . $image;
255
+			$icon = \OC::$SERVERROOT.'/core/img/'.$image;
256 256
 			if (file_exists($icon)) {
257 257
 				return $icon;
258 258
 			}
@@ -264,23 +264,23 @@  discard block
 block discarded – undo
264 264
 			return false;
265 265
 		}
266 266
 
267
-		$icon = $appPath . '/img/' . $image;
267
+		$icon = $appPath.'/img/'.$image;
268 268
 		if (file_exists($icon)) {
269 269
 			return $icon;
270 270
 		}
271
-		$icon = $appPath . '/img/' . $image . '.svg';
271
+		$icon = $appPath.'/img/'.$image.'.svg';
272 272
 		if (file_exists($icon)) {
273 273
 			return $icon;
274 274
 		}
275
-		$icon = $appPath . '/img/' . $image . '.png';
275
+		$icon = $appPath.'/img/'.$image.'.png';
276 276
 		if (file_exists($icon)) {
277 277
 			return $icon;
278 278
 		}
279
-		$icon = $appPath . '/img/' . $image . '.gif';
279
+		$icon = $appPath.'/img/'.$image.'.gif';
280 280
 		if (file_exists($icon)) {
281 281
 			return $icon;
282 282
 		}
283
-		$icon = $appPath . '/img/' . $image . '.jpg';
283
+		$icon = $appPath.'/img/'.$image.'.jpg';
284 284
 		if (file_exists($icon)) {
285 285
 			return $icon;
286 286
 		}
@@ -334,10 +334,10 @@  discard block
 block discarded – undo
334 334
 		$themingAppVersion = $this->appManager->getAppVersion('theming');
335 335
 		$userCacheBuster = '';
336 336
 		if ($userId) {
337
-			$userCacheBusterValue = (int)$this->config->getUserValue($userId, 'theming', 'userCacheBuster', '0');
338
-			$userCacheBuster = $userId . '_' . $userCacheBusterValue;
337
+			$userCacheBusterValue = (int) $this->config->getUserValue($userId, 'theming', 'userCacheBuster', '0');
338
+			$userCacheBuster = $userId.'_'.$userCacheBusterValue;
339 339
 		}
340 340
 		$systemCacheBuster = $this->config->getAppValue('theming', 'cachebuster', '0');
341
-		return substr(sha1($serverVersion . $themingAppVersion . $userCacheBuster . $systemCacheBuster), 0, 8);
341
+		return substr(sha1($serverVersion.$themingAppVersion.$userCacheBuster.$systemCacheBuster), 0, 8);
342 342
 	}
343 343
 }
Please login to merge, or discard this patch.
apps/theming/lib/IconBuilder.php 2 patches
Indentation   +218 added lines, -218 removed lines patch added patch discarded remove patch
@@ -12,222 +12,222 @@
 block discarded – undo
12 12
 use OCP\Files\SimpleFS\ISimpleFile;
13 13
 
14 14
 class IconBuilder {
15
-	/**
16
-	 * IconBuilder constructor.
17
-	 *
18
-	 * @param ThemingDefaults $themingDefaults
19
-	 * @param Util $util
20
-	 * @param ImageManager $imageManager
21
-	 */
22
-	public function __construct(
23
-		private ThemingDefaults $themingDefaults,
24
-		private Util $util,
25
-		private ImageManager $imageManager,
26
-	) {
27
-	}
28
-
29
-	/**
30
-	 * @param $app string app name
31
-	 * @return string|false image blob
32
-	 */
33
-	public function getFavicon($app) {
34
-		if (!$this->imageManager->canConvert('PNG')) {
35
-			return false;
36
-		}
37
-		try {
38
-			$icon = $this->renderAppIcon($app, 128);
39
-			if ($icon === false) {
40
-				return false;
41
-			}
42
-			$icon->setImageFormat('PNG32');
43
-
44
-			$favicon = new Imagick();
45
-			$favicon->setFormat('ICO');
46
-
47
-			$clone = clone $icon;
48
-			$clone->scaleImage(16, 0);
49
-			$favicon->addImage($clone);
50
-
51
-			$clone = clone $icon;
52
-			$clone->scaleImage(32, 0);
53
-			$favicon->addImage($clone);
54
-
55
-			$clone = clone $icon;
56
-			$clone->scaleImage(64, 0);
57
-			$favicon->addImage($clone);
58
-
59
-			$clone = clone $icon;
60
-			$clone->scaleImage(128, 0);
61
-			$favicon->addImage($clone);
62
-
63
-			$data = $favicon->getImagesBlob();
64
-			$favicon->destroy();
65
-			$icon->destroy();
66
-			$clone->destroy();
67
-			return $data;
68
-		} catch (\ImagickException $e) {
69
-			return false;
70
-		}
71
-	}
72
-
73
-	/**
74
-	 * @param $app string app name
75
-	 * @return string|false image blob
76
-	 */
77
-	public function getTouchIcon($app) {
78
-		try {
79
-			$icon = $this->renderAppIcon($app, 512);
80
-			if ($icon === false) {
81
-				return false;
82
-			}
83
-			$icon->setImageFormat('png32');
84
-			$data = $icon->getImageBlob();
85
-			$icon->destroy();
86
-			return $data;
87
-		} catch (\ImagickException $e) {
88
-			return false;
89
-		}
90
-	}
91
-
92
-	/**
93
-	 * Render app icon on themed background color
94
-	 * fallback to logo
95
-	 *
96
-	 * @param string $app app name
97
-	 * @param int $size size of the icon in px
98
-	 * @return Imagick|false
99
-	 */
100
-	public function renderAppIcon($app, $size) {
101
-		$supportSvg = $this->imageManager->canConvert('SVG');
102
-		// retrieve app icon
103
-		$appIcon = $this->util->getAppIcon($app, $supportSvg);
104
-		if ($appIcon instanceof ISimpleFile) {
105
-			$appIconContent = $appIcon->getContent();
106
-			$mime = $appIcon->getMimeType();
107
-		} elseif (!file_exists($appIcon)) {
108
-			return false;
109
-		} else {
110
-			$appIconContent = file_get_contents($appIcon);
111
-			$mime = mime_content_type($appIcon);
112
-		}
113
-
114
-		if ($appIconContent === false || $appIconContent === '') {
115
-			return false;
116
-		}
117
-
118
-		$appIconIsSvg = ($mime === 'image/svg+xml' || str_starts_with($appIconContent, '<svg') || str_starts_with($appIconContent, '<?xml'));
119
-		// if source image is svg but svg not supported, abort.
120
-		// source images are both user and developer set, and there is guarantees that mime and extension match actual contents type
121
-		if ($appIconIsSvg && !$supportSvg) {
122
-			return false;
123
-		}
124
-
125
-		// construct original image object
126
-		try {
127
-			$appIconFile = new Imagick();
128
-			$appIconFile->setBackgroundColor(new ImagickPixel('transparent'));
129
-
130
-			if ($appIconIsSvg) {
131
-				// handle SVG images
132
-				// ensure proper XML declaration
133
-				if (!str_starts_with($appIconContent, '<?xml')) {
134
-					$svg = '<?xml version="1.0"?>' . $appIconContent;
135
-				} else {
136
-					$svg = $appIconContent;
137
-				}
138
-				// get dimensions for resolution calculation
139
-				$tmp = new Imagick();
140
-				$tmp->setBackgroundColor(new ImagickPixel('transparent'));
141
-				$tmp->setResolution(72, 72);
142
-				$tmp->readImageBlob($svg);
143
-				$x = $tmp->getImageWidth();
144
-				$y = $tmp->getImageHeight();
145
-				$tmp->destroy();
146
-				// set resolution for proper scaling
147
-				$resX = (int)(72 * $size / $x);
148
-				$resY = (int)(72 * $size / $y);
149
-				$appIconFile->setResolution($resX, $resY);
150
-				$appIconFile->readImageBlob($svg);
151
-			} else {
152
-				// handle non-SVG images
153
-				$appIconFile->readImageBlob($appIconContent);
154
-			}
155
-		} catch (\ImagickException $e) {
156
-			return false;
157
-		}
158
-		// calculate final image size and position
159
-		$padding = 0.85;
160
-		$original_w = $appIconFile->getImageWidth();
161
-		$original_h = $appIconFile->getImageHeight();
162
-		$contentSize = (int)floor($size * $padding);
163
-		$scale = min($contentSize / $original_w, $contentSize / $original_h);
164
-		$new_w = max(1, (int)floor($original_w * $scale));
165
-		$new_h = max(1, (int)floor($original_h * $scale));
166
-		$offset_w = (int)floor(($size - $new_w) / 2);
167
-		$offset_h = (int)floor(($size - $new_h) / 2);
168
-		$cornerRadius = 0.2 * $size;
169
-		$color = $this->themingDefaults->getColorPrimary();
170
-		// resize original image
171
-		$appIconFile->resizeImage($new_w, $new_h, Imagick::FILTER_LANCZOS, 1);
172
-		/**
173
-		 * invert app icons for bright primary colors
174
-		 * the default nextcloud logo will not be inverted to black
175
-		 */
176
-		if ($this->util->isBrightColor($color)
177
-			&& !$appIcon instanceof ISimpleFile
178
-			&& $app !== 'core'
179
-		) {
180
-			$appIconFile->negateImage(false);
181
-		}
182
-		// construct final image object
183
-		try {
184
-			// image background
185
-			$finalIconFile = new Imagick();
186
-			$finalIconFile->setBackgroundColor(new ImagickPixel('transparent'));
187
-			// icon background
188
-			$finalIconFile->newImage($size, $size, new ImagickPixel('transparent'));
189
-			$draw = new ImagickDraw();
190
-			$draw->setFillColor($color);
191
-			$draw->roundRectangle(0, 0, $size - 1, $size - 1, $cornerRadius, $cornerRadius);
192
-			$finalIconFile->drawImage($draw);
193
-			$draw->destroy();
194
-			// overlay icon
195
-			$finalIconFile->setImageVirtualPixelMethod(Imagick::VIRTUALPIXELMETHOD_TRANSPARENT);
196
-			$finalIconFile->setImageArtifact('compose:args', '1,0,-0.5,0.5');
197
-			$finalIconFile->compositeImage($appIconFile, Imagick::COMPOSITE_ATOP, $offset_w, $offset_h);
198
-			$finalIconFile->setImageFormat('PNG32');
199
-			if (defined('Imagick::INTERPOLATE_BICUBIC') === true) {
200
-				$filter = Imagick::INTERPOLATE_BICUBIC;
201
-			} else {
202
-				$filter = Imagick::FILTER_LANCZOS;
203
-			}
204
-			$finalIconFile->resizeImage($size, $size, $filter, 1, false);
205
-
206
-			return $finalIconFile;
207
-		} finally {
208
-			unset($appIconFile);
209
-		}
210
-
211
-		return false;
212
-	}
213
-
214
-	/**
215
-	 * @param string $app app name
216
-	 * @param string $image relative path to svg file in app directory
217
-	 * @return string|false content of a colorized svg file
218
-	 */
219
-	public function colorSvg($app, $image) {
220
-		$imageFile = $this->util->getAppImage($app, $image);
221
-		if ($imageFile === false || $imageFile === '' || !file_exists($imageFile)) {
222
-			return false;
223
-		}
224
-		$svg = file_get_contents($imageFile);
225
-		if ($svg !== false && $svg !== '') {
226
-			$color = $this->util->elementColor($this->themingDefaults->getColorPrimary());
227
-			$svg = $this->util->colorizeSvg($svg, $color);
228
-			return $svg;
229
-		} else {
230
-			return false;
231
-		}
232
-	}
15
+    /**
16
+     * IconBuilder constructor.
17
+     *
18
+     * @param ThemingDefaults $themingDefaults
19
+     * @param Util $util
20
+     * @param ImageManager $imageManager
21
+     */
22
+    public function __construct(
23
+        private ThemingDefaults $themingDefaults,
24
+        private Util $util,
25
+        private ImageManager $imageManager,
26
+    ) {
27
+    }
28
+
29
+    /**
30
+     * @param $app string app name
31
+     * @return string|false image blob
32
+     */
33
+    public function getFavicon($app) {
34
+        if (!$this->imageManager->canConvert('PNG')) {
35
+            return false;
36
+        }
37
+        try {
38
+            $icon = $this->renderAppIcon($app, 128);
39
+            if ($icon === false) {
40
+                return false;
41
+            }
42
+            $icon->setImageFormat('PNG32');
43
+
44
+            $favicon = new Imagick();
45
+            $favicon->setFormat('ICO');
46
+
47
+            $clone = clone $icon;
48
+            $clone->scaleImage(16, 0);
49
+            $favicon->addImage($clone);
50
+
51
+            $clone = clone $icon;
52
+            $clone->scaleImage(32, 0);
53
+            $favicon->addImage($clone);
54
+
55
+            $clone = clone $icon;
56
+            $clone->scaleImage(64, 0);
57
+            $favicon->addImage($clone);
58
+
59
+            $clone = clone $icon;
60
+            $clone->scaleImage(128, 0);
61
+            $favicon->addImage($clone);
62
+
63
+            $data = $favicon->getImagesBlob();
64
+            $favicon->destroy();
65
+            $icon->destroy();
66
+            $clone->destroy();
67
+            return $data;
68
+        } catch (\ImagickException $e) {
69
+            return false;
70
+        }
71
+    }
72
+
73
+    /**
74
+     * @param $app string app name
75
+     * @return string|false image blob
76
+     */
77
+    public function getTouchIcon($app) {
78
+        try {
79
+            $icon = $this->renderAppIcon($app, 512);
80
+            if ($icon === false) {
81
+                return false;
82
+            }
83
+            $icon->setImageFormat('png32');
84
+            $data = $icon->getImageBlob();
85
+            $icon->destroy();
86
+            return $data;
87
+        } catch (\ImagickException $e) {
88
+            return false;
89
+        }
90
+    }
91
+
92
+    /**
93
+     * Render app icon on themed background color
94
+     * fallback to logo
95
+     *
96
+     * @param string $app app name
97
+     * @param int $size size of the icon in px
98
+     * @return Imagick|false
99
+     */
100
+    public function renderAppIcon($app, $size) {
101
+        $supportSvg = $this->imageManager->canConvert('SVG');
102
+        // retrieve app icon
103
+        $appIcon = $this->util->getAppIcon($app, $supportSvg);
104
+        if ($appIcon instanceof ISimpleFile) {
105
+            $appIconContent = $appIcon->getContent();
106
+            $mime = $appIcon->getMimeType();
107
+        } elseif (!file_exists($appIcon)) {
108
+            return false;
109
+        } else {
110
+            $appIconContent = file_get_contents($appIcon);
111
+            $mime = mime_content_type($appIcon);
112
+        }
113
+
114
+        if ($appIconContent === false || $appIconContent === '') {
115
+            return false;
116
+        }
117
+
118
+        $appIconIsSvg = ($mime === 'image/svg+xml' || str_starts_with($appIconContent, '<svg') || str_starts_with($appIconContent, '<?xml'));
119
+        // if source image is svg but svg not supported, abort.
120
+        // source images are both user and developer set, and there is guarantees that mime and extension match actual contents type
121
+        if ($appIconIsSvg && !$supportSvg) {
122
+            return false;
123
+        }
124
+
125
+        // construct original image object
126
+        try {
127
+            $appIconFile = new Imagick();
128
+            $appIconFile->setBackgroundColor(new ImagickPixel('transparent'));
129
+
130
+            if ($appIconIsSvg) {
131
+                // handle SVG images
132
+                // ensure proper XML declaration
133
+                if (!str_starts_with($appIconContent, '<?xml')) {
134
+                    $svg = '<?xml version="1.0"?>' . $appIconContent;
135
+                } else {
136
+                    $svg = $appIconContent;
137
+                }
138
+                // get dimensions for resolution calculation
139
+                $tmp = new Imagick();
140
+                $tmp->setBackgroundColor(new ImagickPixel('transparent'));
141
+                $tmp->setResolution(72, 72);
142
+                $tmp->readImageBlob($svg);
143
+                $x = $tmp->getImageWidth();
144
+                $y = $tmp->getImageHeight();
145
+                $tmp->destroy();
146
+                // set resolution for proper scaling
147
+                $resX = (int)(72 * $size / $x);
148
+                $resY = (int)(72 * $size / $y);
149
+                $appIconFile->setResolution($resX, $resY);
150
+                $appIconFile->readImageBlob($svg);
151
+            } else {
152
+                // handle non-SVG images
153
+                $appIconFile->readImageBlob($appIconContent);
154
+            }
155
+        } catch (\ImagickException $e) {
156
+            return false;
157
+        }
158
+        // calculate final image size and position
159
+        $padding = 0.85;
160
+        $original_w = $appIconFile->getImageWidth();
161
+        $original_h = $appIconFile->getImageHeight();
162
+        $contentSize = (int)floor($size * $padding);
163
+        $scale = min($contentSize / $original_w, $contentSize / $original_h);
164
+        $new_w = max(1, (int)floor($original_w * $scale));
165
+        $new_h = max(1, (int)floor($original_h * $scale));
166
+        $offset_w = (int)floor(($size - $new_w) / 2);
167
+        $offset_h = (int)floor(($size - $new_h) / 2);
168
+        $cornerRadius = 0.2 * $size;
169
+        $color = $this->themingDefaults->getColorPrimary();
170
+        // resize original image
171
+        $appIconFile->resizeImage($new_w, $new_h, Imagick::FILTER_LANCZOS, 1);
172
+        /**
173
+         * invert app icons for bright primary colors
174
+         * the default nextcloud logo will not be inverted to black
175
+         */
176
+        if ($this->util->isBrightColor($color)
177
+            && !$appIcon instanceof ISimpleFile
178
+            && $app !== 'core'
179
+        ) {
180
+            $appIconFile->negateImage(false);
181
+        }
182
+        // construct final image object
183
+        try {
184
+            // image background
185
+            $finalIconFile = new Imagick();
186
+            $finalIconFile->setBackgroundColor(new ImagickPixel('transparent'));
187
+            // icon background
188
+            $finalIconFile->newImage($size, $size, new ImagickPixel('transparent'));
189
+            $draw = new ImagickDraw();
190
+            $draw->setFillColor($color);
191
+            $draw->roundRectangle(0, 0, $size - 1, $size - 1, $cornerRadius, $cornerRadius);
192
+            $finalIconFile->drawImage($draw);
193
+            $draw->destroy();
194
+            // overlay icon
195
+            $finalIconFile->setImageVirtualPixelMethod(Imagick::VIRTUALPIXELMETHOD_TRANSPARENT);
196
+            $finalIconFile->setImageArtifact('compose:args', '1,0,-0.5,0.5');
197
+            $finalIconFile->compositeImage($appIconFile, Imagick::COMPOSITE_ATOP, $offset_w, $offset_h);
198
+            $finalIconFile->setImageFormat('PNG32');
199
+            if (defined('Imagick::INTERPOLATE_BICUBIC') === true) {
200
+                $filter = Imagick::INTERPOLATE_BICUBIC;
201
+            } else {
202
+                $filter = Imagick::FILTER_LANCZOS;
203
+            }
204
+            $finalIconFile->resizeImage($size, $size, $filter, 1, false);
205
+
206
+            return $finalIconFile;
207
+        } finally {
208
+            unset($appIconFile);
209
+        }
210
+
211
+        return false;
212
+    }
213
+
214
+    /**
215
+     * @param string $app app name
216
+     * @param string $image relative path to svg file in app directory
217
+     * @return string|false content of a colorized svg file
218
+     */
219
+    public function colorSvg($app, $image) {
220
+        $imageFile = $this->util->getAppImage($app, $image);
221
+        if ($imageFile === false || $imageFile === '' || !file_exists($imageFile)) {
222
+            return false;
223
+        }
224
+        $svg = file_get_contents($imageFile);
225
+        if ($svg !== false && $svg !== '') {
226
+            $color = $this->util->elementColor($this->themingDefaults->getColorPrimary());
227
+            $svg = $this->util->colorizeSvg($svg, $color);
228
+            return $svg;
229
+        } else {
230
+            return false;
231
+        }
232
+    }
233 233
 }
Please login to merge, or discard this patch.
Spacing   +8 added lines, -8 removed lines patch added patch discarded remove patch
@@ -131,7 +131,7 @@  discard block
 block discarded – undo
131 131
 				// handle SVG images
132 132
 				// ensure proper XML declaration
133 133
 				if (!str_starts_with($appIconContent, '<?xml')) {
134
-					$svg = '<?xml version="1.0"?>' . $appIconContent;
134
+					$svg = '<?xml version="1.0"?>'.$appIconContent;
135 135
 				} else {
136 136
 					$svg = $appIconContent;
137 137
 				}
@@ -144,8 +144,8 @@  discard block
 block discarded – undo
144 144
 				$y = $tmp->getImageHeight();
145 145
 				$tmp->destroy();
146 146
 				// set resolution for proper scaling
147
-				$resX = (int)(72 * $size / $x);
148
-				$resY = (int)(72 * $size / $y);
147
+				$resX = (int) (72 * $size / $x);
148
+				$resY = (int) (72 * $size / $y);
149 149
 				$appIconFile->setResolution($resX, $resY);
150 150
 				$appIconFile->readImageBlob($svg);
151 151
 			} else {
@@ -159,12 +159,12 @@  discard block
 block discarded – undo
159 159
 		$padding = 0.85;
160 160
 		$original_w = $appIconFile->getImageWidth();
161 161
 		$original_h = $appIconFile->getImageHeight();
162
-		$contentSize = (int)floor($size * $padding);
162
+		$contentSize = (int) floor($size * $padding);
163 163
 		$scale = min($contentSize / $original_w, $contentSize / $original_h);
164
-		$new_w = max(1, (int)floor($original_w * $scale));
165
-		$new_h = max(1, (int)floor($original_h * $scale));
166
-		$offset_w = (int)floor(($size - $new_w) / 2);
167
-		$offset_h = (int)floor(($size - $new_h) / 2);
164
+		$new_w = max(1, (int) floor($original_w * $scale));
165
+		$new_h = max(1, (int) floor($original_h * $scale));
166
+		$offset_w = (int) floor(($size - $new_w) / 2);
167
+		$offset_h = (int) floor(($size - $new_h) / 2);
168 168
 		$cornerRadius = 0.2 * $size;
169 169
 		$color = $this->themingDefaults->getColorPrimary();
170 170
 		// resize original image
Please login to merge, or discard this patch.
apps/theming/lib/ThemingDefaults.php 1 patch
Indentation   +512 added lines, -512 removed lines patch added patch discarded remove patch
@@ -22,516 +22,516 @@
 block discarded – undo
22 22
 
23 23
 class ThemingDefaults extends \OC_Defaults {
24 24
 
25
-	private string $name;
26
-	private string $title;
27
-	private string $entity;
28
-	private string $productName;
29
-	private string $url;
30
-	private string $backgroundColor;
31
-	private string $primaryColor;
32
-	private string $docBaseUrl;
33
-
34
-	private string $iTunesAppId;
35
-	private string $iOSClientUrl;
36
-	private string $AndroidClientUrl;
37
-	private string $FDroidClientUrl;
38
-
39
-	/**
40
-	 * ThemingDefaults constructor.
41
-	 */
42
-	public function __construct(
43
-		private readonly IAppConfig $appConfig,
44
-		private readonly IUserConfig $userConfig,
45
-		private readonly IL10N $l,
46
-		private readonly IUserSession $userSession,
47
-		private readonly IURLGenerator $urlGenerator,
48
-		private readonly ICacheFactory $cacheFactory,
49
-		private readonly Util $util,
50
-		private readonly ImageManager $imageManager,
51
-		private readonly IAppManager $appManager,
52
-		private readonly INavigationManager $navigationManager,
53
-		private readonly BackgroundService $backgroundService,
54
-	) {
55
-		parent::__construct();
56
-
57
-		$this->name = parent::getName();
58
-		$this->title = parent::getTitle();
59
-		$this->entity = parent::getEntity();
60
-		$this->productName = parent::getProductName();
61
-		$this->url = parent::getBaseUrl();
62
-		$this->primaryColor = parent::getColorPrimary();
63
-		$this->backgroundColor = parent::getColorBackground();
64
-		$this->iTunesAppId = parent::getiTunesAppId();
65
-		$this->iOSClientUrl = parent::getiOSClientUrl();
66
-		$this->AndroidClientUrl = parent::getAndroidClientUrl();
67
-		$this->FDroidClientUrl = parent::getFDroidClientUrl();
68
-		$this->docBaseUrl = parent::getDocBaseUrl();
69
-	}
70
-
71
-	public function getName() {
72
-		return strip_tags($this->appConfig->getAppValueString(ConfigLexicon::INSTANCE_NAME, $this->name));
73
-	}
74
-
75
-	public function getHTMLName() {
76
-		return $this->appConfig->getAppValueString(ConfigLexicon::INSTANCE_NAME, $this->name);
77
-	}
78
-
79
-	public function getTitle() {
80
-		return strip_tags($this->appConfig->getAppValueString(ConfigLexicon::INSTANCE_NAME, $this->title));
81
-	}
82
-
83
-	public function getEntity() {
84
-		return strip_tags($this->appConfig->getAppValueString(ConfigLexicon::INSTANCE_NAME, $this->entity));
85
-	}
86
-
87
-	public function getProductName() {
88
-		return strip_tags($this->appConfig->getAppValueString(ConfigLexicon::PRODUCT_NAME, $this->productName));
89
-	}
90
-
91
-	public function getBaseUrl() {
92
-		return $this->appConfig->getAppValueString(ConfigLexicon::BASE_URL, $this->url);
93
-	}
94
-
95
-	/**
96
-	 * We pass a string and sanitizeHTML will return a string too in that case
97
-	 * @psalm-suppress InvalidReturnStatement
98
-	 * @psalm-suppress InvalidReturnType
99
-	 */
100
-	public function getSlogan(?string $lang = null): string {
101
-		return \OCP\Util::sanitizeHTML($this->appConfig->getAppValueString(ConfigLexicon::INSTANCE_SLOGAN, parent::getSlogan($lang)));
102
-	}
103
-
104
-	public function getImprintUrl(): string {
105
-		return $this->appConfig->getAppValueString(ConfigLexicon::INSTANCE_IMPRINT_URL, '');
106
-	}
107
-
108
-	public function getPrivacyUrl(): string {
109
-		return $this->appConfig->getAppValueString(ConfigLexicon::INSTANCE_PRIVACY_URL, '');
110
-	}
111
-
112
-	public function getDocBaseUrl(): string {
113
-		return $this->appConfig->getAppValueString(ConfigLexicon::DOC_BASE_URL, $this->docBaseUrl);
114
-	}
115
-
116
-	public function getShortFooter() {
117
-		$slogan = $this->getSlogan();
118
-		$baseUrl = $this->getBaseUrl();
119
-		$entity = $this->getEntity();
120
-		$footer = '';
121
-
122
-		if ($entity !== '') {
123
-			if ($baseUrl !== '') {
124
-				$footer = '<a href="' . $baseUrl . '" target="_blank"'
125
-					. ' rel="noreferrer noopener" class="entity-name">' . $entity . '</a>';
126
-			} else {
127
-				$footer = '<span class="entity-name">' . $entity . '</span>';
128
-			}
129
-		}
130
-		$footer .= ($slogan !== '' ? ' – ' . $slogan : '');
131
-
132
-		$links = [
133
-			[
134
-				'text' => $this->l->t('Legal notice'),
135
-				'url' => $this->getImprintUrl()
136
-			],
137
-			[
138
-				'text' => $this->l->t('Privacy policy'),
139
-				'url' => $this->getPrivacyUrl()
140
-			],
141
-		];
142
-
143
-		$navigation = $this->navigationManager->getAll(INavigationManager::TYPE_GUEST);
144
-		$guestNavigation = array_map(function ($nav) {
145
-			return [
146
-				'text' => $nav['name'],
147
-				'url' => $nav['href']
148
-			];
149
-		}, $navigation);
150
-		$links = array_merge($links, $guestNavigation);
151
-
152
-		$legalLinks = '';
153
-		$divider = '';
154
-		foreach ($links as $link) {
155
-			if ($link['url'] !== ''
156
-				&& filter_var($link['url'], FILTER_VALIDATE_URL)
157
-			) {
158
-				$legalLinks .= $divider . '<a href="' . $link['url'] . '" class="legal" target="_blank"'
159
-					. ' rel="noreferrer noopener">' . $link['text'] . '</a>';
160
-				$divider = ' · ';
161
-			}
162
-		}
163
-		if ($legalLinks !== '') {
164
-			$footer .= '<br/><span class="footer__legal-links">' . $legalLinks . '</span>';
165
-		}
166
-
167
-		return $footer;
168
-	}
169
-
170
-	/**
171
-	 * Color that is used for highlighting elements like important buttons
172
-	 * If user theming is enabled then the user defined value is returned
173
-	 */
174
-	public function getColorPrimary(): string {
175
-		$user = $this->userSession->getUser();
176
-
177
-		// admin-defined primary color
178
-		$defaultColor = $this->getDefaultColorPrimary();
179
-
180
-		if ($this->isUserThemingDisabled()) {
181
-			return $defaultColor;
182
-		}
183
-
184
-		// user-defined primary color
185
-		if (!empty($user)) {
186
-			$userPrimaryColor = $this->userConfig->getValueString($user->getUID(), Application::APP_ID, 'primary_color');
187
-			if (preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $userPrimaryColor)) {
188
-				return $userPrimaryColor;
189
-			}
190
-		}
191
-
192
-		// Finally, return the system global primary color
193
-		return $defaultColor;
194
-	}
195
-
196
-	/**
197
-	 * Color that is used for the page background (e.g. the header)
198
-	 * If user theming is enabled then the user defined value is returned
199
-	 */
200
-	public function getColorBackground(): string {
201
-		$user = $this->userSession->getUser();
202
-
203
-		// admin-defined background color
204
-		$defaultColor = $this->getDefaultColorBackground();
205
-
206
-		if ($this->isUserThemingDisabled()) {
207
-			return $defaultColor;
208
-		}
209
-
210
-		// user-defined background color
211
-		if (!empty($user)) {
212
-			$userBackgroundColor = $this->userConfig->getValueString($user->getUID(), Application::APP_ID, 'background_color');
213
-			if (preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $userBackgroundColor)) {
214
-				return $userBackgroundColor;
215
-			}
216
-		}
217
-
218
-		// Finally, return the system global background color
219
-		return $defaultColor;
220
-	}
221
-
222
-	/**
223
-	 * Return the default primary color - only taking admin setting into account
224
-	 */
225
-	public function getDefaultColorPrimary(): string {
226
-		// try admin color
227
-		$defaultColor = $this->appConfig->getAppValueString('primary_color', '');
228
-		if (preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $defaultColor)) {
229
-			return $defaultColor;
230
-		}
231
-
232
-		// fall back to default primary color
233
-		return $this->primaryColor;
234
-	}
235
-
236
-	/**
237
-	 * Default background color only taking admin setting into account
238
-	 */
239
-	public function getDefaultColorBackground(): string {
240
-		$defaultColor = $this->appConfig->getAppValueString('background_color');
241
-		if (preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $defaultColor)) {
242
-			return $defaultColor;
243
-		}
244
-
245
-		return $this->backgroundColor;
246
-	}
247
-
248
-	/**
249
-	 * Themed logo url
250
-	 *
251
-	 * @param bool $useSvg Whether to point to the SVG image or a fallback
252
-	 * @return string
253
-	 */
254
-	public function getLogo($useSvg = true): string {
255
-		$logo = $this->appConfig->getAppValueString('logoMime', '');
256
-
257
-		// short cut to avoid setting up the filesystem just to check if the logo is there
258
-		//
259
-		// explanation: if an SVG is requested and the app config value for logoMime is set then the logo is there.
260
-		// otherwise we need to check it and maybe also generate a PNG from the SVG (that's done in getImage() which
261
-		// needs to be called then)
262
-		if ($useSvg === true && $logo !== '') {
263
-			$logoExists = true;
264
-		} else {
265
-			try {
266
-				$this->imageManager->getImage('logo', $useSvg);
267
-				$logoExists = true;
268
-			} catch (\Exception $e) {
269
-				$logoExists = false;
270
-			}
271
-		}
272
-
273
-		$cacheBusterCounter = (string)$this->appConfig->getAppValueInt(ConfigLexicon::CACHE_BUSTER);
274
-		if (!$logo || !$logoExists) {
275
-			if ($useSvg) {
276
-				$logo = $this->urlGenerator->imagePath('core', 'logo/logo.svg');
277
-			} else {
278
-				$logo = $this->urlGenerator->imagePath('core', 'logo/logo.png');
279
-			}
280
-			return $logo . '?v=' . $cacheBusterCounter;
281
-		}
282
-
283
-		return $this->urlGenerator->linkToRoute('theming.Theming.getImage', [ 'key' => 'logo', 'useSvg' => $useSvg, 'v' => $cacheBusterCounter ]);
284
-	}
285
-
286
-	/**
287
-	 * Themed background image url
288
-	 *
289
-	 * @param bool $darkVariant if the dark variant (if available) of the background should be used
290
-	 * @return string
291
-	 */
292
-	public function getBackground(bool $darkVariant = false): string {
293
-		return $this->imageManager->getImageUrl('background' . ($darkVariant ? 'Dark' : ''));
294
-	}
295
-
296
-	/**
297
-	 * @return string
298
-	 */
299
-	public function getiTunesAppId() {
300
-		return $this->appConfig->getAppValueString('iTunesAppId', $this->iTunesAppId);
301
-	}
302
-
303
-	/**
304
-	 * @return string
305
-	 */
306
-	public function getiOSClientUrl() {
307
-		return $this->appConfig->getAppValueString('iOSClientUrl', $this->iOSClientUrl);
308
-	}
309
-
310
-	/**
311
-	 * @return string
312
-	 */
313
-	public function getAndroidClientUrl() {
314
-		return $this->appConfig->getAppValueString('AndroidClientUrl', $this->AndroidClientUrl);
315
-	}
316
-
317
-	/**
318
-	 * @return string
319
-	 */
320
-	public function getFDroidClientUrl() {
321
-		return $this->appConfig->getAppValueString('FDroidClientUrl', $this->FDroidClientUrl);
322
-	}
323
-
324
-	/**
325
-	 * @return array scss variables to overwrite
326
-	 * @deprecated since Nextcloud 22 - https://github.com/nextcloud/server/issues/9940
327
-	 */
328
-	public function getScssVariables() {
329
-		$cacheBuster = $this->appConfig->getAppValueInt(ConfigLexicon::CACHE_BUSTER);
330
-		$cache = $this->cacheFactory->createDistributed('theming-' . (string)$cacheBuster . '-' . $this->urlGenerator->getBaseUrl());
331
-		if ($value = $cache->get('getScssVariables')) {
332
-			return $value;
333
-		}
334
-
335
-		$variables = [
336
-			'theming-cachebuster' => "'" . $cacheBuster . "'",
337
-			'theming-logo-mime' => "'" . $this->appConfig->getAppValueString('logoMime') . "'",
338
-			'theming-background-mime' => "'" . $this->appConfig->getAppValueString('backgroundMime') . "'",
339
-			'theming-logoheader-mime' => "'" . $this->appConfig->getAppValueString('logoheaderMime') . "'",
340
-			'theming-favicon-mime' => "'" . $this->appConfig->getAppValueString('faviconMime') . "'"
341
-		];
342
-
343
-		$variables['image-logo'] = "url('" . $this->imageManager->getImageUrl('logo') . "')";
344
-		$variables['image-logoheader'] = "url('" . $this->imageManager->getImageUrl('logoheader') . "')";
345
-		$variables['image-favicon'] = "url('" . $this->imageManager->getImageUrl('favicon') . "')";
346
-		$variables['image-login-background'] = "url('" . $this->imageManager->getImageUrl('background') . "')";
347
-		$variables['image-login-plain'] = 'false';
348
-
349
-		if ($this->appConfig->getAppValueString('primary_color', '') !== '') {
350
-			$variables['color-primary'] = $this->getColorPrimary();
351
-			$variables['color-primary-text'] = $this->getTextColorPrimary();
352
-			$variables['color-primary-element'] = $this->util->elementColor($this->getColorPrimary());
353
-		}
354
-
355
-		if ($this->appConfig->getAppValueString('backgroundMime', '') === 'backgroundColor') {
356
-			$variables['image-login-plain'] = 'true';
357
-		}
358
-
359
-		$variables['has-legal-links'] = 'false';
360
-		if ($this->getImprintUrl() !== '' || $this->getPrivacyUrl() !== '') {
361
-			$variables['has-legal-links'] = 'true';
362
-		}
363
-
364
-		$cache->set('getScssVariables', $variables);
365
-		return $variables;
366
-	}
367
-
368
-	/**
369
-	 * Check if the image should be replaced by the theming app
370
-	 * and return the new image location then
371
-	 *
372
-	 * @param string $app name of the app
373
-	 * @param string $image filename of the image
374
-	 * @return bool|string false if image should not replaced, otherwise the location of the image
375
-	 */
376
-	public function replaceImagePath($app, $image) {
377
-		if ($app === '' || $app === 'files_sharing') {
378
-			$app = 'core';
379
-		}
380
-
381
-		$route = false;
382
-		if ($image === 'favicon.ico' && ($this->imageManager->canConvert('ICO') || $this->getCustomFavicon() !== null)) {
383
-			$route = $this->urlGenerator->linkToRoute('theming.Icon.getFavicon', ['app' => $app]);
384
-		}
385
-		if (($image === 'favicon-touch.png' || $image === 'favicon-fb.png') && ($this->imageManager->canConvert('PNG') || $this->getCustomFavicon() !== null)) {
386
-			$route = $this->urlGenerator->linkToRoute('theming.Icon.getTouchIcon', ['app' => $app]);
387
-		}
388
-		if ($image === 'manifest.json') {
389
-			try {
390
-				$appPath = $this->appManager->getAppPath($app);
391
-				if (file_exists($appPath . '/img/manifest.json')) {
392
-					return false;
393
-				}
394
-			} catch (AppPathNotFoundException $e) {
395
-			}
396
-			$route = $this->urlGenerator->linkToRoute('theming.Theming.getManifest', ['app' => $app ]);
397
-		}
398
-		if (str_starts_with($image, 'filetypes/') && file_exists(\OC::$SERVERROOT . '/core/img/' . $image)) {
399
-			$route = $this->urlGenerator->linkToRoute('theming.Icon.getThemedIcon', ['app' => $app, 'image' => $image]);
400
-		}
401
-
402
-		if ($route) {
403
-			return $route . '?v=' . $this->util->getCacheBuster();
404
-		}
405
-
406
-		return false;
407
-	}
408
-
409
-	protected function getCustomFavicon(): ?ISimpleFile {
410
-		try {
411
-			return $this->imageManager->getImage('favicon');
412
-		} catch (NotFoundException $e) {
413
-			return null;
414
-		}
415
-	}
416
-
417
-	/**
418
-	 * Increases the cache buster key
419
-	 */
420
-	public function increaseCacheBuster(): void {
421
-		$cacheBusterKey = $this->appConfig->getAppValueInt(ConfigLexicon::CACHE_BUSTER);
422
-		$this->appConfig->setAppValueInt(ConfigLexicon::CACHE_BUSTER, $cacheBusterKey + 1);
423
-		$this->cacheFactory->createDistributed('theming-')->clear();
424
-		$this->cacheFactory->createDistributed('imagePath')->clear();
425
-	}
426
-
427
-	/**
428
-	 * Update setting in the database
429
-	 *
430
-	 * @param string $setting
431
-	 * @param string $value
432
-	 */
433
-	public function set($setting, $value): void {
434
-		switch ($setting) {
435
-			case ConfigLexicon::CACHE_BUSTER:
436
-				$this->appConfig->setAppValueInt(ConfigLexicon::CACHE_BUSTER, (int)$value);
437
-				break;
438
-			case ConfigLexicon::USER_THEMING_DISABLED:
439
-				$value = in_array($value, ['1', 'true', 'yes', 'on']);
440
-				$this->appConfig->setAppValueBool(ConfigLexicon::USER_THEMING_DISABLED, $value);
441
-				break;
442
-			default:
443
-				$this->appConfig->setAppValueString($setting, $value);
444
-				break;
445
-		}
446
-		$this->increaseCacheBuster();
447
-	}
448
-
449
-	/**
450
-	 * Revert all settings to the default value
451
-	 */
452
-	public function undoAll(): void {
453
-		// Remember the current cachebuster value, as we do not want to reset this value
454
-		// Otherwise this can lead to caching issues as the value might be known to a browser already
455
-		$cacheBusterKey = $this->appConfig->getAppValueInt(ConfigLexicon::CACHE_BUSTER);
456
-		$this->appConfig->deleteAppValues();
457
-		$this->appConfig->setAppValueInt(ConfigLexicon::CACHE_BUSTER, $cacheBusterKey);
458
-		$this->increaseCacheBuster();
459
-	}
460
-
461
-	/**
462
-	 * Revert admin settings to the default value
463
-	 *
464
-	 * @param string $setting setting which should be reverted
465
-	 * @return string default value
466
-	 */
467
-	public function undo($setting): string {
468
-		$this->appConfig->deleteAppValue($setting);
469
-		$this->increaseCacheBuster();
470
-
471
-		$returnValue = '';
472
-		switch ($setting) {
473
-			case 'name':
474
-				$returnValue = $this->getEntity();
475
-				break;
476
-			case 'url':
477
-				$returnValue = $this->getBaseUrl();
478
-				break;
479
-			case 'slogan':
480
-				$returnValue = $this->getSlogan();
481
-				break;
482
-			case 'primary_color':
483
-				$returnValue = BackgroundService::DEFAULT_COLOR;
484
-				break;
485
-			case 'background_color':
486
-				// If a background image is set we revert to the mean image color
487
-				if ($this->imageManager->hasImage('background')) {
488
-					$file = $this->imageManager->getImage('background');
489
-					$returnValue = $this->backgroundService->setGlobalBackground($file->read()) ?? '';
490
-				}
491
-				break;
492
-			case 'logo':
493
-			case 'logoheader':
494
-			case 'background':
495
-			case 'favicon':
496
-				$this->imageManager->delete($setting);
497
-				$this->appConfig->deleteAppValue($setting . 'Mime');
498
-				break;
499
-		}
500
-
501
-		return $returnValue;
502
-	}
503
-
504
-	/**
505
-	 * Color of text in the header menu
506
-	 *
507
-	 * @return string
508
-	 */
509
-	public function getTextColorBackground() {
510
-		return $this->util->invertTextColor($this->getColorBackground()) ? '#000000' : '#ffffff';
511
-	}
512
-
513
-	/**
514
-	 * Color of text on primary buttons and other elements
515
-	 *
516
-	 * @return string
517
-	 */
518
-	public function getTextColorPrimary() {
519
-		return $this->util->invertTextColor($this->getColorPrimary()) ? '#000000' : '#ffffff';
520
-	}
521
-
522
-	/**
523
-	 * Color of text in the header and primary buttons
524
-	 *
525
-	 * @return string
526
-	 */
527
-	public function getDefaultTextColorPrimary() {
528
-		return $this->util->invertTextColor($this->getDefaultColorPrimary()) ? '#000000' : '#ffffff';
529
-	}
530
-
531
-	/**
532
-	 * Has the admin disabled user customization
533
-	 */
534
-	public function isUserThemingDisabled(): bool {
535
-		return $this->appConfig->getAppValueBool(ConfigLexicon::USER_THEMING_DISABLED, false);
536
-	}
25
+    private string $name;
26
+    private string $title;
27
+    private string $entity;
28
+    private string $productName;
29
+    private string $url;
30
+    private string $backgroundColor;
31
+    private string $primaryColor;
32
+    private string $docBaseUrl;
33
+
34
+    private string $iTunesAppId;
35
+    private string $iOSClientUrl;
36
+    private string $AndroidClientUrl;
37
+    private string $FDroidClientUrl;
38
+
39
+    /**
40
+     * ThemingDefaults constructor.
41
+     */
42
+    public function __construct(
43
+        private readonly IAppConfig $appConfig,
44
+        private readonly IUserConfig $userConfig,
45
+        private readonly IL10N $l,
46
+        private readonly IUserSession $userSession,
47
+        private readonly IURLGenerator $urlGenerator,
48
+        private readonly ICacheFactory $cacheFactory,
49
+        private readonly Util $util,
50
+        private readonly ImageManager $imageManager,
51
+        private readonly IAppManager $appManager,
52
+        private readonly INavigationManager $navigationManager,
53
+        private readonly BackgroundService $backgroundService,
54
+    ) {
55
+        parent::__construct();
56
+
57
+        $this->name = parent::getName();
58
+        $this->title = parent::getTitle();
59
+        $this->entity = parent::getEntity();
60
+        $this->productName = parent::getProductName();
61
+        $this->url = parent::getBaseUrl();
62
+        $this->primaryColor = parent::getColorPrimary();
63
+        $this->backgroundColor = parent::getColorBackground();
64
+        $this->iTunesAppId = parent::getiTunesAppId();
65
+        $this->iOSClientUrl = parent::getiOSClientUrl();
66
+        $this->AndroidClientUrl = parent::getAndroidClientUrl();
67
+        $this->FDroidClientUrl = parent::getFDroidClientUrl();
68
+        $this->docBaseUrl = parent::getDocBaseUrl();
69
+    }
70
+
71
+    public function getName() {
72
+        return strip_tags($this->appConfig->getAppValueString(ConfigLexicon::INSTANCE_NAME, $this->name));
73
+    }
74
+
75
+    public function getHTMLName() {
76
+        return $this->appConfig->getAppValueString(ConfigLexicon::INSTANCE_NAME, $this->name);
77
+    }
78
+
79
+    public function getTitle() {
80
+        return strip_tags($this->appConfig->getAppValueString(ConfigLexicon::INSTANCE_NAME, $this->title));
81
+    }
82
+
83
+    public function getEntity() {
84
+        return strip_tags($this->appConfig->getAppValueString(ConfigLexicon::INSTANCE_NAME, $this->entity));
85
+    }
86
+
87
+    public function getProductName() {
88
+        return strip_tags($this->appConfig->getAppValueString(ConfigLexicon::PRODUCT_NAME, $this->productName));
89
+    }
90
+
91
+    public function getBaseUrl() {
92
+        return $this->appConfig->getAppValueString(ConfigLexicon::BASE_URL, $this->url);
93
+    }
94
+
95
+    /**
96
+     * We pass a string and sanitizeHTML will return a string too in that case
97
+     * @psalm-suppress InvalidReturnStatement
98
+     * @psalm-suppress InvalidReturnType
99
+     */
100
+    public function getSlogan(?string $lang = null): string {
101
+        return \OCP\Util::sanitizeHTML($this->appConfig->getAppValueString(ConfigLexicon::INSTANCE_SLOGAN, parent::getSlogan($lang)));
102
+    }
103
+
104
+    public function getImprintUrl(): string {
105
+        return $this->appConfig->getAppValueString(ConfigLexicon::INSTANCE_IMPRINT_URL, '');
106
+    }
107
+
108
+    public function getPrivacyUrl(): string {
109
+        return $this->appConfig->getAppValueString(ConfigLexicon::INSTANCE_PRIVACY_URL, '');
110
+    }
111
+
112
+    public function getDocBaseUrl(): string {
113
+        return $this->appConfig->getAppValueString(ConfigLexicon::DOC_BASE_URL, $this->docBaseUrl);
114
+    }
115
+
116
+    public function getShortFooter() {
117
+        $slogan = $this->getSlogan();
118
+        $baseUrl = $this->getBaseUrl();
119
+        $entity = $this->getEntity();
120
+        $footer = '';
121
+
122
+        if ($entity !== '') {
123
+            if ($baseUrl !== '') {
124
+                $footer = '<a href="' . $baseUrl . '" target="_blank"'
125
+                    . ' rel="noreferrer noopener" class="entity-name">' . $entity . '</a>';
126
+            } else {
127
+                $footer = '<span class="entity-name">' . $entity . '</span>';
128
+            }
129
+        }
130
+        $footer .= ($slogan !== '' ? ' – ' . $slogan : '');
131
+
132
+        $links = [
133
+            [
134
+                'text' => $this->l->t('Legal notice'),
135
+                'url' => $this->getImprintUrl()
136
+            ],
137
+            [
138
+                'text' => $this->l->t('Privacy policy'),
139
+                'url' => $this->getPrivacyUrl()
140
+            ],
141
+        ];
142
+
143
+        $navigation = $this->navigationManager->getAll(INavigationManager::TYPE_GUEST);
144
+        $guestNavigation = array_map(function ($nav) {
145
+            return [
146
+                'text' => $nav['name'],
147
+                'url' => $nav['href']
148
+            ];
149
+        }, $navigation);
150
+        $links = array_merge($links, $guestNavigation);
151
+
152
+        $legalLinks = '';
153
+        $divider = '';
154
+        foreach ($links as $link) {
155
+            if ($link['url'] !== ''
156
+                && filter_var($link['url'], FILTER_VALIDATE_URL)
157
+            ) {
158
+                $legalLinks .= $divider . '<a href="' . $link['url'] . '" class="legal" target="_blank"'
159
+                    . ' rel="noreferrer noopener">' . $link['text'] . '</a>';
160
+                $divider = ' · ';
161
+            }
162
+        }
163
+        if ($legalLinks !== '') {
164
+            $footer .= '<br/><span class="footer__legal-links">' . $legalLinks . '</span>';
165
+        }
166
+
167
+        return $footer;
168
+    }
169
+
170
+    /**
171
+     * Color that is used for highlighting elements like important buttons
172
+     * If user theming is enabled then the user defined value is returned
173
+     */
174
+    public function getColorPrimary(): string {
175
+        $user = $this->userSession->getUser();
176
+
177
+        // admin-defined primary color
178
+        $defaultColor = $this->getDefaultColorPrimary();
179
+
180
+        if ($this->isUserThemingDisabled()) {
181
+            return $defaultColor;
182
+        }
183
+
184
+        // user-defined primary color
185
+        if (!empty($user)) {
186
+            $userPrimaryColor = $this->userConfig->getValueString($user->getUID(), Application::APP_ID, 'primary_color');
187
+            if (preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $userPrimaryColor)) {
188
+                return $userPrimaryColor;
189
+            }
190
+        }
191
+
192
+        // Finally, return the system global primary color
193
+        return $defaultColor;
194
+    }
195
+
196
+    /**
197
+     * Color that is used for the page background (e.g. the header)
198
+     * If user theming is enabled then the user defined value is returned
199
+     */
200
+    public function getColorBackground(): string {
201
+        $user = $this->userSession->getUser();
202
+
203
+        // admin-defined background color
204
+        $defaultColor = $this->getDefaultColorBackground();
205
+
206
+        if ($this->isUserThemingDisabled()) {
207
+            return $defaultColor;
208
+        }
209
+
210
+        // user-defined background color
211
+        if (!empty($user)) {
212
+            $userBackgroundColor = $this->userConfig->getValueString($user->getUID(), Application::APP_ID, 'background_color');
213
+            if (preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $userBackgroundColor)) {
214
+                return $userBackgroundColor;
215
+            }
216
+        }
217
+
218
+        // Finally, return the system global background color
219
+        return $defaultColor;
220
+    }
221
+
222
+    /**
223
+     * Return the default primary color - only taking admin setting into account
224
+     */
225
+    public function getDefaultColorPrimary(): string {
226
+        // try admin color
227
+        $defaultColor = $this->appConfig->getAppValueString('primary_color', '');
228
+        if (preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $defaultColor)) {
229
+            return $defaultColor;
230
+        }
231
+
232
+        // fall back to default primary color
233
+        return $this->primaryColor;
234
+    }
235
+
236
+    /**
237
+     * Default background color only taking admin setting into account
238
+     */
239
+    public function getDefaultColorBackground(): string {
240
+        $defaultColor = $this->appConfig->getAppValueString('background_color');
241
+        if (preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $defaultColor)) {
242
+            return $defaultColor;
243
+        }
244
+
245
+        return $this->backgroundColor;
246
+    }
247
+
248
+    /**
249
+     * Themed logo url
250
+     *
251
+     * @param bool $useSvg Whether to point to the SVG image or a fallback
252
+     * @return string
253
+     */
254
+    public function getLogo($useSvg = true): string {
255
+        $logo = $this->appConfig->getAppValueString('logoMime', '');
256
+
257
+        // short cut to avoid setting up the filesystem just to check if the logo is there
258
+        //
259
+        // explanation: if an SVG is requested and the app config value for logoMime is set then the logo is there.
260
+        // otherwise we need to check it and maybe also generate a PNG from the SVG (that's done in getImage() which
261
+        // needs to be called then)
262
+        if ($useSvg === true && $logo !== '') {
263
+            $logoExists = true;
264
+        } else {
265
+            try {
266
+                $this->imageManager->getImage('logo', $useSvg);
267
+                $logoExists = true;
268
+            } catch (\Exception $e) {
269
+                $logoExists = false;
270
+            }
271
+        }
272
+
273
+        $cacheBusterCounter = (string)$this->appConfig->getAppValueInt(ConfigLexicon::CACHE_BUSTER);
274
+        if (!$logo || !$logoExists) {
275
+            if ($useSvg) {
276
+                $logo = $this->urlGenerator->imagePath('core', 'logo/logo.svg');
277
+            } else {
278
+                $logo = $this->urlGenerator->imagePath('core', 'logo/logo.png');
279
+            }
280
+            return $logo . '?v=' . $cacheBusterCounter;
281
+        }
282
+
283
+        return $this->urlGenerator->linkToRoute('theming.Theming.getImage', [ 'key' => 'logo', 'useSvg' => $useSvg, 'v' => $cacheBusterCounter ]);
284
+    }
285
+
286
+    /**
287
+     * Themed background image url
288
+     *
289
+     * @param bool $darkVariant if the dark variant (if available) of the background should be used
290
+     * @return string
291
+     */
292
+    public function getBackground(bool $darkVariant = false): string {
293
+        return $this->imageManager->getImageUrl('background' . ($darkVariant ? 'Dark' : ''));
294
+    }
295
+
296
+    /**
297
+     * @return string
298
+     */
299
+    public function getiTunesAppId() {
300
+        return $this->appConfig->getAppValueString('iTunesAppId', $this->iTunesAppId);
301
+    }
302
+
303
+    /**
304
+     * @return string
305
+     */
306
+    public function getiOSClientUrl() {
307
+        return $this->appConfig->getAppValueString('iOSClientUrl', $this->iOSClientUrl);
308
+    }
309
+
310
+    /**
311
+     * @return string
312
+     */
313
+    public function getAndroidClientUrl() {
314
+        return $this->appConfig->getAppValueString('AndroidClientUrl', $this->AndroidClientUrl);
315
+    }
316
+
317
+    /**
318
+     * @return string
319
+     */
320
+    public function getFDroidClientUrl() {
321
+        return $this->appConfig->getAppValueString('FDroidClientUrl', $this->FDroidClientUrl);
322
+    }
323
+
324
+    /**
325
+     * @return array scss variables to overwrite
326
+     * @deprecated since Nextcloud 22 - https://github.com/nextcloud/server/issues/9940
327
+     */
328
+    public function getScssVariables() {
329
+        $cacheBuster = $this->appConfig->getAppValueInt(ConfigLexicon::CACHE_BUSTER);
330
+        $cache = $this->cacheFactory->createDistributed('theming-' . (string)$cacheBuster . '-' . $this->urlGenerator->getBaseUrl());
331
+        if ($value = $cache->get('getScssVariables')) {
332
+            return $value;
333
+        }
334
+
335
+        $variables = [
336
+            'theming-cachebuster' => "'" . $cacheBuster . "'",
337
+            'theming-logo-mime' => "'" . $this->appConfig->getAppValueString('logoMime') . "'",
338
+            'theming-background-mime' => "'" . $this->appConfig->getAppValueString('backgroundMime') . "'",
339
+            'theming-logoheader-mime' => "'" . $this->appConfig->getAppValueString('logoheaderMime') . "'",
340
+            'theming-favicon-mime' => "'" . $this->appConfig->getAppValueString('faviconMime') . "'"
341
+        ];
342
+
343
+        $variables['image-logo'] = "url('" . $this->imageManager->getImageUrl('logo') . "')";
344
+        $variables['image-logoheader'] = "url('" . $this->imageManager->getImageUrl('logoheader') . "')";
345
+        $variables['image-favicon'] = "url('" . $this->imageManager->getImageUrl('favicon') . "')";
346
+        $variables['image-login-background'] = "url('" . $this->imageManager->getImageUrl('background') . "')";
347
+        $variables['image-login-plain'] = 'false';
348
+
349
+        if ($this->appConfig->getAppValueString('primary_color', '') !== '') {
350
+            $variables['color-primary'] = $this->getColorPrimary();
351
+            $variables['color-primary-text'] = $this->getTextColorPrimary();
352
+            $variables['color-primary-element'] = $this->util->elementColor($this->getColorPrimary());
353
+        }
354
+
355
+        if ($this->appConfig->getAppValueString('backgroundMime', '') === 'backgroundColor') {
356
+            $variables['image-login-plain'] = 'true';
357
+        }
358
+
359
+        $variables['has-legal-links'] = 'false';
360
+        if ($this->getImprintUrl() !== '' || $this->getPrivacyUrl() !== '') {
361
+            $variables['has-legal-links'] = 'true';
362
+        }
363
+
364
+        $cache->set('getScssVariables', $variables);
365
+        return $variables;
366
+    }
367
+
368
+    /**
369
+     * Check if the image should be replaced by the theming app
370
+     * and return the new image location then
371
+     *
372
+     * @param string $app name of the app
373
+     * @param string $image filename of the image
374
+     * @return bool|string false if image should not replaced, otherwise the location of the image
375
+     */
376
+    public function replaceImagePath($app, $image) {
377
+        if ($app === '' || $app === 'files_sharing') {
378
+            $app = 'core';
379
+        }
380
+
381
+        $route = false;
382
+        if ($image === 'favicon.ico' && ($this->imageManager->canConvert('ICO') || $this->getCustomFavicon() !== null)) {
383
+            $route = $this->urlGenerator->linkToRoute('theming.Icon.getFavicon', ['app' => $app]);
384
+        }
385
+        if (($image === 'favicon-touch.png' || $image === 'favicon-fb.png') && ($this->imageManager->canConvert('PNG') || $this->getCustomFavicon() !== null)) {
386
+            $route = $this->urlGenerator->linkToRoute('theming.Icon.getTouchIcon', ['app' => $app]);
387
+        }
388
+        if ($image === 'manifest.json') {
389
+            try {
390
+                $appPath = $this->appManager->getAppPath($app);
391
+                if (file_exists($appPath . '/img/manifest.json')) {
392
+                    return false;
393
+                }
394
+            } catch (AppPathNotFoundException $e) {
395
+            }
396
+            $route = $this->urlGenerator->linkToRoute('theming.Theming.getManifest', ['app' => $app ]);
397
+        }
398
+        if (str_starts_with($image, 'filetypes/') && file_exists(\OC::$SERVERROOT . '/core/img/' . $image)) {
399
+            $route = $this->urlGenerator->linkToRoute('theming.Icon.getThemedIcon', ['app' => $app, 'image' => $image]);
400
+        }
401
+
402
+        if ($route) {
403
+            return $route . '?v=' . $this->util->getCacheBuster();
404
+        }
405
+
406
+        return false;
407
+    }
408
+
409
+    protected function getCustomFavicon(): ?ISimpleFile {
410
+        try {
411
+            return $this->imageManager->getImage('favicon');
412
+        } catch (NotFoundException $e) {
413
+            return null;
414
+        }
415
+    }
416
+
417
+    /**
418
+     * Increases the cache buster key
419
+     */
420
+    public function increaseCacheBuster(): void {
421
+        $cacheBusterKey = $this->appConfig->getAppValueInt(ConfigLexicon::CACHE_BUSTER);
422
+        $this->appConfig->setAppValueInt(ConfigLexicon::CACHE_BUSTER, $cacheBusterKey + 1);
423
+        $this->cacheFactory->createDistributed('theming-')->clear();
424
+        $this->cacheFactory->createDistributed('imagePath')->clear();
425
+    }
426
+
427
+    /**
428
+     * Update setting in the database
429
+     *
430
+     * @param string $setting
431
+     * @param string $value
432
+     */
433
+    public function set($setting, $value): void {
434
+        switch ($setting) {
435
+            case ConfigLexicon::CACHE_BUSTER:
436
+                $this->appConfig->setAppValueInt(ConfigLexicon::CACHE_BUSTER, (int)$value);
437
+                break;
438
+            case ConfigLexicon::USER_THEMING_DISABLED:
439
+                $value = in_array($value, ['1', 'true', 'yes', 'on']);
440
+                $this->appConfig->setAppValueBool(ConfigLexicon::USER_THEMING_DISABLED, $value);
441
+                break;
442
+            default:
443
+                $this->appConfig->setAppValueString($setting, $value);
444
+                break;
445
+        }
446
+        $this->increaseCacheBuster();
447
+    }
448
+
449
+    /**
450
+     * Revert all settings to the default value
451
+     */
452
+    public function undoAll(): void {
453
+        // Remember the current cachebuster value, as we do not want to reset this value
454
+        // Otherwise this can lead to caching issues as the value might be known to a browser already
455
+        $cacheBusterKey = $this->appConfig->getAppValueInt(ConfigLexicon::CACHE_BUSTER);
456
+        $this->appConfig->deleteAppValues();
457
+        $this->appConfig->setAppValueInt(ConfigLexicon::CACHE_BUSTER, $cacheBusterKey);
458
+        $this->increaseCacheBuster();
459
+    }
460
+
461
+    /**
462
+     * Revert admin settings to the default value
463
+     *
464
+     * @param string $setting setting which should be reverted
465
+     * @return string default value
466
+     */
467
+    public function undo($setting): string {
468
+        $this->appConfig->deleteAppValue($setting);
469
+        $this->increaseCacheBuster();
470
+
471
+        $returnValue = '';
472
+        switch ($setting) {
473
+            case 'name':
474
+                $returnValue = $this->getEntity();
475
+                break;
476
+            case 'url':
477
+                $returnValue = $this->getBaseUrl();
478
+                break;
479
+            case 'slogan':
480
+                $returnValue = $this->getSlogan();
481
+                break;
482
+            case 'primary_color':
483
+                $returnValue = BackgroundService::DEFAULT_COLOR;
484
+                break;
485
+            case 'background_color':
486
+                // If a background image is set we revert to the mean image color
487
+                if ($this->imageManager->hasImage('background')) {
488
+                    $file = $this->imageManager->getImage('background');
489
+                    $returnValue = $this->backgroundService->setGlobalBackground($file->read()) ?? '';
490
+                }
491
+                break;
492
+            case 'logo':
493
+            case 'logoheader':
494
+            case 'background':
495
+            case 'favicon':
496
+                $this->imageManager->delete($setting);
497
+                $this->appConfig->deleteAppValue($setting . 'Mime');
498
+                break;
499
+        }
500
+
501
+        return $returnValue;
502
+    }
503
+
504
+    /**
505
+     * Color of text in the header menu
506
+     *
507
+     * @return string
508
+     */
509
+    public function getTextColorBackground() {
510
+        return $this->util->invertTextColor($this->getColorBackground()) ? '#000000' : '#ffffff';
511
+    }
512
+
513
+    /**
514
+     * Color of text on primary buttons and other elements
515
+     *
516
+     * @return string
517
+     */
518
+    public function getTextColorPrimary() {
519
+        return $this->util->invertTextColor($this->getColorPrimary()) ? '#000000' : '#ffffff';
520
+    }
521
+
522
+    /**
523
+     * Color of text in the header and primary buttons
524
+     *
525
+     * @return string
526
+     */
527
+    public function getDefaultTextColorPrimary() {
528
+        return $this->util->invertTextColor($this->getDefaultColorPrimary()) ? '#000000' : '#ffffff';
529
+    }
530
+
531
+    /**
532
+     * Has the admin disabled user customization
533
+     */
534
+    public function isUserThemingDisabled(): bool {
535
+        return $this->appConfig->getAppValueBool(ConfigLexicon::USER_THEMING_DISABLED, false);
536
+    }
537 537
 }
Please login to merge, or discard this patch.