Completed
Push — master ( 89b6ee...61842f )
by Morris
34:27 queued 17:16
created

Factory::findLocale()   C

Complexity

Conditions 12
Paths 21

Size

Total Lines 35

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 12
c 1
b 0
f 0
nc 21
nop 1
dl 0
loc 35
rs 6.9666

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 * @copyright 2016 Roeland Jago Douma <[email protected]>
5
 * @copyright 2016 Lukas Reschke <[email protected]>
6
 *
7
 * @author Bart Visscher <[email protected]>
8
 * @author Joas Schilling <[email protected]>
9
 * @author Lukas Reschke <[email protected]>
10
 * @author Morris Jobke <[email protected]>
11
 * @author Robin Appelman <[email protected]>
12
 * @author Robin McCorkell <[email protected]>
13
 * @author Roeland Jago Douma <[email protected]>
14
 *
15
 * @license AGPL-3.0
16
 *
17
 * This code is free software: you can redistribute it and/or modify
18
 * it under the terms of the GNU Affero General Public License, version 3,
19
 * as published by the Free Software Foundation.
20
 *
21
 * This program is distributed in the hope that it will be useful,
22
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
23
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24
 * GNU Affero General Public License for more details.
25
 *
26
 * You should have received a copy of the GNU Affero General Public License, version 3,
27
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
28
 *
29
 */
30
31
namespace OC\L10N;
32
33
use OCP\IConfig;
34
use OCP\IRequest;
35
use OCP\IUserSession;
36
use OCP\L10N\IFactory;
37
38
/**
39
 * A factory that generates language instances
40
 */
41
class Factory implements IFactory {
42
43
	/** @var string */
44
	protected $requestLanguage = '';
45
46
	/**
47
	 * cached instances
48
	 * @var array Structure: Lang => App => \OCP\IL10N
49
	 */
50
	protected $instances = [];
51
52
	/**
53
	 * @var array Structure: App => string[]
54
	 */
55
	protected $availableLanguages = [];
56
57
	/**
58
	 * @var array
59
	 */
60
	protected $availableLocales = [];
61
62
	/**
63
	 * @var array Structure: string => callable
64
	 */
65
	protected $pluralFunctions = [];
66
67
	const COMMON_LANGUAGE_CODES = [
68
		'en', 'es', 'fr', 'de', 'de_DE', 'ja', 'ar', 'ru', 'nl', 'it',
69
		'pt_BR', 'pt_PT', 'da', 'fi_FI', 'nb_NO', 'sv', 'tr', 'zh_CN', 'ko'
70
	];
71
72
	/** @var IConfig */
73
	protected $config;
74
75
	/** @var IRequest */
76
	protected $request;
77
78
	/** @var IUserSession */
79
	protected $userSession;
80
81
	/** @var string */
82
	protected $serverRoot;
83
84
	/**
85
	 * @param IConfig $config
86
	 * @param IRequest $request
87
	 * @param IUserSession $userSession
88
	 * @param string $serverRoot
89
	 */
90
	public function __construct(IConfig $config,
91
								IRequest $request,
92
								IUserSession $userSession,
93
								$serverRoot) {
94
		$this->config = $config;
95
		$this->request = $request;
96
		$this->userSession = $userSession;
97
		$this->serverRoot = $serverRoot;
98
	}
99
100
	/**
101
	 * Get a language instance
102
	 *
103
	 * @param string $app
104
	 * @param string|null $lang
105
	 * @param string|null $locale
106
	 * @return \OCP\IL10N
107
	 */
108
	public function get($app, $lang = null, $locale = null) {
109
		$app = \OC_App::cleanAppId($app);
110
		if ($lang !== null) {
111
			$lang = str_replace(array('\0', '/', '\\', '..'), '', (string) $lang);
112
		}
113
114
		$forceLang = $this->config->getSystemValue('force_language', false);
115
		if (is_string($forceLang)) {
116
			$lang = $forceLang;
117
		}
118
119
		$forceLocale = $this->config->getSystemValue('force_locale', false);
120
		if (is_string($forceLocale)) {
121
			$locale = $forceLocale;
122
		}
123
124
		if ($lang === null || !$this->languageExists($app, $lang)) {
125
			$lang = $this->findLanguage($app);
126
		}
127
128
		if ($locale === null || !$this->localeExists($locale)) {
129
			$locale = $this->findLocale($lang);
130
		}
131
132
		if (!isset($this->instances[$lang][$app])) {
133
			$this->instances[$lang][$app] = new L10N(
134
				$this, $app, $lang, $locale,
135
				$this->getL10nFilesForApp($app, $lang)
136
			);
137
		}
138
139
		return $this->instances[$lang][$app];
140
	}
141
142
	/**
143
	 * Find the best language
144
	 *
145
	 * @param string|null $app App id or null for core
146
	 * @return string language If nothing works it returns 'en'
147
	 */
148
	public function findLanguage($app = null) {
149
		$forceLang = $this->config->getSystemValue('force_language', false);
150
		if (is_string($forceLang)) {
151
			$this->requestLanguage = $forceLang;
152
		}
153
154
		if ($this->requestLanguage !== '' && $this->languageExists($app, $this->requestLanguage)) {
155
			return $this->requestLanguage;
156
		}
157
158
		/**
159
		 * At this point Nextcloud might not yet be installed and thus the lookup
160
		 * in the preferences table might fail. For this reason we need to check
161
		 * whether the instance has already been installed
162
		 *
163
		 * @link https://github.com/owncloud/core/issues/21955
164
		 */
165
		if ($this->config->getSystemValue('installed', false)) {
166
			$userId = !is_null($this->userSession->getUser()) ? $this->userSession->getUser()->getUID() :  null;
167
			if (!is_null($userId)) {
168
				$userLang = $this->config->getUserValue($userId, 'core', 'lang', null);
169
			} else {
170
				$userLang = null;
171
			}
172
		} else {
173
			$userId = null;
174
			$userLang = null;
175
		}
176
177
		if ($userLang) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $userLang of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
178
			$this->requestLanguage = $userLang;
179
			if ($this->languageExists($app, $userLang)) {
180
				return $userLang;
181
			}
182
		}
183
184
		try {
185
			// Try to get the language from the Request
186
			$lang = $this->getLanguageFromRequest($app);
187
			if ($userId !== null && $app === null && !$userLang) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $userLang of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
188
				$this->config->setUserValue($userId, 'core', 'lang', $lang);
189
			}
190
			return $lang;
191
		} catch (LanguageNotFoundException $e) {
192
			// Finding language from request failed fall back to default language
193
			$defaultLanguage = $this->config->getSystemValue('default_language', false);
194
			if ($defaultLanguage !== false && $this->languageExists($app, $defaultLanguage)) {
195
				return $defaultLanguage;
196
			}
197
		}
