1 | <?php |
||
33 | class Translator { |
||
34 | |||
35 | /** |
||
36 | * None error |
||
37 | */ |
||
38 | const ERROR_NONE = 0; |
||
39 | /** |
||
40 | * File does not exist |
||
41 | */ |
||
42 | const ERROR_DOES_NOT_EXIST = 1; |
||
43 | /** |
||
44 | * File has bad magic number |
||
45 | */ |
||
46 | const ERROR_BAD_MAGIC = 2; |
||
47 | /** |
||
48 | * Error while reading file, probably too short |
||
49 | */ |
||
50 | const ERROR_READING = 3; |
||
51 | |||
52 | /** |
||
53 | * Big endian mo file magic bytes. |
||
54 | */ |
||
55 | const MAGIC_BE = "\x95\x04\x12\xde"; |
||
56 | /** |
||
57 | * Little endian mo file magic bytes. |
||
58 | */ |
||
59 | const MAGIC_LE = "\xde\x12\x04\x95"; |
||
60 | |||
61 | /** |
||
62 | * Parse error code (0 if no error) |
||
63 | * |
||
64 | * @var int |
||
65 | */ |
||
66 | public $error = Translator::ERROR_NONE; |
||
67 | |||
68 | /** |
||
69 | * Cache header field for plural forms |
||
70 | * |
||
71 | * @var string|null |
||
72 | */ |
||
73 | private $pluralheader = NULL; |
||
74 | /** |
||
75 | * |
||
76 | * |
||
77 | * @var int|null number of plurals |
||
78 | */ |
||
79 | private $pluralcount = NULL; |
||
80 | /** |
||
81 | * Array with original -> translation mapping |
||
82 | * |
||
83 | * @var array |
||
84 | */ |
||
85 | private $cache_translations = array(); |
||
86 | |||
87 | /** |
||
88 | * Constructor |
||
89 | * |
||
90 | * @param string $filename Name of mo file to load |
||
91 | */ |
||
92 | 23 | public function __construct($filename) |
|
132 | |||
133 | /** |
||
134 | * Translates a string |
||
135 | * |
||
136 | * @param string $msgid String to be translated |
||
137 | * |
||
138 | * @return string translated string (or original, if not found) |
||
139 | */ |
||
140 | 20 | public function gettext($msgid) |
|
148 | |||
149 | /** |
||
150 | * Sanitize plural form expression for use in PHP eval call. |
||
151 | * |
||
152 | * @param string $expr Expression to sanitize |
||
153 | * |
||
154 | * @return string sanitized plural form expression |
||
155 | */ |
||
156 | 10 | public static function sanitizePluralExpression($expr) |
|
157 | { |
||
158 | // Parse equation |
||
159 | 10 | $expr = explode(';', $expr, 2); |
|
160 | 10 | if (count($expr) == 2) { |
|
161 | 8 | $expr = $expr[1]; |
|
162 | 8 | } else { |
|
163 | 2 | $expr = $expr[0]; |
|
164 | } |
||
165 | 10 | $expr = trim(strtolower($expr)); |
|
166 | // Strip plural prefix |
||
167 | 10 | if (substr($expr, 0, 6) === 'plural') { |
|
168 | 9 | $expr = trim(substr($expr, 6)); |
|
169 | 9 | } |
|
170 | // Strip equals |
||
171 | 10 | if (substr($expr, 0, 1) === '=') { |
|
172 | 9 | $expr = trim(substr($expr, 1)); |
|
173 | 9 | } |
|
174 | // Get rid of disallowed characters. |
||
175 | 10 | $expr = preg_replace('@[^n0-9:\(\)\?=!<>/%&|]@', '', $expr); |
|
176 | |||
177 | // Add parenthesis for tertiary '?' operator. |
||
178 | 10 | $expr .= ';'; |
|
179 | 10 | $res = ''; |
|
180 | 10 | $p = 0; |
|
181 | 10 | $len = strlen($expr); |
|
182 | 10 | for ($i = 0; $i < $len; $i++) { |
|
183 | 10 | $ch = $expr[$i]; |
|
184 | switch ($ch) { |
||
185 | 10 | case '?': |
|
186 | 6 | $res .= ' ? ('; |
|
187 | 6 | $p++; |
|
188 | 6 | break; |
|
189 | 10 | case ':': |
|
190 | 6 | $res .= ') : ('; |
|
191 | 6 | break; |
|
192 | 10 | case ';': |
|
193 | 10 | $res .= str_repeat(')', $p) . ';'; |
|
194 | 10 | $p = 0; |
|
195 | 10 | break; |
|
196 | 9 | default: |
|
197 | 9 | $res .= $ch; |
|
198 | 9 | } |
|
199 | 10 | } |
|
200 | 10 | $res = str_replace('n', '$n', $res); |
|
201 | 10 | if ($res === ';') { |
|
202 | 1 | return $res; |
|
203 | } |
||
204 | 9 | return '$plural = ' . $res; |
|
205 | } |
||
206 | |||
207 | /** |
||
208 | * Extracts number of plurals from plurals form expression |
||
209 | * |
||
210 | * @param string $expr Expression to process |
||
211 | * |
||
212 | * @return int Total number of plurals |
||
213 | */ |
||
214 | 9 | public static function extractPluralCount($expr) |
|
215 | { |
||
216 | 9 | $parts = explode(';', $expr, 2); |
|
217 | 9 | $nplurals = explode('=', trim($parts[0]), 2); |
|
218 | 9 | if (strtolower(trim($nplurals[0])) != 'nplurals') { |
|
219 | 2 | return 1; |
|
220 | } |
||
221 | 7 | return intval($nplurals[1]); |
|
222 | } |
||
223 | |||
224 | /** |
||
225 | * Parse full PO header and extract only plural forms line. |
||
226 | * |
||
227 | * @param string $header Gettext header |
||
228 | * |
||
229 | * @return string verbatim plural form header field |
||
230 | */ |
||
231 | 8 | public static function extractPluralsForms($header) |
|
232 | { |
||
233 | 8 | $headers = explode("\n", $header); |
|
234 | 8 | $expr = 'nplurals=2; plural=n == 1 ? 0 : 1;'; |
|
235 | 8 | foreach ($headers as $header) { |
|
236 | 8 | if (stripos($header, 'Plural-Forms:') === 0) { |
|
237 | 6 | $expr = substr($header, 13); |
|
238 | 6 | } |
|
239 | 8 | } |
|
240 | 8 | return $expr; |
|
241 | } |
||
242 | |||
243 | /** |
||
244 | * Get possible plural forms from MO header |
||
245 | * |
||
246 | * @return string plural form header |
||
247 | */ |
||
248 | 5 | private function getPluralForms() |
|
249 | { |
||
250 | // lets assume message number 0 is header |
||
251 | // this is true, right? |
||
252 | |||
253 | // cache header field for plural forms |
||
254 | 5 | if (is_null($this->pluralheader)) { |
|
255 | 4 | $header = $this->cache_translations['']; |
|
256 | 4 | $expr = $this->extractPluralsForms($header); |
|
257 | 4 | $this->pluralheader = $this->sanitizePluralExpression($expr); |
|
258 | 4 | $this->pluralcount = $this->extractPluralCount($expr); |
|
259 | 4 | } |
|
260 | 5 | return $this->pluralheader; |
|
261 | } |
||
262 | |||
263 | /** |
||
264 | * Detects which plural form to take |
||
265 | * |
||
266 | * @param int $n count of objects |
||
267 | * |
||
268 | * @return int array index of the right plural form |
||
269 | */ |
||
270 | 5 | private function selectString($n) |
|
271 | { |
||
272 | 5 | $string = $this->getPluralForms(); |
|
273 | |||
274 | 5 | $plural = 0; |
|
275 | |||
276 | 5 | eval($string); |
|
|
|||
277 | 5 | if ($plural >= $this->pluralcount) { |
|
278 | 1 | $plural = $this->pluralcount - 1; |
|
279 | 1 | } |
|
280 | 5 | return $plural; |
|
281 | } |
||
282 | |||
283 | /** |
||
284 | * Plural version of gettext |
||
285 | * |
||
286 | * @param string $msgid Single form |
||
287 | * @param string $msgid_plural Plural form |
||
288 | * @param string $number Number of objects |
||
289 | * |
||
290 | * @return string translated plural form |
||
291 | */ |
||
292 | 12 | public function ngettext($msgid, $msgid_plural, $number) |
|
307 | |||
308 | /** |
||
309 | * Translate with context |
||
310 | * |
||
311 | * @param string $msgctxt Context |
||
312 | * @param string $msgid String to be translated |
||
313 | * |
||
314 | * @return string translated plural form |
||
315 | */ |
||
316 | 10 | public function pgettext($msgctxt, $msgid) |
|
326 | |||
327 | /** |
||
328 | * Plural version of pgettext |
||
329 | * |
||
330 | * @param string $msgctxt Context |
||
331 | * @param string $msgid Single form |
||
332 | * @param string $msgid_plural Plural form |
||
333 | * @param string $number Number of objects |
||
334 | * |
||
335 | * @return string translated plural form |
||
336 | */ |
||
337 | 4 | public function npgettext($msgctxt, $msgid, $msgid_plural, $number) |
|
347 | } |
||
348 |
On one hand,
eval
might be exploited by malicious users if they somehow manage to inject dynamic content. On the other hand, with the emergence of faster PHP runtimes like the HHVM,eval
prevents some optimization that they perform.