Passed
Push — master ( fcd6c8...62e46d )
by
unknown
20:15
created

LanguagePackService::updateMirrorBaseUrl()   B

Complexity

Conditions 6
Paths 18

Size

Total Lines 29
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 20
nc 18
nop 0
dl 0
loc 29
rs 8.9777
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the TYPO3 CMS project.
7
 *
8
 * It is free software; you can redistribute it and/or modify it under
9
 * the terms of the GNU General Public License, either version 2
10
 * of the License, or any later version.
11
 *
12
 * For the full copyright and license information, please read the
13
 * LICENSE.txt file that was distributed with this source code.
14
 *
15
 * The TYPO3 project - inspiring people to share!
16
 */
17
18
namespace TYPO3\CMS\Install\Service;
19
20
use Psr\EventDispatcher\EventDispatcherInterface;
21
use Psr\Log\LoggerAwareInterface;
22
use Psr\Log\LoggerAwareTrait;
23
use Symfony\Component\Finder\Finder;
24
use TYPO3\CMS\Core\Core\Environment;
25
use TYPO3\CMS\Core\Http\RequestFactory;
26
use TYPO3\CMS\Core\Http\Uri;
27
use TYPO3\CMS\Core\Information\Typo3Version;
28
use TYPO3\CMS\Core\Localization\Locales;
29
use TYPO3\CMS\Core\Package\PackageManager;
30
use TYPO3\CMS\Core\Registry;
31
use TYPO3\CMS\Core\Service\Archive\ZipService;
32
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
33
use TYPO3\CMS\Core\Utility\GeneralUtility;
34
use TYPO3\CMS\Core\Utility\PathUtility;
35
use TYPO3\CMS\Install\Service\Event\ModifyLanguagePackRemoteBaseUrlEvent;
36
37
/**
38
 * Service class handling language pack details
39
 * Used by 'manage language packs' module and 'language packs command'
40
 *
41
 * @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
42
 */
