Completed
Push — master ( 84d990...7c1130 )
by Iurii
01:32
created

Translator::prepareImportTranslations()   C

Complexity

Conditions 7
Paths 5

Size

Total Lines 42
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 42
rs 6.7272
c 0
b 0
f 0
cc 7
eloc 25
nc 5
nop 3
1
<?php
2
3
/**
4
 * @package Translator
5
 * @author Iurii Makukh <[email protected]>
6
 * @copyright Copyright (c) 2017, Iurii Makukh <[email protected]>
7
 * @license https://www.gnu.org/licenses/gpl-3.0.en.html GPL-3.0+
8
 */
9
10
namespace gplcart\modules\translator\models;
11
12
use gplcart\core\Model,
13
    gplcart\core\Cache;
14
use gplcart\core\helpers\Zip as ZipHelper;
15
use gplcart\core\models\Language as LanguageModel,
16
    gplcart\core\models\File as FileModel;
17
18
/**
19
 * Manages basic behaviors and data related to Translator module
20
 */
21
class Translator extends Model
22
{
23
24
    /**
25
     * File model class instance
26
     * @var \gplcart\core\models\File $file
27
     */
28
    protected $file;
29
30
    /**
31
     * Zip helper class instance
32
     * @var \gplcart\core\helpers\Zip $zip
33
     */
34
    protected $zip;
35
36
    /**
37
     * Cache class instance
38
     * @var \gplcart\core\Cache $cache
39
     */
40
    protected $cache;
41
42
    /**
43
     * Language model class instance
44
     * @var \gplcart\core\models\Language $language
45
     */
46
    protected $language;
47
48
    /**
49
     * @param ZipHelper $zip
50
     * @param FileModel $file
51
     * @param LanguageModel $language
52
     * @param Cache $cache
53
     */
54
    public function __construct(ZipHelper $zip, FileModel $file,
55
            LanguageModel $language, Cache $cache)
56
    {
57
        parent::__construct();
58
59
        $this->zip = $zip;
60
        $this->file = $file;
61
        $this->cache = $cache;
62
        $this->language = $language;
63
    }
64
65
    /**
66
     * Returns an array of information about the translation file
67
     * @param string $file
68
     * @return array
69
     */
70
    public function getFileInfo($file)
71
    {
72
        $lines = array();
73
74
        if (is_file($file)) {
75
            $lines = $this->language->parseCsv($file);
76
        }
77
78
        $result = array(
79
            'progress' => 0,
80
            'translated' => 0,
81
            'total' => count($lines)
82
        );
83
84
        if (empty($result['total'])) {
85
            return $result;
86
        }
87
88
        $result['translated'] = $this->countTranslated($lines);
89
        $result['progress'] = round(($result['translated'] / $result['total']) * 100);
90
        return $result;
91
    }
92
93
    /**
94
     * Copy a translation file
95
     * @param string $source
96
     * @param string $destination
97
     * @return boolean
98
     */
99
    public function copy($source, $destination)
100
    {
101
        $result = null;
102
        $this->hook->attach('module.translator.copy.before', $source, $destination, $result, $this);
103
104
        if (isset($result)) {
105
            return $result;
106
        }
107
108
        $result = false;
109
        if ($this->prepareDirectory($destination)) {
110
            $result = copy($source, $destination);
111
        }
112
113
        $this->hook->attach('module.translator.copy.after', $source, $destination, $result, $this);
114
        return (bool) $result;
115
    }
116
117
    /**
118
     * Deletes a translation file
119
     * @param string $file
120
     * @param string $langcode
121
     * @return boolean
122
     */
123
    public function delete($file, $langcode)
124
    {
125
        $result = null;
126
        $this->hook->attach('module.translator.delete.before', $file, $langcode, $result, $this);
127
128
        if (isset($result)) {
129
            return $result;
130
        }
131
132
        if (!$this->canDelete($file, $langcode)) {
133
            return false;
134
        }
135
136
        $result = unlink($file);
137
        $this->hook->attach('module.translator.delete.after', $file, $langcode, $result, $this);
138
        return $result;
139
    }
140
141
    /**
142
     * Whether the translation file can be deleted
143
     * @param string $file
144
     * @param string $langcode
145
     * @return bool
146
     */
147
    public function canDelete($file, $langcode)
148
    {
149
        return $this->isTranslationFile($file, $langcode);
150
    }
151
152
    /**
153
     * Whether the file is a translation file
154
     * @param string $file
155
     * @param string $langcode
156
     * @return bool
157
     */
158
    public function isTranslationFile($file, $langcode)
159
    {
160
        return is_file($file)//
161
                && pathinfo($file, PATHINFO_EXTENSION) === 'csv'//
162
                && (strpos($file, $this->language->getDirectory($langcode)) === 0//
163
                || $this->getModuleIdFromPath($file));
164
    }
165
166
    /**
167
     * Returns a module ID from the translation file path
168
     * @param string $file
169
     * @return string
170
     */
171
    public function getModuleIdFromPath($file)
172
    {
173
        $module_id = basename(dirname(dirname($file)));
174
        $module = $this->config->getModule($module_id);
175
176
        if (!empty($module) && strpos($file, $this->language->getModuleDirectory($module_id)) === 0) {
177
            return $module_id;
178
        }
179
180
        return '';
181
    }
182
183
    /**
184
     * Returns an array of scanned and prepared translations
185
     * @return array
186
     */
187
    public function getImportList($langcode = null)
188
    {
189
        $key = "module.translator.import.$langcode";
190
        $list = $this->cache->get($key);
191
192
        if (empty($list)) {
193
            $file = $this->getImportFile();
194
            $scanned = $this->scanImportFile($file);
195
            $list = $this->buildImportList($scanned, $file, $langcode);
196
            $this->config->set('module_translator_saved', GC_TIME);
197
            $this->cache->set($key, $list);
198
        }
199
200
        $this->hook->attach('module.translator.import.list', $langcode, $file, $list);
0 ignored issues
show
Bug introduced by
The variable $file 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...
201
        return $list;
202
    }
203
204
    /**
205
     * Delete both cache and ZIP file
206
     * @return boolean
207
     */
208
    public function clearImport()
209
    {
210
        $this->cache->clear('module.translator.import', array('pattern' => '*'));
211
        $file = $this->getImportFilePath();
212
        return is_file($file) && unlink($file);
213
    }
214
215
    /**
216
     * Copy translation from the source ZIP file
217
     * @param string $module_id
218
     * @param string $file
219
     * @param string $langcode
220
     * @return boolean
221
     */
222
    public function importContent($module_id, $file, $langcode)
223
    {
224
        $result = null;
225
        $this->hook->attach('module.translator.copy.before', $module_id, $file, $langcode, $result);
226
227
        if (isset($result)) {
228
            return $result;
229
        }
230
231
        $content = $this->readZip($module_id, $file, $langcode);
232
233
        if (empty($content)) {
234
            return false;
235
        }
236
237
        if ($module_id === 'core') {
238
            $module_id = '';
239
        }
240
241
        $destination = $this->language->getFile($langcode, $module_id);
242
243
        $result = false;
244
        if ($this->prepareDirectory($destination)) {
245
            $result = file_put_contents($destination, $content) !== false;
246
        }
247
248
        $this->hook->attach('module.translator.copy.after', $module_id, $file, $langcode, $destination, $result);
249
        return $result;
250
    }
251
252
    /**
253
     * Ensure that directory exists and contains no the same file
254
     * @param string $file
255
     * @return boolean
256
     */
257
    protected function prepareDirectory($file)
258
    {
259
        $directory = dirname($file);
260
        if (!file_exists($directory) && !mkdir($directory, 0775, true)) {
261
            return false;
262
        }
263
264
        if (file_exists($file)) {
265
            unlink($file);
266
        }
267
268
        return true;
269
    }
270
271
    /**
272
     * Returns an array of scanned translations
273
     * @param string|bool $file
274
     * @return array
275
     */
276
    public function scanImportFile($file)
277
    {
278
        if (empty($file)) {
279
            return array();
280
        }
281
282
        try {
283
            $items = $this->zip->set($file)->getList();
284
        } catch (\Exception $ex) {
285
            trigger_error($ex->getMessage());
286
            return array();
287
        }
288
289
        // Convert to nested array
290
        $nested = array();
291
        foreach ($items as $item) {
292
            $parents = explode('/', trim($item, '/'));
293
            gplcart_array_set($nested, $parents, $item);
294
        }
295
296
        return $nested;
297
    }
298
299
    /**
300
     * Build translation data
301
     * @param array $items
302
     * @param string $file
303
     * @param null|string $langcode
304
     * @return array
305
     */
306
    public function buildImportList(array $items, $file, $langcode)
307
    {
308
        $list = array();
309
        $version = gplcart_version(true);
310
311
        if (!empty($items["$version.x"])) {
312
            $modules = $this->config->getModules();
313
            foreach ($items["$version.x"] as $module_id => $translations) {
314
                if ($module_id === 'core' || isset($modules[$module_id])) {
315
                    $list[$module_id] = $this->prepareImportTranslations($translations, $file, $langcode);
316
                }
317
            }
318
        }
319
320
        ksort($list);
321
322
        // Put core translations on the top
323
        if (isset($list['core'])) {
324
            $core = $list['core'];
325
            unset($list['core']);
326
            $list = array_merge(array('core' => $core), $list);
327
        }
328
329
        return $list;
330
    }
331
332
    /**
333
     * Prepare an array of translations
334
     * @param array $data
335
     * @param string $file
336
     * @return array
337
     */
338
    protected function prepareImportTranslations(array $data, $file, $langcode)
339
    {
340
        $languages = $this->language->getList();
341
342
        $prepared = array();
343
        foreach ($data as $filename => $path) {
344
345
            $pathinfo = pathinfo($filename);
346
            if (empty($pathinfo['extension']) || $pathinfo['extension'] !== 'csv') {
347
                continue;
348
            }
349
350
            list($lang, $version) = array_pad(explode('.', $pathinfo['filename'], 2), 2, '');
351
352
            if (!empty($langcode) && $langcode !== $lang) {
353
                continue;
354
            }
355
356
            if (empty($languages[$lang])) {
357
                continue;
358
            }
359
360
            $content = $this->language->parseCsv("zip://$file#$path");
361
            $total = count($content);
362
            $translated = $this->countTranslated($content);
363
364
            $prepared[$lang][$filename] = array(
365
                'total' => $total,
366
                'file' => $filename,
367
                'version' => $version,
368
                'content' => $content,
369
                'translated' => $translated,
370
                'progress' => round(($translated / $total) * 100)
371
            );
372
373
            uasort($prepared[$lang], function ($a, $b) {
374
                return version_compare($a['version'], $b['version']);
375
            });
376
        }
377
378
        return $prepared;
379
    }
380
381
    /**
382
     * Read CSV from ZIP file
383
     * @param string $module_id
384
     * @param string $file
385
     * @param string $langcode
386
     * @return string
387
     */
388
    public function readZip($module_id, $file, $langcode)
389
    {
390
        $list = $this->getImportList();
391
392
        $content = '';
393
        if (!empty($list[$module_id][$langcode][$file]['content'])) {
394
            // Use php://temp stream?
395
            $data = stream_get_meta_data(tmpfile());
396
            foreach ($list[$module_id][$langcode][$file]['content'] as $line) {
397
                gplcart_file_csv($data['uri'], $line);
398
            }
399
400
            $content = file_get_contents($data['uri']);
401
            unlink($data['uri']);
402
        }
403
404
        return $content;
405
    }
406
407
    /**
408
     * Returns a total number of translated strings
409
     * @param array $lines
410
     * @return integer
411
     */
412
    protected function countTranslated(array $lines)
413
    {
414
        $count = 0;
415
        foreach ($lines as $line) {
416
            if (isset($line[1]) && $line[1] !== '') {
417
                $count ++;
418
            }
419
        }
420
421
        return $count;
422
    }
423
424
    /**
425
     * Downloads a remote ZIP file
426
     * @param string $destination
427
     * @return boolean
428
     */
429
    public function downloadImportFile($destination)
430
    {
431
        try {
432
            $result = $this->file->download($this->getImportDownloadUrl(), 'zip', $destination);
433
            if ($result !== true) {
434
                trigger_error($result);
435
                return false;
436
            }
437
        } catch (\Exception $ex) {
438
            trigger_error($ex->getMessage());
439
            return false;
440
        }
441
442
        return true;
443
    }
444
445
    /**
446
     * Returns URL of source ZIP file
447
     * @return string
448
     */
449
    public function getImportDownloadUrl()
450
    {
451
        return 'https://crowdin.com/download/project/gplcart.zip';
452
    }
453
454
    /**
455
     * Returns the path of a downloaded ZIP file
456
     * @return bool|string
457
     */
458
    public function getImportFile()
459
    {
460
        $file = $this->getImportFilePath();
461
462
        if (is_file($file)) {
463
            return $file;
464
        }
465
466
        return $this->downloadImportFile($file) ? $file : false;
467
    }
468
469
    /**
470
     * Returns the absolute path of downloaded ZIP file
471
     * @return string|bool
472
     */
473
    public function getImportFilePath()
474
    {
475
        return GC_PRIVATE_TEMP_DIR . '/translator-import.zip';
476
    }
477
478
}
479