198
199
		// We could not find any language so fall back to english
200
		return 'en';
201
	}
202
203
	/**
204
	 * find the best locale
205
	 *
206
	 * @param string $lang
207
	 * @return null|string
208
	 */
209
	public function findLocale($lang = null) {
210
		$forceLocale = $this->config->getSystemValue('force_locale', false);
211
		if (is_string($forceLocale) && $this->localeExists($forceLocale)) {
212
			return $forceLocale;
213
		}
214
215
		if ($this->config->getSystemValue('installed', false)) {
216
			$userId = null !== $this->userSession->getUser() ? $this->userSession->getUser()->getUID() :  null;
217
			$userLocale = null;
218
			if (null !== $userId) {
219
				$userLocale = $this->config->getUserValue($userId, 'core', 'locale', null);
220
			}
221
		} else {
222
			$userId = null;
0 ignored issues
show
Unused Code introduced by
$userId is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
223
			$userLocale = null;
224
		}
225
226
		if ($userLocale && $this->localeExists($userLocale)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $userLocale of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
227
			return $userLocale;
228
		}
229
230
		// Default : use system default locale
231
		$defaultLocale = $this->config->getSystemValue('default_locale', false);
232
		if ($defaultLocale !== false && $this->localeExists($defaultLocale)) {
233
			return $defaultLocale;
234
		}
235
236
		// If no user locale set, use lang as locale
237
		if (null !== $lang && $this->localeExists($lang)) {
238
			return $lang;
239
		}
240
241
		// At last, return USA
242
		return 'en_US';
243
	}
