Completed
Push — master ( e8cbe7...f3c40f )
by Victor
21:02 queued 11:32
created

Factory::getActiveThemeDirectory()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 0
dl 0
loc 8
rs 9.4285
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
use OCP\Theme\ITheme;
35
use OCP\Theme\IThemeService;
36
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 IThemeService */
72
	protected $themeService;
73
74
	/** @var string */
75
	protected $serverRoot;
76
77
	/**
78
	 * @param IConfig $config
79
	 * @param IRequest $request
80
	 * @param IThemeService $themeService
81
	 * @param IUserSession $userSession
82
	 * @param string $serverRoot
83
	 */
84
	public function __construct(IConfig $config,
85
								IRequest $request,
86
								IThemeService $themeService,
87
								IUserSession $userSession = null,
88
								$serverRoot) {
89
		$this->config = $config;
90
		$this->request = $request;
91
		$this->userSession = $userSession;
92
		$this->themeService = $themeService;
93
		$this->serverRoot = $serverRoot;
94
	}
95
96
	/**
97
	 * Get a language instance
98
	 *
99
	 * @param string $app
100
	 * @param string|null $lang
101
	 * @return \OCP\IL10N
102
	 */
103
	public function get($app, $lang = null) {
104
		$app = \OC_App::cleanAppId($app);
105
		if ($lang !== null) {
106
			$lang = str_replace(['\0', '/', '\\', '..'], '', (string) $lang);
107
		}
108
		if ($lang === null || !$this->languageExists($app, $lang)) {
109
			$lang = $this->findLanguage($app);
110
		}
111
112
		if (!isset($this->instances[$lang][$app])) {
113
			$this->instances[$lang][$app] = new L10N(
114
				$this, $app, $lang,
115
				$this->getL10nFilesForApp($app, $lang)
116
			);
117
		}
118
119
		return $this->instances[$lang][$app];
120
	}
121
122
	/**
123
	 * Find the best language
124
	 *
125
	 * @param string|null $app App id or null for core
126
	 * @return string language If nothing works it returns 'en'
127
	 */
128
	public function findLanguage($app = null) {
129
		if ($this->requestLanguage !== '' && $this->languageExists($app, $this->requestLanguage)) {
130
			return $this->requestLanguage;
131
		}
132
133
		/**
134
		 * At this point ownCloud might not yet be installed and thus the lookup
135
		 * in the preferences table might fail. For this reason we need to check
136
		 * whether the instance has already been installed
137
		 *
138
		 * @link https://github.com/owncloud/core/issues/21955
139
		 */
140
		if(!is_null($this->userSession) && $this->config->getSystemValue('installed', false)) {
141
			$userId = !is_null($this->userSession->getUser()) ? $this->userSession->getUser()->getUID() :  null;
142
			if(!is_null($userId)) {
143
				$userLang = $this->config->getUserValue($userId, 'core', 'lang', null);
144
			} else {
145
				$userLang = null;
146
			}
147
		} else {
148
			$userId = null;
149
			$userLang = null;
150
		}
151
152
		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...
153
			$this->requestLanguage = $userLang;
154
			if ($this->languageExists($app, $userLang)) {
155
				return $userLang;
156
			}
157
		}
158
159
		$defaultLanguage = $this->config->getSystemValue('default_language', false);
160
161
		if ($defaultLanguage !== false && $this->languageExists($app, $defaultLanguage)) {
162
			return $defaultLanguage;
163
		}
164
165
		$lang = $this->setLanguageFromRequest($app);
166
		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...
167
			$this->config->setUserValue($userId, 'core', 'lang', $lang);
168
		}
169
170
		return $lang;
171
	}
172
173
	/**
174
	 * Find all available languages for an app
175
	 *
176
	 * @param string|null $app App id or null for core
177
	 * @return array an array of available languages
178
	 */
179
	public function findAvailableLanguages($app = null) {
180
		$key = $app;
181
		if ($key === null) {
182
			$key = 'null';
183
		}
184
185
		// also works with null as key
186
		if (!empty($this->availableLanguages[$key])) {
187
			return $this->availableLanguages[$key];
188
		}
189
190
		$available = ['en']; //english is always available
191
		$dir = $this->findL10nDir($app);
192
		$available = array_merge($available, $this->findAvailableLanguageFiles($dir));
193
194
		// merge with translations from themes
195
		$relativePath = substr($dir, strlen($this->serverRoot));
196
		$themeDir = $this->getActiveThemeDirectory();
197
		if ($themeDir !== '') {
198
			$themeDir .= $relativePath;
199
			$available = array_merge($available, $this->findAvailableLanguageFiles($themeDir));
200
		}
201
202
		$this->availableLanguages[$key] = $available;
203
		return $available;
204
	}
205
206
	/**
207
	 * @param string|null $app App id or null for core
208
	 * @param string $lang
209
	 * @return bool
210
	 */
211
	public function languageExists($app, $lang) {
212
		if ($lang === 'en') {//english is always available
213
			return true;
214
		}
215
216
		$languages = $this->findAvailableLanguages($app);
217
		return array_search($lang, $languages) !== false;
218
	}
219
220
	/**
221
	 * @param string|null $app App id or null for core
222
	 * @return string
223
	 */
