TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
<?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 Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use TYPO3\CMS\Core\Crypto\HashAlgo;
|
||||
use TYPO3\CMS\Core\Crypto\HashService;
|
||||
use TYPO3\CMS\Core\Exception\Crypto\InvalidHashStringException;
|
||||
use TYPO3\CMS\Core\Serializer\Exception\DeserializerException;
|
||||
|
||||
/**
|
||||
* @internal Only to be used by TYPO3 core
|
||||
*/
|
||||
#[Autoconfigure(public: true)]
|
||||
final readonly class AuthenticatedMessageDeserializer
|
||||
{
|
||||
private const HASH_ALGO = HashAlgo::SHA3_384;
|
||||
|
||||
public function __construct(
|
||||
private HashService $hashService,
|
||||
private DeserializationService $deserializationService,
|
||||
) {}
|
||||
|
||||
public function serialize(mixed $payload, string $additionalSecret): string
|
||||
{
|
||||
return $this->hashService->appendHmac(
|
||||
serialize($payload),
|
||||
$additionalSecret,
|
||||
self::HASH_ALGO
|
||||
);
|
||||
}
|
||||
|
||||
public function deserialize(string $payload, string $additionalSecret): mixed
|
||||
{
|
||||
try {
|
||||
$serialized = $this->hashService->validateAndStripHmac(
|
||||
$payload,
|
||||
$additionalSecret,
|
||||
self::HASH_ALGO
|
||||
);
|
||||
} catch (InvalidHashStringException $e) {
|
||||
$classNames = $this->deserializationService->parseClassNames($payload);
|
||||
// in case the payload does not contain any class names, continue with
|
||||
// a secure deserialization attempt, not allowing any class names
|
||||
if ($classNames === []) {
|
||||
return @unserialize($payload, ['allowed_classes' => false]);
|
||||
}
|
||||
throw new DeserializerException(
|
||||
'Authenticated Message Deserialization failed',
|
||||
1780317744,
|
||||
$e
|
||||
);
|
||||
}
|
||||
// explicitly allowing all classes here after successful HMAC validation
|
||||
/* @phpstan-ignore unserialize.allowedClasses.insecure (Integrity check already happens via HMAC validation) */
|
||||
return unserialize($serialized, ['allowed_classes' => true]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
<?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 Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend;
|
||||
use TYPO3\CMS\Core\Crypto\HashService;
|
||||
use TYPO3\CMS\Core\Security\BlockSerializationTrait;
|
||||
use TYPO3\CMS\Core\Serializer\Exception\DeserializerException;
|
||||
|
||||
/**
|
||||
* Deserializes a PHP-serialized payload while refusing any class that carries
|
||||
* a user-defined __destruct() or an exploitable __wakeup() (one not provided
|
||||
* solely by BlockSerializationTrait).
|
||||
*
|
||||
* The per-class deny/allow decision is made lazily via ReflectionClass at the
|
||||
* first encounter of each class name, then cached in cache:core so that
|
||||
* reflection is never repeated for the same class within a cache lifetime.
|
||||
*
|
||||
* Use this instead of a raw unserialize() call when the set of expected classes
|
||||
* is not known upfront but dangerous gadget classes must still be excluded.
|
||||
*
|
||||
* @internal Only to be used by TYPO3 core
|
||||
*/
|
||||
#[Autoconfigure(public: true)]
|
||||
final readonly class DenyListDeserializer
|
||||
{
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
private array $allowedClassNames;
|
||||
private \ReflectionMethod $blockSerializationWakeup;
|
||||
|
||||
public function __construct(
|
||||
#[Autowire(service: 'cache.core')]
|
||||
private PhpFrontend $cache,
|
||||
private HashService $hashService,
|
||||
private DeserializationService $deserializationService,
|
||||
) {
|
||||
$allowedClassNames = $GLOBALS['TYPO3_CONF_VARS']['SYS']['deserialization']['allowedClassNames'] ?? null;
|
||||
$this->allowedClassNames = is_array($allowedClassNames) ? $allowedClassNames : [];
|
||||
$this->blockSerializationWakeup = (new \ReflectionClass(BlockSerializationTrait::class))->getMethod('__wakeup');
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes $payload, throwing DeserializerException if any class name
|
||||
* found in the payload is a deserialization gadget, or if the payload is
|
||||
* syntactically malformed.
|
||||
*/
|
||||
public function deserialize(string $payload): mixed
|
||||
{
|
||||
$classNames = $this->deserializationService->parseClassNames($payload);
|
||||
foreach ($classNames as $className) {
|
||||
if ($this->shallClassBeDenied($className)) {
|
||||
throw new DeserializerException(
|
||||
'Denied class name "' . $className . '" found in payload',
|
||||
1778594101
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->deserializationService->deserialize($payload, $classNames ?: false);
|
||||
}
|
||||
|
||||
private function shallClassBeDenied(string $className): bool
|
||||
{
|
||||
if (in_array($className, $this->allowedClassNames, true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$cacheKey = 'DenyListDeserializer_' . hash('xxh128', $className);
|
||||
if ($this->cache->has($cacheKey)) {
|
||||
$entry = $this->cache->require($cacheKey);
|
||||
if (is_array($entry)
|
||||
&& isset($entry['denied'], $entry['hmac'])
|
||||
&& $this->hashService->validateHmac(
|
||||
$this->createHmacPayload($className, (bool)$entry['denied']),
|
||||
DenyListDeserializer::class,
|
||||
$entry['hmac']
|
||||
)
|
||||
) {
|
||||
return (bool)$entry['denied'];
|
||||
}
|
||||
// Tampered or stale entry — fall through to recompute
|
||||
}
|
||||
|
||||
$denied = $this->resolveClassDenyStatus($className);
|
||||
$hmac = $this->hashService->hmac($this->createHmacPayload($className, $denied), DenyListDeserializer::class);
|
||||
$this->cache->set($cacheKey, 'return ' . var_export(['denied' => $denied, 'hmac' => $hmac], true) . ';');
|
||||
return $denied;
|
||||
}
|
||||
|
||||
private function createHmacPayload(string $className, bool $denied): string
|
||||
{
|
||||
return $className . ':' . ($denied ? '1' : '0');
|
||||
}
|
||||
|
||||
private function resolveClassDenyStatus(string $className): bool
|
||||
{
|
||||
try {
|
||||
$rc = new \ReflectionClass($className);
|
||||
} catch (\ReflectionException) {
|
||||
// The class does not exist or cannot be reflected (and not instantiated).
|
||||
// Thus, the class is allowed, since it cannot be a gadget and would
|
||||
// result in a `__PHP_Incomplete_Class` during deserialization.
|
||||
return false;
|
||||
}
|
||||
if ($rc->isInterface() || $rc->isTrait()) {
|
||||
return false;
|
||||
}
|
||||
return $this->getUserDefinedMethod($rc, '__destruct') !== null
|
||||
|| $this->hasDeniableWakeupMethod($rc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the method when $methodName is declared in user-defined (non-internal) code
|
||||
* somewhere in the class hierarchy. This excludes methods like Exception::__wakeup()
|
||||
* that PHP declares internally and that are harmless for deserialization purposes.
|
||||
*/
|
||||
private function getUserDefinedMethod(\ReflectionClass $rc, string $methodName): ?\ReflectionMethod
|
||||
{
|
||||
if (!$rc->hasMethod($methodName)) {
|
||||
return null;
|
||||
}
|
||||
$method = $rc->getMethod($methodName);
|
||||
if ($method->getDeclaringClass()->isInternal()) {
|
||||
return null;
|
||||
}
|
||||
return $method;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when the class has a user-defined __wakeup() that is NOT
|
||||
* BlockSerializationTrait::__wakeup(). Classes whose only __wakeup comes
|
||||
* from BlockSerializationTrait are already protected against deserialization
|
||||
* (the trait throws unconditionally) and must not be treated as gadgets.
|
||||
*
|
||||
* Note: for trait methods getDeclaringClass() returns the using class, not the
|
||||
* trait — so the origin is identified by comparing the method's source file and line
|
||||
* against the trait's own __wakeup declaration.
|
||||
*/
|
||||
private function hasDeniableWakeupMethod(\ReflectionClass $rc): bool
|
||||
{
|
||||
$method = $this->getUserDefinedMethod($rc, '__wakeup');
|
||||
if ($method === null) {
|
||||
return false;
|
||||
}
|
||||
return $method->getFileName() !== $this->blockSerializationWakeup->getFileName()
|
||||
|| $method->getStartLine() !== $this->blockSerializationWakeup->getStartLine();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<?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\DeserializerException;
|
||||
|
||||
/**
|
||||
* Low-level utilities for PHP serialization format inspection.
|
||||
*
|
||||
* @internal Only to be used by TYPO3 core
|
||||
*/
|
||||
final readonly class DeserializationService
|
||||
{
|
||||
/**
|
||||
* Extracts all class names from a PHP-serialized payload, ignoring any
|
||||
* class-name tokens that appear inside serialized string values.
|
||||
*
|
||||
* Returns an empty array for payloads that contain no objects, and skips
|
||||
* any token whose declared byte-length does not match the actual class-name
|
||||
* length (malformed entries).
|
||||
*
|
||||
* @return list<class-string>
|
||||
*/
|
||||
public function parseClassNames(string $payload): array
|
||||
{
|
||||
// Build string ranges once upfront to avoid re-scanning the payload per class-name token
|
||||
$stringRanges = [];
|
||||
if (preg_match_all('/s:(\d+):"/', $payload, $stringMatches, PREG_OFFSET_CAPTURE)) {
|
||||
foreach ($stringMatches[0] as $i => $match) {
|
||||
$contentStart = $match[1] + strlen($match[0]);
|
||||
$stringRanges[] = [$contentStart, $contentStart + (int)$stringMatches[1][$i][0]];
|
||||
}
|
||||
}
|
||||
|
||||
$classNames = [];
|
||||
if (preg_match_all('/[CO]:(?P<length>\d+):"(?P<className>[^"]+)"/', $payload, $matches, PREG_OFFSET_CAPTURE)) {
|
||||
foreach ($matches['className'] as $i => $classNameMatch) {
|
||||
$className = $classNameMatch[0];
|
||||
$matchOffset = (int)$matches[0][$i][1];
|
||||
$declaredLength = (int)$matches['length'][$i][0];
|
||||
|
||||
if (strlen($className) !== $declaredLength) {
|
||||
continue;
|
||||
}
|
||||
if (in_array($className, $classNames, true)) {
|
||||
continue;
|
||||
}
|
||||
$insideString = false;
|
||||
foreach ($stringRanges as [$start, $end]) {
|
||||
if ($matchOffset >= $start && $matchOffset < $end) {
|
||||
$insideString = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$insideString) {
|
||||
$classNames[] = $className;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $classNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $payload
|
||||
* @param bool|list<class-string> $allowedClasses
|
||||
*/
|
||||
public function deserialize(string $payload, bool|array $allowedClasses = false): mixed
|
||||
{
|
||||
$result = @unserialize($payload, ['allowed_classes' => $allowedClasses]);
|
||||
if ($result === false) {
|
||||
if ($payload === serialize(false)) {
|
||||
// Do not throw an exception in case the serialized string is *actually* false
|
||||
// See https://www.php.net/manual/en/function.unserialize.php#refsect1-function.unserialize-notes
|
||||
return false;
|
||||
}
|
||||
$exceptionMessage = 'Syntax error in payload, unable to de-serialize';
|
||||
$lastError = error_get_last();
|
||||
if ($lastError !== null) {
|
||||
$exceptionMessage .= ': ' . $lastError['message'];
|
||||
}
|
||||
throw new DeserializerException($exceptionMessage, 1768212616);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?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\Exception;
|
||||
|
||||
use TYPO3\CMS\Core\Exception;
|
||||
|
||||
/**
|
||||
* Base exception for deserialization failures.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class DeserializerException extends Exception {}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?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\Exception;
|
||||
|
||||
use TYPO3\CMS\Core\Exception;
|
||||
|
||||
/**
|
||||
* An exception if something is wrong with the data to be encoded or decoded
|
||||
*/
|
||||
class InvalidDataException extends Exception {}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?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\DeserializerException;
|
||||
|
||||
/**
|
||||
* @internal Only to be used by TYPO3 core
|
||||
*/
|
||||
final readonly class PolymorphicDeserializer
|
||||
{
|
||||
public function __construct(
|
||||
private DeserializationService $deserializationService = new DeserializationService(),
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Validates the serialized payload by checking a static list of base classes or interfaces to be included in the
|
||||
* de-serialized output. If a non-allowed class is hit, the method throws an PolymorphicDeserializerException.
|
||||
* If the serialized payload is syntactically incorrect, PolymorphicDeserializerException is thrown as well.
|
||||
*
|
||||
* @param list<class-string> $allowedClasses
|
||||
* @throws DeserializerException
|
||||
*/
|
||||
public function deserialize(string $payload, array $allowedClasses): mixed
|
||||
{
|
||||
// When allowing inheritance, extract all class names from payload and validate them
|
||||
$classNames = $this->deserializationService->parseClassNames($payload);
|
||||
|
||||
foreach ($classNames as $className) {
|
||||
if (!$this->isInstanceOf($className, $allowedClasses)) {
|
||||
throw new DeserializerException('Invalid class name "' . $className . '" found in payload', 1767987405);
|
||||
}
|
||||
|
||||
// Add the class if it's a valid subclass of any allowed class
|
||||
if (!in_array($className, $allowedClasses, true)) {
|
||||
$allowedClasses[] = $className;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->deserializationService->deserialize($payload, $allowedClasses);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<class-string>
|
||||
* @deprecated use DeserializationService::parseClassNames instead; will be removed in v15
|
||||
*/
|
||||
public function parseClassNames(string $payload): array
|
||||
{
|
||||
return $this->deserializationService->parseClassNames($payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<class-string> $allowedClassNames
|
||||
*/
|
||||
private function isInstanceOf(string $className, array $allowedClassNames): bool
|
||||
{
|
||||
foreach ($allowedClassNames as $allowedClassName) {
|
||||
if (is_a($className, $allowedClassName, true) || is_subclass_of($className, $allowedClassName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* @internal still experimental
|
||||
*/
|
||||
class Typo3XmlParserOptions
|
||||
{
|
||||
public const FORMAT = 'format';
|
||||
public const FORMAT_INLINE = -1;
|
||||
public const FORMAT_PRETTY_WITH_TAB = 0;
|
||||
public const NAMESPACE_PREFIX = 'namespace_prefix';
|
||||
public const ROOT_NODE_NAME = 'root_node_name';
|
||||
|
||||
protected array $options = [
|
||||
// Format XML with
|
||||
// - "-1" is inline XML
|
||||
// - "0" is pretty XML with tabs
|
||||
// - "1...x" is pretty XML with x spaces.
|
||||
self::FORMAT => self::FORMAT_PRETTY_WITH_TAB,
|
||||
// This XML namespace is prepended to each XML node, for example "T3:".
|
||||
self::NAMESPACE_PREFIX => '',
|
||||
// Wrap the XML with a root node of that name or set it to '' to skip wrapping.
|
||||
self::ROOT_NODE_NAME => 'phparray',
|
||||
];
|
||||
|
||||
public function __construct(array $options = [])
|
||||
{
|
||||
$this->options = array_merge($this->options, $options);
|
||||
}
|
||||
|
||||
public function getRootNodeName(): string
|
||||
{
|
||||
return $this->options[self::ROOT_NODE_NAME];
|
||||
}
|
||||
|
||||
public function getNewlineChar(): string
|
||||
{
|
||||
return $this->options[self::FORMAT] === self::FORMAT_INLINE ? '' : LF;
|
||||
}
|
||||
|
||||
public function getIndentationStep(): string
|
||||
{
|
||||
return match ($this->options[self::FORMAT]) {
|
||||
self::FORMAT_INLINE => '',
|
||||
self::FORMAT_PRETTY_WITH_TAB => "\t",
|
||||
default => str_repeat(' ', max(0, $this->options[self::FORMAT])),
|
||||
};
|
||||
}
|
||||
|
||||
public function getNamespacePrefix(): string
|
||||
{
|
||||
return $this->options[self::NAMESPACE_PREFIX];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
<?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\Utility\MathUtility;
|
||||
|
||||
/**
|
||||
* Encodes PHP array to XML string.
|
||||
*
|
||||
* A dedicated set of entry properties is stored in XML during conversion:
|
||||
* - XML node attribute "index" stores original entry key if XML node name differs from entry
|
||||
* key
|
||||
* - XML node attribute "type" stores entry value type ("bool", "int", "double", ...)
|
||||
* - XML node attribute "base64" specifies if entry value is binary (for example an image)
|
||||
* These attributes are interpreted during decoding of the XML string with XmlDecoder::decode().
|
||||
*
|
||||
* Specific encoding configuration can be set by $additionalOptions - for the full array or array paths.
|
||||
* For example
|
||||
* ```php
|
||||
* $input = [
|
||||
* 'numeric' => [
|
||||
* 'value1',
|
||||
* 'value2'
|
||||
* ],
|
||||
* 'numeric-n-index' => [
|
||||
* 'value1',
|
||||
* 'value2'
|
||||
* ],
|
||||
* 'nested' => [
|
||||
* 'node1' => 'value1',
|
||||
* 'node2' => [
|
||||
* 'node' => 'value'
|
||||
* ]
|
||||
* ]
|
||||
* ];
|
||||
* $additionalOptions = [
|
||||
* 'useIndexTagForNum' => 'numbered-index'
|
||||
* 'alt_options' => [
|
||||
* '/numeric-n-index' => [
|
||||
* 'useNindex' => true
|
||||
* ],
|
||||
* '/nested' => [
|
||||
* 'useIndexTagForAssoc' => 'nested-outer',
|
||||
* 'clearStackPath' => true,
|
||||
* 'alt_options' => [
|
||||
* '/nested-outer' => [
|
||||
* 'useIndexTagForAssoc' => 'nested-inner'
|
||||
* ]
|
||||
* ]
|
||||
* ]
|
||||
* ]
|
||||
* ];
|
||||
* ```
|
||||
* =>
|
||||
* ```xml
|
||||
* <phparray>
|
||||
* <numeric type="array">
|
||||
* <numbered-index index="0">value1</numbered-index>
|
||||
* <numbered-index index="1">value2</numbered-index>
|
||||
* </numeric>
|
||||
* <numeric-n-index type="array">
|
||||
* <n0>value1</n0>
|
||||
* <n1>value2</n1>
|
||||
* </numeric-n-index>
|
||||
* <nested type="array">
|
||||
* <nested-outer index="node1">value1</nested-outer>
|
||||
* <nested-outer index="node2" type="array">
|
||||
* <nested-inner index="node">value</nested-inner>
|
||||
* </nested-outer>
|
||||
* </nested>
|
||||
* </phparray>
|
||||
* ```
|
||||
* Available options are:
|
||||
* - grandParentTagMap[grandParentTagName/parentTagName] [string]
|
||||
* Convert array key X to XML node name "{grandParentTagMap}" with node attribute "index=X"
|
||||
* - if grand-parent is "{grandParentTagName}" and parent node is "{parentTagName}".
|
||||
* - parentTagMap[parentTagName:_IS_NUM] [string]
|
||||
* Convert array key X to XML node name "{parentTagMap}" with node attribute "index=X"
|
||||
* - if parent node is "{parentTagName}" and current node is number-indexed.
|
||||
* - parentTagMap[parentTagName:nodeName] [string]
|
||||
* Convert array key X to XML node name "{parentTagMap}" with node attribute "index=X"
|
||||
* - if parent node is "{parentTagName}" and current node is "{nodeName}".
|
||||
* - parentTagMap[parentTagName] [string]
|
||||
* Convert array key X to XML node name "{parentTagMap}" with node attribute "index=X"
|
||||
* - if parent node is "{parentTagName}".
|
||||
* - useNindex [bool]
|
||||
* Convert number-indexed array key X to XML node name "nX".
|
||||
* - useIndexTagForNum [string]
|
||||
* Convert number-indexed array key X to XML node name "{useIndexTagForNum}" with node
|
||||
* attribute "index=X".
|
||||
* - useIndexTagForAssoc [string]
|
||||
* Convert associative array key X to XML node name "{useIndexTagForAssoc}" with node
|
||||
* attribute "index=X".
|
||||
* - disableTypeAttrib [bool|int]
|
||||
* Disable node attribute "type" for all value types
|
||||
* (true = disable for all except arrays, 2 = disable for all).
|
||||
* - alt_options[/.../nodeName] [array]
|
||||
* Set new options for specific array path.
|
||||
* - clearStackPath [bool]
|
||||
* Resetting internal counter when descending the array hierarchy: Allows using relative
|
||||
* array path in nested "alt_options" instead of absolute path.
|
||||
*
|
||||
* @internal still experimental
|
||||
*/
|
||||
readonly class Typo3XmlSerializer
|
||||
{
|
||||
/**
|
||||
* This method serves as a wrapper for encode() and is used to replace
|
||||
* GeneralUtility::array2xml(), which returns an exception as a string instead of throwing it.
|
||||
* In perspective, all uses of this method should be replaced by encode() and the exceptions
|
||||
* should be handled locally.
|
||||
*
|
||||
* @param array $input PHP array
|
||||
* @param Typo3XmlParserOptions|null $options Encoding configuration - see encode() for details
|
||||
* @param array $additionalOptions Encoding options - see encode() for details
|
||||
* @return string XML or exception
|
||||
*/
|
||||
public function encodeWithReturningExceptionAsString(
|
||||
array $input,
|
||||
?Typo3XmlParserOptions $options = null,
|
||||
array $additionalOptions = []
|
||||
): string {
|
||||
try {
|
||||
return $this->encode($input, $options, $additionalOptions);
|
||||
} catch (\Throwable $e) {
|
||||
return $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $input PHP array
|
||||
* @param Typo3XmlParserOptions|null $options Apply specific encoding configuration - XML format, namespace prefix and root node name
|
||||
* @param array $additionalOptions Apply specific encoding options - for the full array or specific array paths.
|
||||
* @return string XML string
|
||||
*/
|
||||
public function encode(
|
||||
array $input,
|
||||
?Typo3XmlParserOptions $options = null,
|
||||
array $additionalOptions = []
|
||||
): string {
|
||||
$options = $options ?? new Typo3XmlParserOptions();
|
||||
return $this->parseArray(
|
||||
$input,
|
||||
$options,
|
||||
$additionalOptions
|
||||
);
|
||||
}
|
||||
|
||||
protected function parseArray(
|
||||
array $input,
|
||||
Typo3XmlParserOptions $options,
|
||||
array $additionalOptions,
|
||||
int $level = 0,
|
||||
array $stackData = []
|
||||
): string {
|
||||
$xml = '';
|
||||
|
||||
$rootNodeName = $options->getRootNodeName();
|
||||
if (empty($rootNodeName)) {
|
||||
$indentation = str_repeat($options->getIndentationStep(), $level);
|
||||
} else {
|
||||
$indentation = str_repeat($options->getIndentationStep(), $level + 1);
|
||||
}
|
||||
|
||||
foreach ($input as $key => $value) {
|
||||
// Construct the node name + attributes
|
||||
$nodeName = $key = (string)$key;
|
||||
$nodeAttributes = '';
|
||||
if (isset(
|
||||
$stackData['grandParentTagName'],
|
||||
$stackData['parentTagName'],
|
||||
$additionalOptions['grandParentTagMap'][$stackData['grandParentTagName'] . '/' . $stackData['parentTagName']]
|
||||
)) {
|
||||
// ... based on grand-parent + parent node name
|
||||
$nodeName = (string)$additionalOptions['grandParentTagMap'][$stackData['grandParentTagName'] . '/' . $stackData['parentTagName']];
|
||||
$nodeAttributes = ' index="' . htmlspecialchars($key) . '"';
|
||||
} elseif (isset(
|
||||
$stackData['parentTagName'],
|
||||
$additionalOptions['parentTagMap'][$stackData['parentTagName'] . ':_IS_NUM']
|
||||
) && MathUtility::canBeInterpretedAsInteger($nodeName)
|
||||
) {
|
||||
// ... based on parent node name + if current node name is numeric
|
||||
$nodeName = (string)$additionalOptions['parentTagMap'][$stackData['parentTagName'] . ':_IS_NUM'];
|
||||
$nodeAttributes = ' index="' . htmlspecialchars($key) . '"';
|
||||
} elseif (isset(
|
||||
$stackData['parentTagName'],
|
||||
$additionalOptions['parentTagMap'][$stackData['parentTagName'] . ':' . $nodeName]
|
||||
)) {
|
||||
// ... based on parent node name + current node name
|
||||
$nodeName = (string)$additionalOptions['parentTagMap'][$stackData['parentTagName'] . ':' . $nodeName];
|
||||
$nodeAttributes = ' index="' . htmlspecialchars($key) . '"';
|
||||
} elseif (isset(
|
||||
$stackData['parentTagName'],
|
||||
$additionalOptions['parentTagMap'][$stackData['parentTagName']]
|
||||
)) {
|
||||
// ... based on parent node name
|
||||
$nodeName = (string)$additionalOptions['parentTagMap'][$stackData['parentTagName']];
|
||||
$nodeAttributes = ' index="' . htmlspecialchars($key) . '"';
|
||||
} elseif (MathUtility::canBeInterpretedAsInteger($nodeName)) {
|
||||
// ... if current node name is numeric
|
||||
if ($additionalOptions['useNindex'] ?? false) {
|
||||
$nodeName = 'n' . $nodeName;
|
||||
} else {
|
||||
$nodeName = ($additionalOptions['useIndexTagForNum'] ?? false) ?: 'numIndex';
|
||||
$nodeAttributes = ' index="' . $key . '"';
|
||||
}
|
||||
} elseif (!empty($additionalOptions['useIndexTagForAssoc'])) {
|
||||
// ... if current node name is string
|
||||
$nodeName = $additionalOptions['useIndexTagForAssoc'];
|
||||
$nodeAttributes = ' index="' . htmlspecialchars($key) . '"';
|
||||
}
|
||||
$nodeName = $this->cleanUpNodeName($nodeName);
|
||||
|
||||
// Construct the node value
|
||||
if (is_array($value)) {
|
||||
// ... if has sub elements
|
||||
if (isset($additionalOptions['alt_options'])
|
||||
&& ($additionalOptions['alt_options'][($stackData['path'] ?? '') . '/' . $nodeName] ?? false)
|
||||
) {
|
||||
$subOptions = $additionalOptions['alt_options'][($stackData['path'] ?? '') . '/' . $nodeName];
|
||||
$clearStackPath = (bool)($subOptions['clearStackPath'] ?? false);
|
||||
} else {
|
||||
$subOptions = $additionalOptions;
|
||||
$clearStackPath = false;
|
||||
}
|
||||
if (empty($value)) {
|
||||
$nodeValue = '';
|
||||
} else {
|
||||
$nodeValue = $options->getNewlineChar();
|
||||
$nodeValue .= $this->parseArray(
|
||||
$value,
|
||||
$options,
|
||||
$subOptions,
|
||||
$level + 1,
|
||||
[
|
||||
'parentTagName' => $nodeName,
|
||||
'grandParentTagName' => $stackData['parentTagName'] ?? '',
|
||||
'path' => $clearStackPath ? '' : ($stackData['path'] ?? '') . '/' . $nodeName,
|
||||
]
|
||||
);
|
||||
$nodeValue .= $indentation;
|
||||
}
|
||||
// Dropping the "type=array" attribute makes the XML prettier, but means that empty
|
||||
// arrays are not restored with XmlDecoder::decode().
|
||||
if (($additionalOptions['disableTypeAttrib'] ?? false) !== 2) {
|
||||
$nodeAttributes .= ' type="array"';
|
||||
}
|
||||
} else {
|
||||
// ... if is simple value
|
||||
if ($this->isBinaryValue($value)) {
|
||||
$nodeValue = $options->getNewlineChar() . chunk_split(base64_encode($value));
|
||||
$nodeAttributes .= ' base64="1"';
|
||||
} else {
|
||||
$type = gettype($value);
|
||||
if ($type === 'string') {
|
||||
$nodeValue = htmlspecialchars($value);
|
||||
} else {
|
||||
$nodeValue = $value;
|
||||
if (($additionalOptions['disableTypeAttrib'] ?? false) === false) {
|
||||
$nodeAttributes .= ' type="' . $type . '"';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Construct the node
|
||||
if ($nodeName !== '') {
|
||||
$xml .= $indentation;
|
||||
$xml .= '<' . $options->getNamespacePrefix() . $nodeName . $nodeAttributes . '>';
|
||||
$xml .= $nodeValue;
|
||||
$xml .= '</' . $options->getNamespacePrefix() . $nodeName . '>';
|
||||
$xml .= $options->getNewlineChar();
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap with the root node if it is on the outermost level.
|
||||
if ($level === 0 && !empty($rootNodeName)) {
|
||||
$xml = '<' . $rootNodeName . '>' . $options->getNewlineChar() . $xml . '</' . $rootNodeName . '>';
|
||||
}
|
||||
|
||||
return $xml;
|
||||
}
|
||||
|
||||
/**
|
||||
* The node name is cleaned so that it contains only alphanumeric characters (plus - and _) and
|
||||
* is no longer than 100 characters.
|
||||
*
|
||||
* @param string $nodeName
|
||||
* @return string Cleaned node name
|
||||
*/
|
||||
protected function cleanUpNodeName(string $nodeName): string
|
||||
{
|
||||
return substr((string)preg_replace('/[^[:alnum:]_-]/', '', $nodeName), 0, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is $value the content of a binary file, for example an image? If so, this value must be
|
||||
* stored in a binary-safe manner so that it can be decoded correctly later.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return bool
|
||||
*/
|
||||
protected function isBinaryValue(mixed $value): bool
|
||||
{
|
||||
if (!is_string($value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$binaryChars = "\0" . chr(1) . chr(2) . chr(3) . chr(4) . chr(5)
|
||||
. chr(6) . chr(7) . chr(8) . chr(11) . chr(12)
|
||||
. chr(14) . chr(15) . chr(16) . chr(17) . chr(18)
|
||||
. chr(19) . chr(20) . chr(21) . chr(22) . chr(23)
|
||||
. chr(24) . chr(25) . chr(26) . chr(27) . chr(28)
|
||||
. chr(29) . chr(30) . chr(31);
|
||||
|
||||
$length = strlen($value);
|
||||
|
||||
return $length && strcspn($value, $binaryChars) !== $length;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* @internal still experimental
|
||||
*/
|
||||
class Typo3XmlSerializerOptions
|
||||
{
|
||||
public const INCLUDE_ROOT_NODE = 'include_root_node';
|
||||
public const IGNORED_NODE_TYPES = 'ignored_node_types';
|
||||
public const LOAD_OPTIONS = 'load_options';
|
||||
public const NAMESPACE_PREFIX = 'namespace_prefix';
|
||||
public const ALLOW_UNDEFINED_NAMESPACES = 'allow_undefined_namespaces';
|
||||
public const RETURN_ROOT_NODE_NAME = 'return_root_node_name';
|
||||
|
||||
protected array $options = [
|
||||
// Ignore XML node types when converting to a PHP array.
|
||||
self::IGNORED_NODE_TYPES => [\XML_PI_NODE, \XML_COMMENT_NODE],
|
||||
// Use the XML root node or its children as the first level of the PHP array.
|
||||
self::INCLUDE_ROOT_NODE => false,
|
||||
// Apply these libxml2 options when loading the XML.
|
||||
self::LOAD_OPTIONS => \LIBXML_NONET | \LIBXML_NOBLANKS,
|
||||
// Remove this XML namespace from each XML node, for example "T3:".
|
||||
self::NAMESPACE_PREFIX => '',
|
||||
// Gracefully handle missing namespace declarations, for example <T3:T3FlexForms> without xmlns attribute.
|
||||
self::ALLOW_UNDEFINED_NAMESPACES => false,
|
||||
// Append the name of the XML root node to the PHP array key "_DOCUMENT_TAG".
|
||||
self::RETURN_ROOT_NODE_NAME => false,
|
||||
];
|
||||
|
||||
public function __construct(array $options = [])
|
||||
{
|
||||
$this->options = array_merge($this->options, $options);
|
||||
}
|
||||
public function getLoadOptions(): int
|
||||
{
|
||||
return $this->options[self::LOAD_OPTIONS];
|
||||
}
|
||||
public function getIgnoredNodeTypes(): array
|
||||
{
|
||||
return $this->options[self::IGNORED_NODE_TYPES];
|
||||
}
|
||||
public function includeRootNode(): bool
|
||||
{
|
||||
return $this->options[self::INCLUDE_ROOT_NODE];
|
||||
}
|
||||
public function hasNamespacePrefix(): bool
|
||||
{
|
||||
return $this->options[self::NAMESPACE_PREFIX] !== '';
|
||||
}
|
||||
public function getNamespacePrefix(): string
|
||||
{
|
||||
return $this->options[self::NAMESPACE_PREFIX];
|
||||
}
|
||||
public function allowUndefinedNamespaces(): bool
|
||||
{
|
||||
return $this->options[self::ALLOW_UNDEFINED_NAMESPACES];
|
||||
}
|
||||
public function returnRootNodeName(): bool
|
||||
{
|
||||
return $this->options[self::RETURN_ROOT_NODE_NAME];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user