Completed
Push — master ( 2dc57f...5b6128 )
by Thomas
36:44 queued 22:51
created

Factory::createPluralFunction()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
184
			$files = scandir($dir);
185
			if ($files !== false) {
186
				foreach ($files as $file) {
187
					if (substr($file, -5) === '.json' && substr($file, 0, 4) !== 'l10n') {
188
						$available[] = substr($file, 0, -5);
189
					}
190
				}
191
			}
192
		}
193
194
		// merge with translations from theme
195
		$theme = $this->config->getSystemValue('theme');
196
		if (!empty($theme)) {
197
			$themeDir = $this->serverRoot . '/themes/' . $theme . substr($dir, strlen($this->serverRoot));
198
199 View Code Duplication
			if (is_dir($themeDir)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
200
				$files = scandir($themeDir);
201
				if ($files !== false) {
202
					foreach ($files as $file) {
203
						if (substr($file, -5) === '.json' && substr($file, 0, 4) !== 'l10n') {
204
							$available[] = substr($file, 0, -5);
205
						}
206
					}
207
				}
208
			}
209
		}
210
211
		$this->availableLanguages[$key] = $available;
212
		return $available;
213
	}
214
215
	/**
216
	 * @param string|null $app App id or null for core
217
	 * @param string $lang
218
	 * @return bool
219
	 */
220
	public function languageExists($app, $lang) {
221
		if ($lang === 'en') {//english is always available
222
			return true;
223
		}
224
225
		$languages = $this->findAvailableLanguages($app);
226
		return array_search($lang, $languages) !== false;
227
	}
228
229
	/**
230
	 * @param string|null $app App id or null for core
231
	 * @return string
232
	 */
233
	public function setLanguageFromRequest($app = null) {
234
		$header = $this->request->getHeader('ACCEPT_LANGUAGE');
235
		if ($header) {
236
			$available = $this->findAvailableLanguages($app);
237
238
			// E.g. make sure that 'de' is before 'de_DE'.
239
			sort($available);
240
241
			$preferences = preg_split('/,\s*/', strtolower($header));
242
			foreach ($preferences as $preference) {
243
				list($preferred_language) = explode(';', $preference);
244
				$preferred_language = str_replace('-', '_', $preferred_language);
245
246 View Code Duplication
				foreach ($available as $available_language) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
247
					if ($preferred_language === strtolower($available_language)) {
248
						if ($app === null && !$this->requestLanguage) {
249
							$this->requestLanguage = $available_language;
250
						}
251
						return $available_language;
252
					}
253
				}
254
255
				// Fallback from de_De to de
256 View Code Duplication
				foreach ($available as $available_language) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
257
					if (substr($preferred_language, 0, 2) === $available_language) {
258
						if ($app === null && !$this->requestLanguage) {
259
							$this->requestLanguage = $available_language;
260
						}
261
						return $available_language;
262
					}
263
				}
264
			}
265
		}
266
267
		if ($app === null && !$this->requestLanguage) {
268
			$this->requestLanguage = 'en';
269
		}
270
		return 'en'; // Last try: English
271
	}
272
273
	/**
274
	 * Get a list of language files that should be loaded
275
	 *
276
	 * @param string $app
277
	 * @param string $lang
278
	 * @return string[]
279
	 */
280
	// FIXME This method is only public, until \OCP\IL10N does not need it anymore,
281
	// FIXME This is also the reason, why it is not in the public interface
282
	public function getL10nFilesForApp($app, $lang) {
283
		$languageFiles = [];
284
285
		$i18nDir = $this->findL10nDir($app);
286
		$transFile = strip_tags($i18nDir) . strip_tags($lang) . '.json';
287
288
		if ((\OC_Helper::isSubDirectory($transFile, $this->serverRoot . '/core/l10n/')
289
				|| \OC_Helper::isSubDirectory($transFile, $this->serverRoot . '/lib/l10n/')
290
				|| \OC_Helper::isSubDirectory($transFile, $this->serverRoot . '/settings/l10n/')
291
				|| \OC_Helper::isSubDirectory($transFile, \OC_App::getAppPath($app) . '/l10n/')
292
			)
293
			&& file_exists($transFile)) {
294
			// load the translations file
295
			$languageFiles[] = $transFile;
296
		}
297
298
		// merge with translations from theme
299
		$theme = $this->config->getSystemValue('theme');
300
		if (!empty($theme)) {
301
			$transFile = $this->serverRoot . '/themes/' . $theme . substr($transFile, strlen($this->serverRoot));
302
			if (file_exists($transFile)) {
303
				$languageFiles[] = $transFile;
304
			}
305
		}
306
307
		return $languageFiles;
308
	}
309
310
	/**
311
	 * find the l10n directory
312
	 *
313
	 * @param string $app App id or empty string for core
314
	 * @return string directory
315
	 */
316
	protected function findL10nDir($app = null) {
317
		if (in_array($app, ['core', 'lib', 'settings'])) {
318
			if (file_exists($this->serverRoot . '/' . $app . '/l10n/')) {
319
				return $this->serverRoot . '/' . $app . '/l10n/';
320
			}
321
		} 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...
322
			// Check if the app is in the app folder
323
			return \OC_App::getAppPath($app) . '/l10n/';
324
		}
325
		return $this->serverRoot . '/core/l10n/';
326
	}
327
328
	/**
329
	 * Creates a function from the plural string
330
	 *
331
	 * @param string $string
332
	 * @return string Unique function name
333
	 * @since 9.0.0
334
	 */
335
	public function createPluralFunction($string) {
336
		return '';
337
	}
338
}
339