Passed
Push — master ( 0187c3...ac1ef0 )
by Michal
02:29
created

Translator::sanitizePluralExpression()   B

Complexity

Conditions 4
Paths 8

Size

Total Lines 24
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
eloc 13
nc 8
nop 1
dl 0
loc 24
ccs 12
cts 12
cp 1
crap 4
rs 8.6845
c 0
b 0
f 0
1
<?php
2
/*
3
    Copyright (c) 2003, 2009 Danilo Segan <[email protected]>.
4
    Copyright (c) 2005 Nico Kaiser <[email protected]>
5
    Copyright (c) 2016 Michal Čihař <[email protected]>
6
7
    This file is part of MoTranslator.
8
9
    This program is free software; you can redistribute it and/or modify
10
    it under the terms of the GNU General Public License as published by
11
    the Free Software Foundation; either version 2 of the License, or
12
    (at your option) any later version.
13
14
    This program is distributed in the hope that it will be useful,
15
    but WITHOUT ANY WARRANTY; without even the implied warranty of
16
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17
    GNU General Public License for more details.
18
19
    You should have received a copy of the GNU General Public License along
20
    with this program; if not, write to the Free Software Foundation, Inc.,
21
    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22
*/
23
24
namespace PhpMyAdmin\MoTranslator;
25
26
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
27
28
/**
29
 * Provides a simple gettext replacement that works independently from
30
 * the system's gettext abilities.
31
 * It can read MO files and use them for translating strings.
32
 *
33
 * It caches ll strings and translations to speed up the string lookup.
34
 */
