Completed
Push — master ( 536650...325f92 )
by Lukas
01:47 queued 01:20
created

Factory::findAvailableLanguages()   C

Complexity

Conditions 14
Paths 26

Size

Total Lines 44
Code Lines 25

Duplication

Lines 20
Ratio 45.45 %

Importance

Changes 0
Metric Value
cc 14
eloc 25
nc 26
nop 1
dl 20
loc 44
rs 5.0864
c 0
b 0
f 0

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 Structure: string => callable
59
	 */
60
	protected $pluralFunctions = [];
61
62
	/** @var IConfig */
63
	protected $config;
64
65
	/** @var IRequest */
66
	protected $request;
67
68
	/** @var IUserSession */
69
	protected $userSession;
70
71
	/** @var string */
72
	protected $serverRoot;
73
74
	/**
75
	 * @param IConfig $config
76
	 * @param IRequest $request
77
	 * @param IUserSession $userSession
78
	 * @param string $serverRoot
79
	 */
80
	public function __construct(IConfig $config,
81
								IRequest $request,
82
								IUserSession $userSession,
83
								$serverRoot) {
84
		$this->config = $config;
85
		$this->request = $request;
86
		$this->userSession = $userSession;
87
		$this->serverRoot = $serverRoot;
88
	}
89
90
	/**
91
	 * Get a language instance
92
	 *
93
	 * @param string $app
94
	 * @param string|null $lang
95
	 * @return \OCP\IL10N
96
	 */
97
	public function get($app, $lang = null) {
98
		$app = \OC_App::cleanAppId($app);
99
		if ($lang !== null) {
100
			$lang = str_replace(array('\0', '/', '\\', '..'), '', (string) $lang);
101
		}
102
		if ($lang === null || !$this->languageExists($app, $lang)) {
103
			$lang = $this->findLanguage($app);
104
		}
105
106
		if (!isset($this->instances[$lang][$app])) {
107
			$this->instances[$lang][$app] = new L10N(
108
				$this, $app, $lang,
109
				$this->getL10nFilesForApp($app, $lang)
110
			);
111
		}
112
113
		return $this->instances[$lang][$app];
114
	}
115
116
	/**
117
	 * Find the best language
118
	 *
119
	 * @param string|null $app App id or null for core
120
	 * @return string language If nothing works it returns 'en'
121
	 */
122
	public function findLanguage($app = null) {
123
		if ($this->requestLanguage !== '' && $this->languageExists($app, $this->requestLanguage)) {
124
			return $this->requestLanguage;
125
		}
126
127
		/**
128
		 * At this point ownCloud might not yet be installed and thus the lookup
129
		 * in the preferences table might fail. For this reason we need to check
130
		 * whether the instance has already been installed
131
		 *
132
		 * @link https://github.com/owncloud/core/issues/21955
133
		 */
134
		if($this->config->getSystemValue('installed', false)) {
135
			$userId = !is_null($this->userSession->getUser()) ? $this->userSession->getUser()->getUID() :  null;
136
			if(!is_null($userId)) {
137
				$userLang = $this->config->getUserValue($userId, 'core', 'lang', null);
138
			} else {
139
				$userLang = null;
140
			}
141
		} else {
142
			$userId = null;
143
			$userLang = null;
144
		}
145
146
		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...
147
			$this->requestLanguage = $userLang;
148
			if ($this->languageExists($app, $userLang)) {
149
				return $userLang;
150
			}
151
		}
152
153
		try {
154
			// Try to get the language from the Request
155
			$lang = $this->getLanguageFromRequest($app);
156
			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...
157
				$this->config->setUserValue($userId, 'core', 'lang', $lang);
158
			}
159
			return $lang;
160
		} catch (LanguageNotFoundException $e) {
161
			// Finding language from request failed fall back to default language
162
			$defaultLanguage = $this->config->getSystemValue('default_language', false);
163
			if ($defaultLanguage !== false && $this->languageExists($app, $defaultLanguage)) {
164
				return $defaultLanguage;
165
			}
166
		}
167
168
		// We could not find any language so fall back to english
169
		return 'en';
170
	}
171
172
	/**
173
	 * Find all available languages for an app
174
	 *
175
	 * @param string|null $app App id or null for core
176
	 * @return array an array of available languages
177
	 */
