diff --git a/src/Symfony/Component/Serializer/Encoder/XmlEncoder.php b/src/Symfony/Component/Serializer/Encoder/XmlEncoder.php index 4c03057aca85d..3bf4b4ff9f26e 100644 --- a/src/Symfony/Component/Serializer/Encoder/XmlEncoder.php +++ b/src/Symfony/Component/Serializer/Encoder/XmlEncoder.php @@ -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, @@ -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 = []) @@ -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); diff --git a/src/Symfony/Component/Serializer/Tests/Encoder/XmlEncoderTest.php b/src/Symfony/Component/Serializer/Tests/Encoder/XmlEncoderTest.php index d154461bc435d..4216a32f5ce5c 100644 --- a/src/Symfony/Component/Serializer/Tests/Encoder/XmlEncoderTest.php +++ b/src/Symfony/Component/Serializer/Tests/Encoder/XmlEncoderTest.php @@ -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 = 'value'; + $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 = '1'; + $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 = '1'; + $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 ''."\n".