[TASK] Update php-cs-fixer to 2.5.0
[Packages/TYPO3.CMS.git] / typo3 / sysext / lang / Classes / Service / TerService.php
1 <?php
2 namespace TYPO3\CMS\Lang\Service;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use TYPO3\CMS\Core\Exception;
18 use TYPO3\CMS\Core\SingletonInterface;
19 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
20 use TYPO3\CMS\Core\Utility\GeneralUtility;
21 use TYPO3\CMS\Extensionmanager\Utility\Connection\TerUtility;
22 use TYPO3\CMS\Lang\Exception\Language as LanguageException;
23 use TYPO3\CMS\Lang\Exception\XmlParser as XmlParserException;
24
25 /**
26 * Extends of extensionmanager ter connection to enrich with translation
27 * related methods
28 */
29 class TerService extends TerUtility implements SingletonInterface
30 {
31 /**
32 * Fetches extensions translation status
33 *
34 * @param string $extensionKey Extension Key
35 * @param string $mirrorUrl URL of mirror to use
36 * @return mixed
37 */
38 public function fetchTranslationStatus($extensionKey, $mirrorUrl)
39 {
40 $result = false;
41 $extPath = strtolower($extensionKey);
42 $mirrorUrl .= $extPath[0] . '/' . $extPath[1] . '/' . $extPath . '-l10n/' . $extPath . '-l10n.xml';
43 $remote = GeneralUtility::getUrl($mirrorUrl);
44 if ($remote !== false) {
45 $parsed = $this->parseL10nXML($remote);
46 $result = $parsed['languagePackIndex'];
47 }
48 return $result;
49 }
50
51 /**
52 * Parses content of *-l10n.xml into a suitable array
53 *
54 * @param string $string: XML data to parse
55 * @throws \TYPO3\CMS\Lang\Exception\XmlParser
56 * @return array Array representation of XML data
57 */
58 protected function parseL10nXML($string)
59 {
60 // Create parser:
61 $parser = xml_parser_create();
62 // Disables the functionality to allow external entities to be loaded when parsing the XML, must be kept
63 $previousValueOfEntityLoader = libxml_disable_entity_loader(true);
64 $values = [];
65 $index = [];
66 xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 0);
67 xml_parser_set_option($parser, XML_OPTION_SKIP_WHITE, 0);
68 // Parse content
69 xml_parse_into_struct($parser, $string, $values, $index);
70 libxml_disable_entity_loader($previousValueOfEntityLoader);
71 // If error, return error message
72 if (xml_get_error_code($parser)) {
73 $line = xml_get_current_line_number($parser);
74 $error = xml_error_string(xml_get_error_code($parser));
75 xml_parser_free($parser);
76 throw new XmlParserException('Error in XML parser while decoding l10n XML file. Line ' . $line . ': ' . $error, 1345736517);
77 }
78 // Init vars
79 $stack = [[]];
80 $stacktop = 0;
81 $current = [];
82 $tagName = '';
83 $documentTag = '';
84 // Traverse the parsed XML structure:
85 foreach ($values as $val) {
86 // First, process the tag-name (which is used in both cases, whether "complete" or "close")
87 $tagName = (string)($val['tag'] === 'languagepack' && $val['type'] === 'open') ? $val['attributes']['language'] : $val['tag'];
88 if (!$documentTag) {
89 $documentTag = $tagName;
90 }
91 // Setting tag-values, manage stack:
92 switch ($val['type']) {
93 // If open tag it means there is an array stored in sub-elements.
94 // Therefore increase the stackpointer and reset the accumulation array
95 case 'open':
96 // Setting blank place holder
97 $current[$tagName] = [];
98 $stack[$stacktop++] = $current;
99 $current = [];
100 break;
101 // If the tag is "close" then it is an array which is closing and we decrease the stack pointer.
102 case 'close':
103 $oldCurrent = $current;
104 $current = $stack[--$stacktop];
105 // Going to the end of array to get placeholder key, key($current), and fill in array next
106 end($current);
107 $current[key($current)] = $oldCurrent;
108 unset($oldCurrent);
109 break;
110 // If "complete", then it's a value. Omits the tag if the value is empty.
111 case 'complete':
112 $trimmedValue = trim((string)$val['value']);
113 if ($trimmedValue !== '') {
114 $current[$tagName] = $trimmedValue;
115 }
116 break;
117 }
118 }
119 $result = $current[$tagName];
120
121 return $result;
122 }
123
124 /**
125 * Install translations for all selected languages for an extension
126 *
127 * @param string $extensionKey The extension key to install the translations for
128 * @param string $language Language code of translation to fetch
129 * @param string $mirrorUrl Mirror URL to fetch data from
130 * @return bool TRUE on success, error string on failure
131 */
132 public function updateTranslation($extensionKey, $language, $mirrorUrl)
133 {
134 $result = false;
135 try {
136 $l10n = $this->fetchTranslation($extensionKey, $language, $mirrorUrl);
137 if (is_array($l10n)) {
138 $absolutePathToZipFile = GeneralUtility::getFileAbsFileName('typo3temp/var/transient/' . $extensionKey . '-l10n-' . $language . '.zip');
139 $relativeLanguagePath = 'l10n' . '/' . $language . '/';
140 $absoluteLanguagePath = GeneralUtility::getFileAbsFileName(PATH_typo3conf . $relativeLanguagePath);
141 $absoluteExtensionLanguagePath = GeneralUtility::getFileAbsFileName(PATH_typo3conf . $relativeLanguagePath . $extensionKey . '/');
142 if (empty($absolutePathToZipFile) || empty($absoluteLanguagePath) || empty($absoluteExtensionLanguagePath)) {
143 throw new LanguageException('Given path is invalid.', 1352565336);
144 }
145 if (!is_dir($absoluteLanguagePath)) {
146 GeneralUtility::mkdir_deep(PATH_typo3conf, $relativeLanguagePath);
147 }
148 GeneralUtility::writeFileToTypo3tempDir($absolutePathToZipFile, $l10n[0]);
149 if (is_dir($absoluteExtensionLanguagePath)) {
150 GeneralUtility::rmdir($absoluteExtensionLanguagePath, true);
151 }
152 if ($this->unzipTranslationFile($absolutePathToZipFile, $absoluteLanguagePath)) {
153 $result = true;
154 }
155 }
156 } catch (Exception $exception) {
157 // @todo logging
158 }
159 return $result;
160 }
161
162 /**
163 * Fetches an extensions l10n file from the given mirror
164 *
165 * @param string $extensionKey Extension Key
166 * @param string $language The language code of the translation to fetch
167 * @param string $mirrorUrl URL of mirror to use
168 * @throws \TYPO3\CMS\Lang\Exception\XmlParser
169 * @return array Array containing l10n data
170 */
171 protected function fetchTranslation($extensionKey, $language, $mirrorUrl)
172 {
173 $extensionPath = strtolower($extensionKey);
174 // Typical non sysext path, Hungarian:
175 // http://my.mirror/ter/a/n/anextension-l10n/anextension-l10n-hu.zip
176 $packageUrl = $extensionPath[0] . '/' . $extensionPath[1] . '/' . $extensionPath .
177 '-l10n/' . $extensionPath . '-l10n-' . $language . '.zip';
178
179 try {
180 $path = ExtensionManagementUtility::extPath($extensionPath);
181 if (strpos($path, '/sysext/') !== false) {
182 // This is a system extension and the package URL should be adapted
183 list($majorVersion, ) = explode('.', TYPO3_branch);
184 // Typical non sysext path, mind the additional version part, French
185 // http://my.mirror/ter/b/a/backend-l10n/backend-l10n-fr.v7.zip
186 $packageUrl = $extensionPath[0] . '/' . $extensionPath[1] . '/' . $extensionPath .
187 '-l10n/' . $extensionPath . '-l10n-' . $language . '.v' . $majorVersion . '.zip';
188 }
189 } catch (\BadFunctionCallException $e) {
190 // Nothing to do
191 }
192
193 $l10nResponse = GeneralUtility::getUrl($mirrorUrl . $packageUrl);
194 if ($l10nResponse === false) {
195 throw new XmlParserException('Error: Translation could not be fetched.', 1345736785);
196 }
197 return [$l10nResponse];
198 }
199
200 /**
201 * Unzip an language zip file
202 *
203 * @param string $file path to zip file
204 * @param string $path path to extract to
205 * @throws \TYPO3\CMS\Lang\Exception\Language
206 * @return bool
207 */
208 protected function unzipTranslationFile($file, $path)
209 {
210 $zip = zip_open($file);
211 if (is_resource($zip)) {
212 $result = true;
213 if (!is_dir($path)) {
214 GeneralUtility::mkdir_deep($path);
215 }
216 while (($zipEntry = zip_read($zip)) !== false) {
217 $zipEntryName = zip_entry_name($zipEntry);
218 if (strpos($zipEntryName, '/') !== false) {
219 $zipEntryPathSegments = explode('/', $zipEntryName);
220 $fileName = array_pop($zipEntryPathSegments);
221 // It is a folder, because the last segment is empty, let's create it
222 if (empty($fileName)) {
223 GeneralUtility::mkdir_deep($path, implode('/', $zipEntryPathSegments));
224 } else {
225 $absoluteTargetPath = GeneralUtility::getFileAbsFileName($path . implode('/', $zipEntryPathSegments) . '/' . $fileName);
226 if (trim($absoluteTargetPath) !== '') {
227 $return = GeneralUtility::writeFile(
228 $absoluteTargetPath,
229 zip_entry_read($zipEntry, zip_entry_filesize($zipEntry))
230 );
231 if ($return === false) {
232 throw new LanguageException('Could not write file ' . $zipEntryName, 1345304560);
233 }
234 } else {
235 throw new LanguageException('Could not write file ' . $zipEntryName, 1352566904);
236 }
237 }
238 } else {
239 throw new LanguageException('Extension directory missing in zip file!', 1352566905);
240 }
241 }
242 } else {
243 throw new LanguageException('Unable to open zip file ' . $file, 1345304561);
244 }
245 return $result;
246 }
247 }