35
class Translator
36
{
37
    /**
38
     * None error.
39
     */
40
    const ERROR_NONE = 0;
41
    /**
42
     * File does not exist.
43
     */
44
    const ERROR_DOES_NOT_EXIST = 1;
45
    /**
46
     * File has bad magic number.
47
     */
48
    const ERROR_BAD_MAGIC = 2;
49
    /**
50
     * Error while reading file, probably too short.
51
     */
52
    const ERROR_READING = 3;
53
54
    /**
55
     * Big endian mo file magic bytes.
56
     */
57
    const MAGIC_BE = "\x95\x04\x12\xde";
58
    /**
59
     * Little endian mo file magic bytes.
60
     */
61
    const MAGIC_LE = "\xde\x12\x04\x95";
62
63
    /**
64
     * Parse error code (0 if no error).
65
     *
66
     * @var int
67
     */
68
    public $error = self::ERROR_NONE;
69
70
    /**
71
     * Cache header field for plural forms.
72
     *
73
     * @var string|null
74
     */
75
    private $pluralequation = null;
76
    /**
77
     * @var ExpressionLanguage|null Evaluator for plurals
78
     */
79
    private $pluralexpression = null;
80
    /**
81
     * @var int|null number of plurals
82
     */
83
    private $pluralcount = null;
84
    /**
85
     * Array with original -> translation mapping.
86
     *
87
     * @var array
88
     */
89
    private $cache_translations = array();
90
91
    /**
92
     * Constructor.
93
     *
94
     * @param string $filename Name of mo file to load
95
     */
96 25
    public function __construct($filename)
97
    {
98 25
        if (!is_readable($filename)) {
99 4
            $this->error = self::ERROR_DOES_NOT_EXIST;
100
101 4
            return;
102
        }
103
104 21
        $stream = new StringReader($filename);
105
106
        try {
107 21
            $magic = $stream->read(0, 4);
108 20
            if (strcmp($magic, self::MAGIC_LE) == 0) {
109 13
                $unpack = 'V';
110 7
            } elseif (strcmp($magic, self::MAGIC_BE) == 0) {
111 6
                $unpack = 'N';
112
            } else {
113 1
                $this->error = self::ERROR_BAD_MAGIC;
114
115 1
                return;
116
            }
117
118
            /* Parse header */
119 19
            $total = $stream->readint($unpack, 8);
120 19
            $originals = $stream->readint($unpack, 12);
121 16
            $translations = $stream->readint($unpack, 16);
122
123
            /* get original and translations tables */
124 16
            $table_originals = $stream->readintarray($unpack, $originals, $total * 2);
125 15
            $table_translations = $stream->readintarray($unpack, $translations, $total * 2);
126
127
            /* read all strings to the cache */
128 15
            for ($i = 0; $i < $total; ++$i) {
129 15
                $original = $stream->read($table_originals[$i * 2 + 2], $table_originals[$i * 2 + 1]);
130 15
                $translation = $stream->read($table_translations[$i * 2 + 2], $table_translations[$i * 2 + 1]);
131 15
                $this->cache_translations[$original] = $translation;
132
            }
133 5
        } catch (ReaderException $e) {
134 5
            $this->error = self::ERROR_READING;
135
136 5
            return;
137
        }
138 15
    }
139
140
    /**
141
     * Translates a string.
142
     *
143
     * @param string $msgid String to be translated
144
     *
145
     * @return string translated string (or original, if not found)
146
     */
147 21
    public function gettext($msgid)
148
    {
149 21
        if (array_key_exists($msgid, $this->cache_translations)) {
150 13
            return $this->cache_translations[$msgid];
151
        }
152
153 11
        return $msgid;
154
    }
155
156
    /**
157
     * Sanitize plural form expression for use in ExpressionLanguage.
158
     *
159
     * @param string $expr Expression to sanitize
160
     *
161
     * @return string sanitized plural form expression
162
     */
163 11
    public static function sanitizePluralExpression($expr)
164
    {
165
        // Parse equation
166 11
        $expr = explode(';', $expr);
167 11
        if (count($expr) >= 2) {
168 8
            $expr = $expr[1];
169
        } else {
170 3
            $expr = $expr[0];
171
        }
172 11
        $expr = trim(strtolower($expr));
173
        // Strip plural prefix
174 11
        if (substr($expr, 0, 6) === 'plural') {
175 9
            $expr = ltrim(substr($expr, 6));
176
        }
177
        // Strip equals
178 11
        if (substr($expr, 0, 1) === '=') {
179 9
            $expr = ltrim(substr($expr, 1));
180
        }
181
182
        // Cleanup from unwanted chars
183 11
        $expr = preg_replace('@[^n0-9:\(\)\?=!<>/%&| ]@', '', $expr);
184
185 11
        return $expr;
186
    }
187
188
    /**
189
     * Extracts number of plurals from plurals form expression.
190
     *
191
     * @param string $expr Expression to process
192
     *
193
     * @return int Total number of plurals
194
     */
195 10
    public static function extractPluralCount($expr)
196
    {
197 10
        $parts = explode(';', $expr, 2);
198 10
        $nplurals = explode('=', trim($parts[0]), 2);
199 10
        if (strtolower(rtrim($nplurals[0])) != 'nplurals') {
200 2
            return 1;
201
        }
202 8
        if (count($nplurals) == 1) {
203 1
            return 1;
204
        }
205
206 7
        return intval($nplurals[1]);
207
    }
208
209
    /**
210
     * Parse full PO header and extract only plural forms line.
211
     *
212
     * @param string $header Gettext header
213
     *
214
     * @return string verbatim plural form header field
215
     */
216 8
    public static function extractPluralsForms($header)
217
    {
218 8
        $headers = explode("\n", $header);
219 8
        $expr = 'nplurals=2; plural=n == 1 ? 0 : 1;';
220 8
        foreach ($headers as $header) {
221 8
            if (stripos($header, 'Plural-Forms:') === 0) {
222 8
                $expr = substr($header, 13);
223
            }
224
        }
225
226 8
        return $expr;
227
    }
228
229
    /**
230
     * Get possible plural forms from MO header.
231
     *
232
     * @return string plural form header
233
     */
234 5
    private function getPluralForms()
235
    {
236
        // lets assume message number 0 is header
237
        // this is true, right?
238
239
        // cache header field for plural forms
240 5
        if (is_null($this->pluralequation)) {
241 4
            if (isset($this->cache_translations[''])) {
242 4
                $header = $this->cache_translations[''];
243
            } else {
244
                $hader = '';
0 ignored issues
show
Unused Code introduced by
$hader is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
245
            }
246 4
            $expr = $this->extractPluralsForms($header);
0 ignored issues
show
Bug introduced by
The variable $header does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
247 4
            $this->pluralequation = $this->sanitizePluralExpression($expr);
248 4
            $this->pluralcount = $this->extractPluralCount($expr);
249
        }
250
251 5
        return $this->pluralequation;
252
    }
253
254
    /**
255
     * Detects which plural form to take.
256
     *
257
     * @param int $n count of objects
258
     *
259
     * @return int array index of the right plural form
260
     */
261 5
    private function selectString($n)
262
    {
263 5
        if (is_null($this->pluralexpression)) {
264 4
            $this->pluralexpression = new ExpressionLanguage();
265
        }
266 5
        $plural = $this->pluralexpression->evaluate(
267 5
            $this->getPluralForms(), array('n' => $n)
268
        );
269
270 5
        if ($plural >= $this->pluralcount) {
271 1
            $plural = $this->pluralcount - 1;
272
        }
273
274 5
        return $plural;
275
    }
276
277
    /**
278
     * Plural version of gettext.
279
     *
280
     * @param string $msgid       Single form
281
     * @param string $msgidPlural Plural form
282
     * @param int    $number      Number of objects
283
     *
284
     * @return string translated plural form
285
     */
286 14
    public function ngettext($msgid, $msgidPlural, $number)
287
    {
288
        // this should contains all strings separated by NULLs
289 14
        $key = implode(chr(0), array($msgid, $msgidPlural));
290 14
        if (!array_key_exists($key, $this->cache_translations)) {
291 14
            return ($number != 1) ? $msgidPlural : $msgid;
292
        }
293
294
        // find out the appropriate form
295 5
        $select = $this->selectString($number);
296
297 5
        $result = $this->cache_translations[$key];
298 5
        $list = explode(chr(0), $result);
299
300 5
        return $list[$select];
301
    }
302
303
    /**
304
     * Translate with context.
305
     *
306
     * @param string $msgctxt Context
307
     * @param string $msgid   String to be translated
308
     *
309
     * @return string translated plural form
310
     */
311 11 View Code Duplication
    public function pgettext($msgctxt, $msgid)
312
    {
313 11
        $key = implode(chr(4), array($msgctxt, $msgid));
314 11
        $ret = $this->gettext($key);
315 11
        if (strpos($ret, chr(4)) !== false) {
316 6
            return $msgid;
317
        }
318
319 5
        return $ret;
320
    }
321
322
    /**
323
     * Plural version of pgettext.
324
     *
325
     * @param string $msgctxt     Context
326
     * @param string $msgid       Single form
327
     * @param string $msgidPlural Plural form
328
     * @param int    $number      Number of objects
329
     *
330
     * @return string translated plural form
331
     */
332 4 View Code Duplication
    public function npgettext($msgctxt, $msgid, $msgidPlural, $number)
333
    {
334 4
        $key = implode(chr(4), array($msgctxt, $msgid));
335 4
        $ret = $this->ngettext($key, $msgidPlural, $number);
336 4
        if (strpos($ret, chr(4)) !== false) {
337 1
            return $msgid;
338
        }
339
340 3
        return $ret;
341
    }
342
}
343