244
245
	/**
246
	 * Find all available languages for an app
247
	 *
248
	 * @param string|null $app App id or null for core
249
	 * @return array an array of available languages
250
	 */
251
	public function findAvailableLanguages($app = null) {
252
		$key = $app;
253
		if ($key === null) {
254
			$key = 'null';
255
		}
256
257
		// also works with null as key
258
		if (!empty($this->availableLanguages[$key])) {
259
			return $this->availableLanguages[$key];
260
		}
261
262
		$available = ['en']; //english is always available
263
		$dir = $this->findL10nDir($app);
264 View Code Duplication
		if (is_dir($dir)) {
265
			$files = scandir($dir);
266
			if ($files !== false) {
267
				foreach ($files as $file) {
268
					if (substr($file, -5) === '.json' && substr($file, 0, 4) !== 'l10n') {
269
						$available[] = substr($file, 0, -5);
270
					}
271
				}
272
			}
273
		}
274
275
		// merge with translations from theme
276
		$theme = $this->config->getSystemValue('theme');
277
		if (!empty($theme)) {
278
			$themeDir = $this->serverRoot . '/themes/' . $theme . substr($dir, strlen($this->serverRoot));
279
280 View Code Duplication
			if (is_dir($themeDir)) {
281
				$files = scandir($themeDir);
282
				if ($files !== false) {
283
					foreach ($files as $file) {
284
						if (substr($file, -5) === '.json' && substr($file, 0, 4) !== 'l10n') {
285
							$available[] = substr($file, 0, -5);
286
						}
287
					}
288
				}
289
			}
290
		}
291
292
		$this->availableLanguages[$key] = $available;
293
		return $available;
294
	}
295
296
	/**
297
	 * @return array|mixed
298
	 */
299
	public function findAvailableLocales() {
300
		if (!empty($this->availableLocales)) {
301
			return $this->availableLocales;
302
		}
303
304
		$localeData = file_get_contents(\OC::$SERVERROOT . '/resources/locales.json');
305
		$this->availableLocales = \json_decode($localeData, true);
0 ignored issues
show
Documentation Bug introduced by
It seems like \json_decode($localeData, true) of type * is incompatible with the declared type array of property $availableLocales.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
306
307
		return $this->availableLocales;
308
	}
309
310
	/**
311
	 * @param string|null $app App id or null for core
312
	 * @param string $lang
313
	 * @return bool
314
	 */
315
	public function languageExists($app, $lang) {
316
		if ($lang === 'en') {//english is always available
317
			return true;
318
		}
319
320
		$languages = $this->findAvailableLanguages($app);
321
		return array_search($lang, $languages) !== false;
322
	}
323
324
	/**
325
	 * @param string $locale
326
	 * @return bool
327
	 */
328
	public function localeExists($locale) {
329
		if ($locale === 'en') { //english is always available
330
			return true;
331
		}
332
333
		$locales = $this->findAvailableLocales();
334
		$userLocale = array_filter($locales, function($value) use ($locale) {
335
			return $locale === $value['code'];
336
		});
337
338
		return !empty($userLocale);
339
	}
340
341
	/**
342
	 * @param string|null $app
343
	 * @return string
344
	 * @throws LanguageNotFoundException
345
	 */
346
	private function getLanguageFromRequest($app) {
347
		$header = $this->request->getHeader('ACCEPT_LANGUAGE');
348
		if ($header !== '') {
349
			$available = $this->findAvailableLanguages($app);
350
351
			// E.g. make sure that 'de' is before 'de_DE'.
352
			sort($available);
353
354
			$preferences = preg_split('/,\s*/', strtolower($header));
355
			foreach ($preferences as $preference) {
356
				list($preferred_language) = explode(';', $preference);
357
				$preferred_language = str_replace('-', '_', $preferred_language);
358
359
				foreach ($available as $available_language) {
360
					if ($preferred_language === strtolower($available_language)) {
361
						return $this->respectDefaultLanguage($app, $available_language);
362
					}
363
				}
364
365
				// Fallback from de_De to de
366
				foreach ($available as $available_language) {
367
					if (substr($preferred_language, 0, 2) === $available_language) {
368
						return $available_language;
369
					}
370
				}
371
			}
372
		}
373
374
		throw new LanguageNotFoundException();
375
	}
376
377
	/**
378
	 * if default language is set to de_DE (formal German) this should be
379
	 * preferred to 'de' (non-formal German) if possible
380
	 *
381
	 * @param string|null $app
382
	 * @param string $lang
383
	 * @return string
384
	 */
385
	protected function respectDefaultLanguage($app, $lang) {
386
		$result = $lang;
387
		$defaultLanguage = $this->config->getSystemValue('default_language', false);
388
389
		// use formal version of german ("Sie" instead of "Du") if the default
390
		// language is set to 'de_DE' if possible
391
		if (is_string($defaultLanguage) &&
392
			strtolower($lang) === 'de' &&
393
			strtolower($defaultLanguage) === 'de_de' &&
394
			$this->languageExists($app, 'de_DE')
395
		) {
396
			$result = 'de_DE';
397
		}
398
399
		return $result;
400
	}
401
402
	/**
403
	 * Checks if $sub is a subdirectory of $parent
404
	 *
405
	 * @param string $sub
406
	 * @param string $parent
407
	 * @return bool
408
	 */
409
	private function isSubDirectory($sub, $parent) {
410
		// Check whether $sub contains no ".."
411
		if (strpos($sub, '..') !== false) {
412
			return false;
413
		}
414
415
		// Check whether $sub is a subdirectory of $parent
416
		if (strpos($sub, $parent) === 0) {
417
			return true;
418
		}
419
420
		return false;
421
	}
422
423
	/**
424
	 * Get a list of language files that should be loaded
425
	 *
426
	 * @param string $app
427
	 * @param string $lang
428
	 * @return string[]
429
	 */
430
	// FIXME This method is only public, until OC_L10N does not need it anymore,
431
	// FIXME This is also the reason, why it is not in the public interface
432
	public function getL10nFilesForApp($app, $lang) {
433
		$languageFiles = [];
434
435
		$i18nDir = $this->findL10nDir($app);
436
		$transFile = strip_tags($i18nDir) . strip_tags($lang) . '.json';
437
438
		if (($this->isSubDirectory($transFile, $this->serverRoot . '/core/l10n/')
439
				|| $this->isSubDirectory($transFile, $this->serverRoot . '/lib/l10n/')
440
				|| $this->isSubDirectory($transFile, $this->serverRoot . '/settings/l10n/')
441
				|| $this->isSubDirectory($transFile, \OC_App::getAppPath($app) . '/l10n/')
442
			)
443
			&& file_exists($transFile)) {
444
			// load the translations file
445
			$languageFiles[] = $transFile;
446
		}
447
448
		// merge with translations from theme
449
		$theme = $this->config->getSystemValue('theme');
450
		if (!empty($theme)) {
451
			$transFile = $this->serverRoot . '/themes/' . $theme . substr($transFile, strlen($this->serverRoot));
452
			if (file_exists($transFile)) {
453
				$languageFiles[] = $transFile;
454
			}
455
		}
456
457
		return $languageFiles;
458
	}
459
460
	/**
461
	 * find the l10n directory
462
	 *
463
	 * @param string $app App id or empty string for core
464
	 * @return string directory
465
	 */
466
	protected function findL10nDir($app = null) {
467
		if (in_array($app, ['core', 'lib', 'settings'])) {
468
			if (file_exists($this->serverRoot . '/' . $app . '/l10n/')) {
469
				return $this->serverRoot . '/' . $app . '/l10n/';
470
			}
471
		} else if ($app && \OC_App::getAppPath($app) !== false) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $app of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
472
			// Check if the app is in the app folder
473
			return \OC_App::getAppPath($app) . '/l10n/';
474
		}
475
		return $this->serverRoot . '/core/l10n/';
476
	}
477
478
479
	/**
480
	 * Creates a function from the plural string
481
	 *
482
	 * Parts of the code is copied from Habari:
483
	 * https://github.com/habari/system/blob/master/classes/locale.php
484
	 * @param string $string
485
	 * @return string
486
	 */
