Issues (3627)

app/bundles/CoreBundle/Helper/LanguageHelper.php (1 issue)

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
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

168
        /** @scrutinizer ignore-unhandled */ @unlink($packagePath);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
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