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