178
	public function findAvailableLanguages($app = null) {
179
		$key = $app;
180
		if ($key === null) {
181
			$key = 'null';
182
		}
183
184
		// also works with null as key
185
		if (!empty($this->availableLanguages[$key])) {
186
			return $this->availableLanguages[$key];
187
		}
188
189
		$available = ['en']; //english is always available
190
		$dir = $this->findL10nDir($app);
191 View Code Duplication
		if (is_dir($dir)) {
192
			$files = scandir($dir);
193
			if ($files !== false) {
194
				foreach ($files as $file) {
195
					if (substr($file, -5) === '.json' && substr($file, 0, 4) !== 'l10n') {
196
						$available[] = substr($file, 0, -5);
197
					}
198
				}
199
			}
200
		}
201
202
		// merge with translations from theme
203
		$theme = $this->config->getSystemValue('theme');
204
		if (!empty($theme)) {
205
			$themeDir = $this->serverRoot . '/themes/' . $theme . substr($dir, strlen($this->serverRoot));
206
207 View Code Duplication
			if (is_dir($themeDir)) {
208
				$files = scandir($themeDir);
209
				if ($files !== false) {
210
					foreach ($files as $file) {
211
						if (substr($file, -5) === '.json' && substr($file, 0, 4) !== 'l10n') {
212
							$available[] = substr($file, 0, -5);
213
						}
214
					}
215
				}
216
			}
217
		}
218
219
		$this->availableLanguages[$key] = $available;
220
		return $available;
221
	}
222
223
	/**
224
	 * @param string|null $app App id or null for core
225
	 * @param string $lang
226
	 * @return bool
227
	 */
228
	public function languageExists($app, $lang) {
229
		if ($lang === 'en') {//english is always available
230
			return true;
231
		}
232
233
		$languages = $this->findAvailableLanguages($app);
234
		return array_search($lang, $languages) !== false;
235
	}
236
237
	/**
238
	 * @param string|null $app
239
	 * @return string
240
	 * @throws LanguageNotFoundException
241
	 */
242
	private function getLanguageFromRequest($app) {
243
		$header = $this->request->getHeader('ACCEPT_LANGUAGE');
244
		if ($header) {
245
			$available = $this->findAvailableLanguages($app);
246
247
			// E.g. make sure that 'de' is before 'de_DE'.
248
			sort($available);
249
250
			$preferences = preg_split('/,\s*/', strtolower($header));
251
			foreach ($preferences as $preference) {
252
				list($preferred_language) = explode(';', $preference);
253
				$preferred_language = str_replace('-', '_', $preferred_language);
254
255
				foreach ($available as $available_language) {
256
					if ($preferred_language === strtolower($available_language)) {
257
						return $available_language;
258
					}
259
				}
260
261
				// Fallback from de_De to de
262
				foreach ($available as $available_language) {
263
					if (substr($preferred_language, 0, 2) === $available_language) {
264
						return $available_language;
265
					}
266
				}
267
			}
268
		}
269
270
		throw new LanguageNotFoundException();
271
	}
272
273
	/**
274
	 * Checks if $sub is a subdirectory of $parent
275
	 *
276
	 * @param string $sub
277
	 * @param string $parent
278
	 * @return bool
279
	 */
280
	private function isSubDirectory($sub, $parent) {
281
		// Check whether $sub contains no ".."
282
		if(strpos($sub, '..') !== false) {
283
			return false;
284
		}
285
286
		// Check whether $sub is a subdirectory of $parent
287
		if (strpos($sub, $parent) === 0) {
288
			return true;
289
		}
290
291
		return false;
292
	}
293
294
	/**
295
	 * Get a list of language files that should be loaded
296
	 *
297
	 * @param string $app
298
	 * @param string $lang
299
	 * @return string[]
300
	 */
301
	// FIXME This method is only public, until OC_L10N does not need it anymore,
302
	// FIXME This is also the reason, why it is not in the public interface