43
class LanguagePackService implements LoggerAwareInterface
44
{
45
    use LoggerAwareTrait;
46
47
    /**
48
     * @var Locales
49
     */
50
    protected $locales;
51
52
    /**
53
     * @var Registry
54
     */
55
    protected $registry;
56
57
    /**
58
     * @var EventDispatcherInterface
59
     */
60
    protected $eventDispatcher;
61
62
    /**
63
     * @var RequestFactory
64
     */
65
    protected $requestFactory;
66
67
    private const LANGUAGE_PACK_URL = 'https://localize.typo3.org/xliff/';
68
69
    public function __construct(EventDispatcherInterface $eventDispatcher, RequestFactory $requestFactory)
70
    {
71
        $this->eventDispatcher = $eventDispatcher;
72
        $this->locales = GeneralUtility::makeInstance(Locales::class);
73
        $this->registry = GeneralUtility::makeInstance(Registry::class);
74
        $this->requestFactory = $requestFactory;
75
    }
76
77
    /**
78
     * Get list of available languages
79
     *
80
     * @return array iso=>name
81
     */
82
    public function getAvailableLanguages(): array
83
    {
84
        return $this->locales->getLanguages();
85
    }
86
87
    /**
88
     * List of languages active in this instance
89
     *
90
     * @return array
91
     */
92
    public function getActiveLanguages(): array
93
    {
94
        $availableLanguages = $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['lang']['availableLanguages'] ?? [];
95
        return array_filter($availableLanguages);
96
    }
97
98
    /**
99
     * Create an array with language details: active or not, iso codes, last update, ...
100
     *
101
     * @return array
102
     */
103
    public function getLanguageDetails(): array
104
    {
105
        $availableLanguages = $this->getAvailableLanguages();
106
        $activeLanguages = $this->getActiveLanguages();
107
        $languages = [];
108
        foreach ($availableLanguages as $iso => $name) {
109
            if ($iso === 'default') {
110
                continue;
111
            }
112
            $lastUpdate = $this->registry->get('languagePacks', $iso);
113
            $languages[] = [
114
                'iso' => $iso,
115
                'name' => $name,
116
                'active' => in_array($iso, $activeLanguages, true),
117
                'lastUpdate' => $this->getFormattedDate($lastUpdate),
118
                'dependencies' => $this->locales->getLocaleDependencies($iso),
119
            ];
120
        }
121
        usort($languages, function ($a, $b) {
122
            // Sort languages by name
123
            if ($a['name'] === $b['name']) {
124
                return 0;
125
            }
126
            return $a['name'] < $b['name'] ? -1 : 1;
127
        });
128
        return $languages;
129
    }
130
131
    /**
132
     * Create a list of loaded extensions and their language packs details
133
     *
134
     * @return array
135
     */
136
    public function getExtensionLanguagePackDetails(): array
137
    {
138
        $activeLanguages = $this->getActiveLanguages();
139
        $packageManager = GeneralUtility::makeInstance(PackageManager::class);
140
        $activePackages = $packageManager->getActivePackages();
141
        $extensions = [];
142
        $activeExtensions = [];
143
        foreach ($activePackages as $package) {
144
            $path = $package->getPackagePath();
145
            $finder = new Finder();
146
            try {
147
                $files = $finder->files()->in($path . 'Resources/Private/Language/')->name('*.xlf');
148
                if ($files->count() === 0) {
149
                    // This extension has no .xlf files
150
                    continue;
151
                }
152
            } catch (\InvalidArgumentException $e) {
153
                // Dir does not exist
154
                continue;
155
            }
156
            $key = $package->getPackageKey();
157
            $activeExtensions[] = $key;
158
            $title = $package->getValueFromComposerManifest('description') ?? '';
159
            if (is_file($path . 'ext_emconf.php')) {
160
                $_EXTKEY = $key;
161
                $EM_CONF = [];
162
                include $path . 'ext_emconf.php';
163
                $title = $EM_CONF[$key]['title'] ?? $title;
164
165
                $state = $EM_CONF[$key]['state'] ?? '';
166
                if ($state === 'excludeFromUpdates') {
167
                    continue;
168
                }
169
            }
170
            $extension = [
171
                'key' => $key,
172
                'title' => $title,
173
            ];
174
            if (!empty(ExtensionManagementUtility::getExtensionIcon($path, false))) {
175
                $extension['icon'] = PathUtility::stripPathSitePrefix(ExtensionManagementUtility::getExtensionIcon($path, true));
176
            }
177
            $extension['packs'] = [];
178
            foreach ($activeLanguages as $iso) {
179
                $isLanguagePackDownloaded = is_dir(Environment::getLabelsPath() . '/' . $iso . '/' . $key . '/');
180
                $lastUpdate = $this->registry->get('languagePacks', $iso . '-' . $key);
181
                $extension['packs'][] = [
182
                    'iso' => $iso,
183
                    'exists' => $isLanguagePackDownloaded,
184
                    'lastUpdate' => $this->getFormattedDate($lastUpdate),
185
                ];
186
            }
187
            $extensions[] = $extension;
188
        }
189
        usort($extensions, function ($a, $b) {
190
            // Sort extensions by key
191
            if ($a['key'] === $b['key']) {
192
                return 0;
193
            }
194
            return $a['key'] < $b['key'] ? -1 : 1;
195
        });
196
        return $extensions;
197
    }
198
199
    /**
200
     * Download and unpack a single language pack of one extension.
201
     *
202
     * @param string $key Extension key
203
     * @param string $iso Language iso code
204
     * @return string One of 'update', 'new' or 'failed'
205
     * @throws \RuntimeException
206
     */
207
    public function languagePackDownload(string $key, string $iso): string
208
    {
209
        // Sanitize extension and iso code
210
        $availableLanguages = $this->getAvailableLanguages();
211
        $activeLanguages = $this->getActiveLanguages();
212
        if (!array_key_exists($iso, $availableLanguages) || !in_array($iso, $activeLanguages, true)) {
213
            throw new \RuntimeException('Language iso code ' . (string)$iso . ' not available or active', 1520117054);
214
        }
215
        $packageManager = GeneralUtility::makeInstance(PackageManager::class);
216
        $activePackages = $packageManager->getActivePackages();
217
        $packageActive = false;
218
        foreach ($activePackages as $package) {
219
            if ($package->getPackageKey() === $key) {
220
                $packageActive = true;
221
                break;
222
            }
223
        }
224
        if (!$packageActive) {
225
            throw new \RuntimeException('Extension ' . (string)$key . ' not loaded', 1520117245);
226
        }
227
228
        $languagePackBaseUrl = self::LANGUAGE_PACK_URL;
229
230
        // Allow to modify the base url on the fly
231
        $event = $this->eventDispatcher->dispatch(new ModifyLanguagePackRemoteBaseUrlEvent(new Uri($languagePackBaseUrl), $key));
232
        $languagePackBaseUrl = $event->getBaseUrl();
233
        $path = ExtensionManagementUtility::extPath($key);
234
        $majorVersion = GeneralUtility::makeInstance(Typo3Version::class)->getMajorVersion();
235
        if (strpos($path, '/sysext/') !== false) {
236
            // This is a system extension and the package URL should be adapted to have different packs per core major version
237
            // https://localize.typo3.org/xliff/b/a/backend-l10n/backend-l10n-fr.v9.zip
238
            $packageUrl = $key[0] . '/' . $key[1] . '/' . $key . '-l10n/' . $key . '-l10n-' . $iso . '.v' . $majorVersion . '.zip';
239
        } else {
240
            // Typical non sysext path, Hungarian:
241
            // https://localize.typo3.org/xliff/a/n/anextension-l10n/anextension-l10n-hu.zip
242
            $packageUrl = $key[0] . '/' . $key[1] . '/' . $key . '-l10n/' . $key . '-l10n-' . $iso . '.zip';
243
        }
244
245
        $absoluteLanguagePath = Environment::getLabelsPath() . '/' . $iso . '/';
246
        $absoluteExtractionPath = $absoluteLanguagePath . $key . '/';
247
        $absolutePathToZipFile = Environment::getVarPath() . '/transient/' . $key . '-l10n-' . $iso . '.zip';
248
249
        $packExists = is_dir($absoluteExtractionPath);
250
251
        $packResult = $packExists ? 'update' : 'new';
252
253
        $operationResult = false;
254
        try {
255
            $response = $this->requestFactory->request($languagePackBaseUrl . $packageUrl);
256
            if ($response->getStatusCode() === 200) {
257
                $languagePackContent = $response->getBody()->getContents();
258
                if (!empty($languagePackContent)) {
259
                    $operationResult = true;
260
                    if ($packExists) {
261
                        $operationResult = GeneralUtility::rmdir($absoluteExtractionPath, true);
262
                    }
263
                    if ($operationResult) {
264
                        GeneralUtility::mkdir_deep(Environment::getVarPath() . '/transient/');
265
                        $operationResult = GeneralUtility::writeFileToTypo3tempDir($absolutePathToZipFile, $languagePackContent) === null;
266
                    }
267
                    $this->unzipTranslationFile($absolutePathToZipFile, $absoluteLanguagePath);
268
                    if ($operationResult) {
269
                        $operationResult = unlink($absolutePathToZipFile);
270
                    }
271
                }
272
            } else {
273
                $this->logger->warning(sprintf(
274
                    'Requesting %s was not successful, got status code %d (%s)',
275
                    $languagePackBaseUrl . $packageUrl,
276
                    $response->getStatusCode(),
277
                    $response->getReasonPhrase()
278
                ));
279
            }
280
        } catch (\Exception $e) {
281
            $operationResult = false;
282
        }
283
        if (!$operationResult) {
284
            $packResult = 'failed';
285
            $this->registry->set('languagePacks', $iso . '-' . $key, time());
286
        }
287
        return $packResult;
288
    }
289
290
    /**
291
     * Set 'last update' timestamp in registry for a series of iso codes.
292
     *
293
     * @param string[] $isos List of iso code timestamps to set
294
     * @throws \RuntimeException
295
     */
296
    public function setLastUpdatedIsoCode(array $isos)
297
    {
298
        $activeLanguages = $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['lang']['availableLanguages'] ?? [];
299
        foreach ($isos as $iso) {
300
            if (!in_array($iso, $activeLanguages, true)) {
301
                throw new \RuntimeException('Language iso code ' . (string)$iso . ' not available or active', 1520176318);
302
            }
303
            $this->registry->set('languagePacks', $iso, time());
304
        }
305
    }
306
307
    /**
308
     * Format a timestamp to a formatted date string
309
     *
310
     * @param int|null $timestamp
311
     * @return string|null
312
     */
313
    protected function getFormattedDate($timestamp)
314
    {
315
        if (is_int($timestamp)) {
316
            $date = new \DateTime('@' . $timestamp);
317
            $format = $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'];
318
            $timestamp = $date->format($format);
319
        }
320
        return $timestamp;
321
    }
322
323
    /**
324
     * Unzip a language zip file
325
     *
326
     * @param string $file path to zip file
327
     * @param string $path path to extract to
328
     */
329
    protected function unzipTranslationFile(string $file, string $path)
330
    {
331
        if (!is_dir($path)) {
332
            GeneralUtility::mkdir_deep($path);
333
        }
334
335
        $zipService = GeneralUtility::makeInstance(ZipService::class);
336
        if ($zipService->verify($file)) {
337
            $zipService->extract($file, $path);
338
        }
339
        GeneralUtility::fixPermissions($path, true);
340
    }
341
}
342