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