303
	public function getL10nFilesForApp($app, $lang) {
304
		$languageFiles = [];
305
306
		$i18nDir = $this->findL10nDir($app);
307
		$transFile = strip_tags($i18nDir) . strip_tags($lang) . '.json';
308
309
		if (($this->isSubDirectory($transFile, $this->serverRoot . '/core/l10n/')
310
				|| $this->isSubDirectory($transFile, $this->serverRoot . '/lib/l10n/')
311
				|| $this->isSubDirectory($transFile, $this->serverRoot . '/settings/l10n/')
312
				|| $this->isSubDirectory($transFile, \OC_App::getAppPath($app) . '/l10n/')
313
			)
314
			&& file_exists($transFile)) {
315
			// load the translations file
316
			$languageFiles[] = $transFile;
317
		}
318
319
		// merge with translations from theme
320
		$theme = $this->config->getSystemValue('theme');
321
		if (!empty($theme)) {
322
			$transFile = $this->serverRoot . '/themes/' . $theme . substr($transFile, strlen($this->serverRoot));
323
			if (file_exists($transFile)) {
324
				$languageFiles[] = $transFile;
325
			}
326
		}
327
328
		return $languageFiles;
329
	}
330
331
	/**
332
	 * find the l10n directory
333
	 *
334
	 * @param string $app App id or empty string for core
335
	 * @return string directory
336
	 */
337
	protected function findL10nDir($app = null) {
338
		if (in_array($app, ['core', 'lib', 'settings'])) {
339
			if (file_exists($this->serverRoot . '/' . $app . '/l10n/')) {
340
				return $this->serverRoot . '/' . $app . '/l10n/';
341
			}
342
		} 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...
343
			// Check if the app is in the app folder
344
			return \OC_App::getAppPath($app) . '/l10n/';
345
		}
346
		return $this->serverRoot . '/core/l10n/';
347
	}
348
349
350
	/**
351
	 * Creates a function from the plural string
352
	 *
353
	 * Parts of the code is copied from Habari:
354
	 * https://github.com/habari/system/blob/master/classes/locale.php
355
	 * @param string $string
356
	 * @return string
357
	 */
358
	public function createPluralFunction($string) {
359
		if (isset($this->pluralFunctions[$string])) {
360
			return $this->pluralFunctions[$string];
361
		}
362
363
		if (preg_match( '/^\s*nplurals\s*=\s*(\d+)\s*;\s*plural=(.*)$/u', $string, $matches)) {
364
			// sanitize
365
			$nplurals = preg_replace( '/[^0-9]/', '', $matches[1] );
366
			$plural = preg_replace( '#[^n0-9:\(\)\?\|\&=!<>+*/\%-]#', '', $matches[2] );
367
368
			$body = str_replace(
369
				array( 'plural', 'n', '$n$plurals', ),
370
				array( '$plural', '$n', '$nplurals', ),
371
				'nplurals='. $nplurals . '; plural=' . $plural
372
			);
373
374
			// add parents
375
			// important since PHP's ternary evaluates from left to right
376
			$body .= ';';
377
			$res = '';
378
			$p = 0;
379
			for($i = 0; $i < strlen($body); $i++) {
380
				$ch = $body[$i];
381
				switch ( $ch ) {
382
					case '?':
383
						$res .= ' ? (';
384
						$p++;
385
						break;
386
					case ':':
387
						$res .= ') : (';
388
						break;
389
					case ';':
390
						$res .= str_repeat( ')', $p ) . ';';
391
						$p = 0;
392
						break;
393
					default:
394
						$res .= $ch;
395
				}
396
			}
397
398
			$body = $res . 'return ($plural>=$nplurals?$nplurals-1:$plural);';
399
			$function = create_function('$n', $body);
0 ignored issues
show
Security Best Practice introduced by
The use of create_function is highly discouraged, better use a closure.

create_function can pose a great security vulnerability as it is similar to eval, and could be used for arbitrary code execution. We highly recommend to use a closure instead.

// Instead of
$function = create_function('$a, $b', 'return $a + $b');

// Better use
$function = function($a, $b) { return $a + $b; }
Loading history...
400
			$this->pluralFunctions[$string] = $function;
401
			return $function;
402
		} else {
403
			// default: one plural form for all cases but n==1 (english)
404
			$function = create_function(
0 ignored issues
show
Security Best Practice introduced by
The use of create_function is highly discouraged, better use a closure.

create_function can pose a great security vulnerability as it is similar to eval, and could be used for arbitrary code execution. We highly recommend to use a closure instead.

// Instead of
$function = create_function('$a, $b', 'return $a + $b');

// Better use
$function = function($a, $b) { return $a + $b; }
Loading history...
405
				'$n',
406
				'$nplurals=2;$plural=($n==1?0:1);return ($plural>=$nplurals?$nplurals-1:$plural);'
407
			);
408
			$this->pluralFunctions[$string] = $function;
409
			return $function;
410
		}
411
	}
412
}
413