Completed
Pull Request — master (#2252)
by ྅༻ Ǭɀħ
04:13
created

MO::import_from_reader()   D

Complexity

Conditions 14
Paths 23

Size

Total Lines 87
Code Lines 48

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 14
eloc 48
nc 23
nop 1
dl 0
loc 87
rs 4.9516
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
 * This file is part of the POMO package.
4
 *
5
 * @copyright 2014 POMO
6
 * @license GPL
7
 */
8
9
namespace POMO;
10
11
use POMO\Streams\NOOPReader;
12
use POMO\Streams\FileReader;
13
use POMO\Translations\GettextTranslations;
14
use POMO\Translations\EntryTranslations;
15
16
/**
17
 * Class for working with MO files
18
 */
19
class MO extends GettextTranslations
20
{
21
    public $_nplurals = 2;
22
23
    /**
24
     * Loaded MO file.
25
     *
26
     * @var string
27
     */
28
    private $filename = '';
29
30
    /**
31
     * Returns the loaded MO file.
32
     *
33
     * @return string The loaded MO file.
34
     */
35
    public function get_filename()
36
    {
37
        return $this->filename;
38
    }
39
40
    /**
41
     * Fills up with the entries from MO file $filename
42
     *
43
     * @param  string $filename MO file to load
44
     * @return bool   Success
45
     */
46
    public function import_from_file($filename)
47
    {
48
        $reader = new FileReader($filename);
49
        if (!$reader->is_resource()) {
50
            return false;
51
        }
52
53
        $this->filename = (string) $filename;
54
55
        return $this->import_from_reader($reader);
56
    }
57
58
    /**
59
     * @param string $filename
60
     * @return bool
61
     */
62
    public function export_to_file($filename)
63
    {
64
        $fh = fopen($filename, 'wb');
65
        if (!$fh) {
66
            return false;
67
        }
68
        $res = $this->export_to_file_handle($fh);
69
        fclose($fh);
70
71
        return $res;
72
    }
73
74
    /**
75
     * @return string|false
76
     */
77
    public function export()
78
    {
79
        $tmp_fh = fopen("php://temp", 'r+');
80
        if (!$tmp_fh) {
81
            return false;
82
        }
83
        $this->export_to_file_handle($tmp_fh);
84
        rewind($tmp_fh);
85
86
        return stream_get_contents($tmp_fh);
87
    }
88
89
    /**
90
     * @param EntryTranslations $entry
91
     * @return bool
92
     */
93
    public function is_entry_good_for_export(EntryTranslations $entry)
94
    {
95
        if (empty($entry->translations)) {
96
            return false;
97
        }
98
99
        if (!array_filter($entry->translations)) {
100
            return false;
101
        }
102
103
        return true;
104
    }
105
106
    /**
107
     * @param resource $fh
108
     * @return true
109
     */
110
    public function export_to_file_handle($fh)
111
    {
112
        $entries = array_filter(
113
            $this->entries,
114
            array($this, 'is_entry_good_for_export')
115
        );
116
        ksort($entries);
117
        $magic = 0x950412de;
118
        $revision = 0;
119
        $total = count($entries) + 1; // all the headers are one entry
120
        $originals_lenghts_addr = 28;
121
        $translations_lenghts_addr = $originals_lenghts_addr + 8 * $total;
122
        $size_of_hash = 0;
123
        $hash_addr = $translations_lenghts_addr + 8 * $total;
124
        $current_addr = $hash_addr;
125
        fwrite($fh, pack(
126
            'V*',
127
            $magic,
128
            $revision,
129
            $total,
130
            $originals_lenghts_addr,
131
            $translations_lenghts_addr,
132
            $size_of_hash,
133
            $hash_addr
134
        ));
135
        fseek($fh, $originals_lenghts_addr);
136
137
        // headers' msgid is an empty string
138
        fwrite($fh, pack('VV', 0, $current_addr));
139
        $current_addr++;
140
        $originals_table = chr(0);
141
142
        $reader = new NOOPReader();
143
144 View Code Duplication
        foreach ($entries as $entry) {
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...
145
            $originals_table .= $this->export_original($entry) . chr(0);
146
            $length = $reader->strlen($this->export_original($entry));
147
            fwrite($fh, pack('VV', $length, $current_addr));
148
            $current_addr += $length + 1; // account for the NULL byte after
149
        }
150
151
        $exported_headers = $this->export_headers();
152
        fwrite($fh, pack(
153
            'VV',
154
            $reader->strlen($exported_headers),
155
            $current_addr
156
        ));
157
        $current_addr += strlen($exported_headers) + 1;
158
        $translations_table = $exported_headers . chr(0);
159
160 View Code Duplication
        foreach ($entries as $entry) {
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...
161
            $translations_table .= $this->export_translations($entry).chr(0);
162
            $length = $reader->strlen($this->export_translations($entry));
163
            fwrite($fh, pack('VV', $length, $current_addr));
164
            $current_addr += $length + 1;
165
        }
166
167
        fwrite($fh, $originals_table);
168
        fwrite($fh, $translations_table);
169
170
        return true;
171
    }
172
173
    /**
174
     * @param EntryTranslations $entry
175
     * @return string
176
     */
177
    public function export_original(EntryTranslations $entry)
178
    {
179
        //TODO: warnings for control characters
180
        $exported = $entry->singular;
181
        if ($entry->is_plural) {
182
            $exported .= chr(0).$entry->plural;
183
        }
184
        if (!is_null($entry->context)) {
185
            $exported = $entry->context.chr(4).$exported;
186
        }
187
188
        return $exported;
189
    }
190
191
    /**
192
     * @param EntryTranslations $entry
193
     * @return string
194
     */
195
    public function export_translations(EntryTranslations $entry)
196
    {
197
        //TODO: warnings for control characters
198
        return $entry->is_plural ? implode(chr(0), $entry->translations) : $entry->translations[0];
199
    }
200
201
    /**
202
     * @return string
203
     */
204
    public function export_headers()
205
    {
206
        $exported = '';
207
        foreach ($this->headers as $header => $value) {
208
            $exported .= "$header: $value\n";
209
        }
210
211
        return $exported;
212
    }
213
214
    /**
215
     * @param int $magic
216
     * @return string|false
217
     */
218
    public function get_byteorder($magic)
219
    {
220
        // The magic is 0x950412de
221
        $magic_little = (int) - 1794895138;
222
        $magic_little_64 = (int) 2500072158;
223
        // 0xde120495
224
        $magic_big = ((int) - 569244523) & 0xFFFFFFFF;
225
        if ($magic_little == $magic || $magic_little_64 == $magic) {
226
            return 'little';
227
        } elseif ($magic_big == $magic) {
228
            return 'big';
229
        } else {
230
            return false;
231
        }
232
    }
233
234
    /**
235
     * @param FileReader $reader
236
     * @return bool
237
     */
238
    public function import_from_reader(FileReader $reader)
239
    {
240
        $endian_string = $this->get_byteorder($reader->readint32());
241
        if (false === $endian_string) {
242
            return false;
243
        }
244
        $reader->setEndian($endian_string);
245
246
        $endian = ('big' == $endian_string) ? 'N' : 'V';
247
248
        $header = $reader->read(24);
249
        if ($reader->strlen($header) != 24) {
250
            return false;
251
        }
252
253
        // parse header
254
        $header = unpack("{$endian}revision/{$endian}total/{$endian}originals_lenghts_addr/{$endian}translations_lenghts_addr/{$endian}hash_length/{$endian}hash_addr", $header);
255
        if (!is_array($header)) {
256
            return false;
257
        }
258
259
        // support revision 0 of MO format specs, only
260
        if ($header['revision'] != 0) {
261
            return false;
262
        }
263
264
        // seek to data blocks
265
        $reader->seekto($header['originals_lenghts_addr']);
266
267
        // read originals' indices
268
        $originals_lengths_length = $header['translations_lenghts_addr'] - $header['originals_lenghts_addr'];
269
        if ($originals_lengths_length != $header['total'] * 8) {
270
            return false;
271
        }
272
273
        $originals = $reader->read($originals_lengths_length);
274
        if ($reader->strlen($originals) != $originals_lengths_length) {
275
            return false;
276
        }
277
278
        // read translations' indices
279
        $translations_lenghts_length = $header['hash_addr'] - $header['translations_lenghts_addr'];
280
        if ($translations_lenghts_length != $header['total'] * 8) {
281
            return false;
282
        }
283
284
        $translations = $reader->read($translations_lenghts_length);
285
        if ($reader->strlen($translations) != $translations_lenghts_length) {
286
            return false;
287
        }
288
289
        // transform raw data into set of indices
290
        $originals    = $reader->str_split($originals, 8);
291
        $translations = $reader->str_split($translations, 8);
292
293
        // skip hash table
294
        $strings_addr = $header['hash_addr'] + $header['hash_length'] * 4;
295
296
        $reader->seekto($strings_addr);
297
298
        $strings = $reader->read_all();
299
        $reader->close();
300
301
        for ($i = 0; $i < $header['total']; $i++) {
302
            $o = unpack("{$endian}length/{$endian}pos", $originals[$i]);
303
            $t = unpack("{$endian}length/{$endian}pos", $translations[$i]);
304
            if (!$o || !$t) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $o of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
Bug Best Practice introduced by
The expression $t of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
305
                return false;
306
            }
307
308
            // adjust offset due to reading strings to separate space before
309
            $o['pos'] -= $strings_addr;
310
            $t['pos'] -= $strings_addr;
311
312
            $original    = $reader->substr($strings, $o['pos'], $o['length']);
313
            $translation = $reader->substr($strings, $t['pos'], $t['length']);
314
315
            if ('' === $original) {
316
                $this->set_headers($this->make_headers($translation));
317
            } else {
318
                $entry = &static::make_entry($original, $translation);
319
                $this->entries[$entry->key()] = &$entry;
320
            }
321
        }
322
323
        return true;
324
    }
325
326
    /**
327
     * Build a  from original string and translation strings,
328
     * found in a MO file
329
     *
330
     * @param string $original    original string to translate from MO file.
331
     *                            Might contain 0x04 as context separator or
332
     *                            0x00 as singular/plural separator
333
     * @param string $translation translation string from MO file.Might contain
334
     *                            0x00 as a plural translations separator
335
     * @return EntryTranslations New entry
336
     */
337
    public static function &make_entry($original, $translation)
338
    {
339
        $entry = new EntryTranslations();
340
        // look for context
341
        $parts = explode(chr(4), $original);
342
        if (isset($parts[1])) {
343
            $original = $parts[1];
344
            $entry->context = $parts[0];
345
        }
346
        // look for plural original
347
        $parts = explode(chr(0), $original);
348
        $entry->singular = $parts[0];
349
        if (isset($parts[1])) {
350
            $entry->is_plural = true;
351
            $entry->plural = $parts[1];
352
        }
353
        // plural translations are also separated by \0
354
        $entry->translations = explode(chr(0), $translation);
355
356
        return $entry;
357
    }
358
359
    /**
360
     * @param int $count
361
     * @return string
362
     */
363
    public function select_plural_form($count)
364
    {
365
        return $this->gettext_select_plural_form($count);
366
    }
367
368
    /**
369
     * @return int
370
     */
371
    public function get_plural_forms_count()
372
    {
373
        return $this->_nplurals;
374
    }
375
}
376