Completed
Push — master ( 52cfd5...66c8f9 )
by Joas
56:42 queued 25:52
created
lib/private/L10N/Factory.php 2 patches
Indentation   +658 added lines, -658 removed lines patch added patch discarded remove patch
@@ -24,662 +24,662 @@
 block discarded – undo
24 24
  * A factory that generates language instances
25 25
  */
26 26
 class Factory implements IFactory {
27
-	/** @var string */
28
-	protected $requestLanguage = '';
29
-
30
-	/**
31
-	 * cached instances
32
-	 * @var array Structure: Lang => App => \OCP\IL10N
33
-	 */
34
-	protected $instances = [];
35
-
36
-	/**
37
-	 * @var array Structure: App => string[]
38
-	 */
39
-	protected $availableLanguages = [];
40
-
41
-	/**
42
-	 * @var array
43
-	 */
44
-	protected $localeCache = [];
45
-
46
-	/**
47
-	 * @var array
48
-	 */
49
-	protected $availableLocales = [];
50
-
51
-	/**
52
-	 * @var array Structure: string => callable
53
-	 */
54
-	protected $pluralFunctions = [];
55
-
56
-	public const COMMON_LANGUAGE_CODES = [
57
-		'en', 'es', 'fr', 'de', 'de_DE', 'ja', 'ar', 'ru', 'nl', 'it',
58
-		'pt_BR', 'pt_PT', 'da', 'fi_FI', 'nb_NO', 'sv', 'tr', 'zh_CN', 'ko'
59
-	];
60
-
61
-	/**
62
-	 * Keep in sync with `build/translation-checker.php`
63
-	 */
64
-	public const RTL_LANGUAGES = [
65
-		'ar', // Arabic
66
-		'fa', // Persian
67
-		'he', // Hebrew
68
-		'ps', // Pashto,
69
-		'ug', // 'Uyghurche / Uyghur
70
-		'ur_PK', // Urdu
71
-	];
72
-
73
-	private ICache $cache;
74
-
75
-	public function __construct(
76
-		protected IConfig $config,
77
-		protected IRequest $request,
78
-		protected IUserSession $userSession,
79
-		ICacheFactory $cacheFactory,
80
-		protected string $serverRoot,
81
-		protected IAppManager $appManager,
82
-	) {
83
-		$this->cache = $cacheFactory->createLocal('L10NFactory');
84
-	}
85
-
86
-	/**
87
-	 * Get a language instance
88
-	 *
89
-	 * @param string $app
90
-	 * @param string|null $lang
91
-	 * @param string|null $locale
92
-	 * @return \OCP\IL10N
93
-	 */
94
-	public function get($app, $lang = null, $locale = null) {
95
-		return new LazyL10N(function () use ($app, $lang, $locale) {
96
-			$app = $this->appManager->cleanAppId($app);
97
-			$lang = $this->cleanLanguage($lang);
98
-			$forceLang = $this->cleanLanguage($this->request->getParam('forceLanguage')) ?? $this->config->getSystemValue('force_language', false);
99
-			if (is_string($forceLang)) {
100
-				$lang = $forceLang;
101
-			}
102
-
103
-			$forceLocale = $this->config->getSystemValue('force_locale', false);
104
-			if (is_string($forceLocale)) {
105
-				$locale = $forceLocale;
106
-			}
107
-
108
-			$lang = $this->validateLanguage($app, $lang);
109
-
110
-			if ($locale === null || !$this->localeExists($locale)) {
111
-				$locale = $this->findLocale($lang);
112
-			}
113
-
114
-			if (!isset($this->instances[$lang][$app])) {
115
-				$this->instances[$lang][$app] = new L10N(
116
-					$this,
117
-					$app,
118
-					$lang,
119
-					$locale,
120
-					$this->getL10nFilesForApp($app, $lang)
121
-				);
122
-			}
123
-
124
-			return $this->instances[$lang][$app];
125
-		});
126
-	}
127
-
128
-	/**
129
-	 * Remove some invalid characters before using a string as a language
130
-	 *
131
-	 * @psalm-taint-escape callable
132
-	 * @psalm-taint-escape cookie
133
-	 * @psalm-taint-escape file
134
-	 * @psalm-taint-escape has_quotes
135
-	 * @psalm-taint-escape header
136
-	 * @psalm-taint-escape html
137
-	 * @psalm-taint-escape include
138
-	 * @psalm-taint-escape ldap
139
-	 * @psalm-taint-escape shell
140
-	 * @psalm-taint-escape sql
141
-	 * @psalm-taint-escape unserialize
142
-	 */
143
-	private function cleanLanguage(?string $lang): ?string {
144
-		if ($lang === null) {
145
-			return null;
146
-		}
147
-		$lang = preg_replace('/[^a-zA-Z0-9.;,=-]/', '', $lang);
148
-		return str_replace('..', '', $lang);
149
-	}
150
-
151
-	/**
152
-	 * Check that $lang is an existing language and not null, otherwise return the language to use instead
153
-	 *
154
-	 * @psalm-taint-escape callable
155
-	 * @psalm-taint-escape cookie
156
-	 * @psalm-taint-escape file
157
-	 * @psalm-taint-escape has_quotes
158
-	 * @psalm-taint-escape header
159
-	 * @psalm-taint-escape html
160
-	 * @psalm-taint-escape include
161
-	 * @psalm-taint-escape ldap
162
-	 * @psalm-taint-escape shell
163
-	 * @psalm-taint-escape sql
164
-	 * @psalm-taint-escape unserialize
165
-	 */
166
-	private function validateLanguage(string $app, ?string $lang): string {
167
-		if ($lang === null || !$this->languageExists($app, $lang)) {
168
-			return $this->findLanguage($app);
169
-		} else {
170
-			return $lang;
171
-		}
172
-	}
173
-
174
-	/**
175
-	 * Find the best language
176
-	 *
177
-	 * @param string|null $appId App id or null for core
178
-	 *
179
-	 * @return string language If nothing works it returns 'en'
180
-	 */
181
-	public function findLanguage(?string $appId = null): string {
182
-		// Step 1: Forced language always has precedence over anything else
183
-		$forceLang = $this->cleanLanguage($this->request->getParam('forceLanguage')) ?? $this->config->getSystemValue('force_language', false);
184
-		if (is_string($forceLang)) {
185
-			$this->requestLanguage = $forceLang;
186
-		}
187
-
188
-		// Step 2: Return cached language
189
-		if ($this->requestLanguage !== '' && $this->languageExists($appId, $this->requestLanguage)) {
190
-			return $this->requestLanguage;
191
-		}
192
-
193
-		/**
194
-		 * Step 3: At this point Nextcloud might not yet be installed and thus the lookup
195
-		 * in the preferences table might fail. For this reason we need to check
196
-		 * whether the instance has already been installed
197
-		 *
198
-		 * @link https://github.com/owncloud/core/issues/21955
199
-		 */
200
-		if ($this->config->getSystemValueBool('installed', false)) {
201
-			$userId = !is_null($this->userSession->getUser()) ? $this->userSession->getUser()->getUID() :  null;
202
-			if (!is_null($userId)) {
203
-				$userLang = $this->config->getUserValue($userId, 'core', 'lang', null);
204
-			} else {
205
-				$userLang = null;
206
-			}
207
-		} else {
208
-			$userId = null;
209
-			$userLang = null;
210
-		}
211
-		if ($userLang) {
212
-			$this->requestLanguage = $userLang;
213
-			if ($this->languageExists($appId, $userLang)) {
214
-				return $userLang;
215
-			}
216
-		}
217
-
218
-		// Step 4: Check the request headers
219
-		try {
220
-			// Try to get the language from the Request
221
-			$lang = $this->getLanguageFromRequest($appId);
222
-			if ($userId !== null && $appId === null && !$userLang) {
223
-				$this->config->setUserValue($userId, 'core', 'lang', $lang);
224
-			}
225
-			return $lang;
226
-		} catch (LanguageNotFoundException $e) {
227
-			// Finding language from request failed fall back to default language
228
-			$defaultLanguage = $this->config->getSystemValue('default_language', false);
229
-			if ($defaultLanguage !== false && $this->languageExists($appId, $defaultLanguage)) {
230
-				return $defaultLanguage;
231
-			}
232
-		}
233
-
234
-		// Step 5: fall back to English
235
-		return 'en';
236
-	}
237
-
238
-	public function findGenericLanguage(?string $appId = null): string {
239
-		// Step 1: Forced language always has precedence over anything else
240
-		$forcedLanguage = $this->cleanLanguage($this->request->getParam('forceLanguage')) ?? $this->config->getSystemValue('force_language', false);
241
-		if ($forcedLanguage !== false) {
242
-			return $forcedLanguage;
243
-		}
244
-
245
-		// Step 2: Check if we have a default language
246
-		$defaultLanguage = $this->config->getSystemValue('default_language', false);
247
-		if ($defaultLanguage !== false && $this->languageExists($appId, $defaultLanguage)) {
248
-			return $defaultLanguage;
249
-		}
250
-
251
-		// Step 3: fall back to English
252
-		return 'en';
253
-	}
254
-
255
-	/**
256
-	 * find the best locale
257
-	 *
258
-	 * @param string $lang
259
-	 * @return null|string
260
-	 */
261
-	public function findLocale($lang = null) {
262
-		$forceLocale = $this->config->getSystemValue('force_locale', false);
263
-		if (is_string($forceLocale) && $this->localeExists($forceLocale)) {
264
-			return $forceLocale;
265
-		}
266
-
267
-		if ($this->config->getSystemValueBool('installed', false)) {
268
-			$userId = $this->userSession->getUser() !== null ? $this->userSession->getUser()->getUID() :  null;
269
-			$userLocale = null;
270
-			if ($userId !== null) {
271
-				$userLocale = $this->config->getUserValue($userId, 'core', 'locale', null);
272
-			}
273
-		} else {
274
-			$userId = null;
275
-			$userLocale = null;
276
-		}
277
-
278
-		if ($userLocale && $this->localeExists($userLocale)) {
279
-			return $userLocale;
280
-		}
281
-
282
-		// Default : use system default locale
283
-		$defaultLocale = $this->config->getSystemValue('default_locale', false);
284
-		if ($defaultLocale !== false && $this->localeExists($defaultLocale)) {
285
-			return $defaultLocale;
286
-		}
287
-
288
-		// If no user locale set, use lang as locale
289
-		if ($lang !== null && $this->localeExists($lang)) {
290
-			return $lang;
291
-		}
292
-
293
-		// At last, return USA
294
-		return 'en_US';
295
-	}
296
-
297
-	/**
298
-	 * find the matching lang from the locale
299
-	 *
300
-	 * @param string $app
301
-	 * @param string $locale
302
-	 * @return null|string
303
-	 */
304
-	public function findLanguageFromLocale(string $app = 'core', ?string $locale = null) {
305
-		if ($this->languageExists($app, $locale)) {
306
-			return $locale;
307
-		}
308
-
309
-		// Try to split e.g: fr_FR => fr
310
-		$locale = explode('_', $locale)[0];
311
-		if ($this->languageExists($app, $locale)) {
312
-			return $locale;
313
-		}
314
-	}
315
-
316
-	/**
317
-	 * Find all available languages for an app
318
-	 *
319
-	 * @param string|null $app App id or null for core
320
-	 * @return string[] an array of available languages
321
-	 */
322
-	public function findAvailableLanguages($app = null): array {
323
-		$key = $app;
324
-		if ($key === null) {
325
-			$key = 'null';
326
-		}
327
-
328
-		if ($availableLanguages = $this->cache->get($key)) {
329
-			$this->availableLanguages[$key] = $availableLanguages;
330
-		}
331
-
332
-		// also works with null as key
333
-		if (!empty($this->availableLanguages[$key])) {
334
-			return $this->availableLanguages[$key];
335
-		}
336
-
337
-		$available = ['en']; //english is always available
338
-		$dir = $this->findL10nDir($app);
339
-		if (is_dir($dir)) {
340
-			$files = scandir($dir);
341
-			if ($files !== false) {
342
-				foreach ($files as $file) {
343
-					if (str_ends_with($file, '.json') && !str_starts_with($file, 'l10n')) {
344
-						$available[] = substr($file, 0, -5);
345
-					}
346
-				}
347
-			}
348
-		}
349
-
350
-		// merge with translations from theme
351
-		$theme = $this->config->getSystemValueString('theme');
352
-		if (!empty($theme)) {
353
-			$themeDir = $this->serverRoot . '/themes/' . $theme . substr($dir, strlen($this->serverRoot));
354
-
355
-			if (is_dir($themeDir)) {
356
-				$files = scandir($themeDir);
357
-				if ($files !== false) {
358
-					foreach ($files as $file) {
359
-						if (str_ends_with($file, '.json') && !str_starts_with($file, 'l10n')) {
360
-							$available[] = substr($file, 0, -5);
361
-						}
362
-					}
363
-				}
364
-			}
365
-		}
366
-
367
-		$this->availableLanguages[$key] = $available;
368
-		$this->cache->set($key, $available, 60);
369
-		return $available;
370
-	}
371
-
372
-	/**
373
-	 * @return array|mixed
374
-	 */
375
-	public function findAvailableLocales() {
376
-		if (!empty($this->availableLocales)) {
377
-			return $this->availableLocales;
378
-		}
379
-
380
-		$localeData = file_get_contents(\OC::$SERVERROOT . '/resources/locales.json');
381
-		$this->availableLocales = \json_decode($localeData, true);
382
-
383
-		return $this->availableLocales;
384
-	}
385
-
386
-	/**
387
-	 * @param string|null $app App id or null for core
388
-	 * @param string $lang
389
-	 * @return bool
390
-	 */
391
-	public function languageExists($app, $lang) {
392
-		if ($lang === 'en') { //english is always available
393
-			return true;
394
-		}
395
-
396
-		$languages = $this->findAvailableLanguages($app);
397
-		return in_array($lang, $languages);
398
-	}
399
-
400
-	public function getLanguageDirection(string $language): string {
401
-		if (in_array($language, self::RTL_LANGUAGES, true)) {
402
-			return 'rtl';
403
-		}
404
-
405
-		return 'ltr';
406
-	}
407
-
408
-	public function getLanguageIterator(?IUser $user = null): ILanguageIterator {
409
-		$user = $user ?? $this->userSession->getUser();
410
-		if ($user === null) {
411
-			throw new \RuntimeException('Failed to get an IUser instance');
412
-		}
413
-		return new LanguageIterator($user, $this->config);
414
-	}
415
-
416
-	/**
417
-	 * Return the language to use when sending something to a user
418
-	 *
419
-	 * @param IUser|null $user
420
-	 * @return string
421
-	 * @since 20.0.0
422
-	 */
423
-	public function getUserLanguage(?IUser $user = null): string {
424
-		$language = $this->config->getSystemValue('force_language', false);
425
-		if ($language !== false) {
426
-			return $language;
427
-		}
428
-
429
-		if ($user instanceof IUser) {
430
-			$language = $this->config->getUserValue($user->getUID(), 'core', 'lang', null);
431
-			if ($language !== null) {
432
-				return $language;
433
-			}
434
-
435
-			$forcedLanguage = $this->cleanLanguage($this->request->getParam('forceLanguage'));
436
-			if ($forcedLanguage !== null) {
437
-				return $forcedLanguage;
438
-			}
439
-
440
-			// Use language from request
441
-			if ($this->userSession->getUser() instanceof IUser
442
-				&& $user->getUID() === $this->userSession->getUser()->getUID()) {
443
-				try {
444
-					return $this->getLanguageFromRequest();
445
-				} catch (LanguageNotFoundException $e) {
446
-				}
447
-			}
448
-		}
449
-
450
-		return $this->cleanLanguage($this->request->getParam('forceLanguage')) ?? $this->config->getSystemValueString('default_language', 'en');
451
-	}
452
-
453
-	/**
454
-	 * @param string $locale
455
-	 * @return bool
456
-	 */
457
-	public function localeExists($locale) {
458
-		if ($locale === 'en') { //english is always available
459
-			return true;
460
-		}
461
-
462
-		if ($this->localeCache === []) {
463
-			$locales = $this->findAvailableLocales();
464
-			foreach ($locales as $l) {
465
-				$this->localeCache[$l['code']] = true;
466
-			}
467
-		}
468
-
469
-		return isset($this->localeCache[$locale]);
470
-	}
471
-
472
-	/**
473
-	 * @throws LanguageNotFoundException
474
-	 */
475
-	private function getLanguageFromRequest(?string $app = null): string {
476
-		$header = $this->cleanLanguage($this->request->getHeader('ACCEPT_LANGUAGE'));
477
-		if ($header !== '') {
478
-			$available = $this->findAvailableLanguages($app);
479
-
480
-			// E.g. make sure that 'de' is before 'de_DE'.
481
-			sort($available);
482
-
483
-			$preferences = preg_split('/,\s*/', strtolower($header));
484
-			foreach ($preferences as $preference) {
485
-				[$preferred_language] = explode(';', $preference);
486
-				$preferred_language = str_replace('-', '_', $preferred_language);
487
-
488
-				$preferred_language_parts = explode('_', $preferred_language);
489
-				foreach ($available as $available_language) {
490
-					if ($preferred_language === strtolower($available_language)) {
491
-						return $this->respectDefaultLanguage($app, $available_language);
492
-					}
493
-					if (strtolower($available_language) === $preferred_language_parts[0] . '_' . end($preferred_language_parts)) {
494
-						return $available_language;
495
-					}
496
-				}
497
-
498
-				// Fallback from de_De to de
499
-				foreach ($available as $available_language) {
500
-					if ($preferred_language_parts[0] === $available_language) {
501
-						return $available_language;
502
-					}
503
-				}
504
-			}
505
-		}
506
-
507
-		throw new LanguageNotFoundException();
508
-	}
509
-
510
-	/**
511
-	 * if default language is set to de_DE (formal German) this should be
512
-	 * preferred to 'de' (non-formal German) if possible
513
-	 */
514
-	protected function respectDefaultLanguage(?string $app, string $lang): string {
515
-		$result = $lang;
516
-		$defaultLanguage = $this->config->getSystemValue('default_language', false);
517
-
518
-		// use formal version of german ("Sie" instead of "Du") if the default
519
-		// language is set to 'de_DE' if possible
520
-		if (
521
-			is_string($defaultLanguage)
522
-			&& strtolower($lang) === 'de'
523
-			&& strtolower($defaultLanguage) === 'de_de'
524
-			&& $this->languageExists($app, 'de_DE')
525
-		) {
526
-			$result = 'de_DE';
527
-		}
528
-
529
-		return $result;
530
-	}
531
-
532
-	/**
533
-	 * Checks if $sub is a subdirectory of $parent
534
-	 *
535
-	 * @param string $sub
536
-	 * @param string $parent
537
-	 * @return bool
538
-	 */
539
-	private function isSubDirectory($sub, $parent) {
540
-		// Check whether $sub contains no ".."
541
-		if (str_contains($sub, '..')) {
542
-			return false;
543
-		}
544
-
545
-		// Check whether $sub is a subdirectory of $parent
546
-		if (str_starts_with($sub, $parent)) {
547
-			return true;
548
-		}
549
-
550
-		return false;
551
-	}
552
-
553
-	/**
554
-	 * Get a list of language files that should be loaded
555
-	 *
556
-	 * @return string[]
557
-	 */
558
-	private function getL10nFilesForApp(string $app, string $lang): array {
559
-		$languageFiles = [];
560
-
561
-		$i18nDir = $this->findL10nDir($app);
562
-		$transFile = strip_tags($i18nDir) . strip_tags($lang) . '.json';
563
-
564
-		if (($this->isSubDirectory($transFile, $this->serverRoot . '/core/l10n/')
565
-				|| $this->isSubDirectory($transFile, $this->serverRoot . '/lib/l10n/')
566
-				|| $this->isSubDirectory($transFile, $this->appManager->getAppPath($app) . '/l10n/'))
567
-			&& file_exists($transFile)
568
-		) {
569
-			// load the translations file
570
-			$languageFiles[] = $transFile;
571
-		}
572
-
573
-		// merge with translations from theme
574
-		$theme = $this->config->getSystemValueString('theme');
575
-		if (!empty($theme)) {
576
-			$transFile = $this->serverRoot . '/themes/' . $theme . substr($transFile, strlen($this->serverRoot));
577
-			if (file_exists($transFile)) {
578
-				$languageFiles[] = $transFile;
579
-			}
580
-		}
581
-
582
-		return $languageFiles;
583
-	}
584
-
585
-	/**
586
-	 * find the l10n directory
587
-	 *
588
-	 * @param string $app App id or empty string for core
589
-	 * @return string directory
590
-	 */
591
-	protected function findL10nDir($app = null) {
592
-		if (in_array($app, ['core', 'lib'])) {
593
-			if (file_exists($this->serverRoot . '/' . $app . '/l10n/')) {
594
-				return $this->serverRoot . '/' . $app . '/l10n/';
595
-			}
596
-		} elseif ($app) {
597
-			try {
598
-				return $this->appManager->getAppPath($app) . '/l10n/';
599
-			} catch (AppPathNotFoundException) {
600
-				/* App not found, continue */
601
-			}
602
-		}
603
-		return $this->serverRoot . '/core/l10n/';
604
-	}
605
-
606
-	/**
607
-	 * @inheritDoc
608
-	 */
609
-	public function getLanguages(): array {
610
-		$forceLanguage = $this->config->getSystemValue('force_language', false);
611
-		if ($forceLanguage !== false) {
612
-			$l = $this->get('lib', $forceLanguage);
613
-			$potentialName = $l->t('__language_name__');
614
-
615
-			return [
616
-				'commonLanguages' => [[
617
-					'code' => $forceLanguage,
618
-					'name' => $potentialName,
619
-				]],
620
-				'otherLanguages' => [],
621
-			];
622
-		}
623
-
624
-		$languageCodes = $this->findAvailableLanguages();
625
-		$reduceToLanguages = $this->config->getSystemValue('reduce_to_languages', []);
626
-		if (!empty($reduceToLanguages)) {
627
-			$languageCodes = array_intersect($languageCodes, $reduceToLanguages);
628
-		}
629
-
630
-		$commonLanguages = [];
631
-		$otherLanguages = [];
632
-
633
-		foreach ($languageCodes as $lang) {
634
-			$l = $this->get('lib', $lang);
635
-			// TRANSLATORS this is the language name for the language switcher in the personal settings and should be the localized version
636
-			$potentialName = $l->t('__language_name__');
637
-			if ($l->getLanguageCode() === $lang && $potentialName[0] !== '_') { //first check if the language name is in the translation file
638
-				$ln = [
639
-					'code' => $lang,
640
-					'name' => $potentialName
641
-				];
642
-			} elseif ($lang === 'en') {
643
-				$ln = [
644
-					'code' => $lang,
645
-					'name' => 'English (US)'
646
-				];
647
-			} else { //fallback to language code
648
-				$ln = [
649
-					'code' => $lang,
650
-					'name' => $lang
651
-				];
652
-			}
653
-
654
-			// put appropriate languages into appropriate arrays, to print them sorted
655
-			// common languages -> divider -> other languages
656
-			if (in_array($lang, self::COMMON_LANGUAGE_CODES)) {
657
-				$commonLanguages[array_search($lang, self::COMMON_LANGUAGE_CODES)] = $ln;
658
-			} else {
659
-				$otherLanguages[] = $ln;
660
-			}
661
-		}
662
-
663
-		ksort($commonLanguages);
664
-
665
-		// sort now by displayed language not the iso-code
666
-		usort($otherLanguages, function ($a, $b) {
667
-			if ($a['code'] === $a['name'] && $b['code'] !== $b['name']) {
668
-				// If a doesn't have a name, but b does, list b before a
669
-				return 1;
670
-			}
671
-			if ($a['code'] !== $a['name'] && $b['code'] === $b['name']) {
672
-				// If a does have a name, but b doesn't, list a before b
673
-				return -1;
674
-			}
675
-			// Otherwise compare the names
676
-			return strcmp($a['name'], $b['name']);
677
-		});
678
-
679
-		return [
680
-			// reset indexes
681
-			'commonLanguages' => array_values($commonLanguages),
682
-			'otherLanguages' => $otherLanguages
683
-		];
684
-	}
27
+    /** @var string */
28
+    protected $requestLanguage = '';
29
+
30
+    /**
31
+     * cached instances
32
+     * @var array Structure: Lang => App => \OCP\IL10N
33
+     */
34
+    protected $instances = [];
35
+
36
+    /**
37
+     * @var array Structure: App => string[]
38
+     */
39
+    protected $availableLanguages = [];
40
+
41
+    /**
42
+     * @var array
43
+     */
44
+    protected $localeCache = [];
45
+
46
+    /**
47
+     * @var array
48
+     */
49
+    protected $availableLocales = [];
50
+
51
+    /**
52
+     * @var array Structure: string => callable
53
+     */
54
+    protected $pluralFunctions = [];
55
+
56
+    public const COMMON_LANGUAGE_CODES = [
57
+        'en', 'es', 'fr', 'de', 'de_DE', 'ja', 'ar', 'ru', 'nl', 'it',
58
+        'pt_BR', 'pt_PT', 'da', 'fi_FI', 'nb_NO', 'sv', 'tr', 'zh_CN', 'ko'
59
+    ];
60
+
61
+    /**
62
+     * Keep in sync with `build/translation-checker.php`
63
+     */
64
+    public const RTL_LANGUAGES = [
65
+        'ar', // Arabic
66
+        'fa', // Persian
67
+        'he', // Hebrew
68
+        'ps', // Pashto,
69
+        'ug', // 'Uyghurche / Uyghur
70
+        'ur_PK', // Urdu
71
+    ];
72
+
73
+    private ICache $cache;
74
+
75
+    public function __construct(
76
+        protected IConfig $config,
77
+        protected IRequest $request,
78
+        protected IUserSession $userSession,
79
+        ICacheFactory $cacheFactory,
80
+        protected string $serverRoot,
81
+        protected IAppManager $appManager,
82
+    ) {
83
+        $this->cache = $cacheFactory->createLocal('L10NFactory');
84
+    }
85
+
86
+    /**
87
+     * Get a language instance
88
+     *
89
+     * @param string $app
90
+     * @param string|null $lang
91
+     * @param string|null $locale
92
+     * @return \OCP\IL10N
93
+     */
94
+    public function get($app, $lang = null, $locale = null) {
95
+        return new LazyL10N(function () use ($app, $lang, $locale) {
96
+            $app = $this->appManager->cleanAppId($app);
97
+            $lang = $this->cleanLanguage($lang);
98
+            $forceLang = $this->cleanLanguage($this->request->getParam('forceLanguage')) ?? $this->config->getSystemValue('force_language', false);
99
+            if (is_string($forceLang)) {
100
+                $lang = $forceLang;
101
+            }
102
+
103
+            $forceLocale = $this->config->getSystemValue('force_locale', false);
104
+            if (is_string($forceLocale)) {
105
+                $locale = $forceLocale;
106
+            }
107
+
108
+            $lang = $this->validateLanguage($app, $lang);
109
+
110
+            if ($locale === null || !$this->localeExists($locale)) {
111
+                $locale = $this->findLocale($lang);
112
+            }
113
+
114
+            if (!isset($this->instances[$lang][$app])) {
115
+                $this->instances[$lang][$app] = new L10N(
116
+                    $this,
117
+                    $app,
118
+                    $lang,
119
+                    $locale,
120
+                    $this->getL10nFilesForApp($app, $lang)
121
+                );
122
+            }
123
+
124
+            return $this->instances[$lang][$app];
125
+        });
126
+    }
127
+
128
+    /**
129
+     * Remove some invalid characters before using a string as a language
130
+     *
131
+     * @psalm-taint-escape callable
132
+     * @psalm-taint-escape cookie
133
+     * @psalm-taint-escape file
134
+     * @psalm-taint-escape has_quotes
135
+     * @psalm-taint-escape header
136
+     * @psalm-taint-escape html
137
+     * @psalm-taint-escape include
138
+     * @psalm-taint-escape ldap
139
+     * @psalm-taint-escape shell
140
+     * @psalm-taint-escape sql
141
+     * @psalm-taint-escape unserialize
142
+     */
143
+    private function cleanLanguage(?string $lang): ?string {
144
+        if ($lang === null) {
145
+            return null;
146
+        }
147
+        $lang = preg_replace('/[^a-zA-Z0-9.;,=-]/', '', $lang);
148
+        return str_replace('..', '', $lang);
149
+    }
150
+
151
+    /**
152
+     * Check that $lang is an existing language and not null, otherwise return the language to use instead
153
+     *
154
+     * @psalm-taint-escape callable
155
+     * @psalm-taint-escape cookie
156
+     * @psalm-taint-escape file
157
+     * @psalm-taint-escape has_quotes
158
+     * @psalm-taint-escape header
159
+     * @psalm-taint-escape html
160
+     * @psalm-taint-escape include
161
+     * @psalm-taint-escape ldap
162
+     * @psalm-taint-escape shell
163
+     * @psalm-taint-escape sql
164
+     * @psalm-taint-escape unserialize
165
+     */
166
+    private function validateLanguage(string $app, ?string $lang): string {
167
+        if ($lang === null || !$this->languageExists($app, $lang)) {
168
+            return $this->findLanguage($app);
169
+        } else {
170
+            return $lang;
171
+        }
172
+    }
173
+
174
+    /**
175
+     * Find the best language
176
+     *
177
+     * @param string|null $appId App id or null for core
178
+     *
179
+     * @return string language If nothing works it returns 'en'
180
+     */
181
+    public function findLanguage(?string $appId = null): string {
182
+        // Step 1: Forced language always has precedence over anything else
183
+        $forceLang = $this->cleanLanguage($this->request->getParam('forceLanguage')) ?? $this->config->getSystemValue('force_language', false);
184
+        if (is_string($forceLang)) {
185
+            $this->requestLanguage = $forceLang;
186
+        }
187
+
188
+        // Step 2: Return cached language
189
+        if ($this->requestLanguage !== '' && $this->languageExists($appId, $this->requestLanguage)) {
190
+            return $this->requestLanguage;
191
+        }
192
+
193
+        /**
194
+         * Step 3: At this point Nextcloud might not yet be installed and thus the lookup
195
+         * in the preferences table might fail. For this reason we need to check
196
+         * whether the instance has already been installed
197
+         *
198
+         * @link https://github.com/owncloud/core/issues/21955
199
+         */
200
+        if ($this->config->getSystemValueBool('installed', false)) {
201
+            $userId = !is_null($this->userSession->getUser()) ? $this->userSession->getUser()->getUID() :  null;
202
+            if (!is_null($userId)) {
203
+                $userLang = $this->config->getUserValue($userId, 'core', 'lang', null);
204
+            } else {
205
+                $userLang = null;
206
+            }
207
+        } else {
208
+            $userId = null;
209
+            $userLang = null;
210
+        }
211
+        if ($userLang) {
212
+            $this->requestLanguage = $userLang;
213
+            if ($this->languageExists($appId, $userLang)) {
214
+                return $userLang;
215
+            }
216
+        }
217
+
218
+        // Step 4: Check the request headers
219
+        try {
220
+            // Try to get the language from the Request
221
+            $lang = $this->getLanguageFromRequest($appId);
222
+            if ($userId !== null && $appId === null && !$userLang) {
223
+                $this->config->setUserValue($userId, 'core', 'lang', $lang);
224
+            }
225
+            return $lang;
226
+        } catch (LanguageNotFoundException $e) {
227
+            // Finding language from request failed fall back to default language
228
+            $defaultLanguage = $this->config->getSystemValue('default_language', false);
229
+            if ($defaultLanguage !== false && $this->languageExists($appId, $defaultLanguage)) {
230
+                return $defaultLanguage;
231
+            }
232
+        }
233
+
234
+        // Step 5: fall back to English
235
+        return 'en';
236
+    }
237
+
238
+    public function findGenericLanguage(?string $appId = null): string {
239
+        // Step 1: Forced language always has precedence over anything else
240
+        $forcedLanguage = $this->cleanLanguage($this->request->getParam('forceLanguage')) ?? $this->config->getSystemValue('force_language', false);
241
+        if ($forcedLanguage !== false) {
242
+            return $forcedLanguage;
243
+        }
244
+
245
+        // Step 2: Check if we have a default language
246
+        $defaultLanguage = $this->config->getSystemValue('default_language', false);
247
+        if ($defaultLanguage !== false && $this->languageExists($appId, $defaultLanguage)) {
248
+            return $defaultLanguage;
249
+        }
250
+
251
+        // Step 3: fall back to English
252
+        return 'en';
253
+    }
254
+
255
+    /**
256
+     * find the best locale
257
+     *
258
+     * @param string $lang
259
+     * @return null|string
260
+     */
261
+    public function findLocale($lang = null) {
262
+        $forceLocale = $this->config->getSystemValue('force_locale', false);
263
+        if (is_string($forceLocale) && $this->localeExists($forceLocale)) {
264
+            return $forceLocale;
265
+        }
266
+
267
+        if ($this->config->getSystemValueBool('installed', false)) {
268
+            $userId = $this->userSession->getUser() !== null ? $this->userSession->getUser()->getUID() :  null;
269
+            $userLocale = null;
270
+            if ($userId !== null) {
271
+                $userLocale = $this->config->getUserValue($userId, 'core', 'locale', null);
272
+            }
273
+        } else {
274
+            $userId = null;
275
+            $userLocale = null;
276
+        }
277
+
278
+        if ($userLocale && $this->localeExists($userLocale)) {
279
+            return $userLocale;
280
+        }
281
+
282
+        // Default : use system default locale
283
+        $defaultLocale = $this->config->getSystemValue('default_locale', false);
284
+        if ($defaultLocale !== false && $this->localeExists($defaultLocale)) {
285
+            return $defaultLocale;
286
+        }
287
+
288
+        // If no user locale set, use lang as locale
289
+        if ($lang !== null && $this->localeExists($lang)) {
290
+            return $lang;
291
+        }
292
+
293
+        // At last, return USA
294
+        return 'en_US';
295
+    }
296
+
297
+    /**
298
+     * find the matching lang from the locale
299
+     *
300
+     * @param string $app
301
+     * @param string $locale
302
+     * @return null|string
303
+     */
304
+    public function findLanguageFromLocale(string $app = 'core', ?string $locale = null) {
305
+        if ($this->languageExists($app, $locale)) {
306
+            return $locale;
307
+        }
308
+
309
+        // Try to split e.g: fr_FR => fr
310
+        $locale = explode('_', $locale)[0];
311
+        if ($this->languageExists($app, $locale)) {
312
+            return $locale;
313
+        }
314
+    }
315
+
316
+    /**
317
+     * Find all available languages for an app
318
+     *
319
+     * @param string|null $app App id or null for core
320
+     * @return string[] an array of available languages
321
+     */
322
+    public function findAvailableLanguages($app = null): array {
323
+        $key = $app;
324
+        if ($key === null) {
325
+            $key = 'null';
326
+        }
327
+
328
+        if ($availableLanguages = $this->cache->get($key)) {
329
+            $this->availableLanguages[$key] = $availableLanguages;
330
+        }
331
+
332
+        // also works with null as key
333
+        if (!empty($this->availableLanguages[$key])) {
334
+            return $this->availableLanguages[$key];
335
+        }
336
+
337
+        $available = ['en']; //english is always available
338
+        $dir = $this->findL10nDir($app);
339
+        if (is_dir($dir)) {
340
+            $files = scandir($dir);
341
+            if ($files !== false) {
342
+                foreach ($files as $file) {
343
+                    if (str_ends_with($file, '.json') && !str_starts_with($file, 'l10n')) {
344
+                        $available[] = substr($file, 0, -5);
345
+                    }
346
+                }
347
+            }
348
+        }
349
+
350
+        // merge with translations from theme
351
+        $theme = $this->config->getSystemValueString('theme');
352
+        if (!empty($theme)) {
353
+            $themeDir = $this->serverRoot . '/themes/' . $theme . substr($dir, strlen($this->serverRoot));
354
+
355
+            if (is_dir($themeDir)) {
356
+                $files = scandir($themeDir);
357
+                if ($files !== false) {
358
+                    foreach ($files as $file) {
359
+                        if (str_ends_with($file, '.json') && !str_starts_with($file, 'l10n')) {
360
+                            $available[] = substr($file, 0, -5);
361
+                        }
362
+                    }
363
+                }
364
+            }
365
+        }
366
+
367
+        $this->availableLanguages[$key] = $available;
368
+        $this->cache->set($key, $available, 60);
369
+        return $available;
370
+    }
371
+
372
+    /**
373
+     * @return array|mixed
374
+     */
375
+    public function findAvailableLocales() {
376
+        if (!empty($this->availableLocales)) {
377
+            return $this->availableLocales;
378
+        }
379
+
380
+        $localeData = file_get_contents(\OC::$SERVERROOT . '/resources/locales.json');
381
+        $this->availableLocales = \json_decode($localeData, true);
382
+
383
+        return $this->availableLocales;
384
+    }
385
+
386
+    /**
387
+     * @param string|null $app App id or null for core
388
+     * @param string $lang
389
+     * @return bool
390
+     */
391
+    public function languageExists($app, $lang) {
392
+        if ($lang === 'en') { //english is always available
393
+            return true;
394
+        }
395
+
396
+        $languages = $this->findAvailableLanguages($app);
397
+        return in_array($lang, $languages);
398
+    }
399
+
400
+    public function getLanguageDirection(string $language): string {
401
+        if (in_array($language, self::RTL_LANGUAGES, true)) {
402
+            return 'rtl';
403
+        }
404
+
405
+        return 'ltr';
406
+    }
407
+
408
+    public function getLanguageIterator(?IUser $user = null): ILanguageIterator {
409
+        $user = $user ?? $this->userSession->getUser();
410
+        if ($user === null) {
411
+            throw new \RuntimeException('Failed to get an IUser instance');
412
+        }
413
+        return new LanguageIterator($user, $this->config);
414
+    }
415
+
416
+    /**
417
+     * Return the language to use when sending something to a user
418
+     *
419
+     * @param IUser|null $user
420
+     * @return string
421
+     * @since 20.0.0
422
+     */
423
+    public function getUserLanguage(?IUser $user = null): string {
424
+        $language = $this->config->getSystemValue('force_language', false);
425
+        if ($language !== false) {
426
+            return $language;
427
+        }
428
+
429
+        if ($user instanceof IUser) {
430
+            $language = $this->config->getUserValue($user->getUID(), 'core', 'lang', null);
431
+            if ($language !== null) {
432
+                return $language;
433
+            }
434
+
435
+            $forcedLanguage = $this->cleanLanguage($this->request->getParam('forceLanguage'));
436
+            if ($forcedLanguage !== null) {
437
+                return $forcedLanguage;
438
+            }
439
+
440
+            // Use language from request
441
+            if ($this->userSession->getUser() instanceof IUser
442
+                && $user->getUID() === $this->userSession->getUser()->getUID()) {
443
+                try {
444
+                    return $this->getLanguageFromRequest();
445
+                } catch (LanguageNotFoundException $e) {
446
+                }
447
+            }
448
+        }
449
+
450
+        return $this->cleanLanguage($this->request->getParam('forceLanguage')) ?? $this->config->getSystemValueString('default_language', 'en');
451
+    }
452
+
453
+    /**
454
+     * @param string $locale
455
+     * @return bool
456
+     */
457
+    public function localeExists($locale) {
458
+        if ($locale === 'en') { //english is always available
459
+            return true;
460
+        }
461
+
462
+        if ($this->localeCache === []) {
463
+            $locales = $this->findAvailableLocales();
464
+            foreach ($locales as $l) {
465
+                $this->localeCache[$l['code']] = true;
466
+            }
467
+        }
468
+
469
+        return isset($this->localeCache[$locale]);
470
+    }
471
+
472
+    /**
473
+     * @throws LanguageNotFoundException
474
+     */
475
+    private function getLanguageFromRequest(?string $app = null): string {
476
+        $header = $this->cleanLanguage($this->request->getHeader('ACCEPT_LANGUAGE'));
477
+        if ($header !== '') {
478
+            $available = $this->findAvailableLanguages($app);
479
+
480
+            // E.g. make sure that 'de' is before 'de_DE'.
481
+            sort($available);
482
+
483
+            $preferences = preg_split('/,\s*/', strtolower($header));
484
+            foreach ($preferences as $preference) {
485
+                [$preferred_language] = explode(';', $preference);
486
+                $preferred_language = str_replace('-', '_', $preferred_language);
487
+
488
+                $preferred_language_parts = explode('_', $preferred_language);
489
+                foreach ($available as $available_language) {
490
+                    if ($preferred_language === strtolower($available_language)) {
491
+                        return $this->respectDefaultLanguage($app, $available_language);
492
+                    }
493
+                    if (strtolower($available_language) === $preferred_language_parts[0] . '_' . end($preferred_language_parts)) {
494
+                        return $available_language;
495
+                    }
496
+                }
497
+
498
+                // Fallback from de_De to de
499
+                foreach ($available as $available_language) {
500
+                    if ($preferred_language_parts[0] === $available_language) {
501
+                        return $available_language;
502
+                    }
503
+                }
504
+            }
505
+        }
506
+
507
+        throw new LanguageNotFoundException();
508
+    }
509
+
510
+    /**
511
+     * if default language is set to de_DE (formal German) this should be
512
+     * preferred to 'de' (non-formal German) if possible
513
+     */
514
+    protected function respectDefaultLanguage(?string $app, string $lang): string {
515
+        $result = $lang;
516
+        $defaultLanguage = $this->config->getSystemValue('default_language', false);
517
+
518
+        // use formal version of german ("Sie" instead of "Du") if the default
519
+        // language is set to 'de_DE' if possible
520
+        if (
521
+            is_string($defaultLanguage)
522
+            && strtolower($lang) === 'de'
523
+            && strtolower($defaultLanguage) === 'de_de'
524
+            && $this->languageExists($app, 'de_DE')
525
+        ) {
526
+            $result = 'de_DE';
527
+        }
528
+
529
+        return $result;
530
+    }
531
+
532
+    /**
533
+     * Checks if $sub is a subdirectory of $parent
534
+     *
535
+     * @param string $sub
536
+     * @param string $parent
537
+     * @return bool
538
+     */
539
+    private function isSubDirectory($sub, $parent) {
540
+        // Check whether $sub contains no ".."
541
+        if (str_contains($sub, '..')) {
542
+            return false;
543
+        }
544
+
545
+        // Check whether $sub is a subdirectory of $parent
546
+        if (str_starts_with($sub, $parent)) {
547
+            return true;
548
+        }
549
+
550
+        return false;
551
+    }
552
+
553
+    /**
554
+     * Get a list of language files that should be loaded
555
+     *
556
+     * @return string[]
557
+     */
558
+    private function getL10nFilesForApp(string $app, string $lang): array {
559
+        $languageFiles = [];
560
+
561
+        $i18nDir = $this->findL10nDir($app);
562
+        $transFile = strip_tags($i18nDir) . strip_tags($lang) . '.json';
563
+
564
+        if (($this->isSubDirectory($transFile, $this->serverRoot . '/core/l10n/')
565
+                || $this->isSubDirectory($transFile, $this->serverRoot . '/lib/l10n/')
566
+                || $this->isSubDirectory($transFile, $this->appManager->getAppPath($app) . '/l10n/'))
567
+            && file_exists($transFile)
568
+        ) {
569
+            // load the translations file
570
+            $languageFiles[] = $transFile;
571
+        }
572
+
573
+        // merge with translations from theme
574
+        $theme = $this->config->getSystemValueString('theme');
575
+        if (!empty($theme)) {
576
+            $transFile = $this->serverRoot . '/themes/' . $theme . substr($transFile, strlen($this->serverRoot));
577
+            if (file_exists($transFile)) {
578
+                $languageFiles[] = $transFile;
579
+            }
580
+        }
581
+
582
+        return $languageFiles;
583
+    }
584
+
585
+    /**
586
+     * find the l10n directory
587
+     *
588
+     * @param string $app App id or empty string for core
589
+     * @return string directory
590
+     */
591
+    protected function findL10nDir($app = null) {
592
+        if (in_array($app, ['core', 'lib'])) {
593
+            if (file_exists($this->serverRoot . '/' . $app . '/l10n/')) {
594
+                return $this->serverRoot . '/' . $app . '/l10n/';
595
+            }
596
+        } elseif ($app) {
597
+            try {
598
+                return $this->appManager->getAppPath($app) . '/l10n/';
599
+            } catch (AppPathNotFoundException) {
600
+                /* App not found, continue */
601
+            }
602
+        }
603
+        return $this->serverRoot . '/core/l10n/';
604
+    }
605
+
606
+    /**
607
+     * @inheritDoc
608
+     */
609
+    public function getLanguages(): array {
610
+        $forceLanguage = $this->config->getSystemValue('force_language', false);
611
+        if ($forceLanguage !== false) {
612
+            $l = $this->get('lib', $forceLanguage);
613
+            $potentialName = $l->t('__language_name__');
614
+
615
+            return [
616
+                'commonLanguages' => [[
617
+                    'code' => $forceLanguage,
618
+                    'name' => $potentialName,
619
+                ]],
620
+                'otherLanguages' => [],
621
+            ];
622
+        }
623
+
624
+        $languageCodes = $this->findAvailableLanguages();
625
+        $reduceToLanguages = $this->config->getSystemValue('reduce_to_languages', []);
626
+        if (!empty($reduceToLanguages)) {
627
+            $languageCodes = array_intersect($languageCodes, $reduceToLanguages);
628
+        }
629
+
630
+        $commonLanguages = [];
631
+        $otherLanguages = [];
632
+
633
+        foreach ($languageCodes as $lang) {
634
+            $l = $this->get('lib', $lang);
635
+            // TRANSLATORS this is the language name for the language switcher in the personal settings and should be the localized version
636
+            $potentialName = $l->t('__language_name__');
637
+            if ($l->getLanguageCode() === $lang && $potentialName[0] !== '_') { //first check if the language name is in the translation file
638
+                $ln = [
639
+                    'code' => $lang,
640
+                    'name' => $potentialName
641
+                ];
642
+            } elseif ($lang === 'en') {
643
+                $ln = [
644
+                    'code' => $lang,
645
+                    'name' => 'English (US)'
646
+                ];
647
+            } else { //fallback to language code
648
+                $ln = [
649
+                    'code' => $lang,
650
+                    'name' => $lang
651
+                ];
652
+            }
653
+
654
+            // put appropriate languages into appropriate arrays, to print them sorted
655
+            // common languages -> divider -> other languages
656
+            if (in_array($lang, self::COMMON_LANGUAGE_CODES)) {
657
+                $commonLanguages[array_search($lang, self::COMMON_LANGUAGE_CODES)] = $ln;
658
+            } else {
659
+                $otherLanguages[] = $ln;
660
+            }
661
+        }
662
+
663
+        ksort($commonLanguages);
664
+
665
+        // sort now by displayed language not the iso-code
666
+        usort($otherLanguages, function ($a, $b) {
667
+            if ($a['code'] === $a['name'] && $b['code'] !== $b['name']) {
668
+                // If a doesn't have a name, but b does, list b before a
669
+                return 1;
670
+            }
671
+            if ($a['code'] !== $a['name'] && $b['code'] === $b['name']) {
672
+                // If a does have a name, but b doesn't, list a before b
673
+                return -1;
674
+            }
675
+            // Otherwise compare the names
676
+            return strcmp($a['name'], $b['name']);
677
+        });
678
+
679
+        return [
680
+            // reset indexes
681
+            'commonLanguages' => array_values($commonLanguages),
682
+            'otherLanguages' => $otherLanguages
683
+        ];
684
+    }
685 685
 }
