[TASK] Make InvalidXmlFileException for language files more verbose
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Localization / Parser / LocallangXmlParser.php
1 <?php
2 namespace TYPO3\CMS\Core\Localization\Parser;
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\Localization\Exception\InvalidXmlFileException;
18 use TYPO3\CMS\Core\Utility\ArrayUtility;
19 use TYPO3\CMS\Core\Utility\GeneralUtility;
20 use TYPO3\CMS\Core\Utility\PathUtility;
21
22 /**
23 * Parser for XML locallang file.
24 */
25 class LocallangXmlParser extends AbstractXmlParser
26 {
27 /**
28 * Associative array of "filename => parsed data" pairs.
29 *
30 * @var array
31 */
32 protected $parsedTargetFiles;
33
34 /**
35 * Returns parsed representation of XML file.
36 *
37 * @param string $sourcePath Source file path
38 * @param string $languageKey Language key
39 * @param string $charset Charset
40 * @return array
41 */
42 public function getParsedData($sourcePath, $languageKey, $charset = '')
43 {
44 $this->sourcePath = $sourcePath;
45 $this->languageKey = $languageKey;
46 $this->charset = $this->getCharset($languageKey, $charset);
47 // Parse source
48 $parsedSource = $this->parseXmlFile();
49 // Parse target
50 $localizedTargetPath = GeneralUtility::getFileAbsFileName(GeneralUtility::llXmlAutoFileName($this->sourcePath, $this->languageKey));
51 $targetPath = $this->languageKey !== 'default' && @is_file($localizedTargetPath) ? $localizedTargetPath : $this->sourcePath;
52 try {
53 $parsedTarget = $this->getParsedTargetData($targetPath);
54 } catch (InvalidXmlFileException $e) {
55 $parsedTarget = $this->getParsedTargetData($this->sourcePath);
56 }
57 $LOCAL_LANG = [];
58 $LOCAL_LANG[$languageKey] = $parsedSource;
59 ArrayUtility::mergeRecursiveWithOverrule($LOCAL_LANG[$languageKey], $parsedTarget);
60 return $LOCAL_LANG;
61 }
62
63 /**
64 * Returns array representation of XLIFF data, starting from a root node.
65 *
66 * @param \SimpleXMLElement $root XML root element
67 * @param string $element Target or Source
68 * @return array
69 * @throws InvalidXmlFileException
70 */
71 protected function doParsingFromRootForElement(\SimpleXMLElement $root, $element)
72 {
73 $bodyOfFileTag = $root->data->languageKey;
74 if ($bodyOfFileTag === null) {
75 throw new InvalidXmlFileException('Invalid locallang.xml language file "' . PathUtility::stripPathSitePrefix($this->sourcePath) . '"', 1487944884);
76 }
77
78 // Check if the source llxml file contains localized records
79 $localizedBodyOfFileTag = $root->data->xpath('languageKey[@index=\'' . $this->languageKey . '\']');
80 if ($element === 'source' || $this->languageKey === 'default') {
81 $parsedData = $this->getParsedDataForElement($bodyOfFileTag, $element);
82 } else {
83 $parsedData = [];
84 }
85 if ($element === 'target' && isset($localizedBodyOfFileTag[0]) && $localizedBodyOfFileTag[0] instanceof \SimpleXMLElement) {
86 $parsedDataTarget = $this->getParsedDataForElement($localizedBodyOfFileTag[0], $element);
87 $mergedData = $parsedDataTarget + $parsedData;
88 if ($this->languageKey === 'default') {
89 $parsedData = array_intersect_key($mergedData, $parsedData, $parsedDataTarget);
90 } else {
91 $parsedData = array_intersect_key($mergedData, $parsedDataTarget);
92 }
93 }
94 return $parsedData;
95 }
96
97 /**
98 * Parse the given language key tag
99 *
100 * @param \SimpleXMLElement $bodyOfFileTag
101 * @param string $element
102 * @return array
103 */
104 protected function getParsedDataForElement(\SimpleXMLElement $bodyOfFileTag, $element)
105 {
106 $parsedData = [];
107 $children = $bodyOfFileTag->children();
108 if ($children->count() === 0) {
109 // Check for externally-referenced resource:
110 // <languageKey index="fr">EXT:yourext/path/to/localized/locallang.xml</languageKey>
111 $reference = sprintf('%s', $bodyOfFileTag);
112 if (substr($reference, -4) === '.xml') {
113 return $this->getParsedTargetData(GeneralUtility::getFileAbsFileName($reference));
114 }
115 }
116 /** @var \SimpleXMLElement $translationElement */
117 foreach ($children as $translationElement) {
118 if ($translationElement->getName() === 'label') {
119 $parsedData[(string)$translationElement['index']][0] = [
120 $element => (string)$translationElement
121 ];
122 }
123 }
124 return $parsedData;
125 }
126
127 /**
128 * Returns array representation of XLIFF data, starting from a root node.
129 *
130 * @param \SimpleXMLElement $root A root node
131 * @return array An array representing parsed XLIFF
132 */
133 protected function doParsingFromRoot(\SimpleXMLElement $root)
134 {
135 return $this->doParsingFromRootForElement($root, 'source');
136 }
137
138 /**
139 * Returns array representation of XLIFF data, starting from a root node.
140 *
141 * @param \SimpleXMLElement $root A root node
142 * @return array An array representing parsed XLIFF
143 */
144 protected function doParsingTargetFromRoot(\SimpleXMLElement $root)
145 {
146 return $this->doParsingFromRootForElement($root, 'target');
147 }
148
149 /**
150 * Returns parsed representation of XML file.
151 *
152 * Parses XML if it wasn't done before. Caches parsed data.
153 *
154 * @param string $path An absolute path to XML file
155 * @return array Parsed XML file
156 */
157 public function getParsedTargetData($path)
158 {
159 if (!isset($this->parsedTargetFiles[$path])) {
160 $this->parsedTargetFiles[$path] = $this->parseXmlTargetFile($path);
161 }
162 return $this->parsedTargetFiles[$path];
163 }
164
165 /**
166 * Reads and parses XML file and returns internal representation of data.
167 *
168 * @param string $targetPath Path of the target file
169 * @return array
170 * @throws \TYPO3\CMS\Core\Localization\Exception\InvalidXmlFileException
171 */
172 protected function parseXmlTargetFile($targetPath)
173 {
174 $rootXmlNode = false;
175 if (file_exists($targetPath)) {
176 $xmlContent = file_get_contents($targetPath);
177 // Disables the functionality to allow external entities to be loaded when parsing the XML, must be kept
178 $previousValueOfEntityLoader = libxml_disable_entity_loader(true);
179 $rootXmlNode = simplexml_load_string($xmlContent, 'SimpleXMLElement', LIBXML_NOWARNING);
180 libxml_disable_entity_loader($previousValueOfEntityLoader);
181 }
182 if (!isset($rootXmlNode) || $rootXmlNode === false) {
183 $xmlError = libxml_get_last_error();
184 throw new InvalidXmlFileException(
185 'The path provided does not point to existing and accessible well-formed XML file. Reason: ' . $xmlError->message . ' in ' . $targetPath . ', line ' . $xmlError->line,
186 1278155987
187 );
188 }
189 return $this->doParsingTargetFromRoot($rootXmlNode);
190 }
191 }