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