1 | <?php |
||
2 | |||
3 | /* |
||
4 | * @copyright 2014 Mautic Contributors. All rights reserved |
||
5 | * @author Mautic |
||
6 | * |
||
7 | * @link http://mautic.org |
||
8 | * |
||
9 | * @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html |
||
10 | */ |
||
11 | |||
12 | namespace Mautic\CoreBundle\Helper; |
||
13 | |||
14 | use Joomla\Http\Http; |
||
15 | use Mautic\CoreBundle\Helper\Language\Installer; |
||
16 | use Monolog\Logger; |
||
17 | use Symfony\Component\Finder\Finder; |
||
18 | |||
19 | /** |
||
20 | * Helper class for managing Mautic's installed languages. |
||
21 | */ |
||
22 | class LanguageHelper |
||
23 | { |
||
24 | /** |
||
25 | * @var string |
||
26 | */ |
||
27 | private $cacheFile; |
||
28 | |||
29 | /** |
||
30 | * @var Http |
||
31 | */ |
||
32 | private $connector; |
||
33 | |||
34 | /** |
||
35 | * @var PathsHelper |
||
36 | */ |
||
37 | private $pathsHelper; |
||
38 | |||
39 | /** |
||
40 | * @var Logger |
||
41 | */ |
||
42 | private $logger; |
||
43 | |||
44 | /** |
||
45 | * @var Installer |
||
46 | */ |
||
47 | private $installer; |
||
48 | |||
49 | /** |
||
50 | * @var CoreParametersHelper |
||
51 | */ |
||
52 | private $coreParametersHelper; |
||
53 | |||
54 | /** |
||
55 | * @var array |
||
56 | */ |
||
57 | private $supportedLanguages = []; |
||
58 | |||
59 | /** |
||
60 | * @var string |
||
61 | */ |
||
62 | private $installedTranslationsDirectory; |
||
63 | |||
64 | /** |
||
65 | * @var string |
||
66 | */ |
||
67 | private $defaultTranslationsDirectory; |
||
68 | |||
69 | public function __construct(PathsHelper $pathsHelper, Logger $logger, CoreParametersHelper $coreParametersHelper, Http $connector) |
||
70 | { |
||
71 | $this->pathsHelper = $pathsHelper; |
||
72 | $this->logger = $logger; |
||
73 | $this->coreParametersHelper = $coreParametersHelper; |
||
74 | $this->connector = $connector; |
||
75 | $this->defaultTranslationsDirectory = __DIR__.'/../Translations'; |
||
76 | $this->installedTranslationsDirectory = $this->pathsHelper->getSystemPath('translations_root').'/translations'; |
||
77 | $this->installer = new Installer($this->installedTranslationsDirectory); |
||
78 | |||
79 | // Moved to outside environment folder so that it doesn't get wiped on each config update |
||
80 | $this->cacheFile = $pathsHelper->getSystemPath('cache').'/../languageList.txt'; |
||
81 | } |
||
82 | |||
83 | public function getSupportedLanguages(): array |
||
84 | { |
||
85 | if (!empty($this->supportedLanguages)) { |
||
86 | return $this->supportedLanguages; |
||
87 | } |
||
88 | |||
89 | $this->loadSupportedLanguages(); |
||
90 | |||
91 | return $this->supportedLanguages; |
||
92 | } |
||
93 | |||
94 | /** |
||
95 | * Extracts a downloaded package for the specified language. |
||
96 | * |
||
97 | * This will attempt to download the package if it is not found |
||
98 | * |
||
99 | * @param $languageCode |
||
100 | * |
||
101 | * @return array |
||
102 | */ |
||
103 | public function extractLanguagePackage($languageCode) |
||
104 | { |
||
105 | $packagePath = $this->pathsHelper->getSystemPath('cache').'/'.$languageCode.'.zip'; |
||
106 | |||
107 | // Make sure the package actually exists |
||
108 | if (!file_exists($packagePath)) { |
||
109 | // Let's try to fetch it |
||
110 | $result = $this->fetchPackage($languageCode); |
||
111 | |||
112 | // If there was a failure, there's nothing else we can do here |
||
113 | if ($result['error']) { |
||
114 | return $result; |
||
115 | } |
||
116 | } |
||
117 | |||
118 | $zipper = new \ZipArchive(); |
||
119 | $archive = $zipper->open($packagePath); |
||
120 | |||
121 | if (true !== $archive) { |
||
122 | // Get the exact error |
||
123 | switch ($archive) { |
||
124 | case \ZipArchive::ER_EXISTS: |
||
125 | $error = 'mautic.core.update.archive_file_exists'; |
||
126 | break; |
||
127 | case \ZipArchive::ER_INCONS: |
||
128 | case \ZipArchive::ER_INVAL: |
||
129 | case \ZipArchive::ER_MEMORY: |
||
130 | $error = 'mautic.core.update.archive_zip_corrupt'; |
||
131 | break; |
||
132 | case \ZipArchive::ER_NOENT: |
||
133 | $error = 'mautic.core.update.archive_no_such_file'; |
||
134 | break; |
||
135 | case \ZipArchive::ER_NOZIP: |
||
136 | $error = 'mautic.core.update.archive_not_valid_zip'; |
||
137 | break; |
||
138 | case \ZipArchive::ER_READ: |
||
139 | case \ZipArchive::ER_SEEK: |
||
140 | case \ZipArchive::ER_OPEN: |
||
141 | default: |
||
142 | $error = 'mautic.core.update.archive_could_not_open'; |
||
143 | break; |
||
144 | } |
||
145 | |||
146 | return [ |
||
147 | 'error' => true, |
||
148 | 'message' => $error, |
||
149 | ]; |
||
150 | } |
||
151 | |||
152 | // Extract the archive file now |
||
153 | $tempDir = $this->pathsHelper->getSystemPath('tmp'); |
||
154 | |||
155 | if (!$zipper->extractTo($tempDir)) { |
||
156 | return [ |
||
157 | 'error' => true, |
||
158 | 'message' => 'mautic.core.update.archive_failed_to_extract', |
||
159 | ]; |
||
160 | } |
||
161 | |||
162 | $this->installer->install($tempDir, $languageCode) |
||
163 | ->cleanup(); |
||
164 | |||
165 | $zipper->close(); |
||
166 | |||
167 | // We can remove the package now |
||
168 | @unlink($packagePath); |
||
0 ignored issues
–
show
|
|||
169 | |||
170 | return [ |
||
171 | 'error' => false, |
||
172 | 'message' => 'mautic.core.language.helper.language.saved.successfully', |
||
173 | ]; |
||
174 | } |
||
175 | |||
176 | /** |
||
177 | * Fetches the list of available languages. |
||
178 | * |
||
179 | * @param bool $overrideCache |
||
180 | * |
||
181 | * @return array |
||
182 | */ |
||
183 | public function fetchLanguages($overrideCache = false, $returnError = true) |
||
184 | { |
||
185 | $overrideFile = $this->coreParametersHelper->get('language_list_file'); |
||
186 | if (!empty($overrideFile) && is_readable($overrideFile)) { |
||
187 | $overrideData = json_decode(file_get_contents($overrideFile), true); |
||
188 | if (isset($overrideData['languages'])) { |
||
189 | return $overrideData['languages']; |
||
190 | } elseif (isset($overrideData['name'])) { |
||
191 | return $overrideData; |
||
192 | } |
||
193 | |||
194 | return []; |
||
195 | } |
||
196 | |||
197 | // Check if we have a cache file and try to return cached data if so |
||
198 | if (!$overrideCache && is_readable($this->cacheFile)) { |
||
199 | $cacheData = json_decode(file_get_contents($this->cacheFile), true); |
||
200 | |||
201 | // If we're within the cache time, return the cached data |
||
202 | if ($cacheData['checkedTime'] > strtotime('-12 hours')) { |
||
203 | return $cacheData['languages']; |
||
204 | } |
||
205 | } |
||
206 | |||
207 | // Get the language data |
||
208 | try { |
||
209 | $data = $this->connector->get($this->coreParametersHelper->get('translations_list_url'), [], 10); |
||
210 | $manifest = json_decode($data->body, true); |
||
211 | $languages = []; |
||
212 | |||
213 | // translate the manifest (plain array) to a format |
||
214 | // expected everywhere else inside mautic (locale keyed sorted array) |
||
215 | foreach ($manifest['languages'] as $lang) { |
||
216 | $languages[$lang['locale']] = $lang; |
||
217 | } |
||
218 | ksort($languages); |
||
219 | } catch (\Exception $exception) { |
||
220 | // Log the error |
||
221 | $this->logger->addError('An error occurred while attempting to fetch the language list: '.$exception->getMessage()); |
||
222 | |||
223 | return (!$returnError) |
||
224 | ? [] |
||
225 | : [ |
||
226 | 'error' => true, |
||
227 | 'message' => 'mautic.core.language.helper.error.fetching.languages', |
||
228 | ]; |
||
229 | } |
||
230 | |||
231 | if (200 != $data->code) { |
||
232 | // Log the error |
||
233 | $this->logger->addError( |
||
234 | sprintf( |
||
235 | 'An unexpected %1$s code was returned while attempting to fetch the language. The message received was: %2$s', |
||
236 | $data->code, |
||
237 | is_string($data->body) ? $data->body : implode('; ', $data->body) |
||
238 | ) |
||
239 | ); |
||
240 | |||
241 | return (!$returnError) |
||
242 | ? [] |
||
243 | : [ |
||
244 | 'error' => true, |
||
245 | 'message' => 'mautic.core.language.helper.error.fetching.languages', |
||
246 | ]; |
||
247 | } |
||
248 | |||
249 | // Store to cache |
||
250 | $cacheData = [ |
||
251 | 'checkedTime' => time(), |
||
252 | 'languages' => $languages, |
||
253 | ]; |
||
254 | |||
255 | file_put_contents($this->cacheFile, json_encode($cacheData)); |
||
256 | |||
257 | return $languages; |
||
258 | } |
||
259 | |||
260 | /** |
||
261 | * Fetches a language package from the remote server. |
||
262 | * |
||
263 | * @param string $languageCode |
||
264 | * |
||
265 | * @return array |
||
266 | */ |
||
267 | public function fetchPackage($languageCode) |
||
268 | { |
||
269 | // Check if we have a cache file, generate it if not |
||
270 | if (!is_readable($this->cacheFile)) { |
||
271 | $this->fetchLanguages(); |
||
272 | } |
||
273 | |||
274 | $cacheData = json_decode(file_get_contents($this->cacheFile), true); |
||
275 | |||
276 | // Make sure the language actually exists |
||
277 | if (!isset($cacheData['languages'][$languageCode])) { |
||
278 | return [ |
||
279 | 'error' => true, |
||
280 | 'message' => 'mautic.core.language.helper.invalid.language', |
||
281 | 'vars' => [ |
||
282 | '%language%' => $languageCode, |
||
283 | ], |
||
284 | ]; |
||
285 | } |
||
286 | |||
287 | $langUrl = $this->coreParametersHelper->get('translations_fetch_url').$languageCode.'.zip'; |
||
288 | |||
289 | // GET the update data |
||
290 | try { |
||
291 | $data = $this->connector->get($langUrl); |
||
292 | } catch (\Exception $exception) { |
||
293 | $this->logger->addError('An error occurred while attempting to fetch the package: '.$exception->getMessage()); |
||
294 | |||
295 | return [ |
||
296 | 'error' => true, |
||
297 | 'message' => 'mautic.core.language.helper.error.fetching.package.exception', |
||
298 | 'vars' => [ |
||
299 | '%exception%' => $exception->getMessage(), |
||
300 | ], |
||
301 | ]; |
||
302 | } |
||
303 | |||
304 | if ($data->code >= 300 && $data->code < 400) { |
||
305 | return [ |
||
306 | 'error' => true, |
||
307 | 'message' => 'mautic.core.language.helper.error.follow.redirects', |
||
308 | 'vars' => [ |
||
309 | '%url%' => $langUrl, |
||
310 | ], |
||
311 | ]; |
||
312 | } elseif (200 != $data->code) { |
||
313 | return [ |
||
314 | 'error' => true, |
||
315 | 'message' => 'mautic.core.language.helper.error.on.language.server.side', |
||
316 | 'vars' => [ |
||
317 | '%code%' => $data->code, |
||
318 | ], |
||
319 | ]; |
||
320 | } |
||
321 | |||
322 | // Set the filesystem target |
||
323 | $target = $this->pathsHelper->getSystemPath('cache').'/'.$languageCode.'.zip'; |
||
324 | |||
325 | // Write the response to the filesystem |
||
326 | file_put_contents($target, $data->body); |
||
327 | |||
328 | // Return an array for the sake of consistency |
||
329 | return [ |
||
330 | 'error' => false, |
||
331 | ]; |
||
332 | } |
||
333 | |||
334 | private function loadSupportedLanguages() |
||
335 | { |
||
336 | // Find available translations |
||
337 | $finder = new Finder(); |
||
338 | $finder |
||
339 | ->directories() |
||
340 | ->in($this->defaultTranslationsDirectory) |
||
341 | ->in($this->installedTranslationsDirectory) |
||
342 | ->ignoreDotFiles(true) |
||
343 | ->depth('== 0'); |
||
344 | |||
345 | foreach ($finder as $dir) { |
||
346 | $locale = $dir->getFilename(); |
||
347 | |||
348 | // Check config exists |
||
349 | $configFile = $dir->getRealpath().'/config.json'; |
||
350 | if (!file_exists($configFile)) { |
||
351 | return; |
||
352 | } |
||
353 | |||
354 | $config = json_decode(file_get_contents($configFile), true); |
||
355 | $this->supportedLanguages[$locale] = (!empty($config['name'])) ? $config['name'] : $locale; |
||
356 | } |
||
357 | } |
||
358 | } |
||
359 |
If you suppress an error, we recommend checking for the error condition explicitly: