Completed
Push — master ( b40bae...a2c518 )
by Morris
16:08
created

Factory   D

Complexity

Total Complexity 83

Size/Duplication

Total Lines 479
Duplicated Lines 5.85 %

Coupling/Cohesion

Components 2
Dependencies 8

Importance

Changes 0
Metric Value
dl 28
loc 479
rs 4.8717
c 0
b 0
f 0
wmc 83
lcom 2
cbo 8

12 Methods

Rating   Name   Duplication   Size   Complexity  
C findLanguage() 0 49 14
A __construct() 0 9 1
B get() 0 24 6
C findAvailableLanguages() 20 44 14
A languageExists() 0 8 2
C getLanguageFromRequest() 0 30 7
B respectDefaultLanguage() 0 16 5
A isSubDirectory() 0 13 3
C getL10nFilesForApp() 0 27 8
B findL10nDir() 0 11 5
B createPluralFunction() 0 55 7
C getLanguages() 8 63 11

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Factory often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Factory, and based on these observations, apply Extract Interface, too.

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 Structure: string => callable
59
	 */
60
	protected $pluralFunctions = [];
61
62
	const COMMON_LANGUAGE_CODES = [
63
		'en', 'es', 'fr', 'de', 'de_DE', 'ja', 'ar', 'ru', 'nl', 'it',
64
		'pt_BR', 'pt_PT', 'da', 'fi_FI', 'nb_NO', 'sv', 'tr', 'zh_CN', 'ko'
65
	];
66
67
	/** @var IConfig */
68
	protected $config;
69
70
	/** @var IRequest */
71
	protected $request;
72
73
	/** @var IUserSession */
74
	protected $userSession;
75
76
	/** @var string */
77
	protected $serverRoot;
78
79
	/**
80
	 * @param IConfig $config
81
	 * @param IRequest $request
82
	 * @param IUserSession $userSession
83
	 * @param string $serverRoot
84
	 */
85
	public function __construct(IConfig $config,
86
								IRequest $request,
87
								IUserSession $userSession,
88
								$serverRoot) {
89
		$this->config = $config;
90
		$this->request = $request;
91
		$this->userSession = $userSession;
92
		$this->serverRoot = $serverRoot;
93
	}
94
95
	/**
96
	 * Get a language instance
97
	 *
98
	 * @param string $app
99
	 * @param string|null $lang
100
	 * @return \OCP\IL10N
101
	 */
102
	public function get($app, $lang = null) {
103
		$app = \OC_App::cleanAppId($app);
104
		if ($lang !== null) {
105
			$lang = str_replace(array('\0', '/', '\\', '..'), '', (string) $lang);
106
		}
107
108
		$forceLang = $this->config->getSystemValue('force_language', false);
109
		if (is_string($forceLang)) {
110
			$lang = $forceLang;
111
		}
112
113
		if ($lang === null || !$this->languageExists($app, $lang)) {
114
			$lang = $this->findLanguage($app);
115
		}
116
117
		if (!isset($this->instances[$lang][$app])) {
118
			$this->instances[$lang][$app] = new L10N(
119
				$this, $app, $lang,
120
				$this->getL10nFilesForApp($app, $lang)
121
			);
122
		}
123
124
		return $this->instances[$lang][$app];
125
	}
126
127
	/**
128
	 * Find the best language
129
	 *
130
	 * @param string|null $app App id or null for core
131
	 * @return string language If nothing works it returns 'en'
132
	 */
133
	public function findLanguage($app = null) {
134
		if ($this->requestLanguage !== '' && $this->languageExists($app, $this->requestLanguage)) {
135
			return $this->requestLanguage;
136
		}
137
138
		/**
139
		 * At this point Nextcloud might not yet be installed and thus the lookup
140
		 * in the preferences table might fail. For this reason we need to check
141
		 * whether the instance has already been installed
142
		 *
143
		 * @link https://github.com/owncloud/core/issues/21955
144
		 */
145
		if ($this->config->getSystemValue('installed', false)) {
146
			$userId = !is_null($this->userSession->getUser()) ? $this->userSession->getUser()->getUID() :  null;
147
			if (!is_null($userId)) {
148
				$userLang = $this->config->getUserValue($userId, 'core', 'lang', null);
149
			} else {
150
				$userLang = null;
151
			}
152
		} else {
153
			$userId = null;
154
			$userLang = null;
155
		}
156
157
		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...
158
			$this->requestLanguage = $userLang;
159
			if ($this->languageExists($app, $userLang)) {
160
				return $userLang;
161
			}
162
		}
