Skip to content
29 changes: 28 additions & 1 deletion src/Symfony/Component/Serializer/Encoder/XmlEncoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ class XmlEncoder implements EncoderInterface, DecoderInterface, NormalizationAwa
public const CDATA_WRAPPING_NAME_PATTERN = 'cdata_wrapping_name_pattern';
public const CDATA_WRAPPING_PATTERN = 'cdata_wrapping_pattern';
public const IGNORE_EMPTY_ATTRIBUTES = 'ignore_empty_attributes';
public const VALIDATE_ROOT_NODE_NAME = 'validate_root_node_name';
public const VALIDATE_ROOT_NODE_EXISTS = 'validate_root_node_exists';

private array $defaultContext = [
self::AS_COLLECTION => false,
Expand All @@ -76,6 +78,8 @@ class XmlEncoder implements EncoderInterface, DecoderInterface, NormalizationAwa
self::CDATA_WRAPPING_NAME_PATTERN => false,
self::CDATA_WRAPPING_PATTERN => '/[<>&]/',
self::IGNORE_EMPTY_ATTRIBUTES => false,
self::VALIDATE_ROOT_NODE_NAME => false,
self::VALIDATE_ROOT_NODE_EXISTS => false,
];

public function __construct(array $defaultContext = [])
Expand Down Expand Up @@ -140,7 +144,30 @@ public function decode(string $data, string $format, array $context = []): mixed
}
}

// todo: throw an exception if the root node name is not correctly configured (bc)
// Validate root node existence and, if configured by the user, its name
$shouldValidateRootExists = (bool) ($context[self::VALIDATE_ROOT_NODE_EXISTS] ?? $this->defaultContext[self::VALIDATE_ROOT_NODE_EXISTS] ?? false);
if (!$rootNode instanceof \DOMNode) {
if ($shouldValidateRootExists) {
throw new NotEncodableValueException('Invalid XML data: no root node found.');
}

trigger_deprecation('symfony/serializer', '7.4', 'Decoding XML without a root node is deprecated and will throw a NotEncodableValueException in 8.0.');

// Return empty result for backward compatibility
return [];
}

$shouldValidateRoot = (bool) ($context[self::VALIDATE_ROOT_NODE_NAME] ?? $this->defaultContext[self::VALIDATE_ROOT_NODE_NAME] ?? false);
if (\array_key_exists(self::ROOT_NODE_NAME, $context)) {
$expectedRootName = (string) $context[self::ROOT_NODE_NAME];
if ($expectedRootName !== $rootNode->nodeName) {
if ($shouldValidateRoot) {
throw new NotEncodableValueException(\sprintf('Expected root node "%s", but found "%s".', $expectedRootName, $rootNode->nodeName));
}

trigger_deprecation('symfony/serializer', '7.4', 'Decoding XML with a mismatching root node name is deprecated and will throw an exception in 8.0. Expected root node "%s", but found "%s". Set the "%s" context option to true to enable validation now.', $expectedRootName, $rootNode->nodeName, self::VALIDATE_ROOT_NODE_NAME);
}
}

if ($rootNode->hasChildNodes()) {
$data = $this->parseXml($rootNode, $context);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -772,6 +772,38 @@ public function testDecodeEmptyXml()
$this->encoder->decode(' ', 'xml');
}

public function testDecodeThrowsOnRootNodeNameMismatch()
{
$this->expectException(NotEncodableValueException::class);
$this->expectExceptionMessage('Expected root node "expectedRoot", but found "wrongRoot".');

$xml = '<?xml version="1.0"?><wrongRoot><item>value</item></wrongRoot>';
$this->encoder->decode($xml, 'xml', ['xml_root_node_name' => 'expectedRoot', XmlEncoder::VALIDATE_ROOT_NODE_NAME => true]);
}

public function testDecodeThrowsWhenNoRootNodeFound()
{
$this->expectException(NotEncodableValueException::class);
$this->expectExceptionMessage('Invalid XML data: no root node found.');

$xml = '<?xml version="1.0"?><response><a>1</a></response>';
$this->encoder->decode($xml, 'xml', [
XmlEncoder::DECODER_IGNORED_NODE_TYPES => [\XML_PI_NODE, \XML_COMMENT_NODE, \XML_ELEMENT_NODE],
XmlEncoder::VALIDATE_ROOT_NODE_EXISTS => true
]);
}

public function testDecodeReturnsEmptyArrayWhenNoRootNodeFoundAndValidationDisabled()
{
$xml = '<?xml version="1.0"?><response><a>1</a></response>';
$result = $this->encoder->decode($xml, 'xml', [
XmlEncoder::DECODER_IGNORED_NODE_TYPES => [\XML_PI_NODE, \XML_COMMENT_NODE, \XML_ELEMENT_NODE],
XmlEncoder::VALIDATE_ROOT_NODE_EXISTS => false
]);

$this->assertSame([], $result);
}

protected static function getXmlSource(): string
{
return '<?xml version="1.0"?>'."\n".
Expand Down
Loading