Translator::canDelete()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 2
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 DOMDocument;
13
use DOMXPath;
14
use Exception;
15
use gplcart\core\Hook;
16
use gplcart\core\models\Translation as TranslationModel;
17
use gplcart\core\Module;
18
use RuntimeException;
19
20
/**
21
 * Manages basic behaviors and data related to Translator module
22
 */
23
class Translator
24
{
25
26
    /**
27
     * Hook class instance
28
     * @var \gplcart\core\Hook $hook
29
     */
30
    protected $hook;
31
32
    /**
33
     * Module class instance
34
     * @var \gplcart\core\Module $module
35
     */
36
    protected $module;
37
38
    /**
39
     * Translation UI model class instance
40
     * @var \gplcart\core\models\Translation $translation
41
     */
42
    protected $translation;
43
44
    /**
45
     * @param Hook $hook
46
     * @param Module $module
47
     * @param TranslationModel $translation
48
     */
49
    public function __construct(Hook $hook, Module $module, TranslationModel $translation)
50
    {
51
        $this->hook = $hook;
52
        $this->module = $module;
53
        $this->translation = $translation;
54
    }
55
56
    /**
57
     * Returns an array of information about the translation file
58
     * @param string $file
59
     * @return array
60
     */
61
    public function getFileInfo($file)
62
    {
63
        $lines = array();
64
65
        if (is_file($file)) {
66
            $lines = $this->translation->parseCsv($file);
67
        }
68
69
        $result = array(
70
            'progress' => 0,
71
            'translated' => 0,
72
            'total' => count($lines)
73
        );
74
75
        if (empty($result['total'])) {
76
            return $result;
77
        }
78
79
        $result['translated'] = $this->countTranslated($lines);
80
        $result['progress'] = round(($result['translated'] / $result['total']) * 100);
81
        return $result;
82
    }
83
84
    /**
85
     * Copy a translation file
86
     * @param string $source
87
     * @param string $destination
88
     * @return boolean
89
     */
90
    public function copy($source, $destination)
91
    {
92
        $result = null;
93
        $this->hook->attach('module.translator.copy.before', $source, $destination, $result, $this);
94
95
        if (isset($result)) {
96
            return $result;
97
        }
98
99
        try {
100
            $this->prepareDirectory($destination);
101
            $result = copy($source, $destination);
102
        } catch (Exception $ex) {
103
            $result = false;
104
        }
105
106
        $this->hook->attach('module.translator.copy.after', $source, $destination, $result, $this);
107
        return (bool) $result;
108
    }
109
110
    /**
111
     * Deletes a translation file
112
     * @param string $file
113
     * @param string $langcode
114
     * @return boolean
115
     */
116
    public function delete($file, $langcode)
117
    {
118
        $result = null;
119
        $this->hook->attach('module.translator.delete.before', $file, $langcode, $result, $this);
120
121
        if (isset($result)) {
122
            return $result;
123
        }
124
125
        if (!$this->canDelete($file, $langcode)) {
126
            return false;
127
        }
128
129
        $result = unlink($file);
130
        $this->hook->attach('module.translator.delete.after', $file, $langcode, $result, $this);
131
        return $result;
132
    }
133
134
    /**
135
     * Whether the translation file can be deleted
136
     * @param string $file
137
     * @param string $langcode
138
     * @return bool
139
     */
140
    public function canDelete($file, $langcode)
141
    {
142
        return $this->isTranslationFile($file, $langcode);
143
    }
144
145
    /**
146
     * Whether the file is a translation file
147
     * @param string $file
148
     * @param string $langcode
149
     * @return bool
150
     */
151
    public function isTranslationFile($file, $langcode)
152
    {
153
        return is_file($file)
154
            && pathinfo($file, PATHINFO_EXTENSION) === 'csv'
155
            && (strpos($file, $this->translation->getDirectory($langcode)) === 0 || $this->getModuleIdFromPath($file));
156
    }
157
158
    /**
159
     * Returns a module ID from the translation file path
160
     * @param string $file
161
     * @return string
162
     */
163
    public function getModuleIdFromPath($file)
164
    {
165
        $module_id = basename(dirname(dirname($file)));
166
        $module = $this->module->get($module_id);
167
168
        if (!empty($module) && strpos($file, $this->translation->getModuleDirectory($module_id)) === 0) {
169
            return $module_id;
170
        }
171
172
        return '';
173
    }
174
175
    /**
176
     * Ensure that directory exists and contains no the same file
177
     * @param string $file
178
     * @throws RuntimeException
179
     */
180
    protected function prepareDirectory($file)
181
    {
182
        $directory = dirname($file);
183
184
        if (!file_exists($directory) && !mkdir($directory, 0775, true)) {
185
            throw new RuntimeException('Failed to create directory');
186
        }
187
188
        if (file_exists($file) && !unlink($file)) {
189
            throw new RuntimeException('Failed to unlink the existing file');
190
        }
191
    }
192
193
    /**
194
     * Returns a total number of translated strings
195
     * @param array $lines
196
     * @return integer
197
     */
198
    protected function countTranslated(array $lines)
199
    {
200
        $count = 0;
201
202
        foreach ($lines as $line) {
203
            if (isset($line[1]) && $line[1] !== '') {
204
                $count++;
205
            }
206
        }
207
208
        return $count;
209
    }
210
211
    /**
212
     * Fix html strings in the translation file
213
     * @param string $file
214
     * @param array $fixed_strings
215
     * @return array
216
     */
217
    public function getFixedStrings($file, array &$fixed_strings = array())
218
    {
219
        $lines = $this->translation->parseCsv($file);
220
221
        foreach ($lines as $num => $line) {
222
            unset($line[0]);
223
            foreach ($line as $col => $string) {
224
                $fixed = $this->fixHtmlString($string);
225
                if ($fixed !== false) {
226
                    $lines[$num][$col] = $fixed;
227
                    $fixed_strings[] = $string;
228
                }
229
            }
230
        }
231
232
        return $lines;
233
    }
234
235
    /**
236
     * Tries to fix invalid HTML
237
     * @param string $string
238
     * @return boolean|string
239
     */
240
    public function fixHtmlString($string)
241
    {
242
        if (strpos($string, '>') === false && strpos($string, '<') === false) {
243
            return false;
244
        }
245
246
        libxml_use_internal_errors(true);
247
248
        $dom = new DOMDocument();
249
        $xml = mb_convert_encoding("<root>$string</root>", 'HTML-ENTITIES', 'UTF-8');
250
        $dom->loadHTML($xml, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
251
        $xpath = new DOMXPath($dom);
252
253
        foreach ($xpath->query('//*[not(node())]') as $node) {
254
            $node->parentNode->removeChild($node);
255
        }
256
257
        $fixed = substr($dom->saveHTML(), 6, -8);
258
        return html_entity_decode($fixed, ENT_QUOTES, 'UTF-8');
259
    }
260
261
}
262