Passed
Branch php-cs-fixer (b9836a)
by Fabio
15:02
created

MessageSource_gettext::loadData()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 11
nc 2
nop 1
dl 0
loc 15
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * MessageSource_gettext class file.
5
 *
6
 * This program is free software; you can redistribute it and/or modify
7
 * it under the terms of the BSD License.
8
 *
9
 * Copyright(c) 2004 by Qiang Xue. All rights reserved.
10
 *
11
 * To contact the author write to {@link mailto:[email protected] Qiang Xue}
12
 * The latest version of PRADO can be obtained from:
13
 * {@link http://prado.sourceforge.net/}
14
 *
15
 * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
16
 * @package Prado\I18N\core
17
 */
18
19
namespace Prado\I18N\core;
20
21
use Prado\Exceptions\TException;
22
use Prado\Exceptions\TIOException;
23
use Prado\I18N\core\Gettext\TGettext;
24
25
/**
26
 * Get the Gettext class.
27
 */
28
require_once(dirname(__FILE__) . '/Gettext/TGettext.php');
29
30
/**
31
 * MessageSource_gettext class.
32
 *
33
 * Using Gettext MO format as the message source for translation.
34
 * The gettext classes are based on PEAR's gettext MO and PO classes.
35
 *
36
 * See the MessageSource::factory() method to instantiate this class.
37
 *
38
 * @author Xiang Wei Zhuo <weizhuo[at]gmail[dot]com>
39
 * @package Prado\I18N\core
40
 */
41
class MessageSource_gettext extends MessageSource
42
{
43
	/**
44
	 * Message data filename extension.
45
	 * @var string
46
	 */
47
	protected $dataExt = '.mo';
48
49
	/**
50
	 * PO data filename extension
51
	 * @var string
52
	 */
53
	protected $poExt = '.po';
54
55
	/**
56
	 * Separator between culture name and source.
57
	 * @var string
58
	 */
59
	protected $dataSeparator = '.';
60
61
	public function __construct($source)
62
	{
63
		$this->source = (string)$source;
64
	}
65
66
67
	/**
68
	 * Load the messages from a MO file.
0 ignored issues
show
Bug introduced by
The type Prado\I18N\core\MO was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
69
	 * @param string MO file.
70
	 * @return array of messages.
71
	 */
72
	protected function &loadData($filename)
73
	{
74
		$mo = TGettext::factory('MO', $filename);
75
		$mo->load();
76
		$result = $mo->toArray();
77
78
		$results = [];
79
		$count = 0;
80
		foreach($result['strings'] as $source => $target)
81
		{
82
			$results[$source][] = $target; //target
83
			$results[$source][] = $count++; //id
84
			$results[$source][] = ''; //comments
85
		}
86
		return $results;
87
	}
88
89
	/**
90
	 * Determin if the MO file source is valid.
91
	 * @param string MO file
92
	 * @return boolean true if valid, false otherwise.
93
	 */
94
	protected function isValidSource($filename)
95
	{
96
		return is_file($filename);
97
	}
98
99
	/**
100
	 * Get the MO file for a specific message catalogue and cultural
101
	 * vairant.
102
	 * @param string message catalogue
103
	 * @return string full path to the MO file.
104
	 */
105
	protected function getSource($variant)
106
	{
107
		return $this->source . '/' . $variant;
108
	}
109
110
	/**
111
	 * Get the last modified unix-time for this particular catalogue+variant.
112
	 * Just use the file modified time.
113
	 * @param string catalogue+variant
114
	 * @return int last modified in unix-time format.
115
	 */
0 ignored issues
show
Documentation Bug introduced by
The doc comment catalogue+variant at position 0 could not be parsed: Unknown type name 'catalogue+variant' at position 0 in catalogue+variant.
Loading history...
116
	protected function getLastModified($source)
117
	{
118
		if(is_file($source))
119
			return filemtime($source);
120
		else
121
			return 0;
122
	}
123
124
	/**
125
	 * Get all the variants of a particular catalogue.
0 ignored issues
show
Bug introduced by
The type Prado\I18N\core\catalogue was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
126
	 * @param string catalogue name
127
	 * @return array list of all variants for this catalogue.
128
	 */
129
	protected function getCatalogueList($catalogue)
130
	{
131
		$variants = explode('_', $this->culture);
132
		$source = $catalogue . $this->dataExt;
133
134
		$catalogues = [$source];
135
136
		$variant = null;
137
138
		for($i = 0, $k = count($variants); $i < $k; ++$i)
139
		{
140
			if(isset($variants[$i]{0}))
141
			{
142
				$variant .= ($variant)?'_' . $variants[$i]:$variants[$i];
143
				$catalogues[] = $catalogue . $this->dataSeparator .
144
								$variant . $this->dataExt;
145
			}
146
		}
147
		$byDir = $this->getCatalogueByDir($catalogue);
148
		$catalogues = array_merge($byDir, array_reverse($catalogues));
149
		return $catalogues;
150
	}
151
152
153
	/**
154
	 * Traverse through the directory structure to find the catalogues.
0 ignored issues
show
Bug introduced by
The type Prado\I18N\core\a was not found. Did you mean a? If so, make sure to prefix the type with \.
Loading history...
155
	 * This should only be called by getCatalogueList()
156
	 * @param string a particular catalogue.
157
	 * @return array a list of catalogues.
158
	 * @see getCatalogueList()
159
	 */
160
	private function getCatalogueByDir($catalogue)
161
	{
162
		$variants = explode('_', $this->culture);
163
		$catalogues = [];
164
165
		$variant = null;
166
167
		for($i = 0, $k = count($variants); $i < $k; ++$i)
168
		{
169
			if(isset($variants[$i]{0}))
170
			{
171
				$variant .= ($variant)?'_' . $variants[$i]:$variants[$i];
172
				$catalogues[] = $variant . '/' . $catalogue . $this->dataExt;
173
			}
174
		}
175
		return array_reverse($catalogues);
176
	}
177
178
	/**
179
	 * Get the variant for a catalogue depending on the current culture.
180
	 * @param string catalogue
181
	 * @return string the variant.
182
	 * @see save()
183
	 * @see update()
184
	 * @see delete()
185
	 */
186
	private function getVariants($catalogue = 'messages')
187
	{
188
		if($catalogue === null) {
189
			$catalogue = 'messages';
190
		}
191
192
		foreach($this->getCatalogueList($catalogue) as $variant)
193
		{
194
			$file = $this->getSource($variant);
195
			$po = $this->getPOFile($file);
196
			if(is_file($file) || is_file($po))
197
				return [$variant, $file, $po];
0 ignored issues
show
Bug Best Practice introduced by
The expression return array($variant, $file, $po) returns the type array<integer,mixed|string> which is incompatible with the documented return type string.
Loading history...
198
		}
199
		return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type string.
Loading history...
200
	}
201
202
	private function getPOFile($MOFile)
203
	{
204
		$filebase = substr($MOFile, 0, strlen($MOFile) - strlen($this->dataExt));
205
		return $filebase . $this->poExt;
206
	}
207
208
	/**
209
	 * Save the list of untranslated blocks to the translation source.
0 ignored issues
show
Bug introduced by
The type Prado\I18N\core\the was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
210
	 * If the translation was not found, you should add those
211
	 * strings to the translation source via the <b>append()</b> method.
212
	 * @param string the catalogue to add to
213
	 * @return boolean true if saved successfuly, false otherwise.
214
	 */
215
	public function save($catalogue = 'messages')
216
	{
217
		$messages = $this->untranslated;
218
219
		if(count($messages) <= 0) return false;
220
221
		$variants = $this->getVariants($catalogue);
222
223
		if($variants)
224
			list($variant, $MOFile, $POFile) = $variants;
225
		else
226
			list($variant, $MOFile, $POFile) = $this->createMessageTemplate($catalogue);
227
228
		if(is_writable($MOFile) == false)
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
229
			throw new TIOException("Unable to save to file {$MOFile}, file must be writable.");
230
		if(is_writable($POFile) == false)
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
231
			throw new TIOException("Unable to save to file {$POFile}, file must be writable.");
232
233
		//set the strings as untranslated.
234
		$strings = [];
235
		foreach($messages as $message)
236
			$strings[$message] = '';
237
238
		//load the PO
239
		$po = TGettext::factory('PO', $POFile);
240
		$po->load();
241
		$result = $po->toArray();
242
243
		$existing = count($result['strings']);
244
245
		//add to strings to the existing message list
246
		$result['strings'] = array_merge($result['strings'], $strings);
247
248
		$new = count($result['strings']);
249
250
		if($new > $existing)
251
		{
252
			//change the date 2004-12-25 12:26
253
			$result['meta']['PO-Revision-Date'] = @date('Y-m-d H:i:s');
254
255
			$po->fromArray($result);
256
			$mo = $po->toMO();
257
			if($po->save() && $mo->save($MOFile))
258
			{
259
				if(!empty($this->cache))
260
					$this->cache->clean($variant, $this->culture);
261
				return true;
262
			}
263
			else
264
				return false;
265
		}
266
		return false;
267
	}
268
269
	/**
270
	 * Delete a particular message from the specified catalogue.
271
	 * @param string the source message to delete.
272
	 * @param string the catalogue to delete from.
273
	 * @return boolean true if deleted, false otherwise.
274
	 */
275
	public function delete($message, $catalogue = 'messages')
276
	{
277
		$variants = $this->getVariants($catalogue);
278
		if($variants)
279
			list($variant, $MOFile, $POFile) = $variants;
280
		else
281
			return false;
282
283
		if(is_writable($MOFile) == false)
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
284
			throw new TIOException("Unable to modify file {$MOFile}, file must be writable.");
285
		if(is_writable($POFile) == false)
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
286
			throw new TIOException("Unable to modify file {$POFile}, file must be writable.");
287
288
		$po = TGettext::factory('PO', $POFile);
289
		$po->load();
290
		$result = $po->toArray();
291
292
		foreach($result['strings'] as $string => $value)
293
		{
294
			if($string == $message)
295
			{
296
				$result['meta']['PO-Revision-Date'] = @date('Y-m-d H:i:s');
297
				unset($result['strings'][$string]);
298
299
				$po->fromArray($result);
300
				$mo = $po->toMO();
301
				if($po->save() && $mo->save($MOFile))
302
				{
303
					if(!empty($this->cache))
304
						$this->cache->clean($variant, $this->culture);
305
					return true;
306
				}
307
				else
308
					return false;
309
			}
310
		}
311
312
		return false;
313
	}
314
315
	/**
316
	 * Update the translation.
317
	 * @param string the source string.
318
	 * @param string the new translation string.
319
	 * @param string comments
320
	 * @param string the catalogue of the translation.
321
	 * @return boolean true if translation was updated, false otherwise.
322
	 */
323
	public function update($text, $target, $comments, $catalogue = 'messages')
324
	{
325
		$variants = $this->getVariants($catalogue);
326
		if($variants)
327
			list($variant, $MOFile, $POFile) = $variants;
328
		else
329
			return false;
330
331
		if(is_writable($MOFile) == false)
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
332
			throw new TIOException("Unable to update file {$MOFile}, file must be writable.");
333
		if(is_writable($POFile) == false)
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
334
			throw new TIOException("Unable to update file {$POFile}, file must be writable.");
335
336
337
		$po = TGettext::factory('PO', $POFile);
338
		$po->load();
339
		$result = $po->toArray();
340
341
		foreach($result['strings'] as $string => $value)
342
		{
343
			if($string == $text)
344
			{
345
				$result['strings'][$string] = $target;
346
				$result['meta']['PO-Revision-Date'] = @date('Y-m-d H:i:s');
347
348
				$po->fromArray($result);
349
				$mo = $po->toMO();
350
351
				if($po->save() && $mo->save($MOFile))
352
				{
353
					if(!empty($this->cache))
354
						$this->cache->clean($variant, $this->culture);
355
					return true;
356
				}
357
				else
358
					return false;
359
			}
360
		}
361
362
		return false;
363
	}
364
365
366
	/**
367
	 * Returns a list of catalogue as key and all it variants as value.
368
	 * @return array list of catalogues
369
	 */
370
	public function catalogues()
371
	{
372
		return $this->getCatalogues();
373
	}
374
375
	/**
376
	 * Returns a list of catalogue and its culture ID. This takes care
377
	 * of directory structures.
378
	 * E.g. array('messages','en_AU')
379
	 * @return array list of catalogues
380
	 */
381
	protected function getCatalogues($dir = null, $variant = null)
382
	{
383
		$dir = $dir?$dir:$this->source;
384
		$files = scandir($dir);
385
386
		$catalogue = [];
387
388
		foreach($files as $file)
389
		{
390
			if(is_dir($dir . '/' . $file)
391
				&& preg_match('/^[a-z]{2}(_[A-Z]{2,3})?$/', $file))
392
			{
393
394
				$catalogue = array_merge($catalogue,
395
								$this->getCatalogues($dir . '/' . $file, $file));
396
			}
397
398
			$pos = strpos($file, $this->dataExt);
399
400
			if($pos > 0
401
				&& substr($file, -1 * strlen($this->dataExt)) == $this->dataExt)
402
			{
403
				$name = substr($file, 0, $pos);
404
				$dot = strrpos($name, $this->dataSeparator);
405
				$culture = $variant;
406
				$cat = $name;
407
				if(is_int($dot))
408
				{
409
					$culture = substr($name, $dot + 1, strlen($name));
410
					$cat = substr($name, 0, $dot);
411
				}
412
				$details[0] = $cat;
413
				$details[1] = $culture;
414
415
416
				$catalogue[] = $details;
417
			}
418
		}
419
		sort($catalogue);
420
421
		return $catalogue;
422
	}
423
424
	protected function createMessageTemplate($catalogue)
425
	{
426
		if($catalogue === null) {
427
			$catalogue = 'messages';
428
		}
429
		$variants = $this->getCatalogueList($catalogue);
430
		$variant = array_shift($variants);
431
		$mo_file = $this->getSource($variant);
432
		$po_file = $this->getPOFile($mo_file);
433
434
		$dir = dirname($mo_file);
435
		if(!is_dir($dir))
436
		{
437
			@mkdir($dir);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for mkdir(). 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

437
			/** @scrutinizer ignore-unhandled */ @mkdir($dir);

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...
438
			@chmod($dir, PRADO_CHMOD);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for chmod(). 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

438
			/** @scrutinizer ignore-unhandled */ @chmod($dir, PRADO_CHMOD);

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...
439
		}
440
		if(!is_dir($dir))
441
			throw new TException("Unable to create directory $dir");
442
443
		$po = TGettext::factory('PO', $po_file);
444
		$result['meta']['PO-Revision-Date'] = @date('Y-m-d H:i:s');
0 ignored issues
show
Comprehensibility Best Practice introduced by
$result was never initialized. Although not strictly required by PHP, it is generally a good practice to add $result = array(); before regardless.
Loading history...
445
		$result['strings'] = [];
446
447
		$po->fromArray($result);
448
		$mo = $po->toMO();
449
		if($po->save() && $mo->save($mo_file))
450
			return [$variant, $mo_file, $po_file];
451
		else
452
			throw new TException("Unable to create file $po_file and $mo_file");
453
	}
454
}
455
456