163
164
		try {
165
			// Try to get the language from the Request
166
			$lang = $this->getLanguageFromRequest($app);
167
			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...
168
				$this->config->setUserValue($userId, 'core', 'lang', $lang);
169
			}
170
			return $lang;
171
		} catch (LanguageNotFoundException $e) {
172
			// Finding language from request failed fall back to default language
173
			$defaultLanguage = $this->config->getSystemValue('default_language', false);
174
			if ($defaultLanguage !== false && $this->languageExists($app, $defaultLanguage)) {
175
				return $defaultLanguage;
176
			}
177
		}
178
179
		// We could not find any language so fall back to english
180
		return 'en';
181
	}
182
183
	/**
184
	 * Find all available languages for an app
185
	 *
186
	 * @param string|null $app App id or null for core
187
	 * @return array an array of available languages
188
	 */
189
	public function findAvailableLanguages($app = null) {
190
		$key = $app;
191
		if ($key === null) {
192
			$key = 'null';
193
		}
194
195
		// also works with null as key
196
		if (!empty($this->availableLanguages[$key])) {
197
			return $this->availableLanguages[$key];
198
		}
199
200
		$available = ['en']; //english is always available
201
		$dir = $this->findL10nDir($app);
202 View Code Duplication
		if (is_dir($dir)) {
203
			$files = scandir($dir);
204
			if ($files !== false) {
205
				foreach ($files as $file) {
206
					if (substr($file, -5) === '.json' && substr($file, 0, 4) !== 'l10n') {
207
						$available[] = substr($file, 0, -5);
208
					}
209
				}
210
			}
211
		}
212
213
		// merge with translations from theme
214
		$theme = $this->config->getSystemValue('theme');
215
		if (!empty($theme)) {
216
			$themeDir = $this->serverRoot . '/themes/' . $theme . substr($dir, strlen($this->serverRoot));
217
218 View Code Duplication
			if (is_dir($themeDir)) {
219
				$files = scandir($themeDir);
220
				if ($files !== false) {
221
					foreach ($files as $file) {
222
						if (substr($file, -5) === '.json' && substr($file, 0, 4) !== 'l10n') {
223
							$available[] = substr($file, 0, -5);
224
						}
225
					}
226
				}
227
			}
228
		}
229
230
		$this->availableLanguages[$key] = $available;
231
		return $available;
232
	}
233
234
	/**
235
	 * @param string|null $app App id or null for core
236
	 * @param string $lang
237
	 * @return bool
238
	 */
239
	public function languageExists($app, $lang) {
240
		if ($lang === 'en') {//english is always available
241
			return true;
242
		}
243
244
		$languages = $this->findAvailableLanguages($app);
245
		return array_search($lang, $languages) !== false;
246
	}
247
248
	/**
249
	 * @param string|null $app
250
	 * @return string
251
	 * @throws LanguageNotFoundException
252
	 */
253
	private function getLanguageFromRequest($app) {
254
		$header = $this->request->getHeader('ACCEPT_LANGUAGE');
255
		if ($header !== '') {
256
			$available = $this->findAvailableLanguages($app);
257
258
			// E.g. make sure that 'de' is before 'de_DE'.
259
			sort($available);
260
261
			$preferences = preg_split('/,\s*/', strtolower($header));
262
			foreach ($preferences as $preference) {
263
				list($preferred_language) = explode(';', $preference);
264
				$preferred_language = str_replace('-', '_', $preferred_language);
265
266
				foreach ($available as $available_language) {
267
					if ($preferred_language === strtolower($available_language)) {
268
						return $this->respectDefaultLanguage($app, $available_language);
269
					}
270
				}
271
272
				// Fallback from de_De to de
273
				foreach ($available as $available_language) {
274
					if (substr($preferred_language, 0, 2) === $available_language) {
275
						return $available_language;
276
					}
277
				}
278
			}
279
		}
280
281
		throw new LanguageNotFoundException();
282
	}
