2021-07-27 14:46:32 +02:00
< ? php
/*
* This file is part of the Symfony package .
*
* ( c ) Fabien Potencier < fabien @ symfony . com >
*
* For the full copyright and license information , please view the LICENSE
* file that was distributed with this source code .
*/
namespace Symfony\Component\Serializer\Encoder ;
use Symfony\Component\Serializer\Exception\BadMethodCallException ;
use Symfony\Component\Serializer\Exception\NotEncodableValueException ;
use Symfony\Component\Serializer\SerializerAwareInterface ;
use Symfony\Component\Serializer\SerializerAwareTrait ;
/**
* @ author Jordi Boggiano < j . boggiano @ seld . be >
* @ author John Wards < jwards @ whiteoctober . co . uk >
* @ author Fabian Vogler < fabian @ equivalence . ch >
* @ author Kévin Dunglas < dunglas @ gmail . com >
* @ author Dany Maillard < danymaillard93b @ gmail . com >
*/
class XmlEncoder implements EncoderInterface , DecoderInterface , NormalizationAwareInterface , SerializerAwareInterface
{
use SerializerAwareTrait ;
public const FORMAT = 'xml' ;
public const AS_COLLECTION = 'as_collection' ;
/**
* An array of ignored XML node types while decoding , each one of the DOM Predefined XML_ * constants .
*/
public const DECODER_IGNORED_NODE_TYPES = 'decoder_ignored_node_types' ;
/**
* An array of ignored XML node types while encoding , each one of the DOM Predefined XML_ * constants .
*/
public const ENCODER_IGNORED_NODE_TYPES = 'encoder_ignored_node_types' ;
public const ENCODING = 'xml_encoding' ;
public const FORMAT_OUTPUT = 'xml_format_output' ;
/**
* A bit field of LIBXML_ * constants .
*/
public const LOAD_OPTIONS = 'load_options' ;
public const REMOVE_EMPTY_TAGS = 'remove_empty_tags' ;
public const ROOT_NODE_NAME = 'xml_root_node_name' ;
public const STANDALONE = 'xml_standalone' ;
/** @deprecated The constant TYPE_CASE_ATTRIBUTES is deprecated since version 4.4 and will be removed in version 5. Use TYPE_CAST_ATTRIBUTES instead. */
public const TYPE_CASE_ATTRIBUTES = 'xml_type_cast_attributes' ;
public const TYPE_CAST_ATTRIBUTES = 'xml_type_cast_attributes' ;
public const VERSION = 'xml_version' ;
private $defaultContext = [
self :: AS_COLLECTION => false ,
self :: DECODER_IGNORED_NODE_TYPES => [ \XML_PI_NODE , \XML_COMMENT_NODE ],
self :: ENCODER_IGNORED_NODE_TYPES => [],
self :: LOAD_OPTIONS => \LIBXML_NONET | \LIBXML_NOBLANKS ,
self :: REMOVE_EMPTY_TAGS => false ,
self :: ROOT_NODE_NAME => 'response' ,
self :: TYPE_CAST_ATTRIBUTES => true ,
];
/**
* @ param array $defaultContext
*/
public function __construct ( $defaultContext = [], int $loadOptions = null , array $decoderIgnoredNodeTypes = [ \XML_PI_NODE , \XML_COMMENT_NODE ], array $encoderIgnoredNodeTypes = [])
{
if ( ! \is_array ( $defaultContext )) {
@ trigger_error ( 'Passing configuration options directly to the constructor is deprecated since Symfony 4.2, use the default context instead.' , \E_USER_DEPRECATED );
$defaultContext = [
self :: DECODER_IGNORED_NODE_TYPES => $decoderIgnoredNodeTypes ,
self :: ENCODER_IGNORED_NODE_TYPES => $encoderIgnoredNodeTypes ,
self :: LOAD_OPTIONS => $loadOptions ? ? \LIBXML_NONET | \LIBXML_NOBLANKS ,
self :: ROOT_NODE_NAME => ( string ) $defaultContext ,
];
}
$this -> defaultContext = array_merge ( $this -> defaultContext , $defaultContext );
}
/**
* { @ inheritdoc }
*/
public function encode ( $data , $format , array $context = [])
{
$encoderIgnoredNodeTypes = $context [ self :: ENCODER_IGNORED_NODE_TYPES ] ? ? $this -> defaultContext [ self :: ENCODER_IGNORED_NODE_TYPES ];
$ignorePiNode = \in_array ( \XML_PI_NODE , $encoderIgnoredNodeTypes , true );
if ( $data instanceof \DOMDocument ) {
return $data -> saveXML ( $ignorePiNode ? $data -> documentElement : null );
}
$xmlRootNodeName = $context [ self :: ROOT_NODE_NAME ] ? ? $this -> defaultContext [ self :: ROOT_NODE_NAME ];
2022-06-16 21:54:19 +02:00
$dom = $this -> createDomDocument ( $context );
2021-07-27 14:46:32 +02:00
2022-10-07 15:20:07 +02:00
if ( null !== $data && ! \is_scalar ( $data )) {
2022-06-16 21:54:19 +02:00
$root = $dom -> createElement ( $xmlRootNodeName );
$dom -> appendChild ( $root );
$this -> buildXml ( $root , $data , $format , $context , $xmlRootNodeName );
2021-07-27 14:46:32 +02:00
} else {
2022-06-16 21:54:19 +02:00
$this -> appendNode ( $dom , $data , $format , $context , $xmlRootNodeName );
2021-07-27 14:46:32 +02:00
}
2022-06-16 21:54:19 +02:00
return $dom -> saveXML ( $ignorePiNode ? $dom -> documentElement : null );
2021-07-27 14:46:32 +02:00
}
/**
* { @ inheritdoc }
*/
public function decode ( $data , $format , array $context = [])
{
if ( '' === trim ( $data )) {
throw new NotEncodableValueException ( 'Invalid XML data, it can not be empty.' );
}
$internalErrors = libxml_use_internal_errors ( true );
if ( \LIBXML_VERSION < 20900 ) {
$disableEntities = libxml_disable_entity_loader ( true );
}
libxml_clear_errors ();
$dom = new \DOMDocument ();
$dom -> loadXML ( $data , $context [ self :: LOAD_OPTIONS ] ? ? $this -> defaultContext [ self :: LOAD_OPTIONS ]);
libxml_use_internal_errors ( $internalErrors );
if ( \LIBXML_VERSION < 20900 ) {
libxml_disable_entity_loader ( $disableEntities );
}
if ( $error = libxml_get_last_error ()) {
libxml_clear_errors ();
throw new NotEncodableValueException ( $error -> message );
}
$rootNode = null ;
$decoderIgnoredNodeTypes = $context [ self :: DECODER_IGNORED_NODE_TYPES ] ? ? $this -> defaultContext [ self :: DECODER_IGNORED_NODE_TYPES ];
foreach ( $dom -> childNodes as $child ) {
2022-06-16 21:54:19 +02:00
if ( \in_array ( $child -> nodeType , $decoderIgnoredNodeTypes , true )) {
continue ;
}
2021-07-27 14:46:32 +02:00
if ( \XML_DOCUMENT_TYPE_NODE === $child -> nodeType ) {
throw new NotEncodableValueException ( 'Document types are not allowed.' );
}
2022-06-16 21:54:19 +02:00
if ( ! $rootNode ) {
2021-07-27 14:46:32 +02:00
$rootNode = $child ;
}
}
// todo: throw an exception if the root node name is not correctly configured (bc)
if ( $rootNode -> hasChildNodes ()) {
$xpath = new \DOMXPath ( $dom );
$data = [];
foreach ( $xpath -> query ( 'namespace::*' , $dom -> documentElement ) as $nsNode ) {
$data [ '@' . $nsNode -> nodeName ] = $nsNode -> nodeValue ;
}
unset ( $data [ '@xmlns:xml' ]);
if ( empty ( $data )) {
return $this -> parseXml ( $rootNode , $context );
}
return array_merge ( $data , ( array ) $this -> parseXml ( $rootNode , $context ));
}
if ( ! $rootNode -> hasAttributes ()) {
return $rootNode -> nodeValue ;
}
$data = [];
foreach ( $rootNode -> attributes as $attrKey => $attr ) {
$data [ '@' . $attrKey ] = $attr -> nodeValue ;
}
$data [ '#' ] = $rootNode -> nodeValue ;
return $data ;
}
/**
* { @ inheritdoc }
*/
public function supportsEncoding ( $format )
{
return self :: FORMAT === $format ;
}
/**
* { @ inheritdoc }
*/
public function supportsDecoding ( $format )
{
return self :: FORMAT === $format ;
}
/**
* Sets the root node name .
*
* @ deprecated since Symfony 4.2
*
* @ param string $name Root node name
*/
public function setRootNodeName ( $name )
{
@ trigger_error ( sprintf ( 'The "%s()" method is deprecated since Symfony 4.2, use the context instead.' , __METHOD__ ), \E_USER_DEPRECATED );
$this -> defaultContext [ self :: ROOT_NODE_NAME ] = $name ;
}
/**
* Returns the root node name .
*
* @ deprecated since Symfony 4.2
*
* @ return string
*/
public function getRootNodeName ()
{
@ trigger_error ( sprintf ( 'The "%s()" method is deprecated since Symfony 4.2, use the context instead.' , __METHOD__ ), \E_USER_DEPRECATED );
return $this -> defaultContext [ self :: ROOT_NODE_NAME ];
}
final protected function appendXMLString ( \DOMNode $node , string $val ) : bool
{
if ( '' !== $val ) {
2022-06-16 21:54:19 +02:00
$frag = $node -> ownerDocument -> createDocumentFragment ();
2021-07-27 14:46:32 +02:00
$frag -> appendXML ( $val );
$node -> appendChild ( $frag );
return true ;
}
return false ;
}
final protected function appendText ( \DOMNode $node , string $val ) : bool
{
2022-06-16 21:54:19 +02:00
$nodeText = $node -> ownerDocument -> createTextNode ( $val );
2021-07-27 14:46:32 +02:00
$node -> appendChild ( $nodeText );
return true ;
}
final protected function appendCData ( \DOMNode $node , string $val ) : bool
{
2022-06-16 21:54:19 +02:00
$nodeText = $node -> ownerDocument -> createCDATASection ( $val );
2021-07-27 14:46:32 +02:00
$node -> appendChild ( $nodeText );
return true ;
}
/**
* @ param \DOMDocumentFragment $fragment
*/
final protected function appendDocumentFragment ( \DOMNode $node , $fragment ) : bool
{
if ( $fragment instanceof \DOMDocumentFragment ) {
$node -> appendChild ( $fragment );
return true ;
}
return false ;
}
final protected function appendComment ( \DOMNode $node , string $data ) : bool
{
2022-06-16 21:54:19 +02:00
$node -> appendChild ( $node -> ownerDocument -> createComment ( $data ));
2021-07-27 14:46:32 +02:00
return true ;
}
/**
* Checks the name is a valid xml element name .
*/
final protected function isElementNameValid ( string $name ) : bool
{
return $name &&
2022-05-03 15:24:29 +02:00
! str_contains ( $name , ' ' ) &&
2021-07-27 14:46:32 +02:00
preg_match ( '#^[\pL_][\pL0-9._:-]*$#ui' , $name );
}
/**
* Parse the input DOMNode into an array or a string .
*
* @ return array | string
*/
private function parseXml ( \DOMNode $node , array $context = [])
{
$data = $this -> parseXmlAttributes ( $node , $context );
$value = $this -> parseXmlValue ( $node , $context );
if ( ! \count ( $data )) {
return $value ;
}
if ( ! \is_array ( $value )) {
$data [ '#' ] = $value ;
return $data ;
}
if ( 1 === \count ( $value ) && key ( $value )) {
$data [ key ( $value )] = current ( $value );
return $data ;
}
foreach ( $value as $key => $val ) {
$data [ $key ] = $val ;
}
return $data ;
}
/**
* Parse the input DOMNode attributes into an array .
*/
private function parseXmlAttributes ( \DOMNode $node , array $context = []) : array
{
if ( ! $node -> hasAttributes ()) {
return [];
}
$data = [];
$typeCastAttributes = ( bool ) ( $context [ self :: TYPE_CAST_ATTRIBUTES ] ? ? $this -> defaultContext [ self :: TYPE_CAST_ATTRIBUTES ]);
foreach ( $node -> attributes as $attr ) {
if ( ! is_numeric ( $attr -> nodeValue ) || ! $typeCastAttributes || ( isset ( $attr -> nodeValue [ 1 ]) && '0' === $attr -> nodeValue [ 0 ] && '.' !== $attr -> nodeValue [ 1 ])) {
$data [ '@' . $attr -> nodeName ] = $attr -> nodeValue ;
continue ;
}
if ( false !== $val = filter_var ( $attr -> nodeValue , \FILTER_VALIDATE_INT )) {
$data [ '@' . $attr -> nodeName ] = $val ;
continue ;
}
$data [ '@' . $attr -> nodeName ] = ( float ) $attr -> nodeValue ;
}
return $data ;
}
/**
* Parse the input DOMNode value ( content and children ) into an array or a string .
*
* @ return array | string
*/
private function parseXmlValue ( \DOMNode $node , array $context = [])
{
if ( ! $node -> hasChildNodes ()) {
return $node -> nodeValue ;
}
if ( 1 === $node -> childNodes -> length && \in_array ( $node -> firstChild -> nodeType , [ \XML_TEXT_NODE , \XML_CDATA_SECTION_NODE ])) {
return $node -> firstChild -> nodeValue ;
}
$value = [];
$decoderIgnoredNodeTypes = $context [ self :: DECODER_IGNORED_NODE_TYPES ] ? ? $this -> defaultContext [ self :: DECODER_IGNORED_NODE_TYPES ];
foreach ( $node -> childNodes as $subnode ) {
if ( \in_array ( $subnode -> nodeType , $decoderIgnoredNodeTypes , true )) {
continue ;
}
$val = $this -> parseXml ( $subnode , $context );
if ( 'item' === $subnode -> nodeName && isset ( $val [ '@key' ])) {
$value [ $val [ '@key' ]] = $val [ '#' ] ? ? $val ;
} else {
$value [ $subnode -> nodeName ][] = $val ;
}
}
$asCollection = $context [ self :: AS_COLLECTION ] ? ? $this -> defaultContext [ self :: AS_COLLECTION ];
foreach ( $value as $key => $val ) {
if ( ! $asCollection && \is_array ( $val ) && 1 === \count ( $val )) {
$value [ $key ] = current ( $val );
}
}
return $value ;
}
/**
* Parse the data and convert it to DOMElements .
*
* @ param array | object $data
*
* @ throws NotEncodableValueException
*/
2022-06-16 21:54:19 +02:00
private function buildXml ( \DOMNode $parentNode , $data , string $format , array $context , string $xmlRootNodeName = null ) : bool
2021-07-27 14:46:32 +02:00
{
$append = true ;
2022-06-16 21:54:19 +02:00
$removeEmptyTags = $context [ self :: REMOVE_EMPTY_TAGS ] ? ? $this -> defaultContext [ self :: REMOVE_EMPTY_TAGS ] ? ? false ;
$encoderIgnoredNodeTypes = $context [ self :: ENCODER_IGNORED_NODE_TYPES ] ? ? $this -> defaultContext [ self :: ENCODER_IGNORED_NODE_TYPES ];
2021-07-27 14:46:32 +02:00
2022-06-16 21:54:19 +02:00
if ( \is_array ( $data ) || ( $data instanceof \Traversable && ( null === $this -> serializer || ! $this -> serializer -> supportsNormalization ( $data , $format )))) {
2021-07-27 14:46:32 +02:00
foreach ( $data as $key => $data ) {
2022-10-07 15:20:07 +02:00
// Ah this is the magic @ attribute types.
2022-05-03 15:24:29 +02:00
if ( str_starts_with ( $key , '@' ) && $this -> isElementNameValid ( $attributeName = substr ( $key , 1 ))) {
2022-10-07 15:20:07 +02:00
if ( ! \is_scalar ( $data )) {
2022-06-16 21:54:19 +02:00
$data = $this -> serializer -> normalize ( $data , $format , $context );
2021-07-27 14:46:32 +02:00
}
2022-10-07 15:20:07 +02:00
if ( \is_bool ( $data )) {
$data = ( int ) $data ;
}
2021-07-27 14:46:32 +02:00
$parentNode -> setAttribute ( $attributeName , $data );
} elseif ( '#' === $key ) {
2022-06-16 21:54:19 +02:00
$append = $this -> selectNodeType ( $parentNode , $data , $format , $context );
2021-07-27 14:46:32 +02:00
} elseif ( '#comment' === $key ) {
if ( ! \in_array ( \XML_COMMENT_NODE , $encoderIgnoredNodeTypes , true )) {
$append = $this -> appendComment ( $parentNode , $data );
}
} elseif ( \is_array ( $data ) && false === is_numeric ( $key )) {
// Is this array fully numeric keys?
if ( ctype_digit ( implode ( '' , array_keys ( $data )))) {
/*
* Create nodes to append to $parentNode based on the $key of this array
* Produces < xml >< item > 0 </ item >< item > 1 </ item ></ xml >
* From [ " item " => [ 0 , 1 ]]; .
*/
foreach ( $data as $subData ) {
2022-06-16 21:54:19 +02:00
$append = $this -> appendNode ( $parentNode , $subData , $format , $context , $key );
2021-07-27 14:46:32 +02:00
}
} else {
2022-06-16 21:54:19 +02:00
$append = $this -> appendNode ( $parentNode , $data , $format , $context , $key );
2021-07-27 14:46:32 +02:00
}
} elseif ( is_numeric ( $key ) || ! $this -> isElementNameValid ( $key )) {
2022-06-16 21:54:19 +02:00
$append = $this -> appendNode ( $parentNode , $data , $format , $context , 'item' , $key );
2021-07-27 14:46:32 +02:00
} elseif ( null !== $data || ! $removeEmptyTags ) {
2022-06-16 21:54:19 +02:00
$append = $this -> appendNode ( $parentNode , $data , $format , $context , $key );
2021-07-27 14:46:32 +02:00
}
}
return $append ;
}
if ( \is_object ( $data )) {
if ( null === $this -> serializer ) {
throw new BadMethodCallException ( sprintf ( 'The serializer needs to be set to allow "%s()" to be used with object data.' , __METHOD__ ));
}
2022-06-16 21:54:19 +02:00
$data = $this -> serializer -> normalize ( $data , $format , $context );
2022-10-07 15:20:07 +02:00
if ( null !== $data && ! \is_scalar ( $data )) {
2022-06-16 21:54:19 +02:00
return $this -> buildXml ( $parentNode , $data , $format , $context , $xmlRootNodeName );
2021-07-27 14:46:32 +02:00
}
// top level data object was normalized into a scalar
if ( ! $parentNode -> parentNode -> parentNode ) {
$root = $parentNode -> parentNode ;
$root -> removeChild ( $parentNode );
2022-06-16 21:54:19 +02:00
return $this -> appendNode ( $root , $data , $format , $context , $xmlRootNodeName );
2021-07-27 14:46:32 +02:00
}
2022-06-16 21:54:19 +02:00
return $this -> appendNode ( $parentNode , $data , $format , $context , 'data' );
2021-07-27 14:46:32 +02:00
}
throw new NotEncodableValueException ( 'An unexpected value could not be serialized: ' . ( ! \is_resource ( $data ) ? var_export ( $data , true ) : sprintf ( '%s resource' , get_resource_type ( $data ))));
}
/**
* Selects the type of node to create and appends it to the parent .
*
* @ param array | object $data
*/
2022-06-16 21:54:19 +02:00
private function appendNode ( \DOMNode $parentNode , $data , string $format , array $context , string $nodeName , string $key = null ) : bool
2021-07-27 14:46:32 +02:00
{
2022-10-07 15:20:07 +02:00
$dom = $parentNode instanceof \DOMDocument ? $parentNode : $parentNode -> ownerDocument ;
2022-06-16 21:54:19 +02:00
$node = $dom -> createElement ( $nodeName );
2021-07-27 14:46:32 +02:00
if ( null !== $key ) {
$node -> setAttribute ( 'key' , $key );
}
2022-06-16 21:54:19 +02:00
$appendNode = $this -> selectNodeType ( $node , $data , $format , $context );
2021-07-27 14:46:32 +02:00
// we may have decided not to append this node, either in error or if its $nodeName is not valid
if ( $appendNode ) {
$parentNode -> appendChild ( $node );
}
return $appendNode ;
}
/**
* Checks if a value contains any characters which would require CDATA wrapping .
*/
private function needsCdataWrapping ( string $val ) : bool
{
2022-06-16 21:54:19 +02:00
return preg_match ( '/[<>&]/' , $val );
2021-07-27 14:46:32 +02:00
}
/**
* Tests the value being passed and decide what sort of element to create .
*
* @ throws NotEncodableValueException
*/
2022-06-16 21:54:19 +02:00
private function selectNodeType ( \DOMNode $node , $val , string $format , array $context ) : bool
2021-07-27 14:46:32 +02:00
{
if ( \is_array ( $val )) {
2022-06-16 21:54:19 +02:00
return $this -> buildXml ( $node , $val , $format , $context );
2021-07-27 14:46:32 +02:00
} elseif ( $val instanceof \SimpleXMLElement ) {
2022-06-16 21:54:19 +02:00
$child = $node -> ownerDocument -> importNode ( dom_import_simplexml ( $val ), true );
2021-07-27 14:46:32 +02:00
$node -> appendChild ( $child );
} elseif ( $val instanceof \Traversable ) {
2022-06-16 21:54:19 +02:00
$this -> buildXml ( $node , $val , $format , $context );
2021-07-27 14:46:32 +02:00
} elseif ( $val instanceof \DOMNode ) {
2022-06-16 21:54:19 +02:00
$child = $node -> ownerDocument -> importNode ( $val , true );
2021-07-27 14:46:32 +02:00
$node -> appendChild ( $child );
} elseif ( \is_object ( $val )) {
if ( null === $this -> serializer ) {
throw new BadMethodCallException ( sprintf ( 'The serializer needs to be set to allow "%s()" to be used with object data.' , __METHOD__ ));
}
2022-06-16 21:54:19 +02:00
return $this -> selectNodeType ( $node , $this -> serializer -> normalize ( $val , $format , $context ), $format , $context );
2021-07-27 14:46:32 +02:00
} elseif ( is_numeric ( $val )) {
return $this -> appendText ( $node , ( string ) $val );
} elseif ( \is_string ( $val ) && $this -> needsCdataWrapping ( $val )) {
return $this -> appendCData ( $node , $val );
} elseif ( \is_string ( $val )) {
return $this -> appendText ( $node , $val );
} elseif ( \is_bool ( $val )) {
return $this -> appendText ( $node , ( int ) $val );
}
return true ;
}
/**
* Create a DOM document , taking serializer options into account .
*/
private function createDomDocument ( array $context ) : \DOMDocument
{
$document = new \DOMDocument ();
// Set an attribute on the DOM document specifying, as part of the XML declaration,
$xmlOptions = [
// nicely formats output with indentation and extra space
self :: FORMAT_OUTPUT => 'formatOutput' ,
// the version number of the document
self :: VERSION => 'xmlVersion' ,
// the encoding of the document
self :: ENCODING => 'encoding' ,
// whether the document is standalone
self :: STANDALONE => 'xmlStandalone' ,
];
foreach ( $xmlOptions as $xmlOption => $documentProperty ) {
if ( $contextOption = $context [ $xmlOption ] ? ? $this -> defaultContext [ $xmlOption ] ? ? false ) {
$document -> $documentProperty = $contextOption ;
}
}
return $document ;
}
}