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\Core\Environment;
19use TYPO3\CMS\Core\Localization\Exception\FileNotFoundException;
20use TYPO3\CMS\Core\Localization\Exception\InvalidXmlFileException;
21use TYPO3\CMS\Core\Utility\GeneralUtility;
22use TYPO3\CMS\Core\Utility\PathUtility;
23
24/**
25 * Abstract class for XML based parser.
26 * @internal This class is a concrete implementation and is not part of the TYPO3 Core API.
27 */
28abstract class AbstractXmlParser implements LocalizationParserInterface
29{
30    /**
31     * @var string
32     */
33    protected $sourcePath;
34
35    /**
36     * @var string
37     */
38    protected $languageKey;
39
40    /**
41     * New method for parsing xml files, not part of an interface as the plan is to replace
42     * the entire API for labels with something different in TYPO3 12
43     *
44     * @param string $sourcePath
45     * @param string $languageKey
46     * @param string $fileNamePattern
47     * @return array
48     */
49    public function parseExtensionResource(string $sourcePath, string $languageKey, string $fileNamePattern): array
50    {
51        $fileName = Environment::getLabelsPath() . sprintf($fileNamePattern, $languageKey);
52
53        return $this->_getParsedData($sourcePath, $languageKey, $fileName);
54    }
55
56    /**
57     * Returns parsed representation of XML file.
58     *
59     * @param string $sourcePath Source file path
60     * @param string $languageKey Language key
61     * @return array
62     * @throws \TYPO3\CMS\Core\Localization\Exception\FileNotFoundException
63     */
64    public function getParsedData($sourcePath, $languageKey)
65    {
66        return $this->_getParsedData($sourcePath, $languageKey, null);
67    }
68
69    /**
70     * Actually doing all the work of parsing an XML file
71     *
72     * @param string $sourcePath Source file path
73     * @param string $languageKey Language key
74     * @return array
75     * @throws \TYPO3\CMS\Core\Localization\Exception\FileNotFoundException
76     */
77    protected function _getParsedData($sourcePath, $languageKey, ?string $labelsPath)
78    {
79        $this->sourcePath = $sourcePath;
80        $this->languageKey = $languageKey;
81        if ($this->languageKey !== 'default') {
82            $this->sourcePath = $labelsPath ?? $this->getLocalizedFileName($this->sourcePath, $this->languageKey);
83            if (!@is_file($this->sourcePath)) {
84                // Global localization is not available, try split localization file
85                $this->sourcePath = $this->getLocalizedFileName($sourcePath, $languageKey, true);
86            }
87            if (!@is_file($this->sourcePath)) {
88                throw new FileNotFoundException('Localization file does not exist', 1306332397);
89            }
90        }
91        $LOCAL_LANG = [];
92        $LOCAL_LANG[$languageKey] = $this->parseXmlFile();
93        return $LOCAL_LANG;
94    }
95
96    /**
97     * Loads the current XML file before processing.
98     *
99     * @return array An array representing parsed XML file (structure depends on concrete parser)
100     * @throws \TYPO3\CMS\Core\Localization\Exception\InvalidXmlFileException
101     */
102    protected function parseXmlFile()
103    {
104        $xmlContent = file_get_contents($this->sourcePath);
105        if ($xmlContent === false) {
106            throw new InvalidXmlFileException(
107                'The path provided does not point to an existing and accessible file.',
108                1278155987
109            );
110        }
111        // Disables the functionality to allow external entities to be loaded when parsing the XML, must be kept
112        $previousValueOfEntityLoader = null;
113        if (PHP_MAJOR_VERSION < 8) {
114            $previousValueOfEntityLoader = libxml_disable_entity_loader(true);
115        }
116        $rootXmlNode = simplexml_load_string($xmlContent, \SimpleXMLElement::class, LIBXML_NOWARNING);
117        if (PHP_MAJOR_VERSION < 8) {
118            libxml_disable_entity_loader($previousValueOfEntityLoader);
119        }
120        if ($rootXmlNode === false) {
121            $xmlError = libxml_get_last_error();
122            throw new InvalidXmlFileException(
123                'The path provided does not point to an existing and accessible well-formed XML file. Reason: ' . $xmlError->message . ' in ' . $this->sourcePath . ', line ' . $xmlError->line,
124                1278155988
125            );
126        }
127        return $this->doParsingFromRoot($rootXmlNode);
128    }
129
130    /**
131     * Checks if a localized file is found in labels pack (e.g. a language pack was downloaded in the backend)
132     * or if $sameLocation is set, then checks for a file located in "{language}.locallang.xlf" at the same directory
133     *
134     * @param string $fileRef Absolute file reference to locallang file
135     * @param string $language Language key
136     * @param bool $sameLocation If TRUE, then locallang localization file name will be returned with same directory as $fileRef
137     * @return string Absolute path to the language file
138     */
139    protected function getLocalizedFileName(string $fileRef, string $language, bool $sameLocation = false)
140    {
141        // If $fileRef is already prefixed with "[language key]" then we should return it as is
142        $fileName = PathUtility::basename($fileRef);
143        if (str_starts_with($fileName, $language . '.')) {
144            return GeneralUtility::getFileAbsFileName($fileRef);
145        }
146
147        if ($sameLocation) {
148            return GeneralUtility::getFileAbsFileName(str_replace($fileName, $language . '.' . $fileName, $fileRef));
149        }
150
151        // Analyze file reference
152        if (str_starts_with($fileRef, Environment::getFrameworkBasePath() . '/')) {
153            // Is system
154            $validatedPrefix = Environment::getFrameworkBasePath() . '/';
155        } elseif (str_starts_with($fileRef, Environment::getBackendPath() . '/ext/')) {
156            // Is global
157            $validatedPrefix = Environment::getBackendPath() . '/ext/';
158        } elseif (str_starts_with($fileRef, Environment::getExtensionsPath() . '/')) {
159            // Is local
160            $validatedPrefix = Environment::getExtensionsPath() . '/';
161        } else {
162            $validatedPrefix = '';
163        }
164        if ($validatedPrefix) {
165            // Divide file reference into extension key, directory (if any) and base name:
166            [$extensionKey, $file_extPath] = explode('/', substr($fileRef, strlen($validatedPrefix)), 2);
167            $temp = GeneralUtility::revExplode('/', $file_extPath, 2);
168            if (count($temp) === 1) {
169                array_unshift($temp, '');
170            }
171            // Add empty first-entry if not there.
172            [$file_extPath, $file_fileName] = $temp;
173            // The filename is prefixed with "[language key]." because it prevents the llxmltranslate tool from detecting it.
174            return Environment::getLabelsPath() . '/' . $language . '/' . $extensionKey . '/' . ($file_extPath ? $file_extPath . '/' : '') . $language . '.' . $file_fileName;
175        }
176        return '';
177    }
178
179    /**
180     * Returns array representation of XML data, starting from a root node.
181     *
182     * @param \SimpleXMLElement $root A root node
183     * @return array An array representing the parsed XML file
184     */
185    abstract protected function doParsingFromRoot(\SimpleXMLElement $root);
186}
187