Completed
Push — master ( 77b89e...301d76 )
by Iurii
01:19
created

Translator::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 14
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 14
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 11
nc 1
nop 8

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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 Exception;
13
use DOMDocument;
14
use gplcart\core\Cache,
15
    gplcart\core\Config,
16
    gplcart\core\Hook,
17
    gplcart\core\Module;
18
use gplcart\core\helpers\Zip as ZipHelper;
19
use gplcart\core\models\Language as LanguageModel,
20
    gplcart\core\models\Translation as TranslationModel,
21
    gplcart\core\models\FileTransfer as FileTransferModel;
22
23
/**
24
 * Manages basic behaviors and data related to Translator module
25
 * @todo Validate HTML before import
26
 */
27
class Translator
28
{
29
30
    /**
31
     * Hook class instance
32
     * @var \gplcart\core\Hook $hook
33
     */
34
    protected $hook;
35
36
    /**
37
     * Config class instance
38
     * @var \gplcart\core\Config $config
39
     */
40
    protected $config;
41
42
    /**
43
     * Module class instance
44
     * @var \gplcart\core\Module $module
45
     */
46
    protected $module;
47
48
    /**
49
     * Cache class instance
50
     * @var \gplcart\core\Cache $cache
51
     */
52
    protected $cache;
53
54
    /**
55
     * File transfer model class instance
56
     * @var \gplcart\core\models\FileTransfer $file_transfer
57
     */
58
    protected $file_transfer;
59
60
    /**
61
     * Zip helper class instance
62
     * @var \gplcart\core\helpers\Zip $zip
63
     */
64
    protected $zip;
65
66
    /**
67
     * Translation UI model class instance
68
     * @var \gplcart\core\models\Translation $translation
69
     */
70
    protected $translation;
71
72
    /**
73
     * Language model class instance
74
     * @var \gplcart\core\models\Language $language
75
     */
76
    protected $language;
77
78
    /**
79
     * Translator constructor.
80
     * @param Hook $hook
81
     * @param Config $config
82
     * @param Cache $cache
83
     * @param Module $module
84
     * @param ZipHelper $zip
85
     * @param FileTransferModel $file_transfer
86
     * @param LanguageModel $language
87
     * @param TranslationModel $translation
88
     */
89
    public function __construct(Hook $hook, Config $config, Cache $cache, Module $module,
90
            ZipHelper $zip, FileTransferModel $file_transfer, LanguageModel $language,
91
            TranslationModel $translation)
92
    {
93
        $this->hook = $hook;
94
        $this->module = $module;
95
        $this->config = $config;
96
97
        $this->zip = $zip;
98
        $this->cache = $cache;
99
        $this->language = $language;
100
        $this->translation = $translation;
101
        $this->file_transfer = $file_transfer;
102
    }
103
104
    /**
105
     * Returns an array of information about the translation file
106
     * @param string $file
107
     * @return array
108
     */
109
    public function getFileInfo($file)
110
    {
111
        $lines = array();
112
113
        if (is_file($file)) {
114
            $lines = $this->translation->parseCsv($file);
115
        }
116
117
        $result = array(
118
            'progress' => 0,
119
            'translated' => 0,
120
            'total' => count($lines)
121
        );
122
123
        if (empty($result['total'])) {
124
            return $result;
125
        }
126
127
        $result['translated'] = $this->countTranslated($lines);
128
        $result['progress'] = round(($result['translated'] / $result['total']) * 100);
129
        return $result;
130
    }
131
132
    /**
133
     * Copy a translation file
134
     * @param string $source
135
     * @param string $destination
136
     * @return boolean
137
     */
138
    public function copy($source, $destination)
139
    {
140
        $result = null;
141
        $this->hook->attach('module.translator.copy.before', $source, $destination, $result, $this);
142
143
        if (isset($result)) {
144
            return $result;
145
        }
146
147
        $result = false;
148
        if ($this->prepareDirectory($destination)) {
149
            $result = copy($source, $destination);
150
        }
151
152
        $this->hook->attach('module.translator.copy.after', $source, $destination, $result, $this);
153
        return (bool) $result;
154
    }
155
156
    /**
157
     * Deletes a translation file
158
     * @param string $file
159
     * @param string $langcode
160
     * @return boolean
161
     */
162
    public function delete($file, $langcode)
163
    {
164
        $result = null;
165
        $this->hook->attach('module.translator.delete.before', $file, $langcode, $result, $this);
166
167
        if (isset($result)) {
168
            return $result;
169
        }
170
171
        if (!$this->canDelete($file, $langcode)) {
172
            return false;
173
        }
174
175
        $result = unlink($file);
176
        $this->hook->attach('module.translator.delete.after', $file, $langcode, $result, $this);
177
        return $result;
178
    }
179
180
    /**
181
     * Whether the translation file can be deleted
182
     * @param string $file
183
     * @param string $langcode
184
     * @return bool
185
     */
186
    public function canDelete($file, $langcode)
187
    {
188
        return $this->isTranslationFile($file, $langcode);
189
    }
190
191
    /**
192
     * Whether the file is a translation file
193
     * @param string $file
194
     * @param string $langcode
195
     * @return bool
196
     */
197
    public function isTranslationFile($file, $langcode)
198
    {
199
        return is_file($file)//
200
                && pathinfo($file, PATHINFO_EXTENSION) === 'csv'//
201
                && (strpos($file, $this->translation->getDirectory($langcode)) === 0//
202
                || $this->getModuleIdFromPath($file));
203
    }
204
205
    /**
206
     * Returns a module ID from the translation file path
207
     * @param string $file
208
     * @return string
209
     */
210
    public function getModuleIdFromPath($file)
211
    {
212
        $module_id = basename(dirname(dirname($file)));
213
        $module = $this->module->get($module_id);
214
215
        if (!empty($module) && strpos($file, $this->translation->getModuleDirectory($module_id)) === 0) {
216
            return $module_id;
217
        }
218
219
        return '';
220
    }
221
222
    /**
223
     * Returns an array of scanned and prepared translations
224
     * @param string|null $langcode
225
     * @return array
226
     */
227
    public function getImportList($langcode = null)
228
    {
229
        $key = "module.translator.import.$langcode";
230
        $list = $this->cache->get($key);
231
232
        if (empty($list)) {
233
            $file = $this->getImportFile();
234
            $scanned = $this->scanImportFile($file);
235
            $list = $this->buildImportList($scanned, $file, $langcode);
0 ignored issues
show
Bug introduced by
It seems like $file defined by $this->getImportFile() on line 233 can also be of type boolean; however, gplcart\modules\translat...ator::buildImportList() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
236
            $this->config->set('module_translator_saved', GC_TIME);
237
            $this->cache->set($key, $list);
238
        }
239
240
        $this->hook->attach('module.translator.import.list', $langcode, $list, $this);
241
        return $list;
242
    }
243
244
    /**
245
     * Delete both cache and ZIP file
246
     * @return boolean
247
     */
248
    public function clearImport()
249
    {
250
        $this->cache->clear('module.translator.import', array('pattern' => '*'));
251
        $file = $this->getImportFilePath();
252
        return is_file($file) && unlink($file);
253
    }
254
255
    /**
256
     * Copy translation from the source ZIP file
257
     * @param string $module_id
258
     * @param string $file
259
     * @param string $langcode
260
     * @return boolean
261
     */
262
    public function importContent($module_id, $file, $langcode)
263
    {
264
        $result = null;
265
        $this->hook->attach('module.translator.copy.before', $module_id, $file, $langcode, $result);
266
267
        if (isset($result)) {
268
            return $result;
269
        }
270
271
        $content = $this->readZip($module_id, $file, $langcode);
272
273
        if (empty($content)) {
274
            return false;
275
        }
276
277
        if ($module_id === 'core') {
278
            $module_id = '';
279
        }
280
281
        $destination = $this->translation->getFile($langcode, $module_id);
282
283
        $result = false;
284
        if ($this->prepareDirectory($destination)) {
285
            $result = file_put_contents($destination, $content) !== false;
286
        }
287
288
        $this->hook->attach('module.translator.copy.after', $module_id, $file, $langcode, $destination, $result);
289
        return $result;
290
    }
291
292
    /**
293
     * Ensure that directory exists and contains no the same file
294
     * @param string $file
295
     * @return boolean
296
     */
297
    protected function prepareDirectory($file)
298
    {
299
        $directory = dirname($file);
300
        if (!file_exists($directory) && !mkdir($directory, 0775, true)) {
301
            return false;
302
        }
303
304
        if (file_exists($file)) {
305
            unlink($file);
306
        }
307
308
        return true;
309
    }
310
311
    /**
312
     * Returns an array of scanned translations
313
     * @param string|bool $file
314
     * @return array
315
     */
316
    public function scanImportFile($file)
317
    {
318
        if (empty($file)) {
319
            return array();
320
        }
321
322
        try {
323
            $items = $this->zip->set($file)->getList();
324
        } catch (Exception $ex) {
325
            trigger_error($ex->getMessage());
326
            return array();
327
        }
328
329
        // Convert to nested array
330
        $nested = array();
331
        foreach ($items as $item) {
332
            $parents = explode('/', trim($item, '/'));
333
            gplcart_array_set($nested, $parents, $item);
334
        }
335
336
        return $nested;
337
    }
338
339
    /**
340
     * Build translation data
341
     * @param array $items
342
     * @param string $file
343
     * @param null|string $langcode
344
     * @return array
345
     */
346
    public function buildImportList(array $items, $file, $langcode)
347
    {
348
        $list = array();
349
        $version = gplcart_version(true);
350
351
        if (!empty($items["$version.x"])) {
352
            $modules = $this->module->getList();
353
            foreach ($items["$version.x"] as $module_id => $translations) {
354
                if ($module_id === 'core' || isset($modules[$module_id])) {
355
                    $list[$module_id] = $this->prepareImportTranslations($translations, $file, $langcode);
356
                }
357
            }
358
        }
359
360
        ksort($list);
361
362
        // Put core translations on the top
363
        if (isset($list['core'])) {
364
            $core = $list['core'];
365
            unset($list['core']);
366
            $list = array_merge(array('core' => $core), $list);
367
        }
368
369
        return $list;
370
    }
371
372
    /**
373
     * Prepare an array of translations
374
     * @param array $data
375
     * @param string $file
376
     * @param string $langcode
377
     * @return array
378
     */
379
    protected function prepareImportTranslations(array $data, $file, $langcode)
380
    {
381
        $languages = $this->language->getList();
382
383
        $prepared = array();
384
        foreach ($data as $filename => $path) {
385
386
            $pathinfo = pathinfo($filename);
387
            if (empty($pathinfo['extension']) || $pathinfo['extension'] !== 'csv') {
388
                continue;
389
            }
390
391
            list($lang, $version) = array_pad(explode('.', $pathinfo['filename'], 2), 2, '');
392
393
            if (!empty($langcode) && $langcode !== $lang) {
394
                continue;
395
            }
396
397
            if (empty($languages[$lang])) {
398
                continue;
399
            }
400
401
            $content = $this->translation->parseCsv("zip://$file#$path");
402
            $total = count($content);
403
            $translated = $this->countTranslated($content);
404
405
            $prepared[$lang][$filename] = array(
406
                'total' => $total,
407
                'file' => $filename,
408
                'version' => $version,
409
                'content' => $content,
410
                'translated' => $translated,
411
                'progress' => round(($translated / $total) * 100)
412
            );
413
414
            uasort($prepared[$lang], function ($a, $b) {
415
                return version_compare($a['version'], $b['version']);
416
            });
417
        }
418
419
        return $prepared;
420
    }
421
422
    /**
423
     * Read CSV from ZIP file
424
     * @param string $module_id
425
     * @param string $file
426
     * @param string $langcode
427
     * @return string
428
     */
429
    public function readZip($module_id, $file, $langcode)
430
    {
431
        $list = $this->getImportList();
432
433
        $content = '';
434
        if (!empty($list[$module_id][$langcode][$file]['content'])) {
435
            // Use php://temp stream?
436
            $data = stream_get_meta_data(tmpfile());
437
            foreach ($list[$module_id][$langcode][$file]['content'] as $line) {
438
                gplcart_file_csv($data['uri'], $line);
439
            }
440
441
            $content = file_get_contents($data['uri']);
442
            unlink($data['uri']);
443
        }
444
445
        return $content;
446
    }
447
448
    /**
449
     * Returns a total number of translated strings
450
     * @param array $lines
451
     * @return integer
452
     */
453
    protected function countTranslated(array $lines)
454
    {
455
        $count = 0;
456
        foreach ($lines as $line) {
457
            if (isset($line[1]) && $line[1] !== '') {
458
                $count ++;
459
            }
460
        }
461
462
        return $count;
463
    }
464
465
    /**
466
     * Downloads a remote ZIP file
467
     * @param string $destination
468
     * @return boolean
469
     */
470
    public function downloadImportFile($destination)
471
    {
472
        try {
473
            $result = $this->file_transfer->download($this->getImportDownloadUrl(), 'zip', $destination);
474
            if ($result !== true) {
475
                trigger_error($result);
476
                return false;
477
            }
478
        } catch (Exception $ex) {
479
            trigger_error($ex->getMessage());
480
            return false;
481
        }
482
483
        return true;
484
    }
485
486
    /**
487
     * Returns URL of source ZIP file
488
     * @return string
489
     */
490
    public function getImportDownloadUrl()
491
    {
492
        return 'https://crowdin.com/download/project/gplcart.zip';
493
    }
494
495
    /**
496
     * Returns the path of a downloaded ZIP file
497
     * @return bool|string
498
     */
499
    public function getImportFile()
500
    {
501
        $file = $this->getImportFilePath();
502
503
        if (is_file($file)) {
504
            return $file;
505
        }
506
507
        return $this->downloadImportFile($file) ? $file : false;
0 ignored issues
show
Bug introduced by
It seems like $file defined by $this->getImportFilePath() on line 501 can also be of type boolean; however, gplcart\modules\translat...r::downloadImportFile() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
508
    }
509
510
    /**
511
     * Returns the absolute path of downloaded ZIP file
512
     * @return string|bool
513
     */
514
    public function getImportFilePath()
515
    {
516
        return gplcart_file_private_temp('translator-import.zip');
517
    }
518
519
    /**
520
     * Detect invalid HTML
521
     * @param string $string
522
     * @return boolean
523
     */
524
    public function isInvalidHtml($string)
525
    {
526
        if (strpos($string, '>') === false && strpos($string, '<') === false) {
527
            return false;
528
        }
529
530
        libxml_use_internal_errors(true);
531
532
        $dom = new DOMDocument;
533
        $dom->loadHTML($string);
534
535
        // Strip wrapping html and body tags
536
        $mock = new DOMDocument;
537
        $body = $dom->getElementsByTagName('body')->item(0);
538
        foreach ($body->childNodes as $child) {
539
            $mock->appendChild($mock->importNode($child, true));
540
        }
541
542
        return trim($mock->saveHTML()) !== $string;
543
    }
544
545
}
546