487
	public function createPluralFunction($string) {
488
		if (isset($this->pluralFunctions[$string])) {
489
			return $this->pluralFunctions[$string];
490
		}
491
492
		if (preg_match( '/^\s*nplurals\s*=\s*(\d+)\s*;\s*plural=(.*)$/u', $string, $matches)) {
493
			// sanitize
494
			$nplurals = preg_replace( '/[^0-9]/', '', $matches[1] );
495
			$plural = preg_replace( '#[^n0-9:\(\)\?\|\&=!<>+*/\%-]#', '', $matches[2] );
496
497
			$body = str_replace(
498
				array( 'plural', 'n', '$n$plurals', ),
499
				array( '$plural', '$n', '$nplurals', ),
500
				'nplurals='. $nplurals . '; plural=' . $plural
501
			);
502
503
			// add parents
504
			// important since PHP's ternary evaluates from left to right
505
			$body .= ';';
506
			$res = '';
507
			$p = 0;
508
			$length = strlen($body);
509
			for($i = 0; $i < $length; $i++) {
510
				$ch = $body[$i];
511
				switch ( $ch ) {
512
					case '?':
513
						$res .= ' ? (';
514
						$p++;
515
						break;
516
					case ':':
517
						$res .= ') : (';
518
						break;
519
					case ';':
520
						$res .= str_repeat( ')', $p ) . ';';
521
						$p = 0;
522
						break;
523
					default:
524
						$res .= $ch;
525
				}
526
			}
527
528
			$body = $res . 'return ($plural>=$nplurals?$nplurals-1:$plural);';
529
			$function = create_function('$n', $body);
530
			$this->pluralFunctions[$string] = $function;
531
			return $function;
532
		} else {
533
			// default: one plural form for all cases but n==1 (english)
534
			$function = create_function(
535
				'$n',
536
				'$nplurals=2;$plural=($n==1?0:1);return ($plural>=$nplurals?$nplurals-1:$plural);'
537
			);
538
			$this->pluralFunctions[$string] = $function;
539
			return $function;
540
		}
541
	}
542
543
	/**
544
	 * returns the common language and other languages in an
545
	 * associative array
546
	 *
547
	 * @return array
548
	 */
549
	public function getLanguages() {
550
		$forceLanguage = $this->config->getSystemValue('force_language', false);
551
		if ($forceLanguage !== false) {
552
			return [];
553
		}
554
555
		$languageCodes = $this->findAvailableLanguages();
556
557
		$commonLanguages = [];
558
		$languages = [];
559
560
		foreach($languageCodes as $lang) {
561
			$l = $this->get('lib', $lang);
562
			// TRANSLATORS this is the language name for the language switcher in the personal settings and should be the localized version
563
			$potentialName = (string) $l->t('__language_name__');
564
			if ($l->getLanguageCode() === $lang && $potentialName[0] !== '_') {//first check if the language name is in the translation file
565
				$ln = array(
566
					'code' => $lang,
567
					'name' => $potentialName
568
				);
569
			} else if ($lang === 'en') {
570
				$ln = array(
571
					'code' => $lang,
572
					'name' => 'English (US)'
573
				);
574
			} else {//fallback to language code
575
				$ln = array(
576
					'code' => $lang,
577
					'name' => $lang
578
				);
579
			}
580
581
			// put appropriate languages into appropriate arrays, to print them sorted
582
			// common languages -> divider -> other languages
583
			if (in_array($lang, self::COMMON_LANGUAGE_CODES)) {
584
				$commonLanguages[array_search($lang, self::COMMON_LANGUAGE_CODES)] = $ln;
585
			} else {
586
				$languages[] = $ln;
587
			}
588
		}
589
590
		ksort($commonLanguages);
591
592
		// sort now by displayed language not the iso-code
593
		usort( $languages, function ($a, $b) {
594 View Code Duplication
			if ($a['code'] === $a['name'] && $b['code'] !== $b['name']) {
595
				// If a doesn't have a name, but b does, list b before a
596
				return 1;
597
			}
598 View Code Duplication
			if ($a['code'] !== $a['name'] && $b['code'] === $b['name']) {
599
				// If a does have a name, but b doesn't, list a before b
600
				return -1;
601
			}
602
			// Otherwise compare the names
603
			return strcmp($a['name'], $b['name']);
604
		});
605
606
		return [
607
			// reset indexes
608
			'commonlanguages' => array_values($commonLanguages),
609
			'languages' => $languages
610
		];
611
	}
612
}
613