224
	public function setLanguageFromRequest($app = null) {
225
		$header = $this->request->getHeader('ACCEPT_LANGUAGE');
226
		if ($header) {
227
			$available = $this->findAvailableLanguages($app);
228
229
			// E.g. make sure that 'de' is before 'de_DE'.
230
			sort($available);
231
232
			$preferences = preg_split('/,\s*/', strtolower($header));
233
			foreach ($preferences as $preference) {
234
				list($preferred_language) = explode(';', $preference);
235
				$preferred_language = str_replace('-', '_', $preferred_language);
236
237 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...
238
					if ($preferred_language === strtolower($available_language)) {
239
						if ($app === null && !$this->requestLanguage) {
240
							$this->requestLanguage = $available_language;
241
						}
242
						return $available_language;
243
					}
244
				}
245
246
				// Fallback from de_De to de
247 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...
248
					if (substr($preferred_language, 0, 2) === $available_language) {
249
						if ($app === null && !$this->requestLanguage) {
250
							$this->requestLanguage = $available_language;
251
						}
252
						return $available_language;
253
					}
254
				}
255
			}
256
		}
257
258
		if ($app === null && !$this->requestLanguage) {
259
			$this->requestLanguage = 'en';
260
		}
261
		return 'en'; // Last try: English
262
	}
263
264
	/**
265
	 * Get a list of language files that should be loaded
266
	 *
267
	 * @param string $app
268
	 * @param string $lang
269
	 * @return string[]
270
	 */
271
	// FIXME This method is only public, until \OCP\IL10N does not need it anymore,
272
	// FIXME This is also the reason, why it is not in the public interface
273
	public function getL10nFilesForApp($app, $lang) {
274
		$languageFiles = [];
275
276
		$i18nDir = $this->findL10nDir($app);
277
		$transFile = strip_tags($i18nDir) . strip_tags($lang) . '.json';
278
279
		if ((\OC_Helper::isSubDirectory($transFile, $this->serverRoot . '/core/l10n/')
280
				|| \OC_Helper::isSubDirectory($transFile, $this->serverRoot . '/lib/l10n/')
281
				|| \OC_Helper::isSubDirectory($transFile, $this->serverRoot . '/settings/l10n/')
282
				|| \OC_Helper::isSubDirectory($transFile, \OC_App::getAppPath($app) . '/l10n/')
283
			)
284
			&& file_exists($transFile)) {
285
			// load the translations file
286
			$languageFiles[] = $transFile;
287
		}
288
289
		// merge with translations from themes
290
		$relativePath = substr($transFile, strlen($this->serverRoot));
291
		$themeDir = $this->getActiveThemeDirectory();
292
		if ($themeDir !== '') {
293
			$themeTransFile = $themeDir . $relativePath;
294
			if (file_exists($themeTransFile)) {
295
				$languageFiles[] = $themeTransFile;
296
			}
297
		}
298
299
		return $languageFiles;
300
	}
301
302
	/**
303
	 * find the l10n directory
304
	 *
305
	 * @param string $app App id or empty string for core
306
	 * @return string directory
307
	 */
308
	protected function findL10nDir($app = null) {
309
		if (in_array($app, ['core', 'lib', 'settings'])) {
310
			if (file_exists($this->serverRoot . '/' . $app . '/l10n/')) {
311
				return $this->serverRoot . '/' . $app . '/l10n/';
312
			}
313
		} 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...
314
			// Check if the app is in the app folder
315
			return \OC_App::getAppPath($app) . '/l10n/';
316
		}
317
		return $this->serverRoot . '/core/l10n/';
318
	}
319
320
	/**
321
	 * @param string $dir
322
	 * @return array
323
	 */
324
	protected function findAvailableLanguageFiles($dir) {
325
		$availableLanguageFiles = [];
326
		if (is_dir($dir)) {
327
			$files = scandir($dir);
328
			if ($files !== false) {
329
				foreach ($files as $file) {
330
					if (substr($file, -5) === '.json' && substr($file, 0, 4) !== 'l10n') {
331
						$availableLanguageFiles[] = substr($file, 0, -5);
332
					}
333
				}
334
			}
335
		}
336
		return $availableLanguageFiles;
337
	}
338
339
	/**
340
	 * Get the currently active theme
341
	 *
342
	 * @return string
343
	 */
344
	protected function getActiveThemeDirectory() {
345
		$themeDir = $this->getActiveAppThemeDirectory();
346
		if ($themeDir === '') {
347
			// fallback to legacy theme
348
			$themeDir = $this->getActiveLegacyThemeDirectory();
349
		}
350
		return $themeDir;
351
	}
352
353
	/**
354
	 * Get the currently active legacy theme
355
	 *
356
	 * @return string
357
	 */
358
	protected function getActiveLegacyThemeDirectory() {
359
		$themeDir = '';
360
		$activeLegacyTheme = $this->config->getSystemValue('theme', '');
361
		if ($activeLegacyTheme !== '') {
362
			$themeDir = $this->serverRoot . '/themes/' . $activeLegacyTheme;
363
		}
364
		return $themeDir;
365
	}
366
367
	/**
368
	 * Get the currently active app-theme
369
	 *
370
	 * @return string
371
	 */
372
	protected function getActiveAppThemeDirectory() {
373
		$theme = $this->themeService->getTheme();
374
		if ($theme instanceof ITheme && $theme->getDirectory() !== '' ) {
375
			return $this->serverRoot . '/' . $theme->getDirectory();
376
		}
377
		return '';
378
	}
379
380
	/**
381
	 * Creates a function from the plural string
382
	 *
383
	 * @param string $string
384
	 * @return string Unique function name
385
	 * @since 9.0.0
386
	 */
387
	public function createPluralFunction($string) {
388
		return '';
389
	}
390
}
391