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