Completed
Push — master ( b17745...0bdabc )
by
unknown
49:10 queued 18:45
created
apps/theming/lib/ThemingDefaults.php 1 patch
Indentation   +503 added lines, -503 removed lines patch added patch discarded remove patch
@@ -22,507 +22,507 @@
 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 IConfig $config,
44
-		private IAppConfig $appConfig,
45
-		private IL10N $l,
46
-		private IUserSession $userSession,
47
-		private IURLGenerator $urlGenerator,
48
-		private ICacheFactory $cacheFactory,
49
-		private Util $util,
50
-		private ImageManager $imageManager,
51
-		private IAppManager $appManager,
52
-		private INavigationManager $navigationManager,
53
-		private 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->config->getAppValue('theming', 'name', $this->name));
73
-	}
74
-
75
-	public function getHTMLName() {
76
-		return $this->config->getAppValue('theming', 'name', $this->name);
77
-	}
78
-
79
-	public function getTitle() {
80
-		return strip_tags($this->config->getAppValue('theming', 'name', $this->title));
81
-	}
82
-
83
-	public function getEntity() {
84
-		return strip_tags($this->config->getAppValue('theming', 'name', $this->entity));
85
-	}
86
-
87
-	public function getProductName() {
88
-		return strip_tags($this->config->getAppValue('theming', 'productName', $this->productName));
89
-	}
90
-
91
-	public function getBaseUrl() {
92
-		return $this->config->getAppValue('theming', '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) {
101
-		return \OCP\Util::sanitizeHTML($this->config->getAppValue('theming', 'slogan', parent::getSlogan($lang)));
102
-	}
103
-
104
-	public function getImprintUrl() {
105
-		return (string)$this->config->getAppValue('theming', 'imprintUrl', '');
106
-	}
107
-
108
-	public function getPrivacyUrl() {
109
-		return (string)$this->config->getAppValue('theming', 'privacyUrl', '');
110
-	}
111
-
112
-	public function getDocBaseUrl() {
113
-		return (string)$this->config->getAppValue('theming', 'docBaseUrl', $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' => (string)$this->getImprintUrl()
136
-			],
137
-			[
138
-				'text' => $this->l->t('Privacy policy'),
139
-				'url' => (string)$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->config->getUserValue($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->config->getUserValue($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->getValueString(Application::APP_ID, '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->getValueString(Application::APP_ID, '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->config->getAppValue('theming', '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 !== false) {
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 = $this->config->getAppValue('theming', 'cachebuster', '0');
274
-
275
-		if (!$logo || !$logoExists) {
276
-			if ($useSvg) {
277
-				$logo = $this->urlGenerator->imagePath('core', 'logo/logo.svg');
278
-			} else {
279
-				$logo = $this->urlGenerator->imagePath('core', 'logo/logo.png');
280
-			}
281
-			return $logo . '?v=' . $cacheBusterCounter;
282
-		}
283
-
284
-		return $this->urlGenerator->linkToRoute('theming.Theming.getImage', [ 'key' => 'logo', 'useSvg' => $useSvg, 'v' => $cacheBusterCounter ]);
285
-	}
286
-
287
-	/**
288
-	 * Themed background image url
289
-	 *
290
-	 * @param bool $darkVariant if the dark variant (if available) of the background should be used
291
-	 * @return string
292
-	 */
293
-	public function getBackground(bool $darkVariant = false): string {
294
-		return $this->imageManager->getImageUrl('background' . ($darkVariant ? 'Dark' : ''));
295
-	}
296
-
297
-	/**
298
-	 * @return string
299
-	 */
300
-	public function getiTunesAppId() {
301
-		return $this->config->getAppValue('theming', 'iTunesAppId', $this->iTunesAppId);
302
-	}
303
-
304
-	/**
305
-	 * @return string
306
-	 */
307
-	public function getiOSClientUrl() {
308
-		return $this->config->getAppValue('theming', 'iOSClientUrl', $this->iOSClientUrl);
309
-	}
310
-
311
-	/**
312
-	 * @return string
313
-	 */
314
-	public function getAndroidClientUrl() {
315
-		return $this->config->getAppValue('theming', 'AndroidClientUrl', $this->AndroidClientUrl);
316
-	}
317
-
318
-	/**
319
-	 * @return string
320
-	 */
321
-	public function getFDroidClientUrl() {
322
-		return $this->config->getAppValue('theming', 'FDroidClientUrl', $this->FDroidClientUrl);
323
-	}
324
-
325
-	/**
326
-	 * @return array scss variables to overwrite
327
-	 * @deprecated since Nextcloud 22 - https://github.com/nextcloud/server/issues/9940
328
-	 */
329
-	public function getScssVariables() {
330
-		$cacheBuster = $this->config->getAppValue('theming', 'cachebuster', '0');
331
-		$cache = $this->cacheFactory->createDistributed('theming-' . $cacheBuster . '-' . $this->urlGenerator->getBaseUrl());
332
-		if ($value = $cache->get('getScssVariables')) {
333
-			return $value;
334
-		}
335
-
336
-		$variables = [
337
-			'theming-cachebuster' => "'" . $cacheBuster . "'",
338
-			'theming-logo-mime' => "'" . $this->config->getAppValue('theming', 'logoMime') . "'",
339
-			'theming-background-mime' => "'" . $this->config->getAppValue('theming', 'backgroundMime') . "'",
340
-			'theming-logoheader-mime' => "'" . $this->config->getAppValue('theming', 'logoheaderMime') . "'",
341
-			'theming-favicon-mime' => "'" . $this->config->getAppValue('theming', 'faviconMime') . "'"
342
-		];
343
-
344
-		$variables['image-logo'] = "url('" . $this->imageManager->getImageUrl('logo') . "')";
345
-		$variables['image-logoheader'] = "url('" . $this->imageManager->getImageUrl('logoheader') . "')";
346
-		$variables['image-favicon'] = "url('" . $this->imageManager->getImageUrl('favicon') . "')";
347
-		$variables['image-login-background'] = "url('" . $this->imageManager->getImageUrl('background') . "')";
348
-		$variables['image-login-plain'] = 'false';
349
-
350
-		if ($this->appConfig->getValueString(Application::APP_ID, 'primary_color', '') !== '') {
351
-			$variables['color-primary'] = $this->getColorPrimary();
352
-			$variables['color-primary-text'] = $this->getTextColorPrimary();
353
-			$variables['color-primary-element'] = $this->util->elementColor($this->getColorPrimary());
354
-		}
355
-
356
-		if ($this->config->getAppValue('theming', 'backgroundMime', '') === 'backgroundColor') {
357
-			$variables['image-login-plain'] = 'true';
358
-		}
359
-
360
-		$variables['has-legal-links'] = 'false';
361
-		if ($this->getImprintUrl() !== '' || $this->getPrivacyUrl() !== '') {
362
-			$variables['has-legal-links'] = 'true';
363
-		}
364
-
365
-		$cache->set('getScssVariables', $variables);
366
-		return $variables;
367
-	}
368
-
369
-	/**
370
-	 * Check if the image should be replaced by the theming app
371
-	 * and return the new image location then
372
-	 *
373
-	 * @param string $app name of the app
374
-	 * @param string $image filename of the image
375
-	 * @return bool|string false if image should not replaced, otherwise the location of the image
376
-	 */
377
-	public function replaceImagePath($app, $image) {
378
-		if ($app === '' || $app === 'files_sharing') {
379
-			$app = 'core';
380
-		}
381
-		$cacheBusterValue = $this->config->getAppValue('theming', 'cachebuster', '0');
382
-
383
-		$route = false;
384
-		if ($image === 'favicon.ico' && ($this->imageManager->shouldReplaceIcons() || $this->getCustomFavicon() !== null)) {
385
-			$route = $this->urlGenerator->linkToRoute('theming.Icon.getFavicon', ['app' => $app]);
386
-		}
387
-		if (($image === 'favicon-touch.png' || $image === 'favicon-fb.png') && ($this->imageManager->shouldReplaceIcons() || $this->getCustomFavicon() !== null)) {
388
-			$route = $this->urlGenerator->linkToRoute('theming.Icon.getTouchIcon', ['app' => $app]);
389
-		}
390
-		if ($image === 'manifest.json') {
391
-			try {
392
-				$appPath = $this->appManager->getAppPath($app);
393
-				if (file_exists($appPath . '/img/manifest.json')) {
394
-					return false;
395
-				}
396
-			} catch (AppPathNotFoundException $e) {
397
-			}
398
-			$route = $this->urlGenerator->linkToRoute('theming.Theming.getManifest', ['app' => $app ]);
399
-		}
400
-		if (str_starts_with($image, 'filetypes/') && file_exists(\OC::$SERVERROOT . '/core/img/' . $image)) {
401
-			$route = $this->urlGenerator->linkToRoute('theming.Icon.getThemedIcon', ['app' => $app, 'image' => $image]);
402
-		}
403
-
404
-		if ($route) {
405
-			return $route . '?v=' . $this->util->getCacheBuster();
406
-		}
407
-
408
-		return false;
409
-	}
410
-
411
-	protected function getCustomFavicon(): ?ISimpleFile {
412
-		try {
413
-			return $this->imageManager->getImage('favicon');
414
-		} catch (NotFoundException $e) {
415
-			return null;
416
-		}
417
-	}
418
-
419
-	/**
420
-	 * Increases the cache buster key
421
-	 */
422
-	public function increaseCacheBuster(): void {
423
-		$cacheBusterKey = (int)$this->config->getAppValue('theming', 'cachebuster', '0');
424
-		$this->config->setAppValue('theming', 'cachebuster', (string)($cacheBusterKey + 1));
425
-		$this->cacheFactory->createDistributed('theming-')->clear();
426
-		$this->cacheFactory->createDistributed('imagePath')->clear();
427
-	}
428
-
429
-	/**
430
-	 * Update setting in the database
431
-	 *
432
-	 * @param string $setting
433
-	 * @param string $value
434
-	 */
435
-	public function set($setting, $value): void {
436
-		$this->appConfig->setValueString('theming', $setting, $value);
437
-		$this->increaseCacheBuster();
438
-	}
439
-
440
-	/**
441
-	 * Revert all settings to the default value
442
-	 */
443
-	public function undoAll(): void {
444
-		// Remember the current cachebuster value, as we do not want to reset this value
445
-		// Otherwise this can lead to caching issues as the value might be known to a browser already
446
-		$cacheBusterKey = $this->config->getAppValue('theming', 'cachebuster', '0');
447
-		$this->config->deleteAppValues('theming');
448
-		$this->config->setAppValue('theming', 'cachebuster', $cacheBusterKey);
449
-		$this->increaseCacheBuster();
450
-	}
451
-
452
-	/**
453
-	 * Revert admin settings to the default value
454
-	 *
455
-	 * @param string $setting setting which should be reverted
456
-	 * @return string default value
457
-	 */
458
-	public function undo($setting): string {
459
-		$this->config->deleteAppValue('theming', $setting);
460
-		$this->increaseCacheBuster();
461
-
462
-		$returnValue = '';
463
-		switch ($setting) {
464
-			case 'name':
465
-				$returnValue = $this->getEntity();
466
-				break;
467
-			case 'url':
468
-				$returnValue = $this->getBaseUrl();
469
-				break;
470
-			case 'slogan':
471
-				$returnValue = $this->getSlogan();
472
-				break;
473
-			case 'primary_color':
474
-				$returnValue = BackgroundService::DEFAULT_COLOR;
475
-				break;
476
-			case 'background_color':
477
-				// If a background image is set we revert to the mean image color
478
-				if ($this->imageManager->hasImage('background')) {
479
-					$file = $this->imageManager->getImage('background');
480
-					$returnValue = $this->backgroundService->setGlobalBackground($file->read()) ?? '';
481
-				}
482
-				break;
483
-			case 'logo':
484
-			case 'logoheader':
485
-			case 'background':
486
-			case 'favicon':
487
-				$this->imageManager->delete($setting);
488
-				$this->config->deleteAppValue('theming', $setting . 'Mime');
489
-				break;
490
-		}
491
-
492
-		return $returnValue;
493
-	}
494
-
495
-	/**
496
-	 * Color of text in the header menu
497
-	 *
498
-	 * @return string
499
-	 */
500
-	public function getTextColorBackground() {
501
-		return $this->util->invertTextColor($this->getColorBackground()) ? '#000000' : '#ffffff';
502
-	}
503
-
504
-	/**
505
-	 * Color of text on primary buttons and other elements
506
-	 *
507
-	 * @return string
508
-	 */
509
-	public function getTextColorPrimary() {
510
-		return $this->util->invertTextColor($this->getColorPrimary()) ? '#000000' : '#ffffff';
511
-	}
512
-
513
-	/**
514
-	 * Color of text in the header and primary buttons
515
-	 *
516
-	 * @return string
517
-	 */
518
-	public function getDefaultTextColorPrimary() {
519
-		return $this->util->invertTextColor($this->getDefaultColorPrimary()) ? '#000000' : '#ffffff';
520
-	}
521
-
522
-	/**
523
-	 * Has the admin disabled user customization
524
-	 */
525
-	public function isUserThemingDisabled(): bool {
526
-		return $this->appConfig->getValueBool(Application::APP_ID, 'disable-user-theming');
527
-	}
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 IConfig $config,
44
+        private IAppConfig $appConfig,
45
+        private IL10N $l,
46
+        private IUserSession $userSession,
47
+        private IURLGenerator $urlGenerator,
48
+        private ICacheFactory $cacheFactory,
49
+        private Util $util,
50
+        private ImageManager $imageManager,
51
+        private IAppManager $appManager,
52
+        private INavigationManager $navigationManager,
53
+        private 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->config->getAppValue('theming', 'name', $this->name));
73
+    }
74
+
75
+    public function getHTMLName() {
76
+        return $this->config->getAppValue('theming', 'name', $this->name);
77
+    }
78
+
79
+    public function getTitle() {
80
+        return strip_tags($this->config->getAppValue('theming', 'name', $this->title));
81
+    }
82
+
83
+    public function getEntity() {
84
+        return strip_tags($this->config->getAppValue('theming', 'name', $this->entity));
85
+    }
86
+
87
+    public function getProductName() {
88
+        return strip_tags($this->config->getAppValue('theming', 'productName', $this->productName));
89
+    }
90
+
91
+    public function getBaseUrl() {
92
+        return $this->config->getAppValue('theming', '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) {
101
+        return \OCP\Util::sanitizeHTML($this->config->getAppValue('theming', 'slogan', parent::getSlogan($lang)));
102
+    }
103
+
104
+    public function getImprintUrl() {
105
+        return (string)$this->config->getAppValue('theming', 'imprintUrl', '');
106
+    }
107
+
108
+    public function getPrivacyUrl() {
109
+        return (string)$this->config->getAppValue('theming', 'privacyUrl', '');
110
+    }
111
+
112
+    public function getDocBaseUrl() {
113
+        return (string)$this->config->getAppValue('theming', 'docBaseUrl', $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' => (string)$this->getImprintUrl()
136
+            ],
137
+            [
138
+                'text' => $this->l->t('Privacy policy'),
139
+                'url' => (string)$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->config->getUserValue($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->config->getUserValue($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->getValueString(Application::APP_ID, '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->getValueString(Application::APP_ID, '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->config->getAppValue('theming', '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 !== false) {
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 = $this->config->getAppValue('theming', 'cachebuster', '0');
274
+
275
+        if (!$logo || !$logoExists) {
276
+            if ($useSvg) {
277
+                $logo = $this->urlGenerator->imagePath('core', 'logo/logo.svg');
278
+            } else {
279
+                $logo = $this->urlGenerator->imagePath('core', 'logo/logo.png');
280
+            }
281
+            return $logo . '?v=' . $cacheBusterCounter;
282
+        }
283
+
284
+        return $this->urlGenerator->linkToRoute('theming.Theming.getImage', [ 'key' => 'logo', 'useSvg' => $useSvg, 'v' => $cacheBusterCounter ]);
285
+    }
286
+
287
+    /**
288
+     * Themed background image url
289
+     *
290
+     * @param bool $darkVariant if the dark variant (if available) of the background should be used
291
+     * @return string
292
+     */
293
+    public function getBackground(bool $darkVariant = false): string {
294
+        return $this->imageManager->getImageUrl('background' . ($darkVariant ? 'Dark' : ''));
295
+    }
296
+
297
+    /**
298
+     * @return string
299
+     */
300
+    public function getiTunesAppId() {
301
+        return $this->config->getAppValue('theming', 'iTunesAppId', $this->iTunesAppId);
302
+    }
303
+
304
+    /**
305
+     * @return string
306
+     */
307
+    public function getiOSClientUrl() {
308
+        return $this->config->getAppValue('theming', 'iOSClientUrl', $this->iOSClientUrl);
309
+    }
310
+
311
+    /**
312
+     * @return string
313
+     */
314
+    public function getAndroidClientUrl() {
315
+        return $this->config->getAppValue('theming', 'AndroidClientUrl', $this->AndroidClientUrl);
316
+    }
317
+
318
+    /**
319
+     * @return string
320
+     */
321
+    public function getFDroidClientUrl() {
322
+        return $this->config->getAppValue('theming', 'FDroidClientUrl', $this->FDroidClientUrl);
323
+    }
324
+
325
+    /**
326
+     * @return array scss variables to overwrite
327
+     * @deprecated since Nextcloud 22 - https://github.com/nextcloud/server/issues/9940
328
+     */
329
+    public function getScssVariables() {
330
+        $cacheBuster = $this->config->getAppValue('theming', 'cachebuster', '0');
331
+        $cache = $this->cacheFactory->createDistributed('theming-' . $cacheBuster . '-' . $this->urlGenerator->getBaseUrl());
332
+        if ($value = $cache->get('getScssVariables')) {
333
+            return $value;
334
+        }
335
+
336
+        $variables = [
337
+            'theming-cachebuster' => "'" . $cacheBuster . "'",
338
+            'theming-logo-mime' => "'" . $this->config->getAppValue('theming', 'logoMime') . "'",
339
+            'theming-background-mime' => "'" . $this->config->getAppValue('theming', 'backgroundMime') . "'",
340
+            'theming-logoheader-mime' => "'" . $this->config->getAppValue('theming', 'logoheaderMime') . "'",
341
+            'theming-favicon-mime' => "'" . $this->config->getAppValue('theming', 'faviconMime') . "'"
342
+        ];
343
+
344
+        $variables['image-logo'] = "url('" . $this->imageManager->getImageUrl('logo') . "')";
345
+        $variables['image-logoheader'] = "url('" . $this->imageManager->getImageUrl('logoheader') . "')";
346
+        $variables['image-favicon'] = "url('" . $this->imageManager->getImageUrl('favicon') . "')";
347
+        $variables['image-login-background'] = "url('" . $this->imageManager->getImageUrl('background') . "')";
348
+        $variables['image-login-plain'] = 'false';
349
+
350
+        if ($this->appConfig->getValueString(Application::APP_ID, 'primary_color', '') !== '') {
351
+            $variables['color-primary'] = $this->getColorPrimary();
352
+            $variables['color-primary-text'] = $this->getTextColorPrimary();
353
+            $variables['color-primary-element'] = $this->util->elementColor($this->getColorPrimary());
354
+        }
355
+
356
+        if ($this->config->getAppValue('theming', 'backgroundMime', '') === 'backgroundColor') {
357
+            $variables['image-login-plain'] = 'true';
358
+        }
359
+
360
+        $variables['has-legal-links'] = 'false';
361
+        if ($this->getImprintUrl() !== '' || $this->getPrivacyUrl() !== '') {
362
+            $variables['has-legal-links'] = 'true';
363
+        }
364
+
365
+        $cache->set('getScssVariables', $variables);
366
+        return $variables;
367
+    }
368
+
369
+    /**
370
+     * Check if the image should be replaced by the theming app
371
+     * and return the new image location then
372
+     *
373
+     * @param string $app name of the app
374
+     * @param string $image filename of the image
375
+     * @return bool|string false if image should not replaced, otherwise the location of the image
376
+     */
377
+    public function replaceImagePath($app, $image) {
378
+        if ($app === '' || $app === 'files_sharing') {
379
+            $app = 'core';
380
+        }
381
+        $cacheBusterValue = $this->config->getAppValue('theming', 'cachebuster', '0');
382
+
383
+        $route = false;
384
+        if ($image === 'favicon.ico' && ($this->imageManager->shouldReplaceIcons() || $this->getCustomFavicon() !== null)) {
385
+            $route = $this->urlGenerator->linkToRoute('theming.Icon.getFavicon', ['app' => $app]);
386
+        }
387
+        if (($image === 'favicon-touch.png' || $image === 'favicon-fb.png') && ($this->imageManager->shouldReplaceIcons() || $this->getCustomFavicon() !== null)) {
388
+            $route = $this->urlGenerator->linkToRoute('theming.Icon.getTouchIcon', ['app' => $app]);
389
+        }
390
+        if ($image === 'manifest.json') {
391
+            try {
392
+                $appPath = $this->appManager->getAppPath($app);
393
+                if (file_exists($appPath . '/img/manifest.json')) {
394
+                    return false;
395
+                }
396
+            } catch (AppPathNotFoundException $e) {
397
+            }
398
+            $route = $this->urlGenerator->linkToRoute('theming.Theming.getManifest', ['app' => $app ]);
399
+        }
400
+        if (str_starts_with($image, 'filetypes/') && file_exists(\OC::$SERVERROOT . '/core/img/' . $image)) {
401
+            $route = $this->urlGenerator->linkToRoute('theming.Icon.getThemedIcon', ['app' => $app, 'image' => $image]);
402
+        }
403
+
404
+        if ($route) {
405
+            return $route . '?v=' . $this->util->getCacheBuster();
406
+        }
407
+
408
+        return false;
409
+    }
410
+
411
+    protected function getCustomFavicon(): ?ISimpleFile {
412
+        try {
413
+            return $this->imageManager->getImage('favicon');
414
+        } catch (NotFoundException $e) {
415
+            return null;
416
+        }
417
+    }
418
+
419
+    /**
420
+     * Increases the cache buster key
421
+     */
422
+    public function increaseCacheBuster(): void {
423
+        $cacheBusterKey = (int)$this->config->getAppValue('theming', 'cachebuster', '0');
424
+        $this->config->setAppValue('theming', 'cachebuster', (string)($cacheBusterKey + 1));
425
+        $this->cacheFactory->createDistributed('theming-')->clear();
426
+        $this->cacheFactory->createDistributed('imagePath')->clear();
427
+    }
428
+
429
+    /**
430
+     * Update setting in the database
431
+     *
432
+     * @param string $setting
433
+     * @param string $value
434
+     */
435
+    public function set($setting, $value): void {
436
+        $this->appConfig->setValueString('theming', $setting, $value);
437
+        $this->increaseCacheBuster();
438
+    }
439
+
440
+    /**
441
+     * Revert all settings to the default value
442
+     */
443
+    public function undoAll(): void {
444
+        // Remember the current cachebuster value, as we do not want to reset this value
445
+        // Otherwise this can lead to caching issues as the value might be known to a browser already
446
+        $cacheBusterKey = $this->config->getAppValue('theming', 'cachebuster', '0');
447
+        $this->config->deleteAppValues('theming');
448
+        $this->config->setAppValue('theming', 'cachebuster', $cacheBusterKey);
449
+        $this->increaseCacheBuster();
450
+    }
451
+
452
+    /**
453
+     * Revert admin settings to the default value
454
+     *
455
+     * @param string $setting setting which should be reverted
456
+     * @return string default value
457
+     */
458
+    public function undo($setting): string {
459
+        $this->config->deleteAppValue('theming', $setting);
460
+        $this->increaseCacheBuster();
461
+
462
+        $returnValue = '';
463
+        switch ($setting) {
464
+            case 'name':
465
+                $returnValue = $this->getEntity();
466
+                break;
467
+            case 'url':
468
+                $returnValue = $this->getBaseUrl();
469
+                break;
470
+            case 'slogan':
471
+                $returnValue = $this->getSlogan();
472
+                break;
473
+            case 'primary_color':
474
+                $returnValue = BackgroundService::DEFAULT_COLOR;
475
+                break;
476
+            case 'background_color':
477
+                // If a background image is set we revert to the mean image color
478
+                if ($this->imageManager->hasImage('background')) {
479
+                    $file = $this->imageManager->getImage('background');
480
+                    $returnValue = $this->backgroundService->setGlobalBackground($file->read()) ?? '';
481
+                }
482
+                break;
483
+            case 'logo':
484
+            case 'logoheader':
485
+            case 'background':
486
+            case 'favicon':
487
+                $this->imageManager->delete($setting);
488
+                $this->config->deleteAppValue('theming', $setting . 'Mime');
489
+                break;
490
+        }
491
+
492
+        return $returnValue;
493
+    }
494
+
495
+    /**
496
+     * Color of text in the header menu
497
+     *
498
+     * @return string
499
+     */
500
+    public function getTextColorBackground() {
501
+        return $this->util->invertTextColor($this->getColorBackground()) ? '#000000' : '#ffffff';
502
+    }
503
+
504
+    /**
505
+     * Color of text on primary buttons and other elements
506
+     *
507
+     * @return string
508
+     */
509
+    public function getTextColorPrimary() {
510
+        return $this->util->invertTextColor($this->getColorPrimary()) ? '#000000' : '#ffffff';
511
+    }
512
+
513
+    /**
514
+     * Color of text in the header and primary buttons
515
+     *
516
+     * @return string
517
+     */
518
+    public function getDefaultTextColorPrimary() {
519
+        return $this->util->invertTextColor($this->getDefaultColorPrimary()) ? '#000000' : '#ffffff';
520
+    }
521
+
522
+    /**
523
+     * Has the admin disabled user customization
524
+     */
525
+    public function isUserThemingDisabled(): bool {
526
+        return $this->appConfig->getValueBool(Application::APP_ID, 'disable-user-theming');
527
+    }
528 528
 }
Please login to merge, or discard this patch.
apps/theming/tests/ThemingDefaultsTest.php 2 patches
Indentation   +781 added lines, -781 removed lines patch added patch discarded remove patch
@@ -26,785 +26,785 @@
 block discarded – undo
26 26
 use Test\TestCase;
27 27
 
28 28
 class ThemingDefaultsTest extends TestCase {
29
-	private IAppConfig&MockObject $appConfig;
30
-	private IConfig&MockObject $config;
31
-	private \OC_Defaults $defaults;
32
-	private IL10N|MockObject $l10n;
33
-	private IUserSession&MockObject $userSession;
34
-	private IURLGenerator&MockObject $urlGenerator;
35
-	private ICacheFactory&MockObject $cacheFactory;
36
-	private Util&MockObject $util;
37
-	private ICache&MockObject $cache;
38
-	private IAppManager&MockObject $appManager;
39
-	private ImageManager&MockObject $imageManager;
40
-	private INavigationManager&MockObject $navigationManager;
41
-	private BackgroundService&MockObject $backgroundService;
42
-	private ThemingDefaults $template;
43
-
44
-	protected function setUp(): void {
45
-		parent::setUp();
46
-		$this->appConfig = $this->createMock(IAppConfig::class);
47
-		$this->config = $this->createMock(IConfig::class);
48
-		$this->l10n = $this->createMock(IL10N::class);
49
-		$this->userSession = $this->createMock(IUserSession::class);
50
-		$this->urlGenerator = $this->createMock(IURLGenerator::class);
51
-		$this->cacheFactory = $this->createMock(ICacheFactory::class);
52
-		$this->cache = $this->createMock(ICache::class);
53
-		$this->util = $this->createMock(Util::class);
54
-		$this->imageManager = $this->createMock(ImageManager::class);
55
-		$this->appManager = $this->createMock(IAppManager::class);
56
-		$this->navigationManager = $this->createMock(INavigationManager::class);
57
-		$this->backgroundService = $this->createMock(BackgroundService::class);
58
-		$this->defaults = new \OC_Defaults();
59
-		$this->urlGenerator
60
-			->expects($this->any())
61
-			->method('getBaseUrl')
62
-			->willReturn('');
63
-		$this->template = new ThemingDefaults(
64
-			$this->config,
65
-			$this->appConfig,
66
-			$this->l10n,
67
-			$this->userSession,
68
-			$this->urlGenerator,
69
-			$this->cacheFactory,
70
-			$this->util,
71
-			$this->imageManager,
72
-			$this->appManager,
73
-			$this->navigationManager,
74
-			$this->backgroundService,
75
-		);
76
-	}
77
-
78
-	public function testGetNameWithDefault(): void {
79
-		$this->config
80
-			->expects($this->once())
81
-			->method('getAppValue')
82
-			->with('theming', 'name', 'Nextcloud')
83
-			->willReturn('Nextcloud');
84
-
85
-		$this->assertEquals('Nextcloud', $this->template->getName());
86
-	}
87
-
88
-	public function testGetNameWithCustom(): void {
89
-		$this->config
90
-			->expects($this->once())
91
-			->method('getAppValue')
92
-			->with('theming', 'name', 'Nextcloud')
93
-			->willReturn('MyCustomCloud');
94
-
95
-		$this->assertEquals('MyCustomCloud', $this->template->getName());
96
-	}
97
-
98
-	public function testGetHTMLNameWithDefault(): void {
99
-		$this->config
100
-			->expects($this->once())
101
-			->method('getAppValue')
102
-			->with('theming', 'name', 'Nextcloud')
103
-			->willReturn('Nextcloud');
104
-
105
-		$this->assertEquals('Nextcloud', $this->template->getHTMLName());
106
-	}
107
-
108
-	public function testGetHTMLNameWithCustom(): void {
109
-		$this->config
110
-			->expects($this->once())
111
-			->method('getAppValue')
112
-			->with('theming', 'name', 'Nextcloud')
113
-			->willReturn('MyCustomCloud');
114
-
115
-		$this->assertEquals('MyCustomCloud', $this->template->getHTMLName());
116
-	}
117
-
118
-	public function testGetTitleWithDefault(): void {
119
-		$this->config
120
-			->expects($this->once())
121
-			->method('getAppValue')
122
-			->with('theming', 'name', 'Nextcloud')
123
-			->willReturn('Nextcloud');
124
-
125
-		$this->assertEquals('Nextcloud', $this->template->getTitle());
126
-	}
127
-
128
-	public function testGetTitleWithCustom(): void {
129
-		$this->config
130
-			->expects($this->once())
131
-			->method('getAppValue')
132
-			->with('theming', 'name', 'Nextcloud')
133
-			->willReturn('MyCustomCloud');
134
-
135
-		$this->assertEquals('MyCustomCloud', $this->template->getTitle());
136
-	}
137
-
138
-
139
-	public function testGetEntityWithDefault(): void {
140
-		$this->config
141
-			->expects($this->once())
142
-			->method('getAppValue')
143
-			->with('theming', 'name', 'Nextcloud')
144
-			->willReturn('Nextcloud');
145
-
146
-		$this->assertEquals('Nextcloud', $this->template->getEntity());
147
-	}
148
-
149
-	public function testGetEntityWithCustom(): void {
150
-		$this->config
151
-			->expects($this->once())
152
-			->method('getAppValue')
153
-			->with('theming', 'name', 'Nextcloud')
154
-			->willReturn('MyCustomCloud');
155
-
156
-		$this->assertEquals('MyCustomCloud', $this->template->getEntity());
157
-	}
158
-
159
-	public function testGetBaseUrlWithDefault(): void {
160
-		$this->config
161
-			->expects($this->once())
162
-			->method('getAppValue')
163
-			->with('theming', 'url', $this->defaults->getBaseUrl())
164
-			->willReturn($this->defaults->getBaseUrl());
165
-
166
-		$this->assertEquals($this->defaults->getBaseUrl(), $this->template->getBaseUrl());
167
-	}
168
-
169
-	public function testGetBaseUrlWithCustom(): void {
170
-		$this->config
171
-			->expects($this->once())
172
-			->method('getAppValue')
173
-			->with('theming', 'url', $this->defaults->getBaseUrl())
174
-			->willReturn('https://example.com/');
175
-
176
-		$this->assertEquals('https://example.com/', $this->template->getBaseUrl());
177
-	}
178
-
179
-	public static function legalUrlProvider(): array {
180
-		return [
181
-			[''],
182
-			['https://example.com/legal.html'],
183
-		];
184
-	}
185
-
186
-	#[\PHPUnit\Framework\Attributes\DataProvider('legalUrlProvider')]
187
-	public function testGetImprintURL(string $imprintUrl): void {
188
-		$this->config
189
-			->expects($this->once())
190
-			->method('getAppValue')
191
-			->with('theming', 'imprintUrl', '')
192
-			->willReturn($imprintUrl);
193
-
194
-		$this->assertEquals($imprintUrl, $this->template->getImprintUrl());
195
-	}
196
-
197
-	#[\PHPUnit\Framework\Attributes\DataProvider('legalUrlProvider')]
198
-	public function testGetPrivacyURL(string $privacyUrl): void {
199
-		$this->config
200
-			->expects($this->once())
201
-			->method('getAppValue')
202
-			->with('theming', 'privacyUrl', '')
203
-			->willReturn($privacyUrl);
204
-
205
-		$this->assertEquals($privacyUrl, $this->template->getPrivacyUrl());
206
-	}
207
-
208
-	public function testGetSloganWithDefault(): void {
209
-		$this->config
210
-			->expects($this->once())
211
-			->method('getAppValue')
212
-			->with('theming', 'slogan', $this->defaults->getSlogan())
213
-			->willReturn($this->defaults->getSlogan());
214
-
215
-		$this->assertEquals($this->defaults->getSlogan(), $this->template->getSlogan());
216
-	}
217
-
218
-	public function testGetSloganWithCustom(): void {
219
-		$this->config
220
-			->expects($this->once())
221
-			->method('getAppValue')
222
-			->with('theming', 'slogan', $this->defaults->getSlogan())
223
-			->willReturn('My custom Slogan');
224
-
225
-		$this->assertEquals('My custom Slogan', $this->template->getSlogan());
226
-	}
227
-
228
-	public function testGetShortFooter(): void {
229
-		$this->config
230
-			->expects($this->exactly(5))
231
-			->method('getAppValue')
232
-			->willReturnMap([
233
-				['theming', 'url', $this->defaults->getBaseUrl(), 'url'],
234
-				['theming', 'name', 'Nextcloud', 'Name'],
235
-				['theming', 'slogan', $this->defaults->getSlogan(), 'Slogan'],
236
-				['theming', 'imprintUrl', '', ''],
237
-				['theming', 'privacyUrl', '', ''],
238
-			]);
239
-
240
-		$this->assertEquals('<a href="url" target="_blank" rel="noreferrer noopener" class="entity-name">Name</a> – Slogan', $this->template->getShortFooter());
241
-	}
242
-
243
-	public function testGetShortFooterEmptyUrl(): void {
244
-		$this->navigationManager->expects($this->once())->method('getAll')->with(INavigationManager::TYPE_GUEST)->willReturn([]);
245
-		$this->config
246
-			->expects($this->exactly(5))
247
-			->method('getAppValue')
248
-			->willReturnMap([
249
-				['theming', 'url', $this->defaults->getBaseUrl(), ''],
250
-				['theming', 'name', 'Nextcloud', 'Name'],
251
-				['theming', 'slogan', $this->defaults->getSlogan(), 'Slogan'],
252
-				['theming', 'imprintUrl', '', ''],
253
-				['theming', 'privacyUrl', '', ''],
254
-			]);
255
-
256
-		$this->assertEquals('<span class="entity-name">Name</span> – Slogan', $this->template->getShortFooter());
257
-	}
258
-
259
-	public function testGetShortFooterEmptySlogan(): void {
260
-		$this->navigationManager->expects($this->once())->method('getAll')->with(INavigationManager::TYPE_GUEST)->willReturn([]);
261
-		$this->config
262
-			->expects($this->exactly(5))
263
-			->method('getAppValue')
264
-			->willReturnMap([
265
-				['theming', 'url', $this->defaults->getBaseUrl(), 'url'],
266
-				['theming', 'name', 'Nextcloud', 'Name'],
267
-				['theming', 'slogan', $this->defaults->getSlogan(), ''],
268
-				['theming', 'imprintUrl', '', ''],
269
-				['theming', 'privacyUrl', '', ''],
270
-			]);
271
-
272
-		$this->assertEquals('<a href="url" target="_blank" rel="noreferrer noopener" class="entity-name">Name</a>', $this->template->getShortFooter());
273
-	}
274
-
275
-	public function testGetShortFooterImprint(): void {
276
-		$this->navigationManager->expects($this->once())->method('getAll')->with(INavigationManager::TYPE_GUEST)->willReturn([]);
277
-		$this->config
278
-			->expects($this->exactly(5))
279
-			->method('getAppValue')
280
-			->willReturnMap([
281
-				['theming', 'url', $this->defaults->getBaseUrl(), 'url'],
282
-				['theming', 'name', 'Nextcloud', 'Name'],
283
-				['theming', 'slogan', $this->defaults->getSlogan(), 'Slogan'],
284
-				['theming', 'imprintUrl', '', 'https://example.com/imprint'],
285
-				['theming', 'privacyUrl', '', ''],
286
-			]);
287
-
288
-		$this->l10n
289
-			->expects($this->any())
290
-			->method('t')
291
-			->willReturnArgument(0);
292
-
293
-		$this->assertEquals('<a href="url" target="_blank" rel="noreferrer noopener" class="entity-name">Name</a> – Slogan<br/><span class="footer__legal-links"><a href="https://example.com/imprint" class="legal" target="_blank" rel="noreferrer noopener">Legal notice</a></span>', $this->template->getShortFooter());
294
-	}
295
-
296
-	public function testGetShortFooterPrivacy(): void {
297
-		$this->navigationManager->expects($this->once())->method('getAll')->with(INavigationManager::TYPE_GUEST)->willReturn([]);
298
-		$this->config
299
-			->expects($this->exactly(5))
300
-			->method('getAppValue')
301
-			->willReturnMap([
302
-				['theming', 'url', $this->defaults->getBaseUrl(), 'url'],
303
-				['theming', 'name', 'Nextcloud', 'Name'],
304
-				['theming', 'slogan', $this->defaults->getSlogan(), 'Slogan'],
305
-				['theming', 'imprintUrl', '', ''],
306
-				['theming', 'privacyUrl', '', 'https://example.com/privacy'],
307
-			]);
308
-
309
-		$this->l10n
310
-			->expects($this->any())
311
-			->method('t')
312
-			->willReturnArgument(0);
313
-
314
-		$this->assertEquals('<a href="url" target="_blank" rel="noreferrer noopener" class="entity-name">Name</a> – Slogan<br/><span class="footer__legal-links"><a href="https://example.com/privacy" class="legal" target="_blank" rel="noreferrer noopener">Privacy policy</a></span>', $this->template->getShortFooter());
315
-	}
316
-
317
-	public function testGetShortFooterAllLegalLinks(): void {
318
-		$this->navigationManager->expects($this->once())->method('getAll')->with(INavigationManager::TYPE_GUEST)->willReturn([]);
319
-		$this->config
320
-			->expects($this->exactly(5))
321
-			->method('getAppValue')
322
-			->willReturnMap([
323
-				['theming', 'url', $this->defaults->getBaseUrl(), 'url'],
324
-				['theming', 'name', 'Nextcloud', 'Name'],
325
-				['theming', 'slogan', $this->defaults->getSlogan(), 'Slogan'],
326
-				['theming', 'imprintUrl', '', 'https://example.com/imprint'],
327
-				['theming', 'privacyUrl', '', 'https://example.com/privacy'],
328
-			]);
329
-
330
-		$this->l10n
331
-			->expects($this->any())
332
-			->method('t')
333
-			->willReturnArgument(0);
334
-
335
-		$this->assertEquals('<a href="url" target="_blank" rel="noreferrer noopener" class="entity-name">Name</a> – Slogan<br/><span class="footer__legal-links"><a href="https://example.com/imprint" class="legal" target="_blank" rel="noreferrer noopener">Legal notice</a> · <a href="https://example.com/privacy" class="legal" target="_blank" rel="noreferrer noopener">Privacy policy</a></span>', $this->template->getShortFooter());
336
-	}
337
-
338
-	public static function invalidLegalUrlProvider(): array {
339
-		return [
340
-			['example.com/legal'],  # missing scheme
341
-			['https:///legal'],     # missing host
342
-		];
343
-	}
344
-
345
-	#[\PHPUnit\Framework\Attributes\DataProvider('invalidLegalUrlProvider')]
346
-	public function testGetShortFooterInvalidImprint(string $invalidImprintUrl): void {
347
-		$this->navigationManager->expects($this->once())->method('getAll')->with(INavigationManager::TYPE_GUEST)->willReturn([]);
348
-		$this->config
349
-			->expects($this->exactly(5))
350
-			->method('getAppValue')
351
-			->willReturnMap([
352
-				['theming', 'url', $this->defaults->getBaseUrl(), 'url'],
353
-				['theming', 'name', 'Nextcloud', 'Name'],
354
-				['theming', 'slogan', $this->defaults->getSlogan(), 'Slogan'],
355
-				['theming', 'imprintUrl', '', $invalidImprintUrl],
356
-				['theming', 'privacyUrl', '', ''],
357
-			]);
358
-
359
-		$this->assertEquals('<a href="url" target="_blank" rel="noreferrer noopener" class="entity-name">Name</a> – Slogan', $this->template->getShortFooter());
360
-	}
361
-
362
-	#[\PHPUnit\Framework\Attributes\DataProvider('invalidLegalUrlProvider')]
363
-	public function testGetShortFooterInvalidPrivacy(string $invalidPrivacyUrl): void {
364
-		$this->navigationManager->expects($this->once())->method('getAll')->with(INavigationManager::TYPE_GUEST)->willReturn([]);
365
-		$this->config
366
-			->expects($this->exactly(5))
367
-			->method('getAppValue')
368
-			->willReturnMap([
369
-				['theming', 'url', $this->defaults->getBaseUrl(), 'url'],
370
-				['theming', 'name', 'Nextcloud', 'Name'],
371
-				['theming', 'slogan', $this->defaults->getSlogan(), 'Slogan'],
372
-				['theming', 'imprintUrl', '', ''],
373
-				['theming', 'privacyUrl', '', $invalidPrivacyUrl],
374
-			]);
375
-
376
-		$this->assertEquals('<a href="url" target="_blank" rel="noreferrer noopener" class="entity-name">Name</a> – Slogan', $this->template->getShortFooter());
377
-	}
378
-
379
-	public function testGetColorPrimaryWithDefault(): void {
380
-		$this->appConfig
381
-			->expects(self::once())
382
-			->method('getValueBool')
383
-			->with('theming', 'disable-user-theming')
384
-			->willReturn(false);
385
-		$this->appConfig
386
-			->expects(self::once())
387
-			->method('getValueString')
388
-			->with('theming', 'primary_color', '')
389
-			->willReturn($this->defaults->getColorPrimary());
390
-
391
-		$this->assertEquals($this->defaults->getColorPrimary(), $this->template->getColorPrimary());
392
-	}
393
-
394
-	public function testGetColorPrimaryWithCustom(): void {
395
-		$this->appConfig
396
-			->expects(self::once())
397
-			->method('getValueBool')
398
-			->with('theming', 'disable-user-theming')
399
-			->willReturn(false);
400
-		$this->appConfig
401
-			->expects(self::once())
402
-			->method('getValueString')
403
-			->with('theming', 'primary_color', '')
404
-			->willReturn('#fff');
405
-
406
-		$this->assertEquals('#fff', $this->template->getColorPrimary());
407
-	}
408
-
409
-	public static function dataGetColorPrimary(): array {
410
-		return [
411
-			'with fallback default' => [
412
-				'disableTheming' => false,
413
-				'primaryColor' => '',
414
-				'userPrimaryColor' => '',
415
-				'expected' => BackgroundService::DEFAULT_COLOR,
416
-			],
417
-			'with custom admin primary' => [
418
-				'disableTheming' => false,
419
-				'primaryColor' => '#aaa',
420
-				'userPrimaryColor' => '',
421
-				'expected' => '#aaa',
422
-			],
423
-			'with custom invalid admin primary' => [
424
-				'disableTheming' => false,
425
-				'primaryColor' => 'invalid',
426
-				'userPrimaryColor' => '',
427
-				'expected' => BackgroundService::DEFAULT_COLOR,
428
-			],
429
-			'with custom invalid user primary' => [
430
-				'disableTheming' => false,
431
-				'primaryColor' => '',
432
-				'userPrimaryColor' => 'invalid-name',
433
-				'expected' => BackgroundService::DEFAULT_COLOR,
434
-			],
435
-			'with custom user primary' => [
436
-				'disableTheming' => false,
437
-				'primaryColor' => '',
438
-				'userPrimaryColor' => '#bbb',
439
-				'expected' => '#bbb',
440
-			],
441
-			'with disabled user theming primary' => [
442
-				'disableTheming' => true,
443
-				'primaryColor' => '#aaa',
444
-				'userPrimaryColor' => '#bbb',
445
-				'expected' => '#aaa',
446
-			],
447
-		];
448
-	}
449
-
450
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataGetColorPrimary')]
451
-	public function testGetColorPrimary(bool $disableTheming, string $primaryColor, string $userPrimaryColor, string $expected): void {
452
-		$user = $this->createMock(IUser::class);
453
-		$this->userSession->expects($this->any())
454
-			->method('getUser')
455
-			->willReturn($user);
456
-		$user->expects($this->any())
457
-			->method('getUID')
458
-			->willReturn('user');
459
-		$this->appConfig
460
-			->expects(self::any())
461
-			->method('getValueBool')
462
-			->with('theming', 'disable-user-theming')
463
-			->willReturn($disableTheming);
464
-		$this->appConfig
465
-			->expects(self::any())
466
-			->method('getValueString')
467
-			->with('theming', 'primary_color', '')
468
-			->willReturn($primaryColor);
469
-		$this->config
470
-			->expects($this->any())
471
-			->method('getUserValue')
472
-			->with('user', 'theming', 'primary_color', '')
473
-			->willReturn($userPrimaryColor);
474
-
475
-		$this->assertEquals($expected, $this->template->getColorPrimary());
476
-	}
477
-
478
-	public function testSet(): void {
479
-		$this->config
480
-			->expects($this->once())
481
-			->method('setAppValue')
482
-			->with('theming', 'cachebuster', 16);
483
-		$this->appConfig
484
-			->expects($this->once())
485
-			->method('setValueString')
486
-			->with('theming', 'MySetting', 'MyValue');
487
-		$this->config
488
-			->expects($this->once())
489
-			->method('getAppValue')
490
-			->with('theming', 'cachebuster', '0')
491
-			->willReturn('15');
492
-		$this->cacheFactory
493
-			->expects($this->exactly(2))
494
-			->method('createDistributed')
495
-			->willReturnMap([
496
-				['theming-', $this->cache],
497
-				['imagePath', $this->cache],
498
-			]);
499
-		$this->cache
500
-			->expects($this->any())
501
-			->method('clear')
502
-			->with('');
503
-		$this->template->set('MySetting', 'MyValue');
504
-	}
505
-
506
-	public function testUndoName(): void {
507
-		$this->config
508
-			->expects($this->once())
509
-			->method('deleteAppValue')
510
-			->with('theming', 'name');
511
-		$this->config
512
-			->expects($this->exactly(2))
513
-			->method('getAppValue')
514
-			->willReturnMap([
515
-				['theming', 'cachebuster', '0', '15'],
516
-				['theming', 'name', 'Nextcloud', 'Nextcloud'],
517
-			]);
518
-		$this->config
519
-			->expects($this->once())
520
-			->method('setAppValue')
521
-			->with('theming', 'cachebuster', 16);
522
-
523
-		$this->assertSame('Nextcloud', $this->template->undo('name'));
524
-	}
525
-
526
-	public function testUndoBaseUrl(): void {
527
-		$this->config
528
-			->expects($this->once())
529
-			->method('deleteAppValue')
530
-			->with('theming', 'url');
531
-		$this->config
532
-			->expects($this->exactly(2))
533
-			->method('getAppValue')
534
-			->willReturnMap([
535
-				['theming', 'cachebuster', '0', '15'],
536
-				['theming', 'url', $this->defaults->getBaseUrl(), $this->defaults->getBaseUrl()],
537
-			]);
538
-		$this->config
539
-			->expects($this->once())
540
-			->method('setAppValue')
541
-			->with('theming', 'cachebuster', 16);
542
-
543
-		$this->assertSame($this->defaults->getBaseUrl(), $this->template->undo('url'));
544
-	}
545
-
546
-	public function testUndoSlogan(): void {
547
-		$this->config
548
-			->expects($this->once())
549
-			->method('deleteAppValue')
550
-			->with('theming', 'slogan');
551
-		$this->config
552
-			->expects($this->exactly(2))
553
-			->method('getAppValue')
554
-			->willReturnMap([
555
-				['theming', 'cachebuster', '0', '15'],
556
-				['theming', 'slogan', $this->defaults->getSlogan(), $this->defaults->getSlogan()],
557
-			]);
558
-		$this->config
559
-			->expects($this->once())
560
-			->method('setAppValue')
561
-			->with('theming', 'cachebuster', 16);
562
-
563
-		$this->assertSame($this->defaults->getSlogan(), $this->template->undo('slogan'));
564
-	}
565
-
566
-	public function testUndoPrimaryColor(): void {
567
-		$this->config
568
-			->expects($this->once())
569
-			->method('deleteAppValue')
570
-			->with('theming', 'primary_color');
571
-		$this->config
572
-			->expects($this->once())
573
-			->method('getAppValue')
574
-			->with('theming', 'cachebuster', '0')
575
-			->willReturn('15');
576
-		$this->config
577
-			->expects($this->once())
578
-			->method('setAppValue')
579
-			->with('theming', 'cachebuster', 16);
580
-
581
-		$this->assertSame($this->defaults->getColorPrimary(), $this->template->undo('primary_color'));
582
-	}
583
-
584
-	public function testUndoDefaultAction(): void {
585
-		$this->config
586
-			->expects($this->once())
587
-			->method('deleteAppValue')
588
-			->with('theming', 'defaultitem');
589
-		$this->config
590
-			->expects($this->once())
591
-			->method('getAppValue')
592
-			->with('theming', 'cachebuster', '0')
593
-			->willReturn('15');
594
-		$this->config
595
-			->expects($this->once())
596
-			->method('setAppValue')
597
-			->with('theming', 'cachebuster', 16);
598
-
599
-		$this->assertSame('', $this->template->undo('defaultitem'));
600
-	}
601
-
602
-	public function testGetBackground(): void {
603
-		$this->imageManager
604
-			->expects($this->once())
605
-			->method('getImageUrl')
606
-			->with('background')
607
-			->willReturn('custom-background?v=0');
608
-		$this->assertEquals('custom-background?v=0', $this->template->getBackground());
609
-	}
610
-
611
-	private function getLogoHelper($withName, $useSvg) {
612
-		$this->imageManager->expects($this->any())
613
-			->method('getImage')
614
-			->with('logo')
615
-			->willThrowException(new NotFoundException());
616
-		$this->config
617
-			->expects($this->exactly(2))
618
-			->method('getAppValue')
619
-			->willReturnMap([
620
-				['theming', 'logoMime', '', ''],
621
-				['theming', 'cachebuster', '0', '0'],
622
-			]);
623
-		$this->urlGenerator->expects($this->once())
624
-			->method('imagePath')
625
-			->with('core', $withName)
626
-			->willReturn('core-logo');
627
-		$this->assertEquals('core-logo?v=0', $this->template->getLogo($useSvg));
628
-	}
629
-
630
-	public function testGetLogoDefaultWithSvg(): void {
631
-		$this->getLogoHelper('logo/logo.svg', true);
632
-	}
633
-
634
-	public function testGetLogoDefaultWithoutSvg(): void {
635
-		$this->getLogoHelper('logo/logo.png', false);
636
-	}
637
-
638
-	public function testGetLogoCustom(): void {
639
-		$this->config
640
-			->expects($this->exactly(2))
641
-			->method('getAppValue')
642
-			->willReturnMap([
643
-				['theming', 'logoMime', '', 'image/svg+xml'],
644
-				['theming', 'cachebuster', '0', '0'],
645
-			]);
646
-		$this->urlGenerator->expects($this->once())
647
-			->method('linkToRoute')
648
-			->with('theming.Theming.getImage')
649
-			->willReturn('custom-logo?v=0');
650
-		$this->assertEquals('custom-logo' . '?v=0', $this->template->getLogo());
651
-	}
652
-
653
-	public function testGetScssVariablesCached(): void {
654
-		$this->config->expects($this->any())->method('getAppValue')->with('theming', 'cachebuster', '0')->willReturn('1');
655
-		$this->cacheFactory->expects($this->once())
656
-			->method('createDistributed')
657
-			->with('theming-1-')
658
-			->willReturn($this->cache);
659
-		$this->cache->expects($this->once())->method('get')->with('getScssVariables')->willReturn(['foo' => 'bar']);
660
-		$this->assertEquals(['foo' => 'bar'], $this->template->getScssVariables());
661
-	}
662
-
663
-	public function testGetScssVariables(): void {
664
-		$this->config
665
-			->expects($this->any())
666
-			->method('getAppValue')
667
-			->willReturnMap([
668
-				['theming', 'cachebuster', '0', '0'],
669
-				['theming', 'logoMime', '', 'jpeg'],
670
-				['theming', 'backgroundMime', '', 'jpeg'],
671
-				['theming', 'logoheaderMime', '', 'jpeg'],
672
-				['theming', 'faviconMime', '', 'jpeg'],
673
-			]);
674
-
675
-		$this->appConfig
676
-			->expects(self::atLeastOnce())
677
-			->method('getValueString')
678
-			->willReturnMap([
679
-				['theming', 'primary_color', '', false, $this->defaults->getColorPrimary()],
680
-				['theming', 'primary_color', $this->defaults->getColorPrimary(), false, $this->defaults->getColorPrimary()],
681
-			]);
682
-
683
-		$this->util->expects($this->any())->method('invertTextColor')->with($this->defaults->getColorPrimary())->willReturn(false);
684
-		$this->util->expects($this->any())->method('elementColor')->with($this->defaults->getColorPrimary())->willReturn('#aaaaaa');
685
-		$this->cacheFactory->expects($this->once())
686
-			->method('createDistributed')
687
-			->with('theming-0-')
688
-			->willReturn($this->cache);
689
-		$this->cache->expects($this->once())->method('get')->with('getScssVariables')->willReturn(null);
690
-		$this->imageManager->expects($this->exactly(4))
691
-			->method('getImageUrl')
692
-			->willReturnMap([
693
-				['logo', 'custom-logo?v=0'],
694
-				['logoheader', 'custom-logoheader?v=0'],
695
-				['favicon', 'custom-favicon?v=0'],
696
-				['background', 'custom-background?v=0'],
697
-			]);
698
-
699
-		$expected = [
700
-			'theming-cachebuster' => '\'0\'',
701
-			'theming-logo-mime' => '\'jpeg\'',
702
-			'theming-background-mime' => '\'jpeg\'',
703
-			'image-logo' => "url('custom-logo?v=0')",
704
-			'image-login-background' => "url('custom-background?v=0')",
705
-			'color-primary' => $this->defaults->getColorPrimary(),
706
-			'color-primary-text' => '#ffffff',
707
-			'image-login-plain' => 'false',
708
-			'color-primary-element' => '#aaaaaa',
709
-			'theming-logoheader-mime' => '\'jpeg\'',
710
-			'theming-favicon-mime' => '\'jpeg\'',
711
-			'image-logoheader' => "url('custom-logoheader?v=0')",
712
-			'image-favicon' => "url('custom-favicon?v=0')",
713
-			'has-legal-links' => 'false'
714
-		];
715
-		$this->assertEquals($expected, $this->template->getScssVariables());
716
-	}
717
-
718
-	public function testGetDefaultAndroidURL(): void {
719
-		$this->config
720
-			->expects($this->once())
721
-			->method('getAppValue')
722
-			->with('theming', 'AndroidClientUrl', 'https://play.google.com/store/apps/details?id=com.nextcloud.client')
723
-			->willReturn('https://play.google.com/store/apps/details?id=com.nextcloud.client');
724
-
725
-		$this->assertEquals('https://play.google.com/store/apps/details?id=com.nextcloud.client', $this->template->getAndroidClientUrl());
726
-	}
727
-
728
-	public function testGetCustomAndroidURL(): void {
729
-		$this->config
730
-			->expects($this->once())
731
-			->method('getAppValue')
732
-			->with('theming', 'AndroidClientUrl', 'https://play.google.com/store/apps/details?id=com.nextcloud.client')
733
-			->willReturn('https://play.google.com/store/apps/details?id=com.mycloud.client');
734
-
735
-		$this->assertEquals('https://play.google.com/store/apps/details?id=com.mycloud.client', $this->template->getAndroidClientUrl());
736
-	}
737
-
738
-	public function testGetDefaultiOSURL(): void {
739
-		$this->config
740
-			->expects($this->once())
741
-			->method('getAppValue')
742
-			->with('theming', 'iOSClientUrl', 'https://geo.itunes.apple.com/us/app/nextcloud/id1125420102?mt=8')
743
-			->willReturn('https://geo.itunes.apple.com/us/app/nextcloud/id1125420102?mt=8');
744
-
745
-		$this->assertEquals('https://geo.itunes.apple.com/us/app/nextcloud/id1125420102?mt=8', $this->template->getiOSClientUrl());
746
-	}
747
-
748
-	public function testGetCustomiOSURL(): void {
749
-		$this->config
750
-			->expects($this->once())
751
-			->method('getAppValue')
752
-			->with('theming', 'iOSClientUrl', 'https://geo.itunes.apple.com/us/app/nextcloud/id1125420102?mt=8')
753
-			->willReturn('https://geo.itunes.apple.com/us/app/nextcloud/id1234567890?mt=8');
754
-
755
-		$this->assertEquals('https://geo.itunes.apple.com/us/app/nextcloud/id1234567890?mt=8', $this->template->getiOSClientUrl());
756
-	}
757
-
758
-	public function testGetDefaultiTunesAppId(): void {
759
-		$this->config
760
-			->expects($this->once())
761
-			->method('getAppValue')
762
-			->with('theming', 'iTunesAppId', '1125420102')
763
-			->willReturn('1125420102');
764
-
765
-		$this->assertEquals('1125420102', $this->template->getiTunesAppId());
766
-	}
767
-
768
-	public function testGetCustomiTunesAppId(): void {
769
-		$this->config
770
-			->expects($this->once())
771
-			->method('getAppValue')
772
-			->with('theming', 'iTunesAppId', '1125420102')
773
-			->willReturn('1234567890');
774
-
775
-		$this->assertEquals('1234567890', $this->template->getiTunesAppId());
776
-	}
777
-
778
-	public static function dataReplaceImagePath(): array {
779
-		return [
780
-			['core', 'test.png', false],
781
-			['core', 'manifest.json'],
782
-			['core', 'favicon.ico'],
783
-			['core', 'favicon-touch.png']
784
-		];
785
-	}
786
-
787
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataReplaceImagePath')]
788
-	public function testReplaceImagePath(string $app, string $image, string|bool $result = 'themingRoute?v=1234abcd'): void {
789
-		$this->cache->expects($this->any())
790
-			->method('get')
791
-			->with('shouldReplaceIcons')
792
-			->willReturn(true);
793
-		$this->config
794
-			->expects($this->any())
795
-			->method('getAppValue')
796
-			->with('theming', 'cachebuster', '0')
797
-			->willReturn('0');
798
-		$this->urlGenerator
799
-			->expects($this->any())
800
-			->method('linkToRoute')
801
-			->willReturn('themingRoute');
802
-		if ($result) {
803
-			$this->util
804
-				->expects($this->once())
805
-				->method('getCacheBuster')
806
-				->willReturn('1234abcd');
807
-		}
808
-		$this->assertEquals($result, $this->template->replaceImagePath($app, $image));
809
-	}
29
+    private IAppConfig&MockObject $appConfig;
30
+    private IConfig&MockObject $config;
31
+    private \OC_Defaults $defaults;
32
+    private IL10N|MockObject $l10n;
33
+    private IUserSession&MockObject $userSession;
34
+    private IURLGenerator&MockObject $urlGenerator;
35
+    private ICacheFactory&MockObject $cacheFactory;
36
+    private Util&MockObject $util;
37
+    private ICache&MockObject $cache;
38
+    private IAppManager&MockObject $appManager;
39
+    private ImageManager&MockObject $imageManager;
40
+    private INavigationManager&MockObject $navigationManager;
41
+    private BackgroundService&MockObject $backgroundService;
42
+    private ThemingDefaults $template;
43
+
44
+    protected function setUp(): void {
45
+        parent::setUp();
46
+        $this->appConfig = $this->createMock(IAppConfig::class);
47
+        $this->config = $this->createMock(IConfig::class);
48
+        $this->l10n = $this->createMock(IL10N::class);
49
+        $this->userSession = $this->createMock(IUserSession::class);
50
+        $this->urlGenerator = $this->createMock(IURLGenerator::class);
51
+        $this->cacheFactory = $this->createMock(ICacheFactory::class);
52
+        $this->cache = $this->createMock(ICache::class);
53
+        $this->util = $this->createMock(Util::class);
54
+        $this->imageManager = $this->createMock(ImageManager::class);
55
+        $this->appManager = $this->createMock(IAppManager::class);
56
+        $this->navigationManager = $this->createMock(INavigationManager::class);
57
+        $this->backgroundService = $this->createMock(BackgroundService::class);
58
+        $this->defaults = new \OC_Defaults();
59
+        $this->urlGenerator
60
+            ->expects($this->any())
61
+            ->method('getBaseUrl')
62
+            ->willReturn('');
63
+        $this->template = new ThemingDefaults(
64
+            $this->config,
65
+            $this->appConfig,
66
+            $this->l10n,
67
+            $this->userSession,
68
+            $this->urlGenerator,
69
+            $this->cacheFactory,
70
+            $this->util,
71
+            $this->imageManager,
72
+            $this->appManager,
73
+            $this->navigationManager,
74
+            $this->backgroundService,
75
+        );
76
+    }
77
+
78
+    public function testGetNameWithDefault(): void {
79
+        $this->config
80
+            ->expects($this->once())
81
+            ->method('getAppValue')
82
+            ->with('theming', 'name', 'Nextcloud')
83
+            ->willReturn('Nextcloud');
84
+
85
+        $this->assertEquals('Nextcloud', $this->template->getName());
86
+    }
87
+
88
+    public function testGetNameWithCustom(): void {
89
+        $this->config
90
+            ->expects($this->once())
91
+            ->method('getAppValue')
92
+            ->with('theming', 'name', 'Nextcloud')
93
+            ->willReturn('MyCustomCloud');
94
+
95
+        $this->assertEquals('MyCustomCloud', $this->template->getName());
96
+    }
97
+
98
+    public function testGetHTMLNameWithDefault(): void {
99
+        $this->config
100
+            ->expects($this->once())
101
+            ->method('getAppValue')
102
+            ->with('theming', 'name', 'Nextcloud')
103
+            ->willReturn('Nextcloud');
104
+
105
+        $this->assertEquals('Nextcloud', $this->template->getHTMLName());
106
+    }
107
+
108
+    public function testGetHTMLNameWithCustom(): void {
109
+        $this->config
110
+            ->expects($this->once())
111
+            ->method('getAppValue')
112
+            ->with('theming', 'name', 'Nextcloud')
113
+            ->willReturn('MyCustomCloud');
114
+
115
+        $this->assertEquals('MyCustomCloud', $this->template->getHTMLName());
116
+    }
117
+
118
+    public function testGetTitleWithDefault(): void {
119
+        $this->config
120
+            ->expects($this->once())
121
+            ->method('getAppValue')
122
+            ->with('theming', 'name', 'Nextcloud')
123
+            ->willReturn('Nextcloud');
124
+
125
+        $this->assertEquals('Nextcloud', $this->template->getTitle());
126
+    }
127
+
128
+    public function testGetTitleWithCustom(): void {
129
+        $this->config
130
+            ->expects($this->once())
131
+            ->method('getAppValue')
132
+            ->with('theming', 'name', 'Nextcloud')
133
+            ->willReturn('MyCustomCloud');
134
+
135
+        $this->assertEquals('MyCustomCloud', $this->template->getTitle());
136
+    }
137
+
138
+
139
+    public function testGetEntityWithDefault(): void {
140
+        $this->config
141
+            ->expects($this->once())
142
+            ->method('getAppValue')
143
+            ->with('theming', 'name', 'Nextcloud')
144
+            ->willReturn('Nextcloud');
145
+
146
+        $this->assertEquals('Nextcloud', $this->template->getEntity());
147
+    }
148
+
149
+    public function testGetEntityWithCustom(): void {
150
+        $this->config
151
+            ->expects($this->once())
152
+            ->method('getAppValue')
153
+            ->with('theming', 'name', 'Nextcloud')
154
+            ->willReturn('MyCustomCloud');
155
+
156
+        $this->assertEquals('MyCustomCloud', $this->template->getEntity());
157
+    }
158
+
159
+    public function testGetBaseUrlWithDefault(): void {
160
+        $this->config
161
+            ->expects($this->once())
162
+            ->method('getAppValue')
163
+            ->with('theming', 'url', $this->defaults->getBaseUrl())
164
+            ->willReturn($this->defaults->getBaseUrl());
165
+
166
+        $this->assertEquals($this->defaults->getBaseUrl(), $this->template->getBaseUrl());
167
+    }
168
+
169
+    public function testGetBaseUrlWithCustom(): void {
170
+        $this->config
171
+            ->expects($this->once())
172
+            ->method('getAppValue')
173
+            ->with('theming', 'url', $this->defaults->getBaseUrl())
174
+            ->willReturn('https://example.com/');
175
+
176
+        $this->assertEquals('https://example.com/', $this->template->getBaseUrl());
177
+    }
178
+
179
+    public static function legalUrlProvider(): array {
180
+        return [
181
+            [''],
182
+            ['https://example.com/legal.html'],
183
+        ];
184
+    }
185
+
186
+    #[\PHPUnit\Framework\Attributes\DataProvider('legalUrlProvider')]
187
+    public function testGetImprintURL(string $imprintUrl): void {
188
+        $this->config
189
+            ->expects($this->once())
190
+            ->method('getAppValue')
191
+            ->with('theming', 'imprintUrl', '')
192
+            ->willReturn($imprintUrl);
193
+
194
+        $this->assertEquals($imprintUrl, $this->template->getImprintUrl());
195
+    }
196
+
197
+    #[\PHPUnit\Framework\Attributes\DataProvider('legalUrlProvider')]
198
+    public function testGetPrivacyURL(string $privacyUrl): void {
199
+        $this->config
200
+            ->expects($this->once())
201
+            ->method('getAppValue')
202
+            ->with('theming', 'privacyUrl', '')
203
+            ->willReturn($privacyUrl);
204
+
205
+        $this->assertEquals($privacyUrl, $this->template->getPrivacyUrl());
206
+    }
207
+
208
+    public function testGetSloganWithDefault(): void {
209
+        $this->config
210
+            ->expects($this->once())
211
+            ->method('getAppValue')
212
+            ->with('theming', 'slogan', $this->defaults->getSlogan())
213
+            ->willReturn($this->defaults->getSlogan());
214
+
215
+        $this->assertEquals($this->defaults->getSlogan(), $this->template->getSlogan());
216
+    }
217
+
218
+    public function testGetSloganWithCustom(): void {
219
+        $this->config
220
+            ->expects($this->once())
221
+            ->method('getAppValue')
222
+            ->with('theming', 'slogan', $this->defaults->getSlogan())
223
+            ->willReturn('My custom Slogan');
224
+
225
+        $this->assertEquals('My custom Slogan', $this->template->getSlogan());
226
+    }
227
+
228
+    public function testGetShortFooter(): void {
229
+        $this->config
230
+            ->expects($this->exactly(5))
231
+            ->method('getAppValue')
232
+            ->willReturnMap([
233
+                ['theming', 'url', $this->defaults->getBaseUrl(), 'url'],
234
+                ['theming', 'name', 'Nextcloud', 'Name'],
235
+                ['theming', 'slogan', $this->defaults->getSlogan(), 'Slogan'],
236
+                ['theming', 'imprintUrl', '', ''],
237
+                ['theming', 'privacyUrl', '', ''],
238
+            ]);
239
+
240
+        $this->assertEquals('<a href="url" target="_blank" rel="noreferrer noopener" class="entity-name">Name</a> – Slogan', $this->template->getShortFooter());
241
+    }
242
+
243
+    public function testGetShortFooterEmptyUrl(): void {
244
+        $this->navigationManager->expects($this->once())->method('getAll')->with(INavigationManager::TYPE_GUEST)->willReturn([]);
245
+        $this->config
246
+            ->expects($this->exactly(5))
247
+            ->method('getAppValue')
248
+            ->willReturnMap([
249
+                ['theming', 'url', $this->defaults->getBaseUrl(), ''],
250
+                ['theming', 'name', 'Nextcloud', 'Name'],
251
+                ['theming', 'slogan', $this->defaults->getSlogan(), 'Slogan'],
252
+                ['theming', 'imprintUrl', '', ''],
253
+                ['theming', 'privacyUrl', '', ''],
254
+            ]);
255
+
256
+        $this->assertEquals('<span class="entity-name">Name</span> – Slogan', $this->template->getShortFooter());
257
+    }
258
+
259
+    public function testGetShortFooterEmptySlogan(): void {
260
+        $this->navigationManager->expects($this->once())->method('getAll')->with(INavigationManager::TYPE_GUEST)->willReturn([]);
261
+        $this->config
262
+            ->expects($this->exactly(5))
263
+            ->method('getAppValue')
264
+            ->willReturnMap([
265
+                ['theming', 'url', $this->defaults->getBaseUrl(), 'url'],
266
+                ['theming', 'name', 'Nextcloud', 'Name'],
267
+                ['theming', 'slogan', $this->defaults->getSlogan(), ''],
268
+                ['theming', 'imprintUrl', '', ''],
269
+                ['theming', 'privacyUrl', '', ''],
270
+            ]);
271
+
272
+        $this->assertEquals('<a href="url" target="_blank" rel="noreferrer noopener" class="entity-name">Name</a>', $this->template->getShortFooter());
273
+    }
274
+
275
+    public function testGetShortFooterImprint(): void {
276
+        $this->navigationManager->expects($this->once())->method('getAll')->with(INavigationManager::TYPE_GUEST)->willReturn([]);
277
+        $this->config
278
+            ->expects($this->exactly(5))
279
+            ->method('getAppValue')
280
+            ->willReturnMap([
281
+                ['theming', 'url', $this->defaults->getBaseUrl(), 'url'],
282
+                ['theming', 'name', 'Nextcloud', 'Name'],
283
+                ['theming', 'slogan', $this->defaults->getSlogan(), 'Slogan'],
284
+                ['theming', 'imprintUrl', '', 'https://example.com/imprint'],
285
+                ['theming', 'privacyUrl', '', ''],
286
+            ]);
287
+
288
+        $this->l10n
289
+            ->expects($this->any())
290
+            ->method('t')
291
+            ->willReturnArgument(0);
292
+
293
+        $this->assertEquals('<a href="url" target="_blank" rel="noreferrer noopener" class="entity-name">Name</a> – Slogan<br/><span class="footer__legal-links"><a href="https://example.com/imprint" class="legal" target="_blank" rel="noreferrer noopener">Legal notice</a></span>', $this->template->getShortFooter());
294
+    }
295
+
296
+    public function testGetShortFooterPrivacy(): void {
297
+        $this->navigationManager->expects($this->once())->method('getAll')->with(INavigationManager::TYPE_GUEST)->willReturn([]);
298
+        $this->config
299
+            ->expects($this->exactly(5))
300
+            ->method('getAppValue')
301
+            ->willReturnMap([
302
+                ['theming', 'url', $this->defaults->getBaseUrl(), 'url'],
303
+                ['theming', 'name', 'Nextcloud', 'Name'],
304
+                ['theming', 'slogan', $this->defaults->getSlogan(), 'Slogan'],
305
+                ['theming', 'imprintUrl', '', ''],
306
+                ['theming', 'privacyUrl', '', 'https://example.com/privacy'],
307
+            ]);
308
+
309
+        $this->l10n
310
+            ->expects($this->any())
311
+            ->method('t')
312
+            ->willReturnArgument(0);
313
+
314
+        $this->assertEquals('<a href="url" target="_blank" rel="noreferrer noopener" class="entity-name">Name</a> – Slogan<br/><span class="footer__legal-links"><a href="https://example.com/privacy" class="legal" target="_blank" rel="noreferrer noopener">Privacy policy</a></span>', $this->template->getShortFooter());
315
+    }
316
+
317
+    public function testGetShortFooterAllLegalLinks(): void {
318
+        $this->navigationManager->expects($this->once())->method('getAll')->with(INavigationManager::TYPE_GUEST)->willReturn([]);
319
+        $this->config
320
+            ->expects($this->exactly(5))
321
+            ->method('getAppValue')
322
+            ->willReturnMap([
323
+                ['theming', 'url', $this->defaults->getBaseUrl(), 'url'],
324
+                ['theming', 'name', 'Nextcloud', 'Name'],
325
+                ['theming', 'slogan', $this->defaults->getSlogan(), 'Slogan'],
326
+                ['theming', 'imprintUrl', '', 'https://example.com/imprint'],
327
+                ['theming', 'privacyUrl', '', 'https://example.com/privacy'],
328
+            ]);
329
+
330
+        $this->l10n
331
+            ->expects($this->any())
332
+            ->method('t')
333
+            ->willReturnArgument(0);
334
+
335
+        $this->assertEquals('<a href="url" target="_blank" rel="noreferrer noopener" class="entity-name">Name</a> – Slogan<br/><span class="footer__legal-links"><a href="https://example.com/imprint" class="legal" target="_blank" rel="noreferrer noopener">Legal notice</a> · <a href="https://example.com/privacy" class="legal" target="_blank" rel="noreferrer noopener">Privacy policy</a></span>', $this->template->getShortFooter());
336
+    }
337
+
338
+    public static function invalidLegalUrlProvider(): array {
339
+        return [
340
+            ['example.com/legal'],  # missing scheme
341
+            ['https:///legal'],     # missing host
342
+        ];
343
+    }
344
+
345
+    #[\PHPUnit\Framework\Attributes\DataProvider('invalidLegalUrlProvider')]
346
+    public function testGetShortFooterInvalidImprint(string $invalidImprintUrl): void {
347
+        $this->navigationManager->expects($this->once())->method('getAll')->with(INavigationManager::TYPE_GUEST)->willReturn([]);
348
+        $this->config
349
+            ->expects($this->exactly(5))
350
+            ->method('getAppValue')
351
+            ->willReturnMap([
352
+                ['theming', 'url', $this->defaults->getBaseUrl(), 'url'],
353
+                ['theming', 'name', 'Nextcloud', 'Name'],
354
+                ['theming', 'slogan', $this->defaults->getSlogan(), 'Slogan'],
355
+                ['theming', 'imprintUrl', '', $invalidImprintUrl],
356
+                ['theming', 'privacyUrl', '', ''],
357
+            ]);
358
+
359
+        $this->assertEquals('<a href="url" target="_blank" rel="noreferrer noopener" class="entity-name">Name</a> – Slogan', $this->template->getShortFooter());
360
+    }
361
+
362
+    #[\PHPUnit\Framework\Attributes\DataProvider('invalidLegalUrlProvider')]
363
+    public function testGetShortFooterInvalidPrivacy(string $invalidPrivacyUrl): void {
364
+        $this->navigationManager->expects($this->once())->method('getAll')->with(INavigationManager::TYPE_GUEST)->willReturn([]);
365
+        $this->config
366
+            ->expects($this->exactly(5))
367
+            ->method('getAppValue')
368
+            ->willReturnMap([
369
+                ['theming', 'url', $this->defaults->getBaseUrl(), 'url'],
370
+                ['theming', 'name', 'Nextcloud', 'Name'],
371
+                ['theming', 'slogan', $this->defaults->getSlogan(), 'Slogan'],
372
+                ['theming', 'imprintUrl', '', ''],
373
+                ['theming', 'privacyUrl', '', $invalidPrivacyUrl],
374
+            ]);
375
+
376
+        $this->assertEquals('<a href="url" target="_blank" rel="noreferrer noopener" class="entity-name">Name</a> – Slogan', $this->template->getShortFooter());
377
+    }
378
+
379
+    public function testGetColorPrimaryWithDefault(): void {
380
+        $this->appConfig
381
+            ->expects(self::once())
382
+            ->method('getValueBool')
383
+            ->with('theming', 'disable-user-theming')
384
+            ->willReturn(false);
385
+        $this->appConfig
386
+            ->expects(self::once())
387
+            ->method('getValueString')
388
+            ->with('theming', 'primary_color', '')
389
+            ->willReturn($this->defaults->getColorPrimary());
390
+
391
+        $this->assertEquals($this->defaults->getColorPrimary(), $this->template->getColorPrimary());
392
+    }
393
+
394
+    public function testGetColorPrimaryWithCustom(): void {
395
+        $this->appConfig
396
+            ->expects(self::once())
397
+            ->method('getValueBool')
398
+            ->with('theming', 'disable-user-theming')
399
+            ->willReturn(false);
400
+        $this->appConfig
401
+            ->expects(self::once())
402
+            ->method('getValueString')
403
+            ->with('theming', 'primary_color', '')
404
+            ->willReturn('#fff');
405
+
406
+        $this->assertEquals('#fff', $this->template->getColorPrimary());
407
+    }
408
+
409
+    public static function dataGetColorPrimary(): array {
410
+        return [
411
+            'with fallback default' => [
412
+                'disableTheming' => false,
413
+                'primaryColor' => '',
414
+                'userPrimaryColor' => '',
415
+                'expected' => BackgroundService::DEFAULT_COLOR,
416
+            ],
417
+            'with custom admin primary' => [
418
+                'disableTheming' => false,
419
+                'primaryColor' => '#aaa',
420
+                'userPrimaryColor' => '',
421
+                'expected' => '#aaa',
422
+            ],
423
+            'with custom invalid admin primary' => [
424
+                'disableTheming' => false,
425
+                'primaryColor' => 'invalid',
426
+                'userPrimaryColor' => '',
427
+                'expected' => BackgroundService::DEFAULT_COLOR,
428
+            ],
429
+            'with custom invalid user primary' => [
430
+                'disableTheming' => false,
431
+                'primaryColor' => '',
432
+                'userPrimaryColor' => 'invalid-name',
433
+                'expected' => BackgroundService::DEFAULT_COLOR,
434
+            ],
435
+            'with custom user primary' => [
436
+                'disableTheming' => false,
437
+                'primaryColor' => '',
438
+                'userPrimaryColor' => '#bbb',
439
+                'expected' => '#bbb',
440
+            ],
441
+            'with disabled user theming primary' => [
442
+                'disableTheming' => true,
443
+                'primaryColor' => '#aaa',
444
+                'userPrimaryColor' => '#bbb',
445
+                'expected' => '#aaa',
446
+            ],
447
+        ];
448
+    }
449
+
450
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataGetColorPrimary')]
451
+    public function testGetColorPrimary(bool $disableTheming, string $primaryColor, string $userPrimaryColor, string $expected): void {
452
+        $user = $this->createMock(IUser::class);
453
+        $this->userSession->expects($this->any())
454
+            ->method('getUser')
455
+            ->willReturn($user);
456
+        $user->expects($this->any())
457
+            ->method('getUID')
458
+            ->willReturn('user');
459
+        $this->appConfig
460
+            ->expects(self::any())
461
+            ->method('getValueBool')
462
+            ->with('theming', 'disable-user-theming')
463
+            ->willReturn($disableTheming);
464
+        $this->appConfig
465
+            ->expects(self::any())
466
+            ->method('getValueString')
467
+            ->with('theming', 'primary_color', '')
468
+            ->willReturn($primaryColor);
469
+        $this->config
470
+            ->expects($this->any())
471
+            ->method('getUserValue')
472
+            ->with('user', 'theming', 'primary_color', '')
473
+            ->willReturn($userPrimaryColor);
474
+
475
+        $this->assertEquals($expected, $this->template->getColorPrimary());
476
+    }
477
+
478
+    public function testSet(): void {
479
+        $this->config
480
+            ->expects($this->once())
481
+            ->method('setAppValue')
482
+            ->with('theming', 'cachebuster', 16);
483
+        $this->appConfig
484
+            ->expects($this->once())
485
+            ->method('setValueString')
486
+            ->with('theming', 'MySetting', 'MyValue');
487
+        $this->config
488
+            ->expects($this->once())
489
+            ->method('getAppValue')
490
+            ->with('theming', 'cachebuster', '0')
491
+            ->willReturn('15');
492
+        $this->cacheFactory
493
+            ->expects($this->exactly(2))
494
+            ->method('createDistributed')
495
+            ->willReturnMap([
496
+                ['theming-', $this->cache],
497
+                ['imagePath', $this->cache],
498
+            ]);
499
+        $this->cache
500
+            ->expects($this->any())
501
+            ->method('clear')
502
+            ->with('');
503
+        $this->template->set('MySetting', 'MyValue');
504
+    }
505
+
506
+    public function testUndoName(): void {
507
+        $this->config
508
+            ->expects($this->once())
509
+            ->method('deleteAppValue')
510
+            ->with('theming', 'name');
511
+        $this->config
512
+            ->expects($this->exactly(2))
513
+            ->method('getAppValue')
514
+            ->willReturnMap([
515
+                ['theming', 'cachebuster', '0', '15'],
516
+                ['theming', 'name', 'Nextcloud', 'Nextcloud'],
517
+            ]);
518
+        $this->config
519
+            ->expects($this->once())
520
+            ->method('setAppValue')
521
+            ->with('theming', 'cachebuster', 16);
522
+
523
+        $this->assertSame('Nextcloud', $this->template->undo('name'));
524
+    }
525
+
526
+    public function testUndoBaseUrl(): void {
527
+        $this->config
528
+            ->expects($this->once())
529
+            ->method('deleteAppValue')
530
+            ->with('theming', 'url');
531
+        $this->config
532
+            ->expects($this->exactly(2))
533
+            ->method('getAppValue')
534
+            ->willReturnMap([
535
+                ['theming', 'cachebuster', '0', '15'],
536
+                ['theming', 'url', $this->defaults->getBaseUrl(), $this->defaults->getBaseUrl()],
537
+            ]);
538
+        $this->config
539
+            ->expects($this->once())
540
+            ->method('setAppValue')
541
+            ->with('theming', 'cachebuster', 16);
542
+
543
+        $this->assertSame($this->defaults->getBaseUrl(), $this->template->undo('url'));
544
+    }
545
+
546
+    public function testUndoSlogan(): void {
547
+        $this->config
548
+            ->expects($this->once())
549
+            ->method('deleteAppValue')
550
+            ->with('theming', 'slogan');
551
+        $this->config
552
+            ->expects($this->exactly(2))
553
+            ->method('getAppValue')
554
+            ->willReturnMap([
555
+                ['theming', 'cachebuster', '0', '15'],
556
+                ['theming', 'slogan', $this->defaults->getSlogan(), $this->defaults->getSlogan()],
557
+            ]);
558
+        $this->config
559
+            ->expects($this->once())
560
+            ->method('setAppValue')
561
+            ->with('theming', 'cachebuster', 16);
562
+
563
+        $this->assertSame($this->defaults->getSlogan(), $this->template->undo('slogan'));
564
+    }
565
+
566
+    public function testUndoPrimaryColor(): void {
567
+        $this->config
568
+            ->expects($this->once())
569
+            ->method('deleteAppValue')
570
+            ->with('theming', 'primary_color');
571
+        $this->config
572
+            ->expects($this->once())
573
+            ->method('getAppValue')
574
+            ->with('theming', 'cachebuster', '0')
575
+            ->willReturn('15');
576
+        $this->config
577
+            ->expects($this->once())
578
+            ->method('setAppValue')
579
+            ->with('theming', 'cachebuster', 16);
580
+
581
+        $this->assertSame($this->defaults->getColorPrimary(), $this->template->undo('primary_color'));
582
+    }
583
+
584
+    public function testUndoDefaultAction(): void {
585
+        $this->config
586
+            ->expects($this->once())
587
+            ->method('deleteAppValue')
588
+            ->with('theming', 'defaultitem');
589
+        $this->config
590
+            ->expects($this->once())
591
+            ->method('getAppValue')
592
+            ->with('theming', 'cachebuster', '0')
593
+            ->willReturn('15');
594
+        $this->config
595
+            ->expects($this->once())
596
+            ->method('setAppValue')
597
+            ->with('theming', 'cachebuster', 16);
598
+
599
+        $this->assertSame('', $this->template->undo('defaultitem'));
600
+    }
601
+
602
+    public function testGetBackground(): void {
603
+        $this->imageManager
604
+            ->expects($this->once())
605
+            ->method('getImageUrl')
606
+            ->with('background')
607
+            ->willReturn('custom-background?v=0');
608
+        $this->assertEquals('custom-background?v=0', $this->template->getBackground());
609
+    }
610
+
611
+    private function getLogoHelper($withName, $useSvg) {
612
+        $this->imageManager->expects($this->any())
613
+            ->method('getImage')
614
+            ->with('logo')
615
+            ->willThrowException(new NotFoundException());
616
+        $this->config
617
+            ->expects($this->exactly(2))
618
+            ->method('getAppValue')
619
+            ->willReturnMap([
620
+                ['theming', 'logoMime', '', ''],
621
+                ['theming', 'cachebuster', '0', '0'],
622
+            ]);
623
+        $this->urlGenerator->expects($this->once())
624
+            ->method('imagePath')
625
+            ->with('core', $withName)
626
+            ->willReturn('core-logo');
627
+        $this->assertEquals('core-logo?v=0', $this->template->getLogo($useSvg));
628
+    }
629
+
630
+    public function testGetLogoDefaultWithSvg(): void {
631
+        $this->getLogoHelper('logo/logo.svg', true);
632
+    }
633
+
634
+    public function testGetLogoDefaultWithoutSvg(): void {
635
+        $this->getLogoHelper('logo/logo.png', false);
636
+    }
637
+
638
+    public function testGetLogoCustom(): void {
639
+        $this->config
640
+            ->expects($this->exactly(2))
641
+            ->method('getAppValue')
642
+            ->willReturnMap([
643
+                ['theming', 'logoMime', '', 'image/svg+xml'],
644
+                ['theming', 'cachebuster', '0', '0'],
645
+            ]);
646
+        $this->urlGenerator->expects($this->once())
647
+            ->method('linkToRoute')
648
+            ->with('theming.Theming.getImage')
649
+            ->willReturn('custom-logo?v=0');
650
+        $this->assertEquals('custom-logo' . '?v=0', $this->template->getLogo());
651
+    }
652
+
653
+    public function testGetScssVariablesCached(): void {
654
+        $this->config->expects($this->any())->method('getAppValue')->with('theming', 'cachebuster', '0')->willReturn('1');
655
+        $this->cacheFactory->expects($this->once())
656
+            ->method('createDistributed')
657
+            ->with('theming-1-')
658
+            ->willReturn($this->cache);
659
+        $this->cache->expects($this->once())->method('get')->with('getScssVariables')->willReturn(['foo' => 'bar']);
660
+        $this->assertEquals(['foo' => 'bar'], $this->template->getScssVariables());
661
+    }
662
+
663
+    public function testGetScssVariables(): void {
664
+        $this->config
665
+            ->expects($this->any())
666
+            ->method('getAppValue')
667
+            ->willReturnMap([
668
+                ['theming', 'cachebuster', '0', '0'],
669
+                ['theming', 'logoMime', '', 'jpeg'],
670
+                ['theming', 'backgroundMime', '', 'jpeg'],
671
+                ['theming', 'logoheaderMime', '', 'jpeg'],
672
+                ['theming', 'faviconMime', '', 'jpeg'],
673
+            ]);
674
+
675
+        $this->appConfig
676
+            ->expects(self::atLeastOnce())
677
+            ->method('getValueString')
678
+            ->willReturnMap([
679
+                ['theming', 'primary_color', '', false, $this->defaults->getColorPrimary()],
680
+                ['theming', 'primary_color', $this->defaults->getColorPrimary(), false, $this->defaults->getColorPrimary()],
681
+            ]);
682
+
683
+        $this->util->expects($this->any())->method('invertTextColor')->with($this->defaults->getColorPrimary())->willReturn(false);
684
+        $this->util->expects($this->any())->method('elementColor')->with($this->defaults->getColorPrimary())->willReturn('#aaaaaa');
685
+        $this->cacheFactory->expects($this->once())
686
+            ->method('createDistributed')
687
+            ->with('theming-0-')
688
+            ->willReturn($this->cache);
689
+        $this->cache->expects($this->once())->method('get')->with('getScssVariables')->willReturn(null);
690
+        $this->imageManager->expects($this->exactly(4))
691
+            ->method('getImageUrl')
692
+            ->willReturnMap([
693
+                ['logo', 'custom-logo?v=0'],
694
+                ['logoheader', 'custom-logoheader?v=0'],
695
+                ['favicon', 'custom-favicon?v=0'],
696
+                ['background', 'custom-background?v=0'],
697
+            ]);
698
+
699
+        $expected = [
700
+            'theming-cachebuster' => '\'0\'',
701
+            'theming-logo-mime' => '\'jpeg\'',
702
+            'theming-background-mime' => '\'jpeg\'',
703
+            'image-logo' => "url('custom-logo?v=0')",
704
+            'image-login-background' => "url('custom-background?v=0')",
705
+            'color-primary' => $this->defaults->getColorPrimary(),
706
+            'color-primary-text' => '#ffffff',
707
+            'image-login-plain' => 'false',
708
+            'color-primary-element' => '#aaaaaa',
709
+            'theming-logoheader-mime' => '\'jpeg\'',
710
+            'theming-favicon-mime' => '\'jpeg\'',
711
+            'image-logoheader' => "url('custom-logoheader?v=0')",
712
+            'image-favicon' => "url('custom-favicon?v=0')",
713
+            'has-legal-links' => 'false'
714
+        ];
715
+        $this->assertEquals($expected, $this->template->getScssVariables());
716
+    }
717
+
718
+    public function testGetDefaultAndroidURL(): void {
719
+        $this->config
720
+            ->expects($this->once())
721
+            ->method('getAppValue')
722
+            ->with('theming', 'AndroidClientUrl', 'https://play.google.com/store/apps/details?id=com.nextcloud.client')
723
+            ->willReturn('https://play.google.com/store/apps/details?id=com.nextcloud.client');
724
+
725
+        $this->assertEquals('https://play.google.com/store/apps/details?id=com.nextcloud.client', $this->template->getAndroidClientUrl());
726
+    }
727
+
728
+    public function testGetCustomAndroidURL(): void {
729
+        $this->config
730
+            ->expects($this->once())
731
+            ->method('getAppValue')
732
+            ->with('theming', 'AndroidClientUrl', 'https://play.google.com/store/apps/details?id=com.nextcloud.client')
733
+            ->willReturn('https://play.google.com/store/apps/details?id=com.mycloud.client');
734
+
735
+        $this->assertEquals('https://play.google.com/store/apps/details?id=com.mycloud.client', $this->template->getAndroidClientUrl());
736
+    }
737
+
738
+    public function testGetDefaultiOSURL(): void {
739
+        $this->config
740
+            ->expects($this->once())
741
+            ->method('getAppValue')
742
+            ->with('theming', 'iOSClientUrl', 'https://geo.itunes.apple.com/us/app/nextcloud/id1125420102?mt=8')
743
+            ->willReturn('https://geo.itunes.apple.com/us/app/nextcloud/id1125420102?mt=8');
744
+
745
+        $this->assertEquals('https://geo.itunes.apple.com/us/app/nextcloud/id1125420102?mt=8', $this->template->getiOSClientUrl());
746
+    }
747
+
748
+    public function testGetCustomiOSURL(): void {
749
+        $this->config
750
+            ->expects($this->once())
751
+            ->method('getAppValue')
752
+            ->with('theming', 'iOSClientUrl', 'https://geo.itunes.apple.com/us/app/nextcloud/id1125420102?mt=8')
753
+            ->willReturn('https://geo.itunes.apple.com/us/app/nextcloud/id1234567890?mt=8');
754
+
755
+        $this->assertEquals('https://geo.itunes.apple.com/us/app/nextcloud/id1234567890?mt=8', $this->template->getiOSClientUrl());
756
+    }
757
+
758
+    public function testGetDefaultiTunesAppId(): void {
759
+        $this->config
760
+            ->expects($this->once())
761
+            ->method('getAppValue')
762
+            ->with('theming', 'iTunesAppId', '1125420102')
763
+            ->willReturn('1125420102');
764
+
765
+        $this->assertEquals('1125420102', $this->template->getiTunesAppId());
766
+    }
767
+
768
+    public function testGetCustomiTunesAppId(): void {
769
+        $this->config
770
+            ->expects($this->once())
771
+            ->method('getAppValue')
772
+            ->with('theming', 'iTunesAppId', '1125420102')
773
+            ->willReturn('1234567890');
774
+
775
+        $this->assertEquals('1234567890', $this->template->getiTunesAppId());
776
+    }
777
+
778
+    public static function dataReplaceImagePath(): array {
779
+        return [
780
+            ['core', 'test.png', false],
781
+            ['core', 'manifest.json'],
782
+            ['core', 'favicon.ico'],
783
+            ['core', 'favicon-touch.png']
784
+        ];
785
+    }
786
+
787
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataReplaceImagePath')]
788
+    public function testReplaceImagePath(string $app, string $image, string|bool $result = 'themingRoute?v=1234abcd'): void {
789
+        $this->cache->expects($this->any())
790
+            ->method('get')
791
+            ->with('shouldReplaceIcons')
792
+            ->willReturn(true);
793
+        $this->config
794
+            ->expects($this->any())
795
+            ->method('getAppValue')
796
+            ->with('theming', 'cachebuster', '0')
797
+            ->willReturn('0');
798
+        $this->urlGenerator
799
+            ->expects($this->any())
800
+            ->method('linkToRoute')
801
+            ->willReturn('themingRoute');
802
+        if ($result) {
803
+            $this->util
804
+                ->expects($this->once())
805
+                ->method('getCacheBuster')
806
+                ->willReturn('1234abcd');
807
+        }
808
+        $this->assertEquals($result, $this->template->replaceImagePath($app, $image));
809
+    }
810 810
 }
Please login to merge, or discard this patch.
Spacing   +5 added lines, -5 removed lines patch added patch discarded remove patch
@@ -29,7 +29,7 @@  discard block
 block discarded – undo
29 29
 	private IAppConfig&MockObject $appConfig;
30 30
 	private IConfig&MockObject $config;
31 31
 	private \OC_Defaults $defaults;
32
-	private IL10N|MockObject $l10n;
32
+	private IL10N | MockObject $l10n;
33 33
 	private IUserSession&MockObject $userSession;
34 34
 	private IURLGenerator&MockObject $urlGenerator;
35 35
 	private ICacheFactory&MockObject $cacheFactory;
@@ -337,8 +337,8 @@  discard block
 block discarded – undo
337 337
 
338 338
 	public static function invalidLegalUrlProvider(): array {
339 339
 		return [
340
-			['example.com/legal'],  # missing scheme
341
-			['https:///legal'],     # missing host
340
+			['example.com/legal'], # missing scheme
341
+			['https:///legal'], # missing host
342 342
 		];
343 343
 	}
344 344
 
@@ -647,7 +647,7 @@  discard block
 block discarded – undo
647 647
 			->method('linkToRoute')
648 648
 			->with('theming.Theming.getImage')
649 649
 			->willReturn('custom-logo?v=0');
650
-		$this->assertEquals('custom-logo' . '?v=0', $this->template->getLogo());
650
+		$this->assertEquals('custom-logo'.'?v=0', $this->template->getLogo());
651 651
 	}
652 652
 
653 653
 	public function testGetScssVariablesCached(): void {
@@ -785,7 +785,7 @@  discard block
 block discarded – undo
785 785
 	}
786 786
 
787 787
 	#[\PHPUnit\Framework\Attributes\DataProvider('dataReplaceImagePath')]
788
-	public function testReplaceImagePath(string $app, string $image, string|bool $result = 'themingRoute?v=1234abcd'): void {
788
+	public function testReplaceImagePath(string $app, string $image, string | bool $result = 'themingRoute?v=1234abcd'): void {
789 789
 		$this->cache->expects($this->any())
790 790
 			->method('get')
791 791
 			->with('shouldReplaceIcons')
Please login to merge, or discard this patch.