Please login to merge, or discard this patch.
Spacing   +16 added lines, -16 removed lines patch added patch discarded remove patch
@@ -92,7 +92,7 @@  discard block
 block discarded – undo
92 92
 	 * @return \OCP\IL10N
93 93
 	 */
94 94
 	public function get($app, $lang = null, $locale = null) {
95
-		return new LazyL10N(function () use ($app, $lang, $locale) {
95
+		return new LazyL10N(function() use ($app, $lang, $locale) {
96 96
 			$app = $this->appManager->cleanAppId($app);
97 97
 			$lang = $this->cleanLanguage($lang);
98 98
 			$forceLang = $this->cleanLanguage($this->request->getParam('forceLanguage')) ?? $this->config->getSystemValue('force_language', false);
@@ -198,7 +198,7 @@  discard block
 block discarded – undo
198 198
 		 * @link https://github.com/owncloud/core/issues/21955
199 199
 		 */
200 200
 		if ($this->config->getSystemValueBool('installed', false)) {
201
-			$userId = !is_null($this->userSession->getUser()) ? $this->userSession->getUser()->getUID() :  null;
201
+			$userId = !is_null($this->userSession->getUser()) ? $this->userSession->getUser()->getUID() : null;
202 202
 			if (!is_null($userId)) {
203 203
 				$userLang = $this->config->getUserValue($userId, 'core', 'lang', null);
204 204
 			} else {
@@ -265,7 +265,7 @@  discard block
 block discarded – undo
265 265
 		}
266 266
 
267 267
 		if ($this->config->getSystemValueBool('installed', false)) {
268
-			$userId = $this->userSession->getUser() !== null ? $this->userSession->getUser()->getUID() :  null;
268
+			$userId = $this->userSession->getUser() !== null ? $this->userSession->getUser()->getUID() : null;
269 269
 			$userLocale = null;
270 270
 			if ($userId !== null) {
271 271
 				$userLocale = $this->config->getUserValue($userId, 'core', 'locale', null);
@@ -350,7 +350,7 @@  discard block
 block discarded – undo
350 350
 		// merge with translations from theme
351 351
 		$theme = $this->config->getSystemValueString('theme');
352 352
 		if (!empty($theme)) {
353
-			$themeDir = $this->serverRoot . '/themes/' . $theme . substr($dir, strlen($this->serverRoot));
353
+			$themeDir = $this->serverRoot.'/themes/'.$theme.substr($dir, strlen($this->serverRoot));
354 354
 
355 355
 			if (is_dir($themeDir)) {
356 356
 				$files = scandir($themeDir);
@@ -377,7 +377,7 @@  discard block
 block discarded – undo
377 377
 			return $this->availableLocales;
378 378
 		}
379 379
 
380
-		$localeData = file_get_contents(\OC::$SERVERROOT . '/resources/locales.json');
380
+		$localeData = file_get_contents(\OC::$SERVERROOT.'/resources/locales.json');
381 381
 		$this->availableLocales = \json_decode($localeData, true);
382 382
 
383 383
 		return $this->availableLocales;
@@ -490,7 +490,7 @@  discard block
 block discarded – undo
490 490
 					if ($preferred_language === strtolower($available_language)) {
491 491
 						return $this->respectDefaultLanguage($app, $available_language);
492 492
 					}
493
-					if (strtolower($available_language) === $preferred_language_parts[0] . '_' . end($preferred_language_parts)) {
493
+					if (strtolower($available_language) === $preferred_language_parts[0].'_'.end($preferred_language_parts)) {
494 494
 						return $available_language;
495 495
 					}
496 496
 				}
@@ -559,11 +559,11 @@  discard block
 block discarded – undo
559 559
 		$languageFiles = [];
560 560
 
561 561
 		$i18nDir = $this->findL10nDir($app);
562
-		$transFile = strip_tags($i18nDir) . strip_tags($lang) . '.json';
562
+		$transFile = strip_tags($i18nDir).strip_tags($lang).'.json';
563 563
 
564
-		if (($this->isSubDirectory($transFile, $this->serverRoot . '/core/l10n/')
565
-				|| $this->isSubDirectory($transFile, $this->serverRoot . '/lib/l10n/')
566
-				|| $this->isSubDirectory($transFile, $this->appManager->getAppPath($app) . '/l10n/'))
564
+		if (($this->isSubDirectory($transFile, $this->serverRoot.'/core/l10n/')
565
+				|| $this->isSubDirectory($transFile, $this->serverRoot.'/lib/l10n/')
566
+				|| $this->isSubDirectory($transFile, $this->appManager->getAppPath($app).'/l10n/'))
567 567
 			&& file_exists($transFile)
568 568
 		) {
569 569
 			// load the translations file
@@ -573,7 +573,7 @@  discard block
 block discarded – undo
573 573
 		// merge with translations from theme
574 574
 		$theme = $this->config->getSystemValueString('theme');
575 575
 		if (!empty($theme)) {
576
-			$transFile = $this->serverRoot . '/themes/' . $theme . substr($transFile, strlen($this->serverRoot));
576
+			$transFile = $this->serverRoot.'/themes/'.$theme.substr($transFile, strlen($this->serverRoot));
577 577
 			if (file_exists($transFile)) {
578 578
 				$languageFiles[] = $transFile;
579 579
 			}
@@ -590,17 +590,17 @@  discard block
 block discarded – undo
590 590
 	 */
591 591
 	protected function findL10nDir($app = null) {
592 592
 		if (in_array($app, ['core', 'lib'])) {
593
-			if (file_exists($this->serverRoot . '/' . $app . '/l10n/')) {
594
-				return $this->serverRoot . '/' . $app . '/l10n/';
593
+			if (file_exists($this->serverRoot.'/'.$app.'/l10n/')) {
594
+				return $this->serverRoot.'/'.$app.'/l10n/';
595 595
 			}
596 596
 		} elseif ($app) {
597 597
 			try {
598
-				return $this->appManager->getAppPath($app) . '/l10n/';
598
+				return $this->appManager->getAppPath($app).'/l10n/';
599 599
 			} catch (AppPathNotFoundException) {
600 600
 				/* App not found, continue */
601 601
 			}
602 602
 		}
603
-		return $this->serverRoot . '/core/l10n/';
603
+		return $this->serverRoot.'/core/l10n/';
604 604
 	}
605 605
 
606 606
 	/**
@@ -663,7 +663,7 @@  discard block
 block discarded – undo
663 663
 		ksort($commonLanguages);
664 664
 
665 665
 		// sort now by displayed language not the iso-code
666
-		usort($otherLanguages, function ($a, $b) {
666
+		usort($otherLanguages, function($a, $b) {
667 667
 			if ($a['code'] === $a['name'] && $b['code'] !== $b['name']) {
668 668
 				// If a doesn't have a name, but b does, list b before a
669 669
 				return 1;
Please login to merge, or discard this patch.
tests/lib/L10N/FactoryTest.php 1 patch
Indentation   +754 added lines, -754 removed lines patch added patch discarded remove patch
@@ -25,758 +25,758 @@
 block discarded – undo
25 25
 use Test\TestCase;
26 26
 
27 27
 class FactoryTest extends TestCase {
28
-	/** @var IConfig|MockObject */
29
-	protected $config;
30
-
31
-	/** @var IRequest|MockObject */
32
-	protected $request;
33
-
34
-	/** @var IUserSession|MockObject */
35
-	protected $userSession;
36
-
37
-	/** @var ICacheFactory|MockObject */
38
-	protected $cacheFactory;
39
-
40
-	/** @var string */
41
-	protected $serverRoot;
42
-
43
-	/** @var IAppManager|MockObject */
44
-	protected IAppManager $appManager;
45
-
46
-	protected function setUp(): void {
47
-		parent::setUp();
48
-
49
-		$this->config = $this->createMock(IConfig::class);
50
-		$this->request = $this->createMock(IRequest::class);
51
-		$this->userSession = $this->createMock(IUserSession::class);
52
-		$this->cacheFactory = $this->createMock(ICacheFactory::class);
53
-		$this->appManager = $this->createMock(IAppManager::class);
54
-
55
-		$this->serverRoot = \OC::$SERVERROOT;
56
-
57
-		$this->config
58
-			->method('getSystemValueBool')
59
-			->willReturnMap([
60
-				['installed', false, true],
61
-			]);
62
-	}
63
-
64
-	/**
65
-	 * @param string[] $methods
66
-	 * @param bool $mockRequestGetHeaderMethod
67
-	 *
68
-	 * @return Factory|MockObject
69
-	 */
70
-	protected function getFactory(array $methods = [], $mockRequestGetHeaderMethod = false) {
71
-		if ($mockRequestGetHeaderMethod) {
72
-			$this->request->expects(self::any())
73
-				->method('getHeader')
74
-				->willReturn('');
75
-		}
76
-
77
-		if (!empty($methods)) {
78
-			return $this->getMockBuilder(Factory::class)
79
-				->setConstructorArgs([
80
-					$this->config,
81
-					$this->request,
82
-					$this->userSession,
83
-					$this->cacheFactory,
84
-					$this->serverRoot,
85
-					$this->appManager,
86
-				])
87
-				->onlyMethods($methods)
88
-				->getMock();
89
-		}
90
-
91
-		return new Factory($this->config, $this->request, $this->userSession, $this->cacheFactory, $this->serverRoot, $this->appManager);
92
-	}
93
-
94
-	public static function dataCleanLanguage(): array {
95
-		return [
96
-			'null shortcut' => [null, null],
97
-			'default language' => ['de', 'de'],
98
-			'malicious language' => ['de/../fr', 'defr'],
99
-			'request language' => ['kab;q=0.8,ka;q=0.7,de;q=0.6', 'kab;q=0.8,ka;q=0.7,de;q=0.6'],
100
-		];
101
-	}
102
-
103
-	#[DataProvider('dataCleanLanguage')]
104
-	public function testCleanLanguage(?string $lang, ?string $expected): void {
105
-		$factory = $this->getFactory();
106
-		$this->assertSame($expected, self::invokePrivate($factory, 'cleanLanguage', [$lang]));
107
-	}
108
-
109
-	public static function dataFindAvailableLanguages(): array {
110
-		return [
111
-			[null],
112
-			['files'],
113
-		];
114
-	}
115
-
116
-	public function testFindLanguageWithExistingRequestLanguageAndNoApp(): void {
117
-		$factory = $this->getFactory(['languageExists']);
118
-		$this->invokePrivate($factory, 'requestLanguage', ['de']);
119
-		$factory->expects(self::once())
120
-			->method('languageExists')
121
-			->with(null, 'de')
122
-			->willReturn(true);
123
-
124
-		self::assertSame('de', $factory->findLanguage());
125
-	}
126
-
127
-	public function testFindLanguageWithExistingRequestLanguageAndApp(): void {
128
-		$factory = $this->getFactory(['languageExists']);
129
-		$this->invokePrivate($factory, 'requestLanguage', ['de']);
130
-		$factory->expects(self::once())
131
-			->method('languageExists')
132
-			->with('MyApp', 'de')
133
-			->willReturn(true);
134
-
135
-		self::assertSame('de', $factory->findLanguage('MyApp'));
136
-	}
137
-
138
-	public function testFindLanguageWithNotExistingRequestLanguageAndExistingStoredUserLanguage(): void {
139
-		$factory = $this->getFactory(['languageExists']);
140
-		$this->invokePrivate($factory, 'requestLanguage', ['de']);
141
-		$factory->expects($this->exactly(2))
142
-			->method('languageExists')
143
-			->willReturnMap([
144
-				['MyApp', 'de', false],
145
-				['MyApp', 'jp', true],
146
-			]);
147
-		$this->config
148
-			->expects($this->exactly(1))
149
-			->method('getSystemValue')
150
-			->willReturnMap([
151
-				['force_language', false, false],
152
-			]);
153
-		$user = $this->createMock(IUser::class);
154
-		$user->expects(self::once())
155
-			->method('getUID')
156
-			->willReturn('MyUserUid');
157
-		$this->userSession
158
-			->expects(self::exactly(2))
159
-			->method('getUser')
160
-			->willReturn($user);
161
-		$this->config
162
-			->expects(self::once())
163
-			->method('getUserValue')
164
-			->with('MyUserUid', 'core', 'lang', null)
165
-			->willReturn('jp');
166
-
167
-		self::assertSame('jp', $factory->findLanguage('MyApp'));
168
-	}
169
-
170
-	public function testFindLanguageWithNotExistingRequestLanguageAndNotExistingStoredUserLanguage(): void {
171
-		$factory = $this->getFactory(['languageExists'], true);
172
-		$this->invokePrivate($factory, 'requestLanguage', ['de']);
173
-		$factory->expects($this->exactly(3))
174
-			->method('languageExists')
175
-			->willReturnMap([
176
-				['MyApp', 'de', false],
177
-				['MyApp', 'jp', false],
178
-				['MyApp', 'es', true],
179
-			]);
180
-		$this->config
181
-			->expects($this->exactly(2))
182
-			->method('getSystemValue')
183
-			->willReturnMap([
184
-				['force_language', false, false],
185
-				['default_language', false, 'es']
186
-			]);
187
-		$user = $this->createMock(IUser::class);
188
-		$user->expects(self::once())
189
-			->method('getUID')
190
-			->willReturn('MyUserUid');
191
-		$this->userSession
192
-			->expects(self::exactly(2))
193
-			->method('getUser')
194
-			->willReturn($user);
195
-		$this->config
196
-			->expects(self::once())
197
-			->method('getUserValue')
198
-			->with('MyUserUid', 'core', 'lang', null)
199
-			->willReturn('jp');
200
-
201
-		self::assertSame('es', $factory->findLanguage('MyApp'));
202
-	}
203
-
204
-	public function testFindLanguageWithNotExistingRequestLanguageAndNotExistingStoredUserLanguageAndNotExistingDefault(): void {
205
-		$factory = $this->getFactory(['languageExists'], true);
206
-		$this->invokePrivate($factory, 'requestLanguage', ['de']);
207
-		$factory->expects($this->exactly(3))
208
-			->method('languageExists')
209
-			->willReturnMap([
210
-				['MyApp', 'de', false],
211
-				['MyApp', 'jp', false],
212
-				['MyApp', 'es', false],
213
-			]);
214
-		$this->config
215
-			->expects($this->exactly(2))
216
-			->method('getSystemValue')
217
-			->willReturnMap([
218
-				['force_language', false, false],
219
-				['default_language', false, 'es']
220
-			]);
221
-		$user = $this->createMock(IUser::class);
222
-		$user->expects(self::once())
223
-			->method('getUID')
224
-			->willReturn('MyUserUid');
225
-		$this->userSession
226
-			->expects(self::exactly(2))
227
-			->method('getUser')
228
-			->willReturn($user);
229
-		$this->config
230
-			->expects(self::once())
231
-			->method('getUserValue')
232
-			->with('MyUserUid', 'core', 'lang', null)
233
-			->willReturn('jp');
234
-		$this->config
235
-			->expects(self::never())
236
-			->method('setUserValue');
237
-
238
-		self::assertSame('en', $factory->findLanguage('MyApp'));
239
-	}
240
-
241
-	public function testFindLanguageWithNotExistingRequestLanguageAndNotExistingStoredUserLanguageAndNotExistingDefaultAndNoAppInScope(): void {
242
-		$factory = $this->getFactory(['languageExists'], true);
243
-		$this->invokePrivate($factory, 'requestLanguage', ['de']);
244
-		$factory->expects($this->exactly(3))
245
-			->method('languageExists')
246
-			->willReturnMap([
247
-				['MyApp', 'de', false],
248
-				['MyApp', 'jp', false],
249
-				['MyApp', 'es', false],
250
-			]);
251
-		$this->config
252
-			->expects($this->exactly(2))
253
-			->method('getSystemValue')
254
-			->willReturnMap([
255
-				['force_language', false, false],
256
-				['default_language', false, 'es']
257
-			]);
258
-		$user = $this->createMock(IUser::class);
259
-		$user->expects(self::once())
260
-			->method('getUID')
261
-			->willReturn('MyUserUid');
262
-		$this->userSession
263
-			->expects(self::exactly(2))
264
-			->method('getUser')
265
-			->willReturn($user);
266
-		$this->config
267
-			->expects(self::once())
268
-			->method('getUserValue')
269
-			->with('MyUserUid', 'core', 'lang', null)
270
-			->willReturn('jp');
271
-		$this->config
272
-			->expects(self::never())
273
-			->method('setUserValue')
274
-			->with('MyUserUid', 'core', 'lang', 'en');
275
-
276
-
277
-		self::assertSame('en', $factory->findLanguage('MyApp'));
278
-	}
279
-
280
-	public function testFindLanguageWithForcedLanguage(): void {
281
-		$factory = $this->getFactory(['languageExists']);
282
-		$this->config
283
-			->expects($this->once())
284
-			->method('getSystemValue')
285
-			->with('force_language', false)
286
-			->willReturn('de');
287
-
288
-		$factory->expects($this->once())
289
-			->method('languageExists')
290
-			->with('MyApp', 'de')
291
-			->willReturn(true);
292
-
293
-		self::assertSame('de', $factory->findLanguage('MyApp'));
294
-	}
295
-
296
-	/**
297
-	 * @param string|null $app
298
-	 */
299
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataFindAvailableLanguages')]
300
-	public function testFindAvailableLanguages($app): void {
301
-		$factory = $this->getFactory(['findL10nDir']);
302
-		$factory->expects(self::once())
303
-			->method('findL10nDir')
304
-			->with($app)
305
-			->willReturn(\OC::$SERVERROOT . '/tests/data/l10n/');
306
-
307
-		self::assertEqualsCanonicalizing(['cs', 'de', 'en', 'ru'], $factory->findAvailableLanguages($app));
308
-	}
309
-
310
-	public static function dataLanguageExists(): array {
311
-		return [
312
-			[null, 'en', [], true],
313
-			[null, 'de', [], false],
314
-			[null, 'de', ['ru'], false],
315
-			[null, 'de', ['ru', 'de'], true],
316
-			['files', 'en', [], true],
317
-			['files', 'de', [], false],
318
-			['files', 'de', ['ru'], false],
319
-			['files', 'de', ['de', 'ru'], true],
320
-		];
321
-	}
322
-
323
-	public function testFindAvailableLanguagesWithThemes(): void {
324
-		$this->serverRoot .= '/tests/data';
325
-		$app = 'files';
326
-
327
-		$factory = $this->getFactory(['findL10nDir']);
328
-		$factory->expects(self::once())
329
-			->method('findL10nDir')
330
-			->with($app)
331
-			->willReturn($this->serverRoot . '/apps/files/l10n/');
332
-		$this->config
333
-			->expects(self::once())
334
-			->method('getSystemValueString')
335
-			->with('theme')
336
-			->willReturn('abc');
337
-
338
-		self::assertEqualsCanonicalizing(['en', 'zz'], $factory->findAvailableLanguages($app));
339
-	}
340
-
341
-	/**
342
-	 *
343
-	 * @param string|null $app
344
-	 * @param string $lang
345
-	 * @param string[] $availableLanguages
346
-	 * @param string $expected
347
-	 */
348
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataLanguageExists')]
349
-	public function testLanguageExists($app, $lang, array $availableLanguages, $expected): void {
350
-		$factory = $this->getFactory(['findAvailableLanguages']);
351
-		$factory->expects(($lang === 'en') ? self::never() : self::once())
352
-			->method('findAvailableLanguages')
353
-			->with($app)
354
-			->willReturn($availableLanguages);
355
-
356
-		self::assertSame($expected, $factory->languageExists($app, $lang));
357
-	}
358
-
359
-	public static function dataSetLanguageFromRequest(): array {
360
-		return [
361
-			// Language is available
362
-			[null, 'de', ['de'], 'de'],
363
-			[null, 'de,en', ['de'], 'de'],
364
-			[null, 'de-DE,en-US;q=0.8,en;q=0.6', ['de'], 'de'],
365
-			// Language is not available
366
-			[null, 'de', ['ru'], new LanguageNotFoundException()],
367
-			[null, 'de,en', ['ru', 'en'], 'en'],
368
-			[null, 'de-DE,en-US;q=0.8,en;q=0.6', ['ru', 'en'], 'en'],
369
-
370
-			// Don't fall back from kab (Kabyle) to ka (Georgian) - Unless specifically requested
371
-			[null, 'kab;q=0.8,en;q=0.6', ['ka', 'en'], 'en'],
372
-			[null, 'kab;q=0.8,de;q=0.6', ['ka', 'en', 'de'], 'de'],
373
-			[null, 'kab;q=0.8,de;q=0.7,ka;q=0.6', ['ka', 'en', 'de'], 'de'],
374
-			[null, 'kab;q=0.8,ka;q=0.7,de;q=0.6', ['ka', 'en', 'de'], 'ka'],
375
-
376
-			// Language for app
377
-			['files_pdfviewer', 'de', ['de'], 'de'],
378
-			['files_pdfviewer', 'de,en', ['de'], 'de'],
379
-			['files_pdfviewer', 'de-DE,en-US;q=0.8,en;q=0.6', ['de'], 'de'],
380
-			// Language for app is not available
381
-			['files_pdfviewer', 'de', ['ru'], new LanguageNotFoundException()],
382
-			['files_pdfviewer', 'de,en', ['ru', 'en'], 'en'],
383
-			['files_pdfviewer', 'de-DE,en-US;q=0.8,en;q=0.6', ['ru', 'en'], 'en'],
384
-		];
385
-	}
386
-
387
-	/**
388
-	 *
389
-	 * @param string|null $app
390
-	 * @param string $header
391
-	 * @param string[] $availableLanguages
392
-	 * @param string $expected
393
-	 */
394
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataSetLanguageFromRequest')]
395
-	public function testGetLanguageFromRequest($app, $header, array $availableLanguages, $expected): void {
396
-		$factory = $this->getFactory(['findAvailableLanguages', 'respectDefaultLanguage']);
397
-		$factory->expects(self::once())
398
-			->method('findAvailableLanguages')
399
-			->with($app)
400
-			->willReturn($availableLanguages);
401
-
402
-		$factory->expects(self::any())
403
-			->method('respectDefaultLanguage')->willReturnCallback(function ($app, $lang) {
404
-				return $lang;
405
-			});
406
-
407
-		$this->request->expects(self::once())
408
-			->method('getHeader')
409
-			->with('ACCEPT_LANGUAGE')
410
-			->willReturn($header);
411
-
412
-		if ($expected instanceof LanguageNotFoundException) {
413
-			$this->expectException(LanguageNotFoundException::class);
414
-			self::invokePrivate($factory, 'getLanguageFromRequest', [$app]);
415
-		} else {
416
-			self::assertSame($expected, self::invokePrivate($factory, 'getLanguageFromRequest', [$app]), 'Asserting returned language');
417
-		}
418
-	}
419
-
420
-	public static function dataGetL10nFilesForApp(): array {
421
-		return [
422
-			['', 'de', [\OC::$SERVERROOT . '/core/l10n/de.json']],
423
-			['core', 'ru', [\OC::$SERVERROOT . '/core/l10n/ru.json']],
424
-			['lib', 'ru', [\OC::$SERVERROOT . '/lib/l10n/ru.json']],
425
-			['settings', 'de', [\OC::$SERVERROOT . '/apps/settings/l10n/de.json']],
426
-			['files', 'de', [\OC::$SERVERROOT . '/apps/files/l10n/de.json']],
427
-			['files', '_lang_never_exists_', []],
428
-			['_app_never_exists_', 'de', [\OC::$SERVERROOT . '/core/l10n/de.json']],
429
-		];
430
-	}
431
-
432
-	/**
433
-	 *
434
-	 * @param string $app
435
-	 * @param string $expected
436
-	 */
437
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataGetL10nFilesForApp')]
438
-	public function testGetL10nFilesForApp($app, $lang, $expected): void {
439
-		$factory = $this->getFactory();
440
-		if (in_array($app, ['settings','files'])) {
441
-			$this->appManager
442
-				->method('getAppPath')
443
-				->with($app)
444
-				->willReturn(\OC::$SERVERROOT . '/apps/' . $app);
445
-		} else {
446
-			$this->appManager
447
-				->method('getAppPath')
448
-				->with($app)
449
-				->willThrowException(new AppPathNotFoundException());
450
-		}
451
-		self::assertSame($expected, $this->invokePrivate($factory, 'getL10nFilesForApp', [$app, $lang]));
452
-	}
453
-
454
-	public static function dataFindL10NDir(): array {
455
-		return [
456
-			['', \OC::$SERVERROOT . '/core/l10n/'],
457
-			['core', \OC::$SERVERROOT . '/core/l10n/'],
458
-			['lib', \OC::$SERVERROOT . '/lib/l10n/'],
459
-			['settings', \OC::$SERVERROOT . '/apps/settings/l10n/'],
460
-			['files', \OC::$SERVERROOT . '/apps/files/l10n/'],
461
-			['_app_never_exists_', \OC::$SERVERROOT . '/core/l10n/'],
462
-		];
463
-	}
464
-
465
-	/**
466
-	 *
467
-	 * @param string $app
468
-	 * @param string $expected
469
-	 */
470
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataFindL10NDir')]
471
-	public function testFindL10NDir($app, $expected): void {
472
-		$factory = $this->getFactory();
473
-		if (in_array($app, ['settings','files'])) {
474
-			$this->appManager
475
-				->method('getAppPath')
476
-				->with($app)
477
-				->willReturn(\OC::$SERVERROOT . '/apps/' . $app);
478
-		} else {
479
-			$this->appManager
480
-				->method('getAppPath')
481
-				->with($app)
482
-				->willThrowException(new AppPathNotFoundException());
483
-		}
484
-		self::assertSame($expected, $this->invokePrivate($factory, 'findL10nDir', [$app]));
485
-	}
486
-
487
-	public static function dataFindLanguage(): array {
488
-		return [
489
-			// Not logged in
490
-			[false, [], 'en'],
491
-			[false, ['fr'], 'fr'],
492
-			[false, ['de', 'fr'], 'de'],
493
-			[false, ['nl', 'de', 'fr'], 'de'],
494
-
495
-			[true, [], 'en'],
496
-			[true, ['fr'], 'fr'],
497
-			[true, ['de', 'fr'], 'de'],
498
-			[true, ['nl', 'de', 'fr'], 'nl'],
499
-		];
500
-	}
501
-
502
-	/**
503
-	 *
504
-	 * @param bool $loggedIn
505
-	 * @param array $availableLang
506
-	 * @param string $expected
507
-	 */
508
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataFindLanguage')]
509
-	public function testFindLanguage($loggedIn, $availableLang, $expected): void {
510
-		$userLang = 'nl';
511
-		$browserLang = 'de';
512
-		$defaultLang = 'fr';
513
-
514
-		$this->config->expects(self::any())
515
-			->method('getSystemValue')
516
-			->willReturnCallback(function ($var, $default) use ($defaultLang) {
517
-				if ($var === 'default_language') {
518
-					return $defaultLang;
519
-				} else {
520
-					return $default;
521
-				}
522
-			});
523
-
524
-		if ($loggedIn) {
525
-			$user = $this->createMock(IUser::class);
526
-			$user->expects(self::any())
527
-				->method('getUID')
528
-				->willReturn('MyUserUid');
529
-			$this->userSession
530
-				->expects(self::any())
531
-				->method('getUser')
532
-				->willReturn($user);
533
-			$this->config->expects(self::any())
534
-				->method('getUserValue')
535
-				->with('MyUserUid', 'core', 'lang', null)
536
-				->willReturn($userLang);
537
-		} else {
538
-			$this->userSession
539
-				->expects(self::any())
540
-				->method('getUser')
541
-				->willReturn(null);
542
-		}
543
-
544
-		$this->request->expects(self::any())
545
-			->method('getHeader')
546
-			->with($this->equalTo('ACCEPT_LANGUAGE'))
547
-			->willReturn($browserLang);
548
-
549
-		$factory = $this->getFactory(['languageExists', 'findAvailableLanguages', 'respectDefaultLanguage']);
550
-		$factory->expects(self::any())
551
-			->method('languageExists')
552
-			->willReturnCallback(function ($app, $lang) use ($availableLang) {
553
-				return in_array($lang, $availableLang);
554
-			});
555
-		$factory->expects(self::any())
556
-			->method('findAvailableLanguages')
557
-			->willReturnCallback(function ($app) use ($availableLang) {
558
-				return $availableLang;
559
-			});
560
-		$factory->expects(self::any())
561
-			->method('respectDefaultLanguage')->willReturnCallback(function ($app, $lang) {
562
-				return $lang;
563
-			});
564
-
565
-		$lang = $factory->findLanguage();
566
-
567
-		self::assertSame($expected, $lang);
568
-	}
569
-
570
-	public function testFindGenericLanguageByRequestParam(): void {
571
-		$factory = $this->getFactory();
572
-		$this->request->expects(self::once())
573
-			->method('getParam')
574
-			->with('forceLanguage')
575
-			->willReturn('cz');
576
-
577
-		$lang = $factory->findGenericLanguage();
578
-
579
-		self::assertSame('cz', $lang);
580
-	}
581
-
582
-	public function testFindGenericLanguageByEnforcedLanguage(): void {
583
-		$factory = $this->getFactory();
584
-		$this->request->expects(self::once())
585
-			->method('getParam')
586
-			->with('forceLanguage')
587
-			->willReturn(null);
588
-		$this->config->expects(self::once())
589
-			->method('getSystemValue')
590
-			->with('force_language', false)
591
-			->willReturn('cz');
592
-
593
-		$lang = $factory->findGenericLanguage();
594
-
595
-		self::assertSame('cz', $lang);
596
-	}
597
-
598
-	public function testFindGenericLanguageByDefaultLanguage(): void {
599
-		$factory = $this->getFactory(['languageExists']);
600
-		$this->request->expects(self::once())
601
-			->method('getParam')
602
-			->with('forceLanguage')
603
-			->willReturn(null);
604
-		$this->config->expects(self::exactly(2))
605
-			->method('getSystemValue')
606
-			->willReturnMap([
607
-				['force_language', false, false,],
608
-				['default_language', false, 'cz',],
609
-			]);
610
-		$factory->expects(self::once())
611
-			->method('languageExists')
612
-			->with(null, 'cz')
613
-			->willReturn(true);
614
-
615
-		$lang = $factory->findGenericLanguage();
616
-
617
-		self::assertSame('cz', $lang);
618
-	}
619
-
620
-	public function testFindGenericLanguageByDefaultLanguageNotExists(): void {
621
-		$factory = $this->getFactory(['languageExists']);
622
-		$this->request->expects(self::once())
623
-			->method('getParam')
624
-			->with('forceLanguage')
625
-			->willReturn(null);
626
-		$this->config->expects(self::exactly(2))
627
-			->method('getSystemValue')
628
-			->willReturnMap([
629
-				['force_language', false, false,],
630
-				['default_language', false, 'cz',],
631
-			]);
632
-		$factory->expects(self::once())
633
-			->method('languageExists')
634
-			->with(null, 'cz')
635
-			->willReturn(false);
636
-
637
-		$lang = $factory->findGenericLanguage();
638
-
639
-		self::assertSame('en', $lang);
640
-	}
641
-
642
-	public function testFindGenericLanguageFallback(): void {
643
-		$factory = $this->getFactory();
644
-		$this->request->expects(self::once())
645
-			->method('getParam')
646
-			->with('forceLanguage')
647
-			->willReturn(null);
648
-		$this->config->expects(self::exactly(2))
649
-			->method('getSystemValue')
650
-			->willReturnMap([
651
-				['force_language', false, false,],
652
-				['default_language', false, false,],
653
-			]);
654
-
655
-		$lang = $factory->findGenericLanguage();
656
-
657
-		self::assertSame('en', $lang);
658
-	}
659
-
660
-	public static function dataTestRespectDefaultLanguage(): array {
661
-		return [
662
-			['de', 'de_DE', true, 'de_DE'],
663
-			['de', 'de', true, 'de'],
664
-			['de', false, true, 'de'],
665
-			['fr', 'de_DE', true, 'fr'],
666
-		];
667
-	}
668
-
669
-	/**
670
-	 * test if we respect default language if possible
671
-	 *
672
-	 *
673
-	 * @param string $lang
674
-	 * @param string $defaultLanguage
675
-	 * @param bool $langExists
676
-	 * @param string $expected
677
-	 */
678
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataTestRespectDefaultLanguage')]
679
-	public function testRespectDefaultLanguage($lang, $defaultLanguage, $langExists, $expected): void {
680
-		$factory = $this->getFactory(['languageExists']);
681
-		$factory->expects(self::any())
682
-			->method('languageExists')->willReturn($langExists);
683
-		$this->config->expects(self::any())
684
-			->method('getSystemValue')->with('default_language', false)->willReturn($defaultLanguage);
685
-
686
-		$result = $this->invokePrivate($factory, 'respectDefaultLanguage', ['app', $lang]);
687
-		self::assertSame($expected, $result);
688
-	}
689
-
690
-	public static function dataTestReduceToLanguages(): array {
691
-		return [
692
-			['en', ['en', 'de', 'fr', 'it', 'es'], ['en', 'fr', 'de'], ['en', 'fr', 'de']],
693
-			['en', ['en', 'de', 'fr', 'it', 'es'], ['en', 'de'], ['en', 'de']],
694
-			['en', ['en', 'de', 'fr', 'it', 'es'], [], ['de', 'en', 'es', 'fr', 'it']],
695
-		];
696
-	}
697
-
698
-	/**
699
-	 * test
700
-	 * - if available languages set can be reduced by configuration
701
-	 * - if available languages set is not reduced to an empty set if
702
-	 *   the reduce config is an empty set
703
-	 *
704
-	 *
705
-	 * @param string $lang
706
-	 * @param array $availableLanguages
707
-	 * @param array $reducedLanguageSet
708
-	 * @param array $expected
709
-	 */
710
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataTestReduceToLanguages')]
711
-	public function testReduceLanguagesByConfiguration(string $lang, array $availableLanguages, array $reducedLanguageSet, array $expected): void {
712
-		$factory = $this->getFactory(['findAvailableLanguages', 'languageExists']);
713
-		$factory->expects(self::any())
714
-			->method('languageExists')->willReturn(true);
715
-		$factory->expects(self::any())
716
-			->method('findAvailableLanguages')
717
-			->willReturnCallback(function ($app) use ($availableLanguages) {
718
-				return $availableLanguages;
719
-			});
720
-
721
-		$this->config
722
-			->method('getSystemValue')
723
-			->willReturnMap([
724
-				['force_language', false, false],
725
-				['default_language', false, $lang],
726
-				['reduce_to_languages', [], $reducedLanguageSet]
727
-			]);
728
-
729
-		$result = $this->invokePrivate($factory, 'getLanguages');
730
-		$commonLanguagesCodes = array_map(function ($lang) {
731
-			return $lang['code'];
732
-		}, $result['commonLanguages']);
733
-
734
-		self::assertEqualsCanonicalizing($expected, $commonLanguagesCodes);
735
-	}
736
-
737
-	public static function languageIteratorRequestProvider(): array {
738
-		return [
739
-			[ true, true],
740
-			[ false, true],
741
-			[ false, false],
742
-		];
743
-	}
744
-
745
-	#[\PHPUnit\Framework\Attributes\DataProvider('languageIteratorRequestProvider')]
746
-	public function testGetLanguageIterator(bool $hasSession, bool $mockUser): void {
747
-		$factory = $this->getFactory();
748
-		$user = null;
749
-
750
-		if (!$mockUser) {
751
-			$matcher = $this->userSession->expects(self::once())
752
-				->method('getUser');
753
-
754
-			if ($hasSession) {
755
-				$matcher->willReturn($this->createMock(IUser::class));
756
-			} else {
757
-				$this->expectException(\RuntimeException::class);
758
-			}
759
-		} else {
760
-			$user = $this->createMock(IUser::class);
761
-		}
762
-
763
-		$iterator = $factory->getLanguageIterator($user);
764
-		self::assertInstanceOf(ILanguageIterator::class, $iterator);
765
-	}
766
-
767
-	public static function dataGetLanguageDirection(): array {
768
-		return [
769
-			['en', 'ltr'],
770
-			['de', 'ltr'],
771
-			['fa', 'rtl'],
772
-			['ar', 'rtl']
773
-		];
774
-	}
775
-
776
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataGetLanguageDirection')]
777
-	public function testGetLanguageDirection(string $language, string $expectedDirection) {
778
-		$factory = $this->getFactory();
779
-
780
-		self::assertEquals($expectedDirection, $factory->getLanguageDirection($language));
781
-	}
28
+    /** @var IConfig|MockObject */
29
+    protected $config;
30
+
31
+    /** @var IRequest|MockObject */
32
+    protected $request;
33
+
34
+    /** @var IUserSession|MockObject */
35
+    protected $userSession;
36
+
37
+    /** @var ICacheFactory|MockObject */
38
+    protected $cacheFactory;
39
+
40
+    /** @var string */
41
+    protected $serverRoot;
42
+
43
+    /** @var IAppManager|MockObject */
44
+    protected IAppManager $appManager;
45
+
46
+    protected function setUp(): void {
47
+        parent::setUp();
48
+
49
+        $this->config = $this->createMock(IConfig::class);
50
+        $this->request = $this->createMock(IRequest::class);
51
+        $this->userSession = $this->createMock(IUserSession::class);
52
+        $this->cacheFactory = $this->createMock(ICacheFactory::class);
53
+        $this->appManager = $this->createMock(IAppManager::class);
54
+
55
+        $this->serverRoot = \OC::$SERVERROOT;
56
+
57
+        $this->config
58
+            ->method('getSystemValueBool')
59
+            ->willReturnMap([
60
+                ['installed', false, true],
61
+            ]);
62
+    }
63
+
64
+    /**
65
+     * @param string[] $methods
66
+     * @param bool $mockRequestGetHeaderMethod
67
+     *
68
+     * @return Factory|MockObject
69
+     */
70
+    protected function getFactory(array $methods = [], $mockRequestGetHeaderMethod = false) {
71
+        if ($mockRequestGetHeaderMethod) {
72
+            $this->request->expects(self::any())
73
+                ->method('getHeader')
74
+                ->willReturn('');
75
+        }
76
+
77
+        if (!empty($methods)) {
78
+            return $this->getMockBuilder(Factory::class)
79
+                ->setConstructorArgs([
80
+                    $this->config,
81
+                    $this->request,
82
+                    $this->userSession,
83
+                    $this->cacheFactory,
84
+                    $this->serverRoot,
85
+                    $this->appManager,
86
+                ])
87
+                ->onlyMethods($methods)
88
+                ->getMock();
89
+        }
90
+
91
+        return new Factory($this->config, $this->request, $this->userSession, $this->cacheFactory, $this->serverRoot, $this->appManager);
92
+    }
93
+
94
+    public static function dataCleanLanguage(): array {
95
+        return [
96
+            'null shortcut' => [null, null],
97
+            'default language' => ['de', 'de'],
98
+            'malicious language' => ['de/../fr', 'defr'],
99
+            'request language' => ['kab;q=0.8,ka;q=0.7,de;q=0.6', 'kab;q=0.8,ka;q=0.7,de;q=0.6'],
100
+        ];
101
+    }
102
+
103
+    #[DataProvider('dataCleanLanguage')]
104
+    public function testCleanLanguage(?string $lang, ?string $expected): void {
105
+        $factory = $this->getFactory();
106
+        $this->assertSame($expected, self::invokePrivate($factory, 'cleanLanguage', [$lang]));
107
+    }
108
+
109
+    public static function dataFindAvailableLanguages(): array {
110
+        return [
111
+            [null],
112
+            ['files'],
113
+        ];
114
+    }
115
+
116
+    public function testFindLanguageWithExistingRequestLanguageAndNoApp(): void {
117
+        $factory = $this->getFactory(['languageExists']);
118
+        $this->invokePrivate($factory, 'requestLanguage', ['de']);
119
+        $factory->expects(self::once())
120
+            ->method('languageExists')
121
+            ->with(null, 'de')
122
+            ->willReturn(true);
123
+
124
+        self::assertSame('de', $factory->findLanguage());
125
+    }
126
+
127
+    public function testFindLanguageWithExistingRequestLanguageAndApp(): void {
128
+        $factory = $this->getFactory(['languageExists']);
129
+        $this->invokePrivate($factory, 'requestLanguage', ['de']);
130
+        $factory->expects(self::once())
131
+            ->method('languageExists')
132
+            ->with('MyApp', 'de')
133
+            ->willReturn(true);
134
+
135
+        self::assertSame('de', $factory->findLanguage('MyApp'));
136
+    }
137
+
138
+    public function testFindLanguageWithNotExistingRequestLanguageAndExistingStoredUserLanguage(): void {
139
+        $factory = $this->getFactory(['languageExists']);
140
+        $this->invokePrivate($factory, 'requestLanguage', ['de']);
141
+        $factory->expects($this->exactly(2))
142
+            ->method('languageExists')
143
+            ->willReturnMap([
144
+                ['MyApp', 'de', false],
145
+                ['MyApp', 'jp', true],
146
+            ]);
147
+        $this->config
148
+            ->expects($this->exactly(1))
149
+            ->method('getSystemValue')
150
+            ->willReturnMap([
151
+                ['force_language', false, false],
152
+            ]);
153
+        $user = $this->createMock(IUser::class);
154
+        $user->expects(self::once())
155
+            ->method('getUID')
156
+            ->willReturn('MyUserUid');
157
+        $this->userSession
158
+            ->expects(self::exactly(2))
159
+            ->method('getUser')
160
+            ->willReturn($user);
161
+        $this->config
162
+            ->expects(self::once())
163
+            ->method('getUserValue')
164
+            ->with('MyUserUid', 'core', 'lang', null)
165
+            ->willReturn('jp');
166
+
167
+        self::assertSame('jp', $factory->findLanguage('MyApp'));
168
+    }
169
+
170
+    public function testFindLanguageWithNotExistingRequestLanguageAndNotExistingStoredUserLanguage(): void {
171
+        $factory = $this->getFactory(['languageExists'], true);
172
+        $this->invokePrivate($factory, 'requestLanguage', ['de']);
173
+        $factory->expects($this->exactly(3))
174
+            ->method('languageExists')
175
+            ->willReturnMap([
176
+                ['MyApp', 'de', false],
177
+                ['MyApp', 'jp', false],
178
+                ['MyApp', 'es', true],
179
+            ]);
180
+        $this->config
181
+            ->expects($this->exactly(2))
182
+            ->method('getSystemValue')
183
+            ->willReturnMap([
184
+                ['force_language', false, false],
185
+                ['default_language', false, 'es']
186
+            ]);
187
+        $user = $this->createMock(IUser::class);
188
+        $user->expects(self::once())
189
+            ->method('getUID')
190
+            ->willReturn('MyUserUid');
191
+        $this->userSession
192
+            ->expects(self::exactly(2))
193
+            ->method('getUser')
194
+            ->willReturn($user);
195
+        $this->config
196
+            ->expects(self::once())
197
+            ->method('getUserValue')
198
+            ->with('MyUserUid', 'core', 'lang', null)
199
+            ->willReturn('jp');
200
+
201
+        self::assertSame('es', $factory->findLanguage('MyApp'));
202
+    }
203
+
204
+    public function testFindLanguageWithNotExistingRequestLanguageAndNotExistingStoredUserLanguageAndNotExistingDefault(): void {
205
+        $factory = $this->getFactory(['languageExists'], true);
206
+        $this->invokePrivate($factory, 'requestLanguage', ['de']);
207
+        $factory->expects($this->exactly(3))
208
+            ->method('languageExists')
209
+            ->willReturnMap([
210
+                ['MyApp', 'de', false],
211
+                ['MyApp', 'jp', false],
212
+                ['MyApp', 'es', false],
213
+            ]);
214
+        $this->config
215
+            ->expects($this->exactly(2))
216
+            ->method('getSystemValue')
217
+            ->willReturnMap([
218
+                ['force_language', false, false],
219
+                ['default_language', false, 'es']
220
+            ]);
221
+        $user = $this->createMock(IUser::class);
222
+        $user->expects(self::once())
223
+            ->method('getUID')
224
+            ->willReturn('MyUserUid');
225
+        $this->userSession
226
+            ->expects(self::exactly(2))
227
+            ->method('getUser')
228
+            ->willReturn($user);
229
+        $this->config
230
+            ->expects(self::once())
231
+            ->method('getUserValue')
232
+            ->with('MyUserUid', 'core', 'lang', null)
233
+            ->willReturn('jp');
234
+        $this->config
235
+            ->expects(self::never())
236
+            ->method('setUserValue');
237
+
238
+        self::assertSame('en', $factory->findLanguage('MyApp'));
239
+    }
240
+
241
+    public function testFindLanguageWithNotExistingRequestLanguageAndNotExistingStoredUserLanguageAndNotExistingDefaultAndNoAppInScope(): void {
242
+        $factory = $this->getFactory(['languageExists'], true);
243
+        $this->invokePrivate($factory, 'requestLanguage', ['de']);
244
+        $factory->expects($this->exactly(3))
245
+            ->method('languageExists')
246
+            ->willReturnMap([
247
+                ['MyApp', 'de', false],
248
+                ['MyApp', 'jp', false],
249
+                ['MyApp', 'es', false],
250
+            ]);
251
+        $this->config
252
+            ->expects($this->exactly(2))
253
+            ->method('getSystemValue')
254
+            ->willReturnMap([
255
+                ['force_language', false, false],
256
+                ['default_language', false, 'es']
257
+            ]);
258
+        $user = $this->createMock(IUser::class);
259
+        $user->expects(self::once())
260
+            ->method('getUID')
261
+            ->willReturn('MyUserUid');
262
+        $this->userSession
263
+            ->expects(self::exactly(2))
264
+            ->method('getUser')
265
+            ->willReturn($user);
266
+        $this->config
267
+            ->expects(self::once())
268
+            ->method('getUserValue')
269
+            ->with('MyUserUid', 'core', 'lang', null)
270
+            ->willReturn('jp');
271
+        $this->config
272
+            ->expects(self::never())
273
+            ->method('setUserValue')
274
+            ->with('MyUserUid', 'core', 'lang', 'en');
275
+
276
+
277
+        self::assertSame('en', $factory->findLanguage('MyApp'));
278
+    }
279
+
280
+    public function testFindLanguageWithForcedLanguage(): void {
281
+        $factory = $this->getFactory(['languageExists']);
282
+        $this->config
283
+            ->expects($this->once())
284
+            ->method('getSystemValue')
285
+            ->with('force_language', false)
286
+            ->willReturn('de');
287
+
288
+        $factory->expects($this->once())
289
+            ->method('languageExists')
290
+            ->with('MyApp', 'de')
291
+            ->willReturn(true);
292
+
293
+        self::assertSame('de', $factory->findLanguage('MyApp'));
294
+    }
295
+
296
+    /**
297
+     * @param string|null $app
298
+     */
299
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataFindAvailableLanguages')]
300
+    public function testFindAvailableLanguages($app): void {
301
+        $factory = $this->getFactory(['findL10nDir']);
302
+        $factory->expects(self::once())
303
+            ->method('findL10nDir')
304
+            ->with($app)
305
+            ->willReturn(\OC::$SERVERROOT . '/tests/data/l10n/');
306
+
307
+        self::assertEqualsCanonicalizing(['cs', 'de', 'en', 'ru'], $factory->findAvailableLanguages($app));
308
+    }
309
+
310
+    public static function dataLanguageExists(): array {
311
+        return [
312
+            [null, 'en', [], true],
313
+            [null, 'de', [], false],
314
+            [null, 'de', ['ru'], false],
315
+            [null, 'de', ['ru', 'de'], true],
316
+            ['files', 'en', [], true],
317
+            ['files', 'de', [], false],
318
+            ['files', 'de', ['ru'], false],
319
+            ['files', 'de', ['de', 'ru'], true],
320
+        ];
321
+    }
322
+
323
+    public function testFindAvailableLanguagesWithThemes(): void {
324
+        $this->serverRoot .= '/tests/data';
325
+        $app = 'files';
326
+
327
+        $factory = $this->getFactory(['findL10nDir']);
328
+        $factory->expects(self::once())
329
+            ->method('findL10nDir')
330
+            ->with($app)
331
+            ->willReturn($this->serverRoot . '/apps/files/l10n/');
332
+        $this->config
333
+            ->expects(self::once())
334
+            ->method('getSystemValueString')
335
+            ->with('theme')
336
+            ->willReturn('abc');
337
+
338
+        self::assertEqualsCanonicalizing(['en', 'zz'], $factory->findAvailableLanguages($app));
339
+    }
340
+
341
+    /**
342
+     *
343
+     * @param string|null $app
344
+     * @param string $lang
345
+     * @param string[] $availableLanguages
346
+     * @param string $expected
347
+     */
348
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataLanguageExists')]
349
+    public function testLanguageExists($app, $lang, array $availableLanguages, $expected): void {
350
+        $factory = $this->getFactory(['findAvailableLanguages']);
351
+        $factory->expects(($lang === 'en') ? self::never() : self::once())
352
+            ->method('findAvailableLanguages')
353
+            ->with($app)
354
+            ->willReturn($availableLanguages);
355
+
356
+        self::assertSame($expected, $factory->languageExists($app, $lang));
357
+    }
358
+
359
+    public static function dataSetLanguageFromRequest(): array {
360
+        return [
361
+            // Language is available
362
+            [null, 'de', ['de'], 'de'],
363
+            [null, 'de,en', ['de'], 'de'],
364
+            [null, 'de-DE,en-US;q=0.8,en;q=0.6', ['de'], 'de'],
365
+            // Language is not available
366
+            [null, 'de', ['ru'], new LanguageNotFoundException()],
367
+            [null, 'de,en', ['ru', 'en'], 'en'],
368
+            [null, 'de-DE,en-US;q=0.8,en;q=0.6', ['ru', 'en'], 'en'],
369
+
370
+            // Don't fall back from kab (Kabyle) to ka (Georgian) - Unless specifically requested
371
+            [null, 'kab;q=0.8,en;q=0.6', ['ka', 'en'], 'en'],
372
+            [null, 'kab;q=0.8,de;q=0.6', ['ka', 'en', 'de'], 'de'],
373
+            [null, 'kab;q=0.8,de;q=0.7,ka;q=0.6', ['ka', 'en', 'de'], 'de'],
374
+            [null, 'kab;q=0.8,ka;q=0.7,de;q=0.6', ['ka', 'en', 'de'], 'ka'],
375
+
376
+            // Language for app
377
+            ['files_pdfviewer', 'de', ['de'], 'de'],
378
+            ['files_pdfviewer', 'de,en', ['de'], 'de'],
379
+            ['files_pdfviewer', 'de-DE,en-US;q=0.8,en;q=0.6', ['de'], 'de'],
380
+            // Language for app is not available
381
+            ['files_pdfviewer', 'de', ['ru'], new LanguageNotFoundException()],
382
+            ['files_pdfviewer', 'de,en', ['ru', 'en'], 'en'],
383
+            ['files_pdfviewer', 'de-DE,en-US;q=0.8,en;q=0.6', ['ru', 'en'], 'en'],
384
+        ];
385
+    }
386
+
387
+    /**
388
+     *
389
+     * @param string|null $app
390
+     * @param string $header
391
+     * @param string[] $availableLanguages
392
+     * @param string $expected
393
+     */
394
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataSetLanguageFromRequest')]
395
+    public function testGetLanguageFromRequest($app, $header, array $availableLanguages, $expected): void {
396
+        $factory = $this->getFactory(['findAvailableLanguages', 'respectDefaultLanguage']);
397
+        $factory->expects(self::once())
398
+            ->method('findAvailableLanguages')
399
+            ->with($app)
400
+            ->willReturn($availableLanguages);
401
+
402
+        $factory->expects(self::any())
403
+            ->method('respectDefaultLanguage')->willReturnCallback(function ($app, $lang) {
404
+                return $lang;
405
+            });
406
+
407
+        $this->request->expects(self::once())
408
+            ->method('getHeader')
409
+            ->with('ACCEPT_LANGUAGE')
410
+            ->willReturn($header);
411
+
412
+        if ($expected instanceof LanguageNotFoundException) {
413
+            $this->expectException(LanguageNotFoundException::class);
414
+            self::invokePrivate($factory, 'getLanguageFromRequest', [$app]);
415
+        } else {
416
+            self::assertSame($expected, self::invokePrivate($factory, 'getLanguageFromRequest', [$app]), 'Asserting returned language');
417
+        }
418
+    }
419
+
420
+    public static function dataGetL10nFilesForApp(): array {
421
+        return [
422
+            ['', 'de', [\OC::$SERVERROOT . '/core/l10n/de.json']],
423
+            ['core', 'ru', [\OC::$SERVERROOT . '/core/l10n/ru.json']],
424
+            ['lib', 'ru', [\OC::$SERVERROOT . '/lib/l10n/ru.json']],
425
+            ['settings', 'de', [\OC::$SERVERROOT . '/apps/settings/l10n/de.json']],
426
+            ['files', 'de', [\OC::$SERVERROOT . '/apps/files/l10n/de.json']],
427
+            ['files', '_lang_never_exists_', []],
428
+            ['_app_never_exists_', 'de', [\OC::$SERVERROOT . '/core/l10n/de.json']],
429
+        ];
430
+    }
431
+
432
+    /**
433
+     *
434
+     * @param string $app
435
+     * @param string $expected
436
+     */
437
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataGetL10nFilesForApp')]
438
+    public function testGetL10nFilesForApp($app, $lang, $expected): void {
439
+        $factory = $this->getFactory();
440
+        if (in_array($app, ['settings','files'])) {
441
+            $this->appManager
442
+                ->method('getAppPath')
443
+                ->with($app)
444
+                ->willReturn(\OC::$SERVERROOT . '/apps/' . $app);
445
+        } else {
446
+            $this->appManager
447
+                ->method('getAppPath')
448
+                ->with($app)
449
+                ->willThrowException(new AppPathNotFoundException());
450
+        }
451
+        self::assertSame($expected, $this->invokePrivate($factory, 'getL10nFilesForApp', [$app, $lang]));
452
+    }
453
+
454
+    public static function dataFindL10NDir(): array {
455
+        return [
456
+            ['', \OC::$SERVERROOT . '/core/l10n/'],
457
+            ['core', \OC::$SERVERROOT . '/core/l10n/'],
458
+            ['lib', \OC::$SERVERROOT . '/lib/l10n/'],
459
+            ['settings', \OC::$SERVERROOT . '/apps/settings/l10n/'],
460
+            ['files', \OC::$SERVERROOT . '/apps/files/l10n/'],
461
+            ['_app_never_exists_', \OC::$SERVERROOT . '/core/l10n/'],
462
+        ];
463
+    }
464
+
465
+    /**
466
+     *
467
+     * @param string $app
468
+     * @param string $expected
469
+     */
470
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataFindL10NDir')]
471
+    public function testFindL10NDir($app, $expected): void {
472
+        $factory = $this->getFactory();
473
+        if (in_array($app, ['settings','files'])) {
474
+            $this->appManager
475
+                ->method('getAppPath')
476
+                ->with($app)
477
+                ->willReturn(\OC::$SERVERROOT . '/apps/' . $app);
478
+        } else {
479
+            $this->appManager
480
+                ->method('getAppPath')
481
+                ->with($app)
482
+                ->willThrowException(new AppPathNotFoundException());
483
+        }
484
+        self::assertSame($expected, $this->invokePrivate($factory, 'findL10nDir', [$app]));
485
+    }
486
+
487
+    public static function dataFindLanguage(): array {
488
+        return [
489
+            // Not logged in
490
+            [false, [], 'en'],
491
+            [false, ['fr'], 'fr'],
492
+            [false, ['de', 'fr'], 'de'],
493
+            [false, ['nl', 'de', 'fr'], 'de'],
494
+
495
+            [true, [], 'en'],
496
+            [true, ['fr'], 'fr'],
497
+            [true, ['de', 'fr'], 'de'],
498
+            [true, ['nl', 'de', 'fr'], 'nl'],
499
+        ];
500
+    }
501
+
502
+    /**
503
+     *
504
+     * @param bool $loggedIn
505
+     * @param array $availableLang
506
+     * @param string $expected
507
+     */
508
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataFindLanguage')]
509
+    public function testFindLanguage($loggedIn, $availableLang, $expected): void {
510
+        $userLang = 'nl';
511
+        $browserLang = 'de';
512
+        $defaultLang = 'fr';
513
+
514
+        $this->config->expects(self::any())
515
+            ->method('getSystemValue')
516
+            ->willReturnCallback(function ($var, $default) use ($defaultLang) {
517
+                if ($var === 'default_language') {
518
+                    return $defaultLang;
519
+                } else {
520
+                    return $default;
521
+                }
522
+            });
523
+
524
+        if ($loggedIn) {
525
+            $user = $this->createMock(IUser::class);
526
+            $user->expects(self::any())
527
+                ->method('getUID')
528
+                ->willReturn('MyUserUid');
529
+            $this->userSession
530
+                ->expects(self::any())
531
+                ->method('getUser')
532
+                ->willReturn($user);
533
+            $this->config->expects(self::any())
534
+                ->method('getUserValue')
535
+                ->with('MyUserUid', 'core', 'lang', null)
536
+                ->willReturn($userLang);
537
+        } else {
538
+            $this->userSession
539
+                ->expects(self::any())
540
+                ->method('getUser')
541
+                ->willReturn(null);
542
+        }
543
+
544
+        $this->request->expects(self::any())
545
+            ->method('getHeader')
546
+            ->with($this->equalTo('ACCEPT_LANGUAGE'))
547
+            ->willReturn($browserLang);
548
+
549
+        $factory = $this->getFactory(['languageExists', 'findAvailableLanguages', 'respectDefaultLanguage']);
550
+        $factory->expects(self::any())
551
+            ->method('languageExists')
552
+            ->willReturnCallback(function ($app, $lang) use ($availableLang) {
553
+                return in_array($lang, $availableLang);
554
+            });
555
+        $factory->expects(self::any())
556
+            ->method('findAvailableLanguages')
557
+            ->willReturnCallback(function ($app) use ($availableLang) {
558
+                return $availableLang;
559
+            });
560
+        $factory->expects(self::any())
561
+            ->method('respectDefaultLanguage')->willReturnCallback(function ($app, $lang) {
562
+                return $lang;
563
+            });
564
+
565
+        $lang = $factory->findLanguage();
566
+
567
+        self::assertSame($expected, $lang);
568
+    }
569
+
570
+    public function testFindGenericLanguageByRequestParam(): void {
571
+        $factory = $this->getFactory();
572
+        $this->request->expects(self::once())
573
+            ->method('getParam')
574
+            ->with('forceLanguage')
575
+            ->willReturn('cz');
576
+
577
+        $lang = $factory->findGenericLanguage();
578
+
579
+        self::assertSame('cz', $lang);
580
+    }
581
+
582
+    public function testFindGenericLanguageByEnforcedLanguage(): void {
583
+        $factory = $this->getFactory();
584
+        $this->request->expects(self::once())
585
+            ->method('getParam')
586
+            ->with('forceLanguage')
587
+            ->willReturn(null);
588
+        $this->config->expects(self::once())
589
+            ->method('getSystemValue')
590
+            ->with('force_language', false)
591
+            ->willReturn('cz');
592
+
593
+        $lang = $factory->findGenericLanguage();
594
+
595
+        self::assertSame('cz', $lang);
596
+    }
597
+
598
+    public function testFindGenericLanguageByDefaultLanguage(): void {
599
+        $factory = $this->getFactory(['languageExists']);
600
+        $this->request->expects(self::once())
601
+            ->method('getParam')
602
+            ->with('forceLanguage')
603
+            ->willReturn(null);
604
+        $this->config->expects(self::exactly(2))
605
+            ->method('getSystemValue')
606
+            ->willReturnMap([
607
+                ['force_language', false, false,],
608
+                ['default_language', false, 'cz',],
609
+            ]);
610
+        $factory->expects(self::once())
611
+            ->method('languageExists')
612
+            ->with(null, 'cz')
613
+            ->willReturn(true);
614
+
615
+        $lang = $factory->findGenericLanguage();
616
+
617
+        self::assertSame('cz', $lang);
618
+    }
619
+
620
+    public function testFindGenericLanguageByDefaultLanguageNotExists(): void {
621
+        $factory = $this->getFactory(['languageExists']);
622
+        $this->request->expects(self::once())
623
+            ->method('getParam')
624
+            ->with('forceLanguage')
625
+            ->willReturn(null);
626
+        $this->config->expects(self::exactly(2))
627
+            ->method('getSystemValue')
628
+            ->willReturnMap([
629
+                ['force_language', false, false,],
630
+                ['default_language', false, 'cz',],
631
+            ]);
632
+        $factory->expects(self::once())
633
+            ->method('languageExists')
634
+            ->with(null, 'cz')
635
+            ->willReturn(false);
636
+
637
+        $lang = $factory->findGenericLanguage();
638
+
639
+        self::assertSame('en', $lang);
640
+    }
641
+
642
+    public function testFindGenericLanguageFallback(): void {
643
+        $factory = $this->getFactory();
644
+        $this->request->expects(self::once())
645
+            ->method('getParam')
646
+            ->with('forceLanguage')
647
+            ->willReturn(null);
648
+        $this->config->expects(self::exactly(2))
649
+            ->method('getSystemValue')
650
+            ->willReturnMap([
651
+                ['force_language', false, false,],
652
+                ['default_language', false, false,],
653
+            ]);
654
+
655
+        $lang = $factory->findGenericLanguage();
656
+
657
+        self::assertSame('en', $lang);
658
+    }
659
+
660
+    public static function dataTestRespectDefaultLanguage(): array {
661
+        return [
662
+            ['de', 'de_DE', true, 'de_DE'],
663
+            ['de', 'de', true, 'de'],
664
+            ['de', false, true, 'de'],
665
+            ['fr', 'de_DE', true, 'fr'],
666
+        ];
667
+    }
668
+
669
+    /**
670
+     * test if we respect default language if possible
671
+     *
672
+     *
673
+     * @param string $lang
674
+     * @param string $defaultLanguage
675
+     * @param bool $langExists
676
+     * @param string $expected
677
+     */
678
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataTestRespectDefaultLanguage')]
679
+    public function testRespectDefaultLanguage($lang, $defaultLanguage, $langExists, $expected): void {
680
+        $factory = $this->getFactory(['languageExists']);
681
+        $factory->expects(self::any())
682
+            ->method('languageExists')->willReturn($langExists);
683
+        $this->config->expects(self::any())
684
+            ->method('getSystemValue')->with('default_language', false)->willReturn($defaultLanguage);
685
+
686
+        $result = $this->invokePrivate($factory, 'respectDefaultLanguage', ['app', $lang]);
687
+        self::assertSame($expected, $result);
688
+    }
689
+
690
+    public static function dataTestReduceToLanguages(): array {
691
+        return [
692
+            ['en', ['en', 'de', 'fr', 'it', 'es'], ['en', 'fr', 'de'], ['en', 'fr', 'de']],
693
+            ['en', ['en', 'de', 'fr', 'it', 'es'], ['en', 'de'], ['en', 'de']],
694
+            ['en', ['en', 'de', 'fr', 'it', 'es'], [], ['de', 'en', 'es', 'fr', 'it']],
695
+        ];
696
+    }
697
+
698
+    /**
699
+     * test
700
+     * - if available languages set can be reduced by configuration
701
+     * - if available languages set is not reduced to an empty set if
702
+     *   the reduce config is an empty set
703
+     *
704
+     *
705
+     * @param string $lang
706
+     * @param array $availableLanguages
707
+     * @param array $reducedLanguageSet
708
+     * @param array $expected
709
+     */
710
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataTestReduceToLanguages')]
711
+    public function testReduceLanguagesByConfiguration(string $lang, array $availableLanguages, array $reducedLanguageSet, array $expected): void {
712
+        $factory = $this->getFactory(['findAvailableLanguages', 'languageExists']);
713
+        $factory->expects(self::any())
714
+            ->method('languageExists')->willReturn(true);
715
+        $factory->expects(self::any())
716
+            ->method('findAvailableLanguages')
717
+            ->willReturnCallback(function ($app) use ($availableLanguages) {
718
+                return $availableLanguages;
719
+            });
720
+
721
+        $this->config
722
+            ->method('getSystemValue')
723
+            ->willReturnMap([
724
+                ['force_language', false, false],
725
+                ['default_language', false, $lang],
726
+                ['reduce_to_languages', [], $reducedLanguageSet]
727
+            ]);
728
+
729
+        $result = $this->invokePrivate($factory, 'getLanguages');
730
+        $commonLanguagesCodes = array_map(function ($lang) {
731
+            return $lang['code'];
732
+        }, $result['commonLanguages']);
733
+
734
+        self::assertEqualsCanonicalizing($expected, $commonLanguagesCodes);
735
+    }
736
+
737
+    public static function languageIteratorRequestProvider(): array {
738
+        return [
739
+            [ true, true],
740
+            [ false, true],
741
+            [ false, false],
742
+        ];
743
+    }
744
+
745
+    #[\PHPUnit\Framework\Attributes\DataProvider('languageIteratorRequestProvider')]
746
+    public function testGetLanguageIterator(bool $hasSession, bool $mockUser): void {
747
+        $factory = $this->getFactory();
748
+        $user = null;
749
+
750
+        if (!$mockUser) {
751
+            $matcher = $this->userSession->expects(self::once())
752
+                ->method('getUser');
753
+
754
+            if ($hasSession) {
755
+                $matcher->willReturn($this->createMock(IUser::class));
756
+            } else {
757
+                $this->expectException(\RuntimeException::class);
758
+            }
759
+        } else {
760
+            $user = $this->createMock(IUser::class);
761
+        }
762
+
763
+        $iterator = $factory->getLanguageIterator($user);
764
+        self::assertInstanceOf(ILanguageIterator::class, $iterator);
765
+    }
766
+
767
+    public static function dataGetLanguageDirection(): array {
768
+        return [
769
+            ['en', 'ltr'],
770
+            ['de', 'ltr'],
771
+            ['fa', 'rtl'],
772
+            ['ar', 'rtl']
773
+        ];
774
+    }
775
+
776
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataGetLanguageDirection')]
777
+    public function testGetLanguageDirection(string $language, string $expectedDirection) {
778
+        $factory = $this->getFactory();
779
+
780
+        self::assertEquals($expectedDirection, $factory->getLanguageDirection($language));
781
+    }
782 782
 }
Please login to merge, or discard this patch.