‪TYPO3CMS  10.4
LanguagePackService.php
Go to the documentation of this file.
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 
19 
20 use Psr\EventDispatcher\EventDispatcherInterface;
21 use Psr\Log\LoggerInterface;
22 use Symfony\Component\Finder\Finder;
27 use TYPO3\CMS\Core\Package\PackageManager;
34 
42 {
46  protected ‪$locales;
47 
51  protected ‪$registry;
52 
56  protected ‪$eventDispatcher;
57 
61  protected ‪$requestFactory;
62 
63  private const ‪OLD_LANGUAGE_PACK_URLS = [
64  'https://typo3.org/fileadmin/ter/',
65  'https://beta-translation.typo3.org/fileadmin/ter/',
66  'https://localize.typo3.org/fileadmin/ter/'
67  ];
68 
72  protected ‪$logger;
73 
74  private const ‪LANGUAGE_PACK_URL = 'https://localize.typo3.org/xliff/';
75 
76  public function ‪__construct(
77  EventDispatcherInterface ‪$eventDispatcher,
79  LoggerInterface ‪$logger
80  ) {
81  $this->eventDispatcher = ‪$eventDispatcher;
82  $this->locales = GeneralUtility::makeInstance(Locales::class);
83  $this->registry = GeneralUtility::makeInstance(Registry::class);
84  $this->requestFactory = ‪$requestFactory;
85  $this->logger = ‪$logger;
86  }
87 
93  public function ‪getAvailableLanguages(): array
94  {
95  return $this->locales->getLanguages();
96  }
97 
103  public function ‪getActiveLanguages(): array
104  {
105  $availableLanguages = ‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['lang']['availableLanguages'] ?? [];
106  return array_filter($availableLanguages);
107  }
108 
114  public function ‪getLanguageDetails(): array
115  {
116  $availableLanguages = $this->‪getAvailableLanguages();
117  $activeLanguages = $this->‪getActiveLanguages();
118  $languages = [];
119  foreach ($availableLanguages as $iso => $name) {
120  if ($iso === 'default') {
121  continue;
122  }
123  $lastUpdate = $this->registry->get('languagePacks', $iso);
124  $languages[] = [
125  'iso' => $iso,
126  'name' => $name,
127  'active' => in_array($iso, $activeLanguages, true),
128  'lastUpdate' => $this->‪getFormattedDate($lastUpdate),
129  'dependencies' => $this->locales->getLocaleDependencies($iso),
130  ];
131  }
132  usort($languages, function ($a, $b) {
133  // Sort languages by name
134  if ($a['name'] === $b['name']) {
135  return 0;
136  }
137  return $a['name'] < $b['name'] ? -1 : 1;
138  });
139  return $languages;
140  }
141 
147  public function ‪getExtensionLanguagePackDetails(): array
148  {
149  $activeLanguages = $this->‪getActiveLanguages();
150  $packageManager = GeneralUtility::makeInstance(PackageManager::class);
151  $activePackages = $packageManager->getActivePackages();
152  $extensions = [];
153  $activeExtensions = [];
154  foreach ($activePackages as $package) {
155  $path = $package->getPackagePath();
156  ‪$finder = new Finder();
157  try {
158  $files = ‪$finder->files()->in($path . 'Resources/Private/Language/')->name('*.xlf');
159  if ($files->count() === 0) {
160  // This extension has no .xlf files
161  continue;
162  }
163  } catch (\InvalidArgumentException $e) {
164  // Dir does not exist
165  continue;
166  }
167  $key = $package->getPackageKey();
168  $activeExtensions[] = $key;
169  $title = $package->getValueFromComposerManifest('description') ?? '';
170  if (is_file($path . 'ext_emconf.php')) {
171  $_EXTKEY = $key;
172  ‪$EM_CONF = [];
173  include $path . 'ext_emconf.php';
174  $title = ‪$EM_CONF[$key]['title'] ?? $title;
175 
176  $state = ‪$EM_CONF[$key]['state'] ?? '';
177  if ($state === 'excludeFromUpdates') {
178  continue;
179  }
180  }
181  $extension = [
182  'key' => $key,
183  'title' => $title,
184  ];
185  if (!empty(‪ExtensionManagementUtility::getExtensionIcon($path, false))) {
187  }
188  $extension['packs'] = [];
189  foreach ($activeLanguages as $iso) {
190  $isLanguagePackDownloaded = is_dir(‪Environment::getLabelsPath() . '/' . $iso . '/' . $key . '/');
191  $lastUpdate = $this->registry->get('languagePacks', $iso . '-' . $key);
192  $extension['packs'][] = [
193  'iso' => $iso,
194  'exists' => $isLanguagePackDownloaded,
195  'lastUpdate' => $this->‪getFormattedDate($lastUpdate),
196  ];
197  }
198  $extensions[] = $extension;
199  }
200  usort($extensions, function ($a, $b) {
201  // Sort extensions by key
202  if ($a['key'] === $b['key']) {
203  return 0;
204  }
205  return $a['key'] < $b['key'] ? -1 : 1;
206  });
207  return $extensions;
208  }
209 
216  public function ‪updateMirrorBaseUrl(): string
217  {
218  $repositoryUrl = 'https://repositories.typo3.org/mirrors.xml.gz';
219  $downloadBaseUrl = false;
220  try {
221  $response = $this->requestFactory->request($repositoryUrl);
222  if ($response->getStatusCode() === 200) {
223  $xmlContent = @gzdecode($response->getBody()->getContents());
224  if (!empty($xmlContent['mirror']['host']) && !empty($xmlContent['mirror']['path'])) {
225  $downloadBaseUrl = 'https://' . $xmlContent['mirror']['host'] . $xmlContent['mirror']['path'];
226  }
227  } else {
228  $this->logger->warning(sprintf(
229  'Requesting %s was not successful, got status code %d (%s)',
230  $repositoryUrl,
231  $response->getStatusCode(),
232  $response->getReasonPhrase()
233  ));
234  }
235  } catch (\Exception $e) {
236  // Catch generic exception, fallback handled below
237  $this->logger->error('Failed to download list of mirrors', ['exception' => $e]);
238  }
239  if (empty($downloadBaseUrl)) {
240  // Hard coded fallback if something went wrong fetching & parsing mirror list
241  $downloadBaseUrl = ‪self::LANGUAGE_PACK_URL;
242  }
243  $this->registry->set('languagePacks', 'baseUrl', $downloadBaseUrl);
244  return $downloadBaseUrl;
245  }
246 
255  public function ‪languagePackDownload(string $key, string $iso): string
256  {
257  // Sanitize extension and iso code
258  $availableLanguages = $this->‪getAvailableLanguages();
259  $activeLanguages = $this->‪getActiveLanguages();
260  if (!array_key_exists($iso, $availableLanguages) || !in_array($iso, $activeLanguages, true)) {
261  throw new \RuntimeException('Language iso code ' . (string)$iso . ' not available or active', 1520117054);
262  }
263  $packageManager = GeneralUtility::makeInstance(PackageManager::class);
264  $activePackages = $packageManager->getActivePackages();
265  $packageActive = false;
266  foreach ($activePackages as $package) {
267  if ($package->getPackageKey() === $key) {
268  $packageActive = true;
269  break;
270  }
271  }
272  if (!$packageActive) {
273  throw new \RuntimeException('Extension ' . (string)$key . ' not loaded', 1520117245);
274  }
275 
276  $languagePackBaseUrl = $this->registry->get('languagePacks', 'baseUrl');
277  if (empty($languagePackBaseUrl)) {
278  throw new \RuntimeException('Language pack baseUrl not found', 1520169691);
279  }
280 
281  if (in_array($languagePackBaseUrl, self::OLD_LANGUAGE_PACK_URLS, true)) {
282  $languagePackBaseUrl = ‪self::LANGUAGE_PACK_URL;
283  }
284 
285  // Allow to modify the base url on the fly
286  $event = $this->eventDispatcher->dispatch(new ModifyLanguagePackRemoteBaseUrlEvent(new Uri($languagePackBaseUrl), $key));
287  $languagePackBaseUrl = $event->getBaseUrl();
289  $majorVersion = explode('.', TYPO3_branch)[0];
290  if (strpos($path, '/sysext/') !== false) {
291  // This is a system extension and the package URL should be adapted to have different packs per core major version
292  // https://localize.typo3.org/xliff/b/a/backend-l10n/backend-l10n-fr.v9.zip
293  $packageUrl = $key[0] . '/' . $key[1] . '/' . $key . '-l10n/' . $key . '-l10n-' . $iso . '.v' . $majorVersion . '.zip';
294  } else {
295  // Typical non sysext path, Hungarian:
296  // https://localize.typo3.org/xliff/a/n/anextension-l10n/anextension-l10n-hu.zip
297  $packageUrl = $key[0] . '/' . $key[1] . '/' . $key . '-l10n/' . $key . '-l10n-' . $iso . '.zip';
298  }
299 
300  $absoluteLanguagePath = ‪Environment::getLabelsPath() . '/' . $iso . '/';
301  $absoluteExtractionPath = $absoluteLanguagePath . $key . '/';
302  $absolutePathToZipFile = ‪Environment::getVarPath() . '/transient/' . $key . '-l10n-' . $iso . '.zip';
303 
304  $packExists = is_dir($absoluteExtractionPath);
305 
306  $packResult = $packExists ? 'update' : 'new';
307 
308  $operationResult = false;
309  try {
310  $response = $this->requestFactory->request($languagePackBaseUrl . $packageUrl);
311  if ($response->getStatusCode() === 200) {
312  $languagePackContent = $response->getBody()->getContents();
313  if (!empty($languagePackContent)) {
314  $operationResult = true;
315  if ($packExists) {
316  $operationResult = ‪GeneralUtility::rmdir($absoluteExtractionPath, true);
317  }
318  if ($operationResult) {
320  $operationResult = ‪GeneralUtility::writeFileToTypo3tempDir($absolutePathToZipFile, $languagePackContent) === null;
321  }
322  $this->‪unzipTranslationFile($absolutePathToZipFile, $absoluteLanguagePath);
323  if ($operationResult) {
324  $operationResult = unlink($absolutePathToZipFile);
325  }
326  }
327  } else {
328  $this->logger->warning(sprintf(
329  'Requesting %s was not successful, got status code %d (%s)',
330  $languagePackBaseUrl . $packageUrl,
331  $response->getStatusCode(),
332  $response->getReasonPhrase()
333  ));
334  }
335  } catch (\Exception $e) {
336  $operationResult = false;
337  }
338  if (!$operationResult) {
339  $packResult = 'failed';
340  $this->registry->set('languagePacks', $iso . '-' . $key, time());
341  }
342  return $packResult;
343  }
344 
351  public function ‪setLastUpdatedIsoCode(array $isos)
352  {
353  $activeLanguages = ‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['lang']['availableLanguages'] ?? [];
354  ‪$registry = GeneralUtility::makeInstance(Registry::class);
355  foreach ($isos as $iso) {
356  if (!in_array($iso, $activeLanguages, true)) {
357  throw new \RuntimeException('Language iso code ' . (string)$iso . ' not available or active', 1520176318);
358  }
359  ‪$registry->‪set('languagePacks', $iso, time());
360  }
361  }
362 
369  protected function ‪getFormattedDate($timestamp)
370  {
371  if (is_int($timestamp)) {
372  $date = new \DateTime('@' . $timestamp);
373  $format = ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'];
374  $timestamp = $date->format($format);
375  }
376  return $timestamp;
377  }
378 
385  protected function ‪unzipTranslationFile(string $file, string $path)
386  {
387  if (!is_dir($path)) {
389  }
390 
391  $zipService = GeneralUtility::makeInstance(ZipService::class);
392  if ($zipService->verify($file)) {
393  $zipService->extract($file, $path);
394  }
396  }
397 }
‪TYPO3\CMS\Install\Service\LanguagePackService\getFormattedDate
‪string null getFormattedDate($timestamp)
Definition: LanguagePackService.php:364
‪TYPO3\CMS\Core\Utility\PathUtility
Definition: PathUtility.php:24
‪$finder
‪if(PHP_SAPI !=='cli') $finder
Definition: header-comment.php:22
‪TYPO3\CMS\Core\Core\Environment\getLabelsPath
‪static string getLabelsPath()
Definition: Environment.php:236
‪TYPO3\CMS\Core\Utility\PathUtility\stripPathSitePrefix
‪static string stripPathSitePrefix($path)
Definition: PathUtility.php:372
‪TYPO3\CMS\Core\Registry
Definition: Registry.php:33
‪TYPO3\CMS\Core\Localization\Locales
Definition: Locales.php:30
‪TYPO3\CMS\Install\Service\LanguagePackService\getActiveLanguages
‪array getActiveLanguages()
Definition: LanguagePackService.php:98
‪TYPO3\CMS\Install\Service\LanguagePackService\languagePackDownload
‪string languagePackDownload(string $key, string $iso)
Definition: LanguagePackService.php:250
‪TYPO3\CMS\Install\Service\LanguagePackService\updateMirrorBaseUrl
‪string updateMirrorBaseUrl()
Definition: LanguagePackService.php:211
‪TYPO3\CMS\Core\Registry\set
‪set($namespace, $key, $value)
Definition: Registry.php:73
‪TYPO3\CMS\Install\Service\LanguagePackService\LANGUAGE_PACK_URL
‪const LANGUAGE_PACK_URL
Definition: LanguagePackService.php:69
‪TYPO3\CMS\Core\Http\Uri
Definition: Uri.php:29
‪TYPO3\CMS\Core\Utility\ExtensionManagementUtility
Definition: ExtensionManagementUtility.php:43
‪TYPO3\CMS\Install\Service\LanguagePackService\setLastUpdatedIsoCode
‪setLastUpdatedIsoCode(array $isos)
Definition: LanguagePackService.php:346
‪TYPO3\CMS\Install\Service\LanguagePackService\unzipTranslationFile
‪unzipTranslationFile(string $file, string $path)
Definition: LanguagePackService.php:380
‪TYPO3\CMS\Install\Exception
Definition: Exception.php:24
‪TYPO3\CMS\Install\Service\LanguagePackService\__construct
‪__construct(EventDispatcherInterface $eventDispatcher, RequestFactory $requestFactory, LoggerInterface $logger)
Definition: LanguagePackService.php:71
‪TYPO3\CMS\Core\Utility\GeneralUtility\fixPermissions
‪static mixed fixPermissions($path, $recursive=false)
Definition: GeneralUtility.php:1863
‪TYPO3\CMS\Core\Utility\GeneralUtility\mkdir_deep
‪static mkdir_deep($directory)
Definition: GeneralUtility.php:2022
‪TYPO3\CMS\Install\Service\LanguagePackService\$locales
‪Locales $locales
Definition: LanguagePackService.php:45
‪TYPO3\CMS\Core\Utility\GeneralUtility\writeFileToTypo3tempDir
‪static string writeFileToTypo3tempDir($filepath, $content)
Definition: GeneralUtility.php:1928
‪TYPO3\CMS\Install\Service\LanguagePackService\$logger
‪LoggerInterface $logger
Definition: LanguagePackService.php:67
‪TYPO3\CMS\Core\Http\RequestFactory
Definition: RequestFactory.php:31
‪TYPO3\CMS\Install\Service\LanguagePackService
Definition: LanguagePackService.php:42
‪TYPO3\CMS\Core\Service\Archive\ZipService
Definition: ZipService.php:29
‪TYPO3\CMS\Install\Service\LanguagePackService\$eventDispatcher
‪EventDispatcherInterface $eventDispatcher
Definition: LanguagePackService.php:53
‪TYPO3\CMS\Install\Service\LanguagePackService\getExtensionLanguagePackDetails
‪array getExtensionLanguagePackDetails()
Definition: LanguagePackService.php:142
‪TYPO3\CMS\Install\Service\LanguagePackService\OLD_LANGUAGE_PACK_URLS
‪const OLD_LANGUAGE_PACK_URLS
Definition: LanguagePackService.php:59
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:5
‪TYPO3\CMS\Core\Core\Environment
Definition: Environment.php:40
‪TYPO3\CMS\Install\Service\Event\ModifyLanguagePackRemoteBaseUrlEvent
Definition: ModifyLanguagePackRemoteBaseUrlEvent.php:26
‪TYPO3\CMS\Core\Utility\GeneralUtility\rmdir
‪static bool rmdir($path, $removeNonEmpty=false)
Definition: GeneralUtility.php:2075
‪TYPO3\CMS\Core\Utility\ExtensionManagementUtility\extPath
‪static string extPath($key, $script='')
Definition: ExtensionManagementUtility.php:127
‪$EM_CONF
‪$EM_CONF[$_EXTKEY]
Definition: ext_emconf.php:3
‪TYPO3\CMS\Install\Service\LanguagePackService\getAvailableLanguages
‪array getAvailableLanguages()
Definition: LanguagePackService.php:88
‪TYPO3\CMS\Install\Service\LanguagePackService\$registry
‪Registry $registry
Definition: LanguagePackService.php:49
‪TYPO3\CMS\Core\Utility\ExtensionManagementUtility\getExtensionIcon
‪static string getExtensionIcon($extensionPath, $returnFullPath=false)
Definition: ExtensionManagementUtility.php:1518
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:46
‪TYPO3\CMS\Install\Service\LanguagePackService\getLanguageDetails
‪array getLanguageDetails()
Definition: LanguagePackService.php:109
‪TYPO3\CMS\Install\Service
Definition: ClearCacheService.php:16
‪TYPO3\CMS\Install\Service\LanguagePackService\$requestFactory
‪RequestFactory $requestFactory
Definition: LanguagePackService.php:57
‪TYPO3\CMS\Core\Core\Environment\getVarPath
‪static string getVarPath()
Definition: Environment.php:192