Language::getTranslationsFromFile()   C
last analyzed

Complexity

Conditions 12
Paths 98

Size

Total Lines 117
Code Lines 57

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 12
eloc 57
nc 98
nop 1
dl 0
loc 117
rs 6.5115
c 0
b 0
f 0

How to fix   Long Method    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
/**
4
 * Language handling class.
5
 */
6
class Language {
7
	private $languages = ["en_US.UTF-8" => "English"];
8
	private $lang;
9
	private $loaded = false;
10
11
	/**
12
	 * Default constructor.
13
	 *
14
	 * By default, the Language class only knows about en_GB (English). If you want more languages, you
15
	 * must call loadLanguages().
16
	 */
17
	public function __construct() {}
18
19
	/**
20
	 * Loads languages from disk.
21
	 *
22
	 * loadLanguages() reads the languages from disk by reading LANGUAGE_DIR and opening all directories
23
	 * in that directory. Each directory must contain a 'language.txt' file containing:
24
	 *
25
	 * <language display name>
26
	 * <win32 language name>
27
	 *
28
	 * For example:
29
	 * <code>
30
	 * Nederlands
31
	 * nld_NLD
32
	 * </code>
33
	 *
34
	 * Also, the directory names must have a name that is:
35
	 * 1. Available to the server's locale system
36
	 * 2. In the UTF-8 charset
37
	 *
38
	 * For example, nl_NL.UTF-8
39
	 */
40
	public function loadLanguages() {
41
		if ($this->loaded) {
42
			return;
43
		}
44
45
		$languages = explode(";", ENABLED_LANGUAGES);
46
		$dh = opendir(LANGUAGE_DIR);
47
		while (($entry = readdir($dh)) !== false) {
48
			$langcode = str_ireplace(".UTF-8", "", $entry);
49
			if (in_array($langcode, $languages) || in_array($entry, $languages)) {
50
				if (is_dir(LANGUAGE_DIR . $entry . "/LC_MESSAGES") && is_file(LANGUAGE_DIR . $entry . "/language.txt")) {
51
					$fh = fopen(LANGUAGE_DIR . $entry . "/language.txt", "r");
52
					$lang_title = fgets($fh);
53
					fclose($fh);
54
					$this->languages[$entry] = "{$langcode}: " . trim($lang_title);
55
				}
56
			}
57
		}
58
		asort($this->languages, SORT_LOCALE_STRING);
59
		$this->loaded = true;
60
	}
61
62
	/**
63
	 * Attempt to set language.
64
	 *
65
	 * setLanguage attempts to set the language to the specified language. The language passed
66
	 * is the name of the directory containing the language.
67
	 *
68
	 * For setLanguage() to succeed, the language has to have been loaded via loadLanguages() AND
69
	 * the gettext system must 'know' the language specified.
70
	 *
71
	 * @param string $lang Language code (eg nl_NL.UTF-8)
72
	 */
73
	public function setLanguage($lang) {
74
		if (isset($GLOBALS['translations'])) {
75
			return;
76
		}
77
		$lang = (empty($lang) || str_starts_with($lang, '.') || $lang == "C") ? LANG : $lang; // default language fix
78
79
		if ($this->is_language($lang)) {
80
			$this->lang = $lang;
81
			$tmp_translations = $this->getTranslations();
82
			$translations = [];
83
			foreach ($tmp_translations as $resources) {
84
				$resourcesCount = count($resources);
85
				for ($i = 0; $i < $resourcesCount; ++$i) {
86
					$msgid = $resources[$i]['msgid'];
87
					if (isset($msgid)) {
88
						$translations[$msgid] = $resources[$i]['msgstr'];
89
					}
90
				}
91
			}
92
			$GLOBALS['translations'] = $translations;
93
		}
94
		else {
95
			error_log(sprintf("Unknown language: '%s'", $lang));
96
		}
97
	}
98
99
	public static function getstring($string) {
100
		if (isset($GLOBALS['translations'], $GLOBALS['translations'][$string])) {
101
			return $GLOBALS['translations'][$string];
102
		}
103
104
		return $string;
105
	}
106
107
	/**
108
	 * Return a list of supported languages.
109
	 *
110
	 * Returns an associative array in the format langid -> langname, for example "nl_NL.utf8" -> "Nederlands"
111
	 *
112
	 * @return array List of supported languages
113
	 */
114
	public function getLanguages() {
115
		$this->loadLanguages();
116
117
		return $this->languages;
118
	}
119
120
	/**
121
	 * Returns the $getLanguages and formats in JSON so it can be parsed
122
	 * by the javascript.
123
	 *
124
	 * @return string The javascript string
125
	 */
126
	public function getJSON() {
127
		$json = [];
128
		$languages = $this->getLanguages();
129
		foreach ($languages as $key => $lang) {
130
			$json[] = [
131
				"lang" => $key,
132
				"name" => $lang,
133
			];
134
		}
135
136
		return json_encode($json);
137
	}
138
139
	/**
140
	 * Returns the ID of the currently selected language.
141
	 *
142
	 * @return string ID of selected language
143
	 */
144
	public function getSelected() {
145
		return $this->lang;
146
	}
147
148
	/**
149
	 * Returns if the specified language is valid or not.
150
	 *
151
	 * @param string $lang
152
	 *
153
	 * @return bool TRUE if the language is valid
154
	 */
155
	public function is_language($lang) {
156
		return $lang == "en_GB.UTF-8" || is_dir(LANGUAGE_DIR . "/" . $lang);
157
	}
158
159
	/**
160
	 * Returns the resolved language code, i.e. ending on UTF-8.
161
	 * Examples:
162
	 *  - en_GB => en.GB.UTF-8
163
	 *  - en_GB.utf8 => en_GB.UTF-8
164
	 *  - en_GB.UTF-8 => en_GB.UTF-8 (no changes).
165
	 *
166
	 * @param string $lang language code to resolve
167
	 *
168
	 * @return string resolved language name (i.e. language code ending on .UTF-8).
169
	 */
170
	public static function resolveLanguage($lang) {
171
		$normalizedLang = stristr($lang, '.utf-8', true);
172
		if (!empty($normalizedLang) && $normalizedLang !== $lang) {
173
			// Make sure we will use the format UTF-8 (capitals and hyphen)
174
			return $normalizedLang .= '.UTF-8';
175
		}
176
177
		$normalizedLang = stristr($lang, '.utf8', true);
178
		if (!empty($normalizedLang) && $normalizedLang !== $lang) {
179
			// Make sure we will use the format UTF-8 (capitals and hyphen)
180
			return $normalizedLang . '.UTF-8';
181
		}
182
183
		return $lang . '.UTF-8';
184
	}
185
186
	public function getTranslations() {
187
		$memid = @shm_attach(0x950412DE, 16 * 1024 * 1024, 0644);
188
		if ($memid && @shm_has_var($memid, 0)) {
0 ignored issues
show
Bug introduced by
It seems like $memid can also be of type resource; however, parameter $shm of shm_has_var() does only seem to accept SysvSharedMemory, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

188
		if ($memid && @shm_has_var(/** @scrutinizer ignore-type */ $memid, 0)) {
Loading history...
189
			$cache_table = @shm_get_var($memid, 0);
0 ignored issues
show
Bug introduced by
It seems like $memid can also be of type resource; however, parameter $shm of shm_get_var() does only seem to accept SysvSharedMemory, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

189
			$cache_table = @shm_get_var(/** @scrutinizer ignore-type */ $memid, 0);
Loading history...
190
			$selected_lang = $this->getSelected();
191
			if (empty($cache_table) || empty($cache_table[$selected_lang])) {
192
				@shm_remove_var($memid, 0);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for shm_remove_var(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

192
				/** @scrutinizer ignore-unhandled */ @shm_remove_var($memid, 0);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
Bug introduced by
It seems like $memid can also be of type resource; however, parameter $shm of shm_remove_var() does only seem to accept SysvSharedMemory, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

192
				@shm_remove_var(/** @scrutinizer ignore-type */ $memid, 0);
Loading history...
193
				@shm_detach($memid);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for shm_detach(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

193
				/** @scrutinizer ignore-unhandled */ @shm_detach($memid);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
Bug introduced by
It seems like $memid can also be of type resource; however, parameter $shm of shm_detach() does only seem to accept SysvSharedMemory, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

193
				@shm_detach(/** @scrutinizer ignore-type */ $memid);
Loading history...
194
195
				return ['grommunio_web' => []];
196
			}
197
			$translation_id = $cache_table[$selected_lang];
198
			if (empty($translation_id)) {
199
				@shm_remove_var($memid, 0);
200
				@shm_detach($memid);
201
202
				return ['grommunio_web' => []];
203
			}
204
			$translations = @shm_get_var($memid, $translation_id);
205
			if (empty($translations)) {
206
				@shm_remove_var($memid, 0);
207
				@shm_detach($memid);
208
209
				return ['grommunio_web' => []];
210
			}
211
			@shm_detach($memid);
212
213
			return $translations;
214
		}
215
		$handle = opendir(LANGUAGE_DIR);
216
		if ($handle == false) {
217
			if ($memid) {
218
				@shm_detach($memid);
219
			}
220
221
			return ['grommunio_web' => []];
222
		}
223
		$last_id = 1;
224
		$cache_table = [];
225
		while (false !== ($entry = readdir($handle))) {
226
			if (strcmp($entry, ".") == 0 ||
227
				strcmp($entry, "..") == 0) {
228
				continue;
229
			}
230
			$translations = [];
231
			$translations['grommunio_web'] = $this->getTranslationsFromFile(LANGUAGE_DIR . $entry . '/LC_MESSAGES/grommunio_web.mo');
232
			if (!$translations['grommunio_web']) {
233
				continue;
234
			}
235
			if (isset($GLOBALS['PluginManager'])) {
236
				// What we did above, we are also now going to do for each plugin that has translations.
237
				$pluginTranslationPaths = $GLOBALS['PluginManager']->getTranslationFilePaths();
238
				foreach ($pluginTranslationPaths as $pluginname => $path) {
239
					$plugin_translations = $this->getTranslationsFromFile($path . '/' . $entry . '/LC_MESSAGES/plugin_' . $pluginname . '.mo');
240
					if ($plugin_translations) {
241
						$translations['plugin_' . $pluginname] = $plugin_translations;
242
					}
243
				}
244
			}
245
			$cache_table[$entry] = $last_id;
246
			if ($memid) {
247
				@shm_put_var($memid, $last_id, $translations);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for shm_put_var(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

247
				/** @scrutinizer ignore-unhandled */ @shm_put_var($memid, $last_id, $translations);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
Bug introduced by
It seems like $memid can also be of type resource; however, parameter $shm of shm_put_var() does only seem to accept SysvSharedMemory, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

247
				@shm_put_var(/** @scrutinizer ignore-type */ $memid, $last_id, $translations);
Loading history...
248
			}
249
			if (strcmp($entry, $this->getSelected()) == 0) {
250
				$ret_val = $translations;
251
			}
252
			++$last_id;
253
		}
254
		closedir($handle);
255
		if ($memid) {
256
			@shm_put_var($memid, 0, $cache_table);
257
			@shm_detach($memid);
258
		}
259
		if (empty($ret_val)) {
260
			return ['grommunio_web' => []];
261
		}
262
263
		return $ret_val;
264
	}
265
266
	/**
267
	 * getTranslationsFromFile.
268
	 *
269
	 * This file reads the translations from the binary .mo file and returns
270
	 * them in an array containing the original and the translation variant.
271
	 * The .mo file format is described on the following URL.
272
	 * http://www.gnu.org/software/gettext/manual/gettext.html#MO-Files
273
	 *
274
	 *          byte
275
	 *               +------------------------------------------+
276
	 *            0  | magic number = 0x950412de                |
277
	 *               |                                          |
278
	 *            4  | file format revision = 0                 |
279
	 *               |                                          |
280
	 *            8  | number of strings                        |  == N
281
	 *               |                                          |
282
	 *           12  | offset of table with original strings    |  == O
283
	 *               |                                          |
284
	 *           16  | offset of table with translation strings |  == T
285
	 *               |                                          |
286
	 *           20  | size of hashing table                    |  == S
287
	 *               |                                          |
288
	 *           24  | offset of hashing table                  |  == H
289
	 *               |                                          |
290
	 *               .                                          .
291
	 *               .    (possibly more entries later)         .
292
	 *               .                                          .
293
	 *               |                                          |
294
	 *            O  | length & offset 0th string  ----------------.
295
	 *        O + 8  | length & offset 1st string  ------------------.
296
	 *                ...                                    ...   | |
297
	 *  O + ((N-1)*8)| length & offset (N-1)th string           |  | |
298
	 *               |                                          |  | |
299
	 *            T  | length & offset 0th translation  ---------------.
300
	 *        T + 8  | length & offset 1st translation  -----------------.
301
	 *                ...                                    ...   | | | |
302
	 *  T + ((N-1)*8)| length & offset (N-1)th translation      |  | | | |
303
	 *               |                                          |  | | | |
304
	 *            H  | start hash table                         |  | | | |
305
	 *                ...                                    ...   | | | |
306
	 *    H + S * 4  | end hash table                           |  | | | |
307
	 *               |                                          |  | | | |
308
	 *               | NUL terminated 0th string  <----------------' | | |
309
	 *               |                                          |    | | |
310
	 *               | NUL terminated 1st string  <------------------' | |
311
	 *               |                                          |      | |
312
	 *                ...                                    ...       | |
313
	 *               |                                          |      | |
314
	 *               | NUL terminated 0th translation  <---------------' |
315
	 *               |                                          |        |
316
	 *               | NUL terminated 1st translation  <-----------------'
317
	 *               |                                          |
318
	 *                ...                                    ...
319
	 *               |                                          |
320
	 *               +------------------------------------------+
321
	 *
322
	 * @param $filename string Name of the .mo file.
323
	 *
324
	 * @return array|bool false when file is missing otherwise array with
325
	 *                    translations
326
	 */
327
	public function getTranslationsFromFile($filename) {
328
		if (!is_file($filename)) {
329
			return false;
330
		}
331
332
		$fp = fopen($filename, 'r');
333
		if (!$fp) {
0 ignored issues
show
introduced by
$fp is of type resource, thus it always evaluated to false.
Loading history...
334
			return false;
335
		}
336
337
		// Get number of strings in .mo file
338
		fseek($fp, 8, SEEK_SET);
339
		$num_of_str = unpack('Lnum', fread($fp, 4));
340
		$num_of_str = $num_of_str['num'];
341
342
		// Get offset to table with original strings
343
		fseek($fp, 12, SEEK_SET);
344
		$offset_orig_tbl = unpack('Loffset', fread($fp, 4));
345
		$offset_orig_tbl = $offset_orig_tbl['offset'];
346
347
		// Get offset to table with translation strings
348
		fseek($fp, 16, SEEK_SET);
349
		$offset_transl_tbl = unpack('Loffset', fread($fp, 4));
350
		$offset_transl_tbl = $offset_transl_tbl['offset'];
351
352
		// The following arrays will contain the length and offset of the strings
353
		$data_orig_strs = [];
354
		$data_transl_strs = [];
355
356
		/*
357
		 * Get the length and offset to the original strings by using the table
358
		 * with original strings
359
		 */
360
		// Set pointer to start of orig string table
361
		fseek($fp, $offset_orig_tbl, SEEK_SET);
362
		for ($i = 0; $i < $num_of_str; ++$i) {
363
			// Length 4 bytes followed by offset 4 bytes
364
			$length = unpack('Llen', fread($fp, 4));
365
			$offset = unpack('Loffset', fread($fp, 4));
366
			$data_orig_strs[$i] = ['length' => $length['len'], 'offset' => $offset['offset']];
367
		}
368
369
		/*
370
		 * Get the length and offset to the translation strings by using the table
371
		 * with translation strings
372
		 */
373
		// Set pointer to start of translations string table
374
		fseek($fp, $offset_transl_tbl, SEEK_SET);
375
		for ($i = 0; $i < $num_of_str; ++$i) {
376
			// Length 4 bytes followed by offset 4 bytes
377
			$length = unpack('Llen', fread($fp, 4));
378
			$offset = unpack('Loffset', fread($fp, 4));
379
			$data_transl_strs[$i] = ['length' => $length['len'], 'offset' => $offset['offset']];
380
		}
381
382
		// This array will contain the actual original and translation strings
383
		$translation_data = [];
384
385
		// Get the original strings using the length and offset
386
		for ($i = 0, $len = count($data_orig_strs); $i < $len; ++$i) {
387
			$translation_data[$i] = [];
388
389
			// Set pointer to the offset of the string
390
			fseek($fp, $data_orig_strs[$i]['offset'], SEEK_SET);
391
392
			// Set default values for context and plural forms
393
			$translation_data[$i]['msgctxt'] = false;
394
			$translation_data[$i]['msgid_plural'] = false;
395
396
			if ($data_orig_strs[$i]['length'] > 0) {	// fread does not accept length=0
397
				$length = $data_orig_strs[$i]['length'];
398
				$orig_str = unpack('a' . $length . 'str', fread($fp, $length));
399
				$translation_data[$i]['msgid'] = $orig_str['str'];	// unpack converts to array :S
400
401
				// Find context in the original string
402
				if (str_contains((string) $translation_data[$i]['msgid'], "\004")) {
403
					$contextSplit = explode("\004", (string) $translation_data[$i]['msgid']);
404
					$translation_data[$i]['msgctxt'] = $contextSplit[0];
405
					$translation_data[$i]['msgid'] = $contextSplit[1];
406
				}
407
				// Find plural forms in the original string
408
				if (str_contains((string) $translation_data[$i]['msgid'], "\0")) {
409
					$original = explode("\0", (string) $translation_data[$i]['msgid']);
410
					$translation_data[$i]['msgid'] = $original[0];
411
					$translation_data[$i]['msgid_plural'] = $original[1];
412
				}
413
			}
414
			else {
415
				$translation_data[$i]['msgid'] = '';
416
			}
417
		}
418
419
		// Get the translation strings using the length and offset
420
		for ($i = 0, $len = count($data_transl_strs); $i < $len; ++$i) {
421
			// Set pointer to the offset of the string
422
			fseek($fp, $data_transl_strs[$i]['offset'], SEEK_SET);
423
			if ($data_transl_strs[$i]['length'] > 0) {	// fread does not accept length=0
424
				$length = $data_transl_strs[$i]['length'];
425
				$trans_str = unpack('a' . $length . 'str', fread($fp, $length));
426
				$translation_data[$i]['msgstr'] = $trans_str['str'];	// unpack converts to array :S
427
428
				// If there are plural forms in the source string,
429
				// then the translated string must contain plural
430
				// forms as well.  We cannot depend on a \0 being
431
				// present at all times, because languages that
432
				// have only one plural form won't have this
433
				// (e.g. Japanese)
434
				if ($translation_data[$i]['msgid_plural'] !== false) {
435
					$translation_data[$i]['msgstr'] = explode("\0", (string) $translation_data[$i]['msgstr']);
436
				}
437
			}
438
			else {
439
				$translation_data[$i]['msgstr'] = '';
440
			}
441
		}
442
443
		return $translation_data;
444
	}
445
}
446