283
284
	/**
285
	 * if default language is set to de_DE (formal German) this should be
286
	 * preferred to 'de' (non-formal German) if possible
287
	 *
288
	 * @param string|null $app
289
	 * @param string $lang
290
	 * @return string
291
	 */
292
	protected function respectDefaultLanguage($app, $lang) {
293
		$result = $lang;
294
		$defaultLanguage = $this->config->getSystemValue('default_language', false);
295
296
		// use formal version of german ("Sie" instead of "Du") if the default
297
		// language is set to 'de_DE' if possible
298
		if (is_string($defaultLanguage) &&
299
			strtolower($lang) === 'de' &&
300
			strtolower($defaultLanguage) === 'de_de' &&
301
			$this->languageExists($app, 'de_DE')
302
		) {
303
			$result = 'de_DE';
304
		}
305
306
		return $result;
307
	}
308
309
	/**
310
	 * Checks if $sub is a subdirectory of $parent
311
	 *
312
	 * @param string $sub
313
	 * @param string $parent
314
	 * @return bool
315
	 */
316
	private function isSubDirectory($sub, $parent) {
317
		// Check whether $sub contains no ".."
318
		if (strpos($sub, '..') !== false) {
319
			return false;
320
		}
321
322
		// Check whether $sub is a subdirectory of $parent
323
		if (strpos($sub, $parent) === 0) {
324
			return true;
325
		}
326
327
		return false;
328
	}
329
330
	/**
331
	 * Get a list of language files that should be loaded
332
	 *
333
	 * @param string $app
334
	 * @param string $lang
335
	 * @return string[]
336
	 */
337
	// FIXME This method is only public, until OC_L10N does not need it anymore,
338
	// FIXME This is also the reason, why it is not in the public interface
339
	public function getL10nFilesForApp($app, $lang) {
340
		$languageFiles = [];
341
342
		$i18nDir = $this->findL10nDir($app);
343
		$transFile = strip_tags($i18nDir) . strip_tags($lang) . '.json';
344
345
		if (($this->isSubDirectory($transFile, $this->serverRoot . '/core/l10n/')
346
				|| $this->isSubDirectory($transFile, $this->serverRoot . '/lib/l10n/')
347
				|| $this->isSubDirectory($transFile, $this->serverRoot . '/settings/l10n/')
348
				|| $this->isSubDirectory($transFile, \OC_App::getAppPath($app) . '/l10n/')
349
			)
350
			&& file_exists($transFile)) {
351
			// load the translations file
352
			$languageFiles[] = $transFile;
353
		}
354
355
		// merge with translations from theme
356
		$theme = $this->config->getSystemValue('theme');
357
		if (!empty($theme)) {
358
			$transFile = $this->serverRoot . '/themes/' . $theme . substr($transFile, strlen($this->serverRoot));
359
			if (file_exists($transFile)) {
360
				$languageFiles[] = $transFile;
361
			}
362
		}
363
364
		return $languageFiles;
365
	}
366
367
	/**
368
	 * find the l10n directory
369
	 *
370
	 * @param string $app App id or empty string for core
371
	 * @return string directory
372
	 */
373
	protected function findL10nDir($app = null) {
374
		if (in_array($app, ['core', 'lib', 'settings'])) {
375
			if (file_exists($this->serverRoot . '/' . $app . '/l10n/')) {
376
				return $this->serverRoot . '/' . $app . '/l10n/';
377
			}
378
		} 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...
379
			// Check if the app is in the app folder
380
			return \OC_App::getAppPath($app) . '/l10n/';
381
		}
382
		return $this->serverRoot . '/core/l10n/';
383
	}
384
385
386
	/**
387
	 * Creates a function from the plural string
388
	 *
389
	 * Parts of the code is copied from Habari:
390
	 * https://github.com/habari/system/blob/master/classes/locale.php
391
	 * @param string $string
392
	 * @return string
393
	 */
