TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,250 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Serializer;
|
||||
|
||||
use TYPO3\CMS\Core\Serializer\Exception\InvalidDataException;
|
||||
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||
|
||||
/**
|
||||
* Decodes XML string to PHP array.
|
||||
*
|
||||
* A dedicated set of node attributes is considered during conversion:
|
||||
* - attribute "index" specifies the final node name which is used as key in the PHP array
|
||||
* - attribute "type" specifies the node value type which is used for casting
|
||||
* - attribute "base64" specifies the node value type being binary and requiring a
|
||||
* base64-decoding
|
||||
* These attributes were applied during encoding of the PHP array with XmlEncoder::encode().
|
||||
*
|
||||
* The node name "n{number}" is converted to a number-indexed array key "{number}".
|
||||
*
|
||||
* @internal still experimental
|
||||
*/
|
||||
readonly class Typo3XmlParser
|
||||
{
|
||||
/**
|
||||
* This method serves as a wrapper for decode() and is used to replace
|
||||
* GeneralUtility::xml2array(), which returns an exception as a string instead of throwing it.
|
||||
* In perspective, all uses of this method should be replaced by decode() and the exceptions
|
||||
* should be handled locally.
|
||||
*
|
||||
* @param string $xml XML string
|
||||
* @param Typo3XmlSerializerOptions|null $options Decoding configuration - see decode() for details
|
||||
* @return array|string PHP array - or a string if the XML root node is empty or an exception
|
||||
*/
|
||||
public function decodeWithReturningExceptionAsString(
|
||||
string $xml,
|
||||
?Typo3XmlSerializerOptions $options = null
|
||||
): array|string {
|
||||
try {
|
||||
return $this->decode($xml, $options);
|
||||
} catch (\Throwable $e) {
|
||||
return $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $xml XML string
|
||||
* @param Typo3XmlSerializerOptions|null $options Apply specific decoding configuration - Ignored node types, libxml2 options, ...
|
||||
* @return array|string PHP array - or a string if the XML root node is empty
|
||||
* @throws InvalidDataException
|
||||
*/
|
||||
public function decode(
|
||||
string $xml,
|
||||
?Typo3XmlSerializerOptions $options = null
|
||||
): array|string {
|
||||
$xml = trim($xml);
|
||||
if ($xml === '') {
|
||||
throw new InvalidDataException(
|
||||
'Invalid XML data, it can not be empty.',
|
||||
1630773210
|
||||
);
|
||||
}
|
||||
|
||||
$options = $options ?? new Typo3XmlSerializerOptions();
|
||||
|
||||
if ($options->allowUndefinedNamespaces()) {
|
||||
$xml = $this->disableNamespaceInNodeNames($xml);
|
||||
}
|
||||
|
||||
$internalErrors = libxml_use_internal_errors(true);
|
||||
libxml_clear_errors();
|
||||
|
||||
$dom = new \DOMDocument();
|
||||
$dom->loadXML($xml, $options->getLoadOptions());
|
||||
|
||||
libxml_use_internal_errors($internalErrors);
|
||||
|
||||
if ($error = libxml_get_last_error()) {
|
||||
libxml_clear_errors();
|
||||
throw new InvalidDataException(
|
||||
'Line ' . $error->line . ': ' . xml_error_string($error->code),
|
||||
1630773230
|
||||
);
|
||||
}
|
||||
|
||||
$rootNode = null;
|
||||
foreach ($dom->childNodes as $child) {
|
||||
if ($child->nodeType === \XML_DOCUMENT_TYPE_NODE) {
|
||||
throw new InvalidDataException(
|
||||
'Document types are not allowed.',
|
||||
1630773261
|
||||
);
|
||||
}
|
||||
if (in_array($child->nodeType, $options->getIgnoredNodeTypes(), true)) {
|
||||
continue;
|
||||
}
|
||||
$rootNode = $child;
|
||||
break;
|
||||
}
|
||||
if ($rootNode === null) {
|
||||
throw new InvalidDataException(
|
||||
'Root node cannot be determined.',
|
||||
1630773276
|
||||
);
|
||||
}
|
||||
|
||||
$rootNodeName = $rootNode->nodeName;
|
||||
if ($options->allowUndefinedNamespaces()) {
|
||||
$rootNodeName = $this->reactivateNamespaceInNodeName($rootNodeName);
|
||||
}
|
||||
if (!$rootNode->hasChildNodes()) {
|
||||
if ($options->includeRootNode()) {
|
||||
$result = [$rootNodeName => $rootNode->nodeValue];
|
||||
} else {
|
||||
$result = $rootNode->nodeValue;
|
||||
}
|
||||
} else {
|
||||
if ($options->includeRootNode()) {
|
||||
$result = [$rootNodeName => $this->parseXml($rootNode, $options)];
|
||||
} else {
|
||||
$result = $this->parseXml($rootNode, $options);
|
||||
}
|
||||
}
|
||||
if ($options->returnRootNodeName() && is_array($result)) {
|
||||
$result['_DOCUMENT_TAG'] = $rootNodeName;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* DOMDocument::loadXML() breaks if prefixes of undefined namespaces are used in node names:
|
||||
* Replace namespace divider ":" by temporary "___" string before parsing the XML.
|
||||
*/
|
||||
protected function disableNamespaceInNodeNames(string $value): string
|
||||
{
|
||||
return preg_replace(
|
||||
['#<([/]?)([[:alnum:]_-]*):([[:alnum:]_-]*)([ >]?)#'],
|
||||
['<$1$2___$3$4'],
|
||||
$value
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-insert the namespace divider ":" into all node names again after parsing the XML.
|
||||
*/
|
||||
protected function reactivateNamespaceInNodeNames(string $value): string
|
||||
{
|
||||
if (!str_contains($value, '___')) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
return preg_replace(
|
||||
['#<([/]?)([[:alnum:]_-]*)___([[:alnum:]_-]*)([ >]?)#'],
|
||||
['<$1$2:$3$4'],
|
||||
$value
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-insert the namespace divider ":" into single node name again after parsing the XML.
|
||||
*/
|
||||
protected function reactivateNamespaceInNodeName(string $value): string
|
||||
{
|
||||
return str_replace('___', ':', $value);
|
||||
}
|
||||
|
||||
protected function parseXml(\DOMNode $node, Typo3XmlSerializerOptions $options): array|string|null
|
||||
{
|
||||
if (!$node->hasChildNodes()) {
|
||||
return $node->nodeValue;
|
||||
}
|
||||
|
||||
if ($node->childNodes->length === 1
|
||||
&& in_array($node->firstChild->nodeType, [\XML_TEXT_NODE, \XML_CDATA_SECTION_NODE])
|
||||
) {
|
||||
$value = $node->firstChild->nodeValue;
|
||||
if ($options->allowUndefinedNamespaces()) {
|
||||
$value = $this->reactivateNamespaceInNodeNames($value);
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
$result = [];
|
||||
foreach ($node->childNodes as $child) {
|
||||
if (in_array($child->nodeType, $options->getIgnoredNodeTypes(), true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = $this->parseXml($child, $options);
|
||||
|
||||
if ($child instanceof \DOMElement && $child->hasAttribute('index')) {
|
||||
$key = $child->getAttribute('index');
|
||||
} else {
|
||||
$key = $child->nodeName;
|
||||
if ($options->allowUndefinedNamespaces()) {
|
||||
$key = $this->reactivateNamespaceInNodeName($key);
|
||||
}
|
||||
if ($options->hasNamespacePrefix()
|
||||
&& str_starts_with($key, $options->getNamespacePrefix())
|
||||
) {
|
||||
$key = substr($key, strlen($options->getNamespacePrefix()));
|
||||
}
|
||||
if (str_starts_with($key, 'n')
|
||||
&& MathUtility::canBeInterpretedAsInteger($index = substr($key, 1))
|
||||
) {
|
||||
$key = (int)$index;
|
||||
}
|
||||
}
|
||||
|
||||
if ($child instanceof \DOMElement && $child->hasAttribute('base64') && is_string($value)) {
|
||||
$value = base64_decode($value);
|
||||
} elseif ($child instanceof \DOMElement && $child->hasAttribute('type')) {
|
||||
switch ($child->getAttribute('type')) {
|
||||
case 'integer':
|
||||
$value = (int)$value;
|
||||
break;
|
||||
case 'double':
|
||||
$value = (float)$value;
|
||||
break;
|
||||
case 'boolean':
|
||||
$value = (bool)$value;
|
||||
break;
|
||||
case 'NULL':
|
||||
$value = null;
|
||||
break;
|
||||
case 'array':
|
||||
$value = is_array($value) ? $value : (empty(trim($value)) ? [] : (array)$value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
$result[$key] = $value;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user