Passed
Push — master ( bce7f0...b23934 )
by Blizzz
15:58 queued 11s
created
lib/private/L10N/Factory.php 1 patch
Indentation   +682 added lines, -682 removed lines patch added patch discarded remove patch
@@ -53,686 +53,686 @@
 block discarded – undo
53 53
  */
54 54
 class Factory implements IFactory {
55 55
 
56
-	/** @var string */
57
-	protected $requestLanguage = '';
58
-
59
-	/**
60
-	 * cached instances
61
-	 * @var array Structure: Lang => App => \OCP\IL10N
62
-	 */
63
-	protected $instances = [];
64
-
65
-	/**
66
-	 * @var array Structure: App => string[]
67
-	 */
68
-	protected $availableLanguages = [];
69
-
70
-	/**
71
-	 * @var array
72
-	 */
73
-	protected $localeCache = [];
74
-
75
-	/**
76
-	 * @var array
77
-	 */
78
-	protected $availableLocales = [];
79
-
80
-	/**
81
-	 * @var array Structure: string => callable
82
-	 */
83
-	protected $pluralFunctions = [];
84
-
85
-	public const COMMON_LANGUAGE_CODES = [
86
-		'en', 'es', 'fr', 'de', 'de_DE', 'ja', 'ar', 'ru', 'nl', 'it',
87
-		'pt_BR', 'pt_PT', 'da', 'fi_FI', 'nb_NO', 'sv', 'tr', 'zh_CN', 'ko'
88
-	];
89
-
90
-	/** @var IConfig */
91
-	protected $config;
92
-
93
-	/** @var IRequest */
94
-	protected $request;
95
-
96
-	/** @var IUserSession */
97
-	protected $userSession;
98
-
99
-	/** @var string */
100
-	protected $serverRoot;
101
-
102
-	/**
103
-	 * @param IConfig $config
104
-	 * @param IRequest $request
105
-	 * @param IUserSession $userSession
106
-	 * @param string $serverRoot
107
-	 */
108
-	public function __construct(
109
-		IConfig $config,
110
-		IRequest $request,
111
-		IUserSession $userSession,
112
-		$serverRoot
113
-	) {
114
-		$this->config = $config;
115
-		$this->request = $request;
116
-		$this->userSession = $userSession;
117
-		$this->serverRoot = $serverRoot;
118
-	}
119
-
120
-	/**
121
-	 * Get a language instance
122
-	 *
123
-	 * @param string $app
124
-	 * @param string|null $lang
125
-	 * @param string|null $locale
126
-	 * @return \OCP\IL10N
127
-	 */
128
-	public function get($app, $lang = null, $locale = null) {
129
-		return new LazyL10N(function () use ($app, $lang, $locale) {
130
-			$app = \OC_App::cleanAppId($app);
131
-			if ($lang !== null) {
132
-				$lang = str_replace(['\0', '/', '\\', '..'], '', $lang);
133
-			}
134
-
135
-			$forceLang = $this->config->getSystemValue('force_language', false);
136
-			if (is_string($forceLang)) {
137
-				$lang = $forceLang;
138
-			}
139
-
140
-			$forceLocale = $this->config->getSystemValue('force_locale', false);
141
-			if (is_string($forceLocale)) {
142
-				$locale = $forceLocale;
143
-			}
144
-
145
-			if ($lang === null || !$this->languageExists($app, $lang)) {
146
-				$lang = $this->findLanguage($app);
147
-			}
148
-
149
-			if ($locale === null || !$this->localeExists($locale)) {
150
-				$locale = $this->findLocale($lang);
151
-			}
152
-
153
-			if (!isset($this->instances[$lang][$app])) {
154
-				$this->instances[$lang][$app] = new L10N(
155
-					$this,
156
-					$app,
157
-					$lang,
158
-					$locale,
159
-					$this->getL10nFilesForApp($app, $lang)
160
-				);
161
-			}
162
-
163
-			return $this->instances[$lang][$app];
164
-		});
165
-	}
166
-
167
-	/**
168
-	 * Find the best language
169
-	 *
170
-	 * @param string|null $appId App id or null for core
171
-	 *
172
-	 * @return string language If nothing works it returns 'en'
173
-	 */
174
-	public function findLanguage(?string $appId = null): string {
175
-		// Step 1: Forced language always has precedence over anything else
176
-		$forceLang = $this->config->getSystemValue('force_language', false);
177
-		if (is_string($forceLang)) {
178
-			$this->requestLanguage = $forceLang;
179
-		}
180
-
181
-		// Step 2: Return cached language
182
-		if ($this->requestLanguage !== '' && $this->languageExists($appId, $this->requestLanguage)) {
183
-			return $this->requestLanguage;
184
-		}
185
-
186
-		/**
187
-		 * Step 3: At this point Nextcloud might not yet be installed and thus the lookup
188
-		 * in the preferences table might fail. For this reason we need to check
189
-		 * whether the instance has already been installed
190
-		 *
191
-		 * @link https://github.com/owncloud/core/issues/21955
192
-		 */
193
-		if ($this->config->getSystemValue('installed', false)) {
194
-			$userId = !is_null($this->userSession->getUser()) ? $this->userSession->getUser()->getUID() :  null;
195
-			if (!is_null($userId)) {
196
-				$userLang = $this->config->getUserValue($userId, 'core', 'lang', null);
197
-			} else {
198
-				$userLang = null;
199
-			}
200
-		} else {
201
-			$userId = null;
202
-			$userLang = null;
203
-		}
204
-		if ($userLang) {
205
-			$this->requestLanguage = $userLang;
206
-			if ($this->languageExists($appId, $userLang)) {
207
-				return $userLang;
208
-			}
209
-		}
210
-
211
-		// Step 4: Check the request headers
212
-		try {
213
-			// Try to get the language from the Request
214
-			$lang = $this->getLanguageFromRequest($appId);
215
-			if ($userId !== null && $appId === null && !$userLang) {
216
-				$this->config->setUserValue($userId, 'core', 'lang', $lang);
217
-			}
218
-			return $lang;
219
-		} catch (LanguageNotFoundException $e) {
220
-			// Finding language from request failed fall back to default language
221
-			$defaultLanguage = $this->config->getSystemValue('default_language', false);
222
-			if ($defaultLanguage !== false && $this->languageExists($appId, $defaultLanguage)) {
223
-				return $defaultLanguage;
224
-			}
225
-		}
226
-
227
-		// Step 5: fall back to English
228
-		return 'en';
229
-	}
230
-
231
-	public function findGenericLanguage(string $appId = null): string {
232
-		// Step 1: Forced language always has precedence over anything else
233
-		$forcedLanguage = $this->config->getSystemValue('force_language', false);
234
-		if ($forcedLanguage !== false) {
235
-			return $forcedLanguage;
236
-		}
237
-
238
-		// Step 2: Check if we have a default language
239
-		$defaultLanguage = $this->config->getSystemValue('default_language', false);
240
-		if ($defaultLanguage !== false && $this->languageExists($appId, $defaultLanguage)) {
241
-			return $defaultLanguage;
242
-		}
243
-
244
-		// Step 3.1: Check if Nextcloud is already installed before we try to access user info
245
-		if (!$this->config->getSystemValue('installed', false)) {
246
-			return 'en';
247
-		}
248
-		// Step 3.2: Check the current user (if any) for their preferred language
249
-		$user = $this->userSession->getUser();
250
-		if ($user !== null) {
251
-			$userLang = $this->config->getUserValue($user->getUID(), 'core', 'lang', null);
252
-			if ($userLang !== null) {
253
-				return $userLang;
254
-			}
255
-		}
256
-
257
-		// Step 4: Check the request headers
258
-		try {
259
-			return $this->getLanguageFromRequest($appId);
260
-		} catch (LanguageNotFoundException $e) {
261
-			// Ignore and continue
262
-		}
263
-
264
-		// Step 5: fall back to English
265
-		return 'en';
266
-	}
267
-
268
-	/**
269
-	 * find the best locale
270
-	 *
271
-	 * @param string $lang
272
-	 * @return null|string
273
-	 */
274
-	public function findLocale($lang = null) {
275
-		$forceLocale = $this->config->getSystemValue('force_locale', false);
276
-		if (is_string($forceLocale) && $this->localeExists($forceLocale)) {
277
-			return $forceLocale;
278
-		}
279
-
280
-		if ($this->config->getSystemValue('installed', false)) {
281
-			$userId = null !== $this->userSession->getUser() ? $this->userSession->getUser()->getUID() :  null;
282
-			$userLocale = null;
283
-			if (null !== $userId) {
284
-				$userLocale = $this->config->getUserValue($userId, 'core', 'locale', null);
285
-			}
286
-		} else {
287
-			$userId = null;
288
-			$userLocale = null;
289
-		}
290
-
291
-		if ($userLocale && $this->localeExists($userLocale)) {
292
-			return $userLocale;
293
-		}
294
-
295
-		// Default : use system default locale
296
-		$defaultLocale = $this->config->getSystemValue('default_locale', false);
297
-		if ($defaultLocale !== false && $this->localeExists($defaultLocale)) {
298
-			return $defaultLocale;
299
-		}
300
-
301
-		// If no user locale set, use lang as locale
302
-		if (null !== $lang && $this->localeExists($lang)) {
303
-			return $lang;
304
-		}
305
-
306
-		// At last, return USA
307
-		return 'en_US';
308
-	}
309
-
310
-	/**
311
-	 * find the matching lang from the locale
312
-	 *
313
-	 * @param string $app
314
-	 * @param string $locale
315
-	 * @return null|string
316
-	 */
317
-	public function findLanguageFromLocale(string $app = 'core', string $locale = null) {
318
-		if ($this->languageExists($app, $locale)) {
319
-			return $locale;
320
-		}
321
-
322
-		// Try to split e.g: fr_FR => fr
323
-		$locale = explode('_', $locale)[0];
324
-		if ($this->languageExists($app, $locale)) {
325
-			return $locale;
326
-		}
327
-	}
328
-
329
-	/**
330
-	 * Find all available languages for an app
331
-	 *
332
-	 * @param string|null $app App id or null for core
333
-	 * @return string[] an array of available languages
334
-	 */
335
-	public function findAvailableLanguages($app = null): array {
336
-		$key = $app;
337
-		if ($key === null) {
338
-			$key = 'null';
339
-		}
340
-
341
-		// also works with null as key
342
-		if (!empty($this->availableLanguages[$key])) {
343
-			return $this->availableLanguages[$key];
344
-		}
345
-
346
-		$available = ['en']; //english is always available
347
-		$dir = $this->findL10nDir($app);
348
-		if (is_dir($dir)) {
349
-			$files = scandir($dir);
350
-			if ($files !== false) {
351
-				foreach ($files as $file) {
352
-					if (substr($file, -5) === '.json' && substr($file, 0, 4) !== 'l10n') {
353
-						$available[] = substr($file, 0, -5);
354
-					}
355
-				}
356
-			}
357
-		}
358
-
359
-		// merge with translations from theme
360
-		$theme = $this->config->getSystemValue('theme');
361
-		if (!empty($theme)) {
362
-			$themeDir = $this->serverRoot . '/themes/' . $theme . substr($dir, strlen($this->serverRoot));
363
-
364
-			if (is_dir($themeDir)) {
365
-				$files = scandir($themeDir);
366
-				if ($files !== false) {
367
-					foreach ($files as $file) {
368
-						if (substr($file, -5) === '.json' && substr($file, 0, 4) !== 'l10n') {
369
-							$available[] = substr($file, 0, -5);
370
-						}
371
-					}
372
-				}
373
-			}
374
-		}
375
-
376
-		$this->availableLanguages[$key] = $available;
377
-		return $available;
378
-	}
379
-
380
-	/**
381
-	 * @return array|mixed
382
-	 */
383
-	public function findAvailableLocales() {
384
-		if (!empty($this->availableLocales)) {
385
-			return $this->availableLocales;
386
-		}
387
-
388
-		$localeData = file_get_contents(\OC::$SERVERROOT . '/resources/locales.json');
389
-		$this->availableLocales = \json_decode($localeData, true);
390
-
391
-		return $this->availableLocales;
392
-	}
393
-
394
-	/**
395
-	 * @param string|null $app App id or null for core
396
-	 * @param string $lang
397
-	 * @return bool
398
-	 */
399
-	public function languageExists($app, $lang) {
400
-		if ($lang === 'en') { //english is always available
401
-			return true;
402
-		}
403
-
404
-		$languages = $this->findAvailableLanguages($app);
405
-		return in_array($lang, $languages);
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
-			// Use language from request
436
-			if ($this->userSession->getUser() instanceof IUser &&
437
-				$user->getUID() === $this->userSession->getUser()->getUID()) {
438
-				try {
439
-					return $this->getLanguageFromRequest();
440
-				} catch (LanguageNotFoundException $e) {
441
-				}
442
-			}
443
-		}
444
-
445
-		return $this->config->getSystemValue('default_language', 'en');
446
-	}
447
-
448
-	/**
449
-	 * @param string $locale
450
-	 * @return bool
451
-	 */
452
-	public function localeExists($locale) {
453
-		if ($locale === 'en') { //english is always available
454
-			return true;
455
-		}
456
-
457
-		if ($this->localeCache === []) {
458
-			$locales = $this->findAvailableLocales();
459
-			foreach ($locales as $l) {
460
-				$this->localeCache[$l['code']] = true;
461
-			}
462
-		}
463
-
464
-		return isset($this->localeCache[$locale]);
465
-	}
466
-
467
-	/**
468
-	 * @throws LanguageNotFoundException
469
-	 */
470
-	private function getLanguageFromRequest(?string $app = null): string {
471
-		$header = $this->request->getHeader('ACCEPT_LANGUAGE');
472
-		if ($header !== '') {
473
-			$available = $this->findAvailableLanguages($app);
474
-
475
-			// E.g. make sure that 'de' is before 'de_DE'.
476
-			sort($available);
477
-
478
-			$preferences = preg_split('/,\s*/', strtolower($header));
479
-			foreach ($preferences as $preference) {
480
-				[$preferred_language] = explode(';', $preference);
481
-				$preferred_language = str_replace('-', '_', $preferred_language);
482
-
483
-				foreach ($available as $available_language) {
484
-					if ($preferred_language === strtolower($available_language)) {
485
-						return $this->respectDefaultLanguage($app, $available_language);
486
-					}
487
-				}
488
-
489
-				// Fallback from de_De to de
490
-				foreach ($available as $available_language) {
491
-					if (substr($preferred_language, 0, 2) === $available_language) {
492
-						return $available_language;
493
-					}
494
-				}
495
-			}
496
-		}
497
-
498
-		throw new LanguageNotFoundException();
499
-	}
500
-
501
-	/**
502
-	 * if default language is set to de_DE (formal German) this should be
503
-	 * preferred to 'de' (non-formal German) if possible
504
-	 */
505
-	protected function respectDefaultLanguage(?string $app, string $lang): string {
506
-		$result = $lang;
507
-		$defaultLanguage = $this->config->getSystemValue('default_language', false);
508
-
509
-		// use formal version of german ("Sie" instead of "Du") if the default
510
-		// language is set to 'de_DE' if possible
511
-		if (
512
-			is_string($defaultLanguage) &&
513
-			strtolower($lang) === 'de' &&
514
-			strtolower($defaultLanguage) === 'de_de' &&
515
-			$this->languageExists($app, 'de_DE')
516
-		) {
517
-			$result = 'de_DE';
518
-		}
519
-
520
-		return $result;
521
-	}
522
-
523
-	/**
524
-	 * Checks if $sub is a subdirectory of $parent
525
-	 *
526
-	 * @param string $sub
527
-	 * @param string $parent
528
-	 * @return bool
529
-	 */
530
-	private function isSubDirectory($sub, $parent) {
531
-		// Check whether $sub contains no ".."
532
-		if (strpos($sub, '..') !== false) {
533
-			return false;
534
-		}
535
-
536
-		// Check whether $sub is a subdirectory of $parent
537
-		if (strpos($sub, $parent) === 0) {
538
-			return true;
539
-		}
540
-
541
-		return false;
542
-	}
543
-
544
-	/**
545
-	 * Get a list of language files that should be loaded
546
-	 *
547
-	 * @param string $app
548
-	 * @param string $lang
549
-	 * @return string[]
550
-	 */
551
-	// FIXME This method is only public, until OC_L10N does not need it anymore,
552
-	// FIXME This is also the reason, why it is not in the public interface
553
-	public function getL10nFilesForApp($app, $lang) {
554
-		$languageFiles = [];
555
-
556
-		$i18nDir = $this->findL10nDir($app);
557
-		$transFile = strip_tags($i18nDir) . strip_tags($lang) . '.json';
558
-
559
-		if (($this->isSubDirectory($transFile, $this->serverRoot . '/core/l10n/')
560
-				|| $this->isSubDirectory($transFile, $this->serverRoot . '/lib/l10n/')
561
-				|| $this->isSubDirectory($transFile, \OC_App::getAppPath($app) . '/l10n/'))
562
-			&& file_exists($transFile)
563
-		) {
564
-			// load the translations file
565
-			$languageFiles[] = $transFile;
566
-		}
567
-
568
-		// merge with translations from theme
569
-		$theme = $this->config->getSystemValue('theme');
570
-		if (!empty($theme)) {
571
-			$transFile = $this->serverRoot . '/themes/' . $theme . substr($transFile, strlen($this->serverRoot));
572
-			if (file_exists($transFile)) {
573
-				$languageFiles[] = $transFile;
574
-			}
575
-		}
576
-
577
-		return $languageFiles;
578
-	}
579
-
580
-	/**
581
-	 * find the l10n directory
582
-	 *
583
-	 * @param string $app App id or empty string for core
584
-	 * @return string directory
585
-	 */
586
-	protected function findL10nDir($app = null) {
587
-		if (in_array($app, ['core', 'lib'])) {
588
-			if (file_exists($this->serverRoot . '/' . $app . '/l10n/')) {
589
-				return $this->serverRoot . '/' . $app . '/l10n/';
590
-			}
591
-		} elseif ($app && \OC_App::getAppPath($app) !== false) {
592
-			// Check if the app is in the app folder
593
-			return \OC_App::getAppPath($app) . '/l10n/';
594
-		}
595
-		return $this->serverRoot . '/core/l10n/';
596
-	}
597
-
598
-
599
-	/**
600
-	 * Creates a function from the plural string
601
-	 *
602
-	 * Parts of the code is copied from Habari:
603
-	 * https://github.com/habari/system/blob/master/classes/locale.php
604
-	 * @param string $string
605
-	 * @return string
606
-	 */
607
-	public function createPluralFunction($string) {
608
-		if (isset($this->pluralFunctions[$string])) {
609
-			return $this->pluralFunctions[$string];
610
-		}
611
-
612
-		if (preg_match('/^\s*nplurals\s*=\s*(\d+)\s*;\s*plural=(.*)$/u', $string, $matches)) {
613
-			// sanitize
614
-			$nplurals = preg_replace('/[^0-9]/', '', $matches[1]);
615
-			$plural = preg_replace('#[^n0-9:\(\)\?\|\&=!<>+*/\%-]#', '', $matches[2]);
616
-
617
-			$body = str_replace(
618
-				['plural', 'n', '$n$plurals',],
619
-				['$plural', '$n', '$nplurals',],
620
-				'nplurals=' . $nplurals . '; plural=' . $plural
621
-			);
622
-
623
-			// add parents
624
-			// important since PHP's ternary evaluates from left to right
625
-			$body .= ';';
626
-			$res = '';
627
-			$p = 0;
628
-			$length = strlen($body);
629
-			for ($i = 0; $i < $length; $i++) {
630
-				$ch = $body[$i];
631
-				switch ($ch) {
632
-					case '?':
633
-						$res .= ' ? (';
634
-						$p++;
635
-						break;
636
-					case ':':
637
-						$res .= ') : (';
638
-						break;
639
-					case ';':
640
-						$res .= str_repeat(')', $p) . ';';
641
-						$p = 0;
642
-						break;
643
-					default:
644
-						$res .= $ch;
645
-				}
646
-			}
647
-
648
-			$body = $res . 'return ($plural>=$nplurals?$nplurals-1:$plural);';
649
-			$function = create_function('$n', $body);
650
-			$this->pluralFunctions[$string] = $function;
651
-			return $function;
652
-		} else {
653
-			// default: one plural form for all cases but n==1 (english)
654
-			$function = create_function(
655
-				'$n',
656
-				'$nplurals=2;$plural=($n==1?0:1);return ($plural>=$nplurals?$nplurals-1:$plural);'
657
-			);
658
-			$this->pluralFunctions[$string] = $function;
659
-			return $function;
660
-		}
661
-	}
662
-
663
-	/**
664
-	 * @inheritDoc
665
-	 */
666
-	public function getLanguages(): array {
667
-		$forceLanguage = $this->config->getSystemValue('force_language', false);
668
-		if ($forceLanguage !== false) {
669
-			$l = $this->get('lib', $forceLanguage);
670
-			$potentialName = $l->t('__language_name__');
671
-
672
-			return [
673
-				'commonLanguages' => [[
674
-					'code' => $forceLanguage,
675
-					'name' => $potentialName,
676
-				]],
677
-				'otherLanguages' => [],
678
-			];
679
-		}
680
-
681
-		$languageCodes = $this->findAvailableLanguages();
682
-
683
-		$commonLanguages = [];
684
-		$otherLanguages = [];
685
-
686
-		foreach ($languageCodes as $lang) {
687
-			$l = $this->get('lib', $lang);
688
-			// TRANSLATORS this is the language name for the language switcher in the personal settings and should be the localized version
689
-			$potentialName = $l->t('__language_name__');
690
-			if ($l->getLanguageCode() === $lang && $potentialName[0] !== '_') { //first check if the language name is in the translation file
691
-				$ln = [
692
-					'code' => $lang,
693
-					'name' => $potentialName
694
-				];
695
-			} elseif ($lang === 'en') {
696
-				$ln = [
697
-					'code' => $lang,
698
-					'name' => 'English (US)'
699
-				];
700
-			} else { //fallback to language code
701
-				$ln = [
702
-					'code' => $lang,
703
-					'name' => $lang
704
-				];
705
-			}
706
-
707
-			// put appropriate languages into appropriate arrays, to print them sorted
708
-			// common languages -> divider -> other languages
709
-			if (in_array($lang, self::COMMON_LANGUAGE_CODES)) {
710
-				$commonLanguages[array_search($lang, self::COMMON_LANGUAGE_CODES)] = $ln;
711
-			} else {
712
-				$otherLanguages[] = $ln;
713
-			}
714
-		}
715
-
716
-		ksort($commonLanguages);
717
-
718
-		// sort now by displayed language not the iso-code
719
-		usort($otherLanguages, function ($a, $b) {
720
-			if ($a['code'] === $a['name'] && $b['code'] !== $b['name']) {
721
-				// If a doesn't have a name, but b does, list b before a
722
-				return 1;
723
-			}
724
-			if ($a['code'] !== $a['name'] && $b['code'] === $b['name']) {
725
-				// If a does have a name, but b doesn't, list a before b
726
-				return -1;
727
-			}
728
-			// Otherwise compare the names
729
-			return strcmp($a['name'], $b['name']);
730
-		});
731
-
732
-		return [
733
-			// reset indexes
734
-			'commonLanguages' => array_values($commonLanguages),
735
-			'otherLanguages' => $otherLanguages
736
-		];
737
-	}
56
+    /** @var string */
57
+    protected $requestLanguage = '';
58
+
59
+    /**
60
+     * cached instances
61
+     * @var array Structure: Lang => App => \OCP\IL10N
62
+     */
63
+    protected $instances = [];
64
+
65
+    /**
66
+     * @var array Structure: App => string[]
67
+     */
68
+    protected $availableLanguages = [];
69
+
70
+    /**
71
+     * @var array
72
+     */
73
+    protected $localeCache = [];
74
+
75
+    /**
76
+     * @var array
77
+     */
78
+    protected $availableLocales = [];
79
+
80
+    /**
81
+     * @var array Structure: string => callable
82
+     */
83
+    protected $pluralFunctions = [];
84
+
85
+    public const COMMON_LANGUAGE_CODES = [
86
+        'en', 'es', 'fr', 'de', 'de_DE', 'ja', 'ar', 'ru', 'nl', 'it',
87
+        'pt_BR', 'pt_PT', 'da', 'fi_FI', 'nb_NO', 'sv', 'tr', 'zh_CN', 'ko'
88
+    ];
89
+
90
+    /** @var IConfig */
91
+    protected $config;
92
+
93
+    /** @var IRequest */
94
+    protected $request;
95
+
96
+    /** @var IUserSession */
97
+    protected $userSession;
98
+
99
+    /** @var string */
100
+    protected $serverRoot;
101
+
102
+    /**
103
+     * @param IConfig $config
104
+     * @param IRequest $request
105
+     * @param IUserSession $userSession
106
+     * @param string $serverRoot
107
+     */
108
+    public function __construct(
109
+        IConfig $config,
110
+        IRequest $request,
111
+        IUserSession $userSession,
112
+        $serverRoot
113
+    ) {
114
+        $this->config = $config;
115
+        $this->request = $request;
116
+        $this->userSession = $userSession;
117
+        $this->serverRoot = $serverRoot;
118
+    }
119
+
120
+    /**
121
+     * Get a language instance
122
+     *
123
+     * @param string $app
124
+     * @param string|null $lang
125
+     * @param string|null $locale
126
+     * @return \OCP\IL10N
127
+     */
128
+    public function get($app, $lang = null, $locale = null) {
129
+        return new LazyL10N(function () use ($app, $lang, $locale) {
130
+            $app = \OC_App::cleanAppId($app);
131
+            if ($lang !== null) {
132
+                $lang = str_replace(['\0', '/', '\\', '..'], '', $lang);
133
+            }
134
+
135
+            $forceLang = $this->config->getSystemValue('force_language', false);
136
+            if (is_string($forceLang)) {
137
+                $lang = $forceLang;
138
+            }
139
+
140
+            $forceLocale = $this->config->getSystemValue('force_locale', false);
141
+            if (is_string($forceLocale)) {
142
+                $locale = $forceLocale;
143
+            }
144
+
145
+            if ($lang === null || !$this->languageExists($app, $lang)) {
146
+                $lang = $this->findLanguage($app);
147
+            }
148
+
149
+            if ($locale === null || !$this->localeExists($locale)) {
150
+                $locale = $this->findLocale($lang);
151
+            }
152
+
153
+            if (!isset($this->instances[$lang][$app])) {
154
+                $this->instances[$lang][$app] = new L10N(
155
+                    $this,
156
+                    $app,
157
+                    $lang,
158
+                    $locale,
159
+                    $this->getL10nFilesForApp($app, $lang)
160
+                );
161
+            }
162
+
163
+            return $this->instances[$lang][$app];
164
+        });
165
+    }
166
+
167
+    /**
168
+     * Find the best language
169
+     *
170
+     * @param string|null $appId App id or null for core
171
+     *
172
+     * @return string language If nothing works it returns 'en'
173
+     */
174
+    public function findLanguage(?string $appId = null): string {
175
+        // Step 1: Forced language always has precedence over anything else
176
+        $forceLang = $this->config->getSystemValue('force_language', false);
177
+        if (is_string($forceLang)) {
178
+            $this->requestLanguage = $forceLang;
179
+        }
180
+
181
+        // Step 2: Return cached language
182
+        if ($this->requestLanguage !== '' && $this->languageExists($appId, $this->requestLanguage)) {
183
+            return $this->requestLanguage;
184
+        }
185
+
186
+        /**
187
+         * Step 3: At this point Nextcloud might not yet be installed and thus the lookup
188
+         * in the preferences table might fail. For this reason we need to check
189
+         * whether the instance has already been installed
190
+         *
191
+         * @link https://github.com/owncloud/core/issues/21955
192
+         */
193
+        if ($this->config->getSystemValue('installed', false)) {
194
+            $userId = !is_null($this->userSession->getUser()) ? $this->userSession->getUser()->getUID() :  null;
195
+            if (!is_null($userId)) {
196
+                $userLang = $this->config->getUserValue($userId, 'core', 'lang', null);
197
+            } else {
198
+                $userLang = null;
199
+            }
200
+        } else {
201
+            $userId = null;
202
+            $userLang = null;
203
+        }
204
+        if ($userLang) {
205
+            $this->requestLanguage = $userLang;
206
+            if ($this->languageExists($appId, $userLang)) {
207
+                return $userLang;
208
+            }
209
+        }
210
+
211
+        // Step 4: Check the request headers
212
+        try {
213
+            // Try to get the language from the Request
214
+            $lang = $this->getLanguageFromRequest($appId);
215
+            if ($userId !== null && $appId === null && !$userLang) {
216
+                $this->config->setUserValue($userId, 'core', 'lang', $lang);
217
+            }
218
+            return $lang;
219
+        } catch (LanguageNotFoundException $e) {
220
+            // Finding language from request failed fall back to default language
221
+            $defaultLanguage = $this->config->getSystemValue('default_language', false);
222
+            if ($defaultLanguage !== false && $this->languageExists($appId, $defaultLanguage)) {
223
+                return $defaultLanguage;
224
+            }
225
+        }
226
+
227
+        // Step 5: fall back to English
228
+        return 'en';
229
+    }
230
+
231
+    public function findGenericLanguage(string $appId = null): string {
232
+        // Step 1: Forced language always has precedence over anything else
233
+        $forcedLanguage = $this->config->getSystemValue('force_language', false);
234
+        if ($forcedLanguage !== false) {
235
+            return $forcedLanguage;
236
+        }
237
+
238
+        // Step 2: Check if we have a default language
239
+        $defaultLanguage = $this->config->getSystemValue('default_language', false);
240
+        if ($defaultLanguage !== false && $this->languageExists($appId, $defaultLanguage)) {
241
+            return $defaultLanguage;
242
+        }
243
+
244
+        // Step 3.1: Check if Nextcloud is already installed before we try to access user info
245
+        if (!$this->config->getSystemValue('installed', false)) {
246
+            return 'en';
247
+        }
248
+        // Step 3.2: Check the current user (if any) for their preferred language
249
+        $user = $this->userSession->getUser();
250
+        if ($user !== null) {
251
+            $userLang = $this->config->getUserValue($user->getUID(), 'core', 'lang', null);
252
+            if ($userLang !== null) {
253
+                return $userLang;
254
+            }
255
+        }
256
+
257
+        // Step 4: Check the request headers
258
+        try {
259
+            return $this->getLanguageFromRequest($appId);
260
+        } catch (LanguageNotFoundException $e) {
261
+            // Ignore and continue
262
+        }
263
+
264
+        // Step 5: fall back to English
265
+        return 'en';
266
+    }
267
+
268
+    /**
269
+     * find the best locale
270
+     *
271
+     * @param string $lang
272
+     * @return null|string
273
+     */
274
+    public function findLocale($lang = null) {
275
+        $forceLocale = $this->config->getSystemValue('force_locale', false);
276
+        if (is_string($forceLocale) && $this->localeExists($forceLocale)) {
277
+            return $forceLocale;
278
+        }
279
+
280
+        if ($this->config->getSystemValue('installed', false)) {
281
+            $userId = null !== $this->userSession->getUser() ? $this->userSession->getUser()->getUID() :  null;
282
+            $userLocale = null;
283
+            if (null !== $userId) {
284
+                $userLocale = $this->config->getUserValue($userId, 'core', 'locale', null);
285
+            }
286
+        } else {
287
+            $userId = null;
288
+            $userLocale = null;
289
+        }
290
+
291
+        if ($userLocale && $this->localeExists($userLocale)) {
292
+            return $userLocale;
293
+        }
294
+
295
+        // Default : use system default locale
296
+        $defaultLocale = $this->config->getSystemValue('default_locale', false);
297
+        if ($defaultLocale !== false && $this->localeExists($defaultLocale)) {
298
+            return $defaultLocale;
299
+        }
300
+
301
+        // If no user locale set, use lang as locale
302
+        if (null !== $lang && $this->localeExists($lang)) {
303
+            return $lang;
304
+        }
305
+
306
+        // At last, return USA
307
+        return 'en_US';
308
+    }
309
+
310
+    /**
311
+     * find the matching lang from the locale
312
+     *
313
+     * @param string $app
314
+     * @param string $locale
315
+     * @return null|string
316
+     */
317
+    public function findLanguageFromLocale(string $app = 'core', string $locale = null) {
318
+        if ($this->languageExists($app, $locale)) {
319
+            return $locale;
320
+        }
321
+
322
+        // Try to split e.g: fr_FR => fr
323
+        $locale = explode('_', $locale)[0];
324
+        if ($this->languageExists($app, $locale)) {
325
+            return $locale;
326
+        }
327
+    }
328
+
329
+    /**
330
+     * Find all available languages for an app
331
+     *
332
+     * @param string|null $app App id or null for core
333
+     * @return string[] an array of available languages
334
+     */
335
+    public function findAvailableLanguages($app = null): array {
336
+        $key = $app;
337
+        if ($key === null) {
338
+            $key = 'null';
339
+        }
340
+
341
+        // also works with null as key
342
+        if (!empty($this->availableLanguages[$key])) {
343
+            return $this->availableLanguages[$key];
344
+        }
345
+
346
+        $available = ['en']; //english is always available
347
+        $dir = $this->findL10nDir($app);
348
+        if (is_dir($dir)) {
349
+            $files = scandir($dir);
350
+            if ($files !== false) {
351
+                foreach ($files as $file) {
352
+                    if (substr($file, -5) === '.json' && substr($file, 0, 4) !== 'l10n') {
353
+                        $available[] = substr($file, 0, -5);
354
+                    }
355
+                }
356
+            }
357
+        }
358
+
359
+        // merge with translations from theme
360
+        $theme = $this->config->getSystemValue('theme');
361
+        if (!empty($theme)) {
362
+            $themeDir = $this->serverRoot . '/themes/' . $theme . substr($dir, strlen($this->serverRoot));
363
+
364
+            if (is_dir($themeDir)) {
365
+                $files = scandir($themeDir);
366
+                if ($files !== false) {
367
+                    foreach ($files as $file) {
368
+                        if (substr($file, -5) === '.json' && substr($file, 0, 4) !== 'l10n') {
369
+                            $available[] = substr($file, 0, -5);
370
+                        }
371
+                    }
372
+                }
373
+            }
374
+        }
375
+
376
+        $this->availableLanguages[$key] = $available;
377
+        return $available;
378
+    }
379
+
380
+    /**
381
+     * @return array|mixed
382
+     */
383
+    public function findAvailableLocales() {
384
+        if (!empty($this->availableLocales)) {
385
+            return $this->availableLocales;
386
+        }
387
+
388
+        $localeData = file_get_contents(\OC::$SERVERROOT . '/resources/locales.json');
389
+        $this->availableLocales = \json_decode($localeData, true);
390
+
391
+        return $this->availableLocales;
392
+    }
393
+
394
+    /**
395
+     * @param string|null $app App id or null for core
396
+     * @param string $lang
397
+     * @return bool
398
+     */
399
+    public function languageExists($app, $lang) {
400
+        if ($lang === 'en') { //english is always available
401
+            return true;
402
+        }
403
+
404
+        $languages = $this->findAvailableLanguages($app);
405
+        return in_array($lang, $languages);
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
+            // Use language from request
436
+            if ($this->userSession->getUser() instanceof IUser &&
437
+                $user->getUID() === $this->userSession->getUser()->getUID()) {
438
+                try {
439
+                    return $this->getLanguageFromRequest();
440
+                } catch (LanguageNotFoundException $e) {
441
+                }
442
+            }
443
+        }
444
+
445
+        return $this->config->getSystemValue('default_language', 'en');
446
+    }
447
+
448
+    /**
449
+     * @param string $locale
450
+     * @return bool
451
+     */
452
+    public function localeExists($locale) {
453
+        if ($locale === 'en') { //english is always available
454
+            return true;
455
+        }
456
+
457
+        if ($this->localeCache === []) {
458
+            $locales = $this->findAvailableLocales();
459
+            foreach ($locales as $l) {
460
+                $this->localeCache[$l['code']] = true;
461
+            }
462
+        }
463
+
464
+        return isset($this->localeCache[$locale]);
465
+    }
466
+
467
+    /**
468
+     * @throws LanguageNotFoundException
469
+     */
470
+    private function getLanguageFromRequest(?string $app = null): string {
471
+        $header = $this->request->getHeader('ACCEPT_LANGUAGE');
472
+        if ($header !== '') {
473
+            $available = $this->findAvailableLanguages($app);
474
+
475
+            // E.g. make sure that 'de' is before 'de_DE'.
476
+            sort($available);
477
+
478
+            $preferences = preg_split('/,\s*/', strtolower($header));
479
+            foreach ($preferences as $preference) {
480
+                [$preferred_language] = explode(';', $preference);
481
+                $preferred_language = str_replace('-', '_', $preferred_language);
482
+
483
+                foreach ($available as $available_language) {
484
+                    if ($preferred_language === strtolower($available_language)) {
485
+                        return $this->respectDefaultLanguage($app, $available_language);
486
+                    }
487
+                }
488
+
489
+                // Fallback from de_De to de
490
+                foreach ($available as $available_language) {
491
+                    if (substr($preferred_language, 0, 2) === $available_language) {
492
+                        return $available_language;
493
+                    }
494
+                }
495
+            }
496
+        }
497
+
498
+        throw new LanguageNotFoundException();
499
+    }
500
+
501
+    /**
502
+     * if default language is set to de_DE (formal German) this should be
503
+     * preferred to 'de' (non-formal German) if possible
504
+     */
505
+    protected function respectDefaultLanguage(?string $app, string $lang): string {
506
+        $result = $lang;
507
+        $defaultLanguage = $this->config->getSystemValue('default_language', false);
508
+
509
+        // use formal version of german ("Sie" instead of "Du") if the default
510
+        // language is set to 'de_DE' if possible
511
+        if (
512
+            is_string($defaultLanguage) &&
513
+            strtolower($lang) === 'de' &&
514
+            strtolower($defaultLanguage) === 'de_de' &&
515
+            $this->languageExists($app, 'de_DE')
516
+        ) {
517
+            $result = 'de_DE';
518
+        }
519
+
520
+        return $result;
521
+    }
522
+
523
+    /**
524
+     * Checks if $sub is a subdirectory of $parent
525
+     *
526
+     * @param string $sub
527
+     * @param string $parent
528
+     * @return bool
529
+     */
530
+    private function isSubDirectory($sub, $parent) {
531
+        // Check whether $sub contains no ".."
532
+        if (strpos($sub, '..') !== false) {
533
+            return false;
534
+        }
535
+
536
+        // Check whether $sub is a subdirectory of $parent
537
+        if (strpos($sub, $parent) === 0) {
538
+            return true;
539
+        }
540
+
541
+        return false;
542
+    }
543
+
544
+    /**
545
+     * Get a list of language files that should be loaded
546
+     *
547
+     * @param string $app
548
+     * @param string $lang
549
+     * @return string[]
550
+     */
551
+    // FIXME This method is only public, until OC_L10N does not need it anymore,
552
+    // FIXME This is also the reason, why it is not in the public interface
553
+    public function getL10nFilesForApp($app, $lang) {
554
+        $languageFiles = [];
555
+
556
+        $i18nDir = $this->findL10nDir($app);
557
+        $transFile = strip_tags($i18nDir) . strip_tags($lang) . '.json';
558
+
559
+        if (($this->isSubDirectory($transFile, $this->serverRoot . '/core/l10n/')
560
+                || $this->isSubDirectory($transFile, $this->serverRoot . '/lib/l10n/')
561
+                || $this->isSubDirectory($transFile, \OC_App::getAppPath($app) . '/l10n/'))
562
+            && file_exists($transFile)
563
+        ) {
564
+            // load the translations file
565
+            $languageFiles[] = $transFile;
566
+        }
567
+
568
+        // merge with translations from theme
569
+        $theme = $this->config->getSystemValue('theme');
570
+        if (!empty($theme)) {
571
+            $transFile = $this->serverRoot . '/themes/' . $theme . substr($transFile, strlen($this->serverRoot));
572
+            if (file_exists($transFile)) {
573
+                $languageFiles[] = $transFile;
574
+            }
575
+        }
576
+
577
+        return $languageFiles;
578
+    }
579
+
580
+    /**
581
+     * find the l10n directory
582
+     *
583
+     * @param string $app App id or empty string for core
584
+     * @return string directory
585
+     */
586
+    protected function findL10nDir($app = null) {
587
+        if (in_array($app, ['core', 'lib'])) {
588
+            if (file_exists($this->serverRoot . '/' . $app . '/l10n/')) {
589
+                return $this->serverRoot . '/' . $app . '/l10n/';
590
+            }
591
+        } elseif ($app && \OC_App::getAppPath($app) !== false) {
592
+            // Check if the app is in the app folder
593
+            return \OC_App::getAppPath($app) . '/l10n/';
594
+        }
595
+        return $this->serverRoot . '/core/l10n/';
596
+    }
597
+
598
+
599
+    /**
600
+     * Creates a function from the plural string
601
+     *
602
+     * Parts of the code is copied from Habari:
603
+     * https://github.com/habari/system/blob/master/classes/locale.php
604
+     * @param string $string
605
+     * @return string
606
+     */
607
+    public function createPluralFunction($string) {
608
+        if (isset($this->pluralFunctions[$string])) {
609
+            return $this->pluralFunctions[$string];
610
+        }
611
+
612
+        if (preg_match('/^\s*nplurals\s*=\s*(\d+)\s*;\s*plural=(.*)$/u', $string, $matches)) {
613
+            // sanitize
614
+            $nplurals = preg_replace('/[^0-9]/', '', $matches[1]);
615
+            $plural = preg_replace('#[^n0-9:\(\)\?\|\&=!<>+*/\%-]#', '', $matches[2]);
616
+
617
+            $body = str_replace(
618
+                ['plural', 'n', '$n$plurals',],
619
+                ['$plural', '$n', '$nplurals',],
620
+                'nplurals=' . $nplurals . '; plural=' . $plural
621
+            );
622
+
623
+            // add parents
624
+            // important since PHP's ternary evaluates from left to right
625
+            $body .= ';';
626
+            $res = '';
627
+            $p = 0;
628
+            $length = strlen($body);
629
+            for ($i = 0; $i < $length; $i++) {
630
+                $ch = $body[$i];
631
+                switch ($ch) {
632
+                    case '?':
633
+                        $res .= ' ? (';
634
+                        $p++;
635
+                        break;
636
+                    case ':':
637
+                        $res .= ') : (';
638
+                        break;
639
+                    case ';':
640
+                        $res .= str_repeat(')', $p) . ';';
641
+                        $p = 0;
642
+                        break;
643
+                    default:
644
+                        $res .= $ch;
645
+                }
646
+            }
647
+
648
+            $body = $res . 'return ($plural>=$nplurals?$nplurals-1:$plural);';
649
+            $function = create_function('$n', $body);
650
+            $this->pluralFunctions[$string] = $function;
651
+            return $function;
652
+        } else {
653
+            // default: one plural form for all cases but n==1 (english)
654
+            $function = create_function(
655
+                '$n',
656
+                '$nplurals=2;$plural=($n==1?0:1);return ($plural>=$nplurals?$nplurals-1:$plural);'
657
+            );
658
+            $this->pluralFunctions[$string] = $function;
659
+            return $function;
660
+        }
661
+    }
662
+
663
+    /**
664
+     * @inheritDoc
665
+     */
666
+    public function getLanguages(): array {
667
+        $forceLanguage = $this->config->getSystemValue('force_language', false);
668
+        if ($forceLanguage !== false) {
669
+            $l = $this->get('lib', $forceLanguage);
670
+            $potentialName = $l->t('__language_name__');
671
+
672
+            return [
673
+                'commonLanguages' => [[
674
+                    'code' => $forceLanguage,
675
+                    'name' => $potentialName,
676
+                ]],
677
+                'otherLanguages' => [],
678
+            ];
679
+        }
680
+
681
+        $languageCodes = $this->findAvailableLanguages();
682
+
683
+        $commonLanguages = [];
684
+        $otherLanguages = [];
685
+
686
+        foreach ($languageCodes as $lang) {
687
+            $l = $this->get('lib', $lang);
688
+            // TRANSLATORS this is the language name for the language switcher in the personal settings and should be the localized version
689
+            $potentialName = $l->t('__language_name__');
690
+            if ($l->getLanguageCode() === $lang && $potentialName[0] !== '_') { //first check if the language name is in the translation file
691
+                $ln = [
692
+                    'code' => $lang,
693
+                    'name' => $potentialName
694
+                ];
695
+            } elseif ($lang === 'en') {
696
+                $ln = [
697
+                    'code' => $lang,
698
+                    'name' => 'English (US)'
699
+                ];
700
+            } else { //fallback to language code
701
+                $ln = [
702
+                    'code' => $lang,
703
+                    'name' => $lang
704
+                ];
705
+            }
706
+
707
+            // put appropriate languages into appropriate arrays, to print them sorted
708
+            // common languages -> divider -> other languages
709
+            if (in_array($lang, self::COMMON_LANGUAGE_CODES)) {
710
+                $commonLanguages[array_search($lang, self::COMMON_LANGUAGE_CODES)] = $ln;
711
+            } else {
712
+                $otherLanguages[] = $ln;
713
+            }
714
+        }
715
+
716
+        ksort($commonLanguages);
717
+
718
+        // sort now by displayed language not the iso-code
719
+        usort($otherLanguages, function ($a, $b) {
720
+            if ($a['code'] === $a['name'] && $b['code'] !== $b['name']) {
721
+                // If a doesn't have a name, but b does, list b before a
722
+                return 1;
723
+            }
724
+            if ($a['code'] !== $a['name'] && $b['code'] === $b['name']) {
725
+                // If a does have a name, but b doesn't, list a before b
726
+                return -1;
727
+            }
728
+            // Otherwise compare the names
729
+            return strcmp($a['name'], $b['name']);
730
+        });
731
+
732
+        return [
733
+            // reset indexes
734
+            'commonLanguages' => array_values($commonLanguages),
735
+            'otherLanguages' => $otherLanguages
736
+        ];
737
+    }
738 738
 }
Please login to merge, or discard this patch.