394
	public function createPluralFunction($string) {
395
		if (isset($this->pluralFunctions[$string])) {
396
			return $this->pluralFunctions[$string];
397
		}
398
399
		if (preg_match( '/^\s*nplurals\s*=\s*(\d+)\s*;\s*plural=(.*)$/u', $string, $matches)) {
400
			// sanitize
401
			$nplurals = preg_replace( '/[^0-9]/', '', $matches[1] );
402
			$plural = preg_replace( '#[^n0-9:\(\)\?\|\&=!<>+*/\%-]#', '', $matches[2] );
403
404
			$body = str_replace(
405
				array( 'plural', 'n', '$n$plurals', ),
406
				array( '$plural', '$n', '$nplurals', ),
407
				'nplurals='. $nplurals . '; plural=' . $plural
408
			);
409
410
			// add parents
411
			// important since PHP's ternary evaluates from left to right
412
			$body .= ';';
413
			$res = '';
414
			$p = 0;
415
			$length = strlen($body);
416
			for($i = 0; $i < $length; $i++) {
417
				$ch = $body[$i];
418
				switch ( $ch ) {
419
					case '?':
420
						$res .= ' ? (';
421
						$p++;
422
						break;
423
					case ':':
424
						$res .= ') : (';
425
						break;
426
					case ';':
427
						$res .= str_repeat( ')', $p ) . ';';
428
						$p = 0;
429
						break;
430
					default:
431
						$res .= $ch;
432
				}
433
			}
434
435
			$body = $res . 'return ($plural>=$nplurals?$nplurals-1:$plural);';
436
			$function = create_function('$n', $body);
437
			$this->pluralFunctions[$string] = $function;
438
			return $function;
439
		} else {
440
			// default: one plural form for all cases but n==1 (english)
441
			$function = create_function(
442
				'$n',
443
				'$nplurals=2;$plural=($n==1?0:1);return ($plural>=$nplurals?$nplurals-1:$plural);'
444
			);
445
			$this->pluralFunctions[$string] = $function;
446
			return $function;
447
		}
448
	}
449
450
	/**
451
	 * returns the common language and other languages in an
452
	 * associative array
453
	 *
454
	 * @return array
455
	 */
456
	public function getLanguages() {
457
		$forceLanguage = $this->config->getSystemValue('force_language', false);
458
		if ($forceLanguage !== false) {
459
			return [];
460
		}
461
462
		$languageCodes = $this->findAvailableLanguages();
463
464
		$commonLanguages = [];
465
		$languages = [];
466
467
		foreach($languageCodes as $lang) {
468
			$l = $this->get('lib', $lang);
469
			// TRANSLATORS this is the language name for the language switcher in the personal settings and should be the localized version
470
			$potentialName = (string) $l->t('__language_name__');
471
			if ($l->getLanguageCode() === $lang && $potentialName[0] !== '_') {//first check if the language name is in the translation file
472
				$ln = array(
473
					'code' => $lang,
474
					'name' => $potentialName
475
				);
476
			} else if ($lang === 'en') {
477
				$ln = array(
478
					'code' => $lang,
479
					'name' => 'English (US)'
480
				);
481
			} else {//fallback to language code
482
				$ln = array(
483
					'code' => $lang,
484
					'name' => $lang
485
				);
486
			}
487
488
			// put appropriate languages into appropriate arrays, to print them sorted
489
			// common languages -> divider -> other languages
490
			if (in_array($lang, self::COMMON_LANGUAGE_CODES)) {
491
				$commonLanguages[array_search($lang, self::COMMON_LANGUAGE_CODES)] = $ln;
492
			} else {
493
				$languages[] = $ln;
494
			}
495
		}
496
497
		ksort($commonLanguages);
498
499
		// sort now by displayed language not the iso-code
500
		usort( $languages, function ($a, $b) {
501 View Code Duplication
			if ($a['code'] === $a['name'] && $b['code'] !== $b['name']) {
502
				// If a doesn't have a name, but b does, list b before a
503
				return 1;
504
			}
505 View Code Duplication
			if ($a['code'] !== $a['name'] && $b['code'] === $b['name']) {
506
				// If a does have a name, but b doesn't, list a before b
507
				return -1;
508
			}
509
			// Otherwise compare the names
510
			return strcmp($a['name'], $b['name']);
511
		});
512
513
		return [
514
			// reset indexes
515
			'commonlanguages' => array_values($commonLanguages),
516
			'languages' => $languages
517
		];
518
	}
519
}
520