1<?php
2
3/*
4 * This file is part of the TYPO3 CMS project.
5 *
6 * It is free software; you can redistribute it and/or modify it under
7 * the terms of the GNU General Public License, either version 2
8 * of the License, or any later version.
9 *
10 * For the full copyright and license information, please read the
11 * LICENSE.txt file that was distributed with this source code.
12 *
13 * The TYPO3 project - inspiring people to share!
14 */
15
16namespace TYPO3\CMS\Core\Localization\Parser;
17
18use TYPO3\CMS\Core\Localization\Exception\InvalidXmlFileException;
19use TYPO3\CMS\Core\Utility\ArrayUtility;
20use TYPO3\CMS\Core\Utility\GeneralUtility;
21use TYPO3\CMS\Core\Utility\PathUtility;
22
23/**
24 * Parser for XML locallang file.
25 * @internal This class is a concrete implementation and is not part of the TYPO3 Core API.
26 * @deprecated since v10.1 and will be removed in TYPO3 v11
27 */
28class LocallangXmlParser extends AbstractXmlParser
29{
30    public function __construct()
31    {
32        trigger_error(__CLASS__ . ' has been marked as deprecated and will be removed in TYPO3 v11. Consider using xlf files instead.', E_USER_DEPRECATED);
33    }
34
35    /**
36     * Associative array of "filename => parsed data" pairs.
37     *
38     * @var array
39     */
40    protected $parsedTargetFiles;
41
42    /**
43     * Returns parsed representation of XML file.
44     *
45     * @param string $sourcePath Source file path
46     * @param string $languageKey Language key
47     * @return array
48     */
49    public function getParsedData($sourcePath, $languageKey)
50    {
51        $this->sourcePath = $sourcePath;
52        $this->languageKey = $languageKey;
53        // Parse source
54        $parsedSource = $this->parseXmlFile();
55        // Parse target
56        $localizedTargetPath = $this->getLocalizedFileName($this->sourcePath, $this->languageKey);
57        $targetPath = $this->languageKey !== 'default' && @is_file($localizedTargetPath) ? $localizedTargetPath : $this->sourcePath;
58        try {
59            $parsedTarget = $this->getParsedTargetData($targetPath);
60        } catch (InvalidXmlFileException $e) {
61            $parsedTarget = $this->getParsedTargetData($this->sourcePath);
62        }
63        $LOCAL_LANG = [];
64        $LOCAL_LANG[$languageKey] = $parsedSource;
65        ArrayUtility::mergeRecursiveWithOverrule($LOCAL_LANG[$languageKey], $parsedTarget);
66        return $LOCAL_LANG;
67    }
68
69    /**
70     * Returns array representation of XLIFF data, starting from a root node.
71     *
72     * @param \SimpleXMLElement $root XML root element
73     * @param string $element Target or Source
74     * @return array
75     * @throws InvalidXmlFileException
76     */
77    protected function doParsingFromRootForElement(\SimpleXMLElement $root, $element)
78    {
79        $bodyOfFileTag = $root->data->languageKey;
80        if ($bodyOfFileTag === null) {
81            throw new InvalidXmlFileException('Invalid locallang.xml language file "' . PathUtility::stripPathSitePrefix($this->sourcePath) . '"', 1487944884);
82        }
83
84        if ($element === 'source' || $this->languageKey === 'default') {
85            $parsedData = $this->getParsedDataForElement($bodyOfFileTag, $element);
86        } else {
87            $parsedData = [];
88        }
89        if ($element === 'target') {
90            // Check if the source llxml file contains localized records
91            $localizedBodyOfFileTag = $root->data->xpath('languageKey[@index=\'' . $this->languageKey . '\']');
92            if (isset($localizedBodyOfFileTag[0]) && $localizedBodyOfFileTag[0] instanceof \SimpleXMLElement) {
93                $parsedDataTarget = $this->getParsedDataForElement($localizedBodyOfFileTag[0], $element);
94                $mergedData = $parsedDataTarget + $parsedData;
95                if ($this->languageKey === 'default') {
96                    $parsedData = array_intersect_key($mergedData, $parsedData, $parsedDataTarget);
97                } else {
98                    $parsedData = array_intersect_key($mergedData, $parsedDataTarget);
99                }
100            }
101        }
102        return $parsedData;
103    }
104
105    /**
106     * Parse the given language key tag
107     *
108     * @param \SimpleXMLElement $bodyOfFileTag
109     * @param string $element
110     * @return array
111     */
112    protected function getParsedDataForElement(\SimpleXMLElement $bodyOfFileTag, $element)
113    {
114        $parsedData = [];
115        $children = $bodyOfFileTag->children();
116        if ($children->count() === 0) {
117            // Check for externally-referenced resource:
118            // <languageKey index="fr">EXT:yourext/path/to/localized/locallang.xml</languageKey>
119            $reference = sprintf('%s', $bodyOfFileTag);
120            if (substr($reference, -4) === '.xml') {
121                return $this->getParsedTargetData(GeneralUtility::getFileAbsFileName($reference));
122            }
123        }
124        /** @var \SimpleXMLElement $translationElement */
125        foreach ($children as $translationElement) {
126            if ($translationElement->getName() === 'label') {
127                $parsedData[(string)$translationElement['index']][0] = [
128                    $element => (string)$translationElement
129                ];
130            }
131        }
132        return $parsedData;
133    }
134
135    /**
136     * Returns array representation of XLIFF data, starting from a root node.
137     *
138     * @param \SimpleXMLElement $root A root node
139     * @return array An array representing parsed XLIFF
140     */
141    protected function doParsingFromRoot(\SimpleXMLElement $root)
142    {
143        return $this->doParsingFromRootForElement($root, 'source');
144    }
145
146    /**
147     * Returns array representation of XLIFF data, starting from a root node.
148     *
149     * @param \SimpleXMLElement $root A root node
150     * @return array An array representing parsed XLIFF
151     */
152    protected function doParsingTargetFromRoot(\SimpleXMLElement $root)
153    {
154        return $this->doParsingFromRootForElement($root, 'target');
155    }
156
157    /**
158     * Returns parsed representation of XML file.
159     *
160     * Parses XML if it wasn't done before. Caches parsed data.
161     *
162     * @param string $path An absolute path to XML file
163     * @return array Parsed XML file
164     */
165    public function getParsedTargetData($path)
166    {
167        if (!isset($this->parsedTargetFiles[$path])) {
168            $this->parsedTargetFiles[$path] = $this->parseXmlTargetFile($path);
169        }
170        return $this->parsedTargetFiles[$path];
171    }
172
173    /**
174     * Reads and parses XML file and returns internal representation of data.
175     *
176     * @param string $targetPath Path of the target file
177     * @return array
178     * @throws \TYPO3\CMS\Core\Localization\Exception\InvalidXmlFileException
179     */
180    protected function parseXmlTargetFile($targetPath)
181    {
182        $rootXmlNode = false;
183        if (file_exists($targetPath)) {
184            $xmlContent = file_get_contents($targetPath);
185            // Disables the functionality to allow external entities to be loaded when parsing the XML, must be kept
186            $previousValueOfEntityLoader = null;
187            if (PHP_MAJOR_VERSION < 8) {
188                $previousValueOfEntityLoader = libxml_disable_entity_loader(true);
189            }
190            $rootXmlNode = simplexml_load_string($xmlContent, \SimpleXMLElement::class, LIBXML_NOWARNING);
191            if (PHP_MAJOR_VERSION < 8) {
192                libxml_disable_entity_loader($previousValueOfEntityLoader);
193            }
194        }
195        if ($rootXmlNode === false) {
196            $xmlError = libxml_get_last_error();
197            throw new InvalidXmlFileException(
198                'The path provided does not point to existing and accessible well-formed XML file. Reason: ' . $xmlError->message . ' in ' . $targetPath . ', line ' . $xmlError->line,
199                1278155987
200            );
201        }
202        return $this->doParsingTargetFromRoot($rootXmlNode);
203    }
204}
205