vendor/symfony/config/Util/XmlUtils.php line 89

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of the Symfony package.
  4.  *
  5.  * (c) Fabien Potencier <fabien@symfony.com>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace Symfony\Component\Config\Util;
  11. use Symfony\Component\Config\Util\Exception\InvalidXmlException;
  12. use Symfony\Component\Config\Util\Exception\XmlParsingException;
  13. /**
  14.  * XMLUtils is a bunch of utility methods to XML operations.
  15.  *
  16.  * This class contains static methods only and is not meant to be instantiated.
  17.  *
  18.  * @author Fabien Potencier <fabien@symfony.com>
  19.  * @author Martin Hasoň <martin.hason@gmail.com>
  20.  * @author Ole Rößner <ole@roessner.it>
  21.  */
  22. class XmlUtils
  23. {
  24.     /**
  25.      * This class should not be instantiated.
  26.      */
  27.     private function __construct()
  28.     {
  29.     }
  30.     /**
  31.      * Parses an XML string.
  32.      *
  33.      * @param string               $content          An XML string
  34.      * @param string|callable|null $schemaOrCallable An XSD schema file path, a callable, or null to disable validation
  35.      *
  36.      * @return \DOMDocument
  37.      *
  38.      * @throws XmlParsingException When parsing of XML file returns error
  39.      * @throws InvalidXmlException When parsing of XML with schema or callable produces any errors unrelated to the XML parsing itself
  40.      * @throws \RuntimeException   When DOM extension is missing
  41.      */
  42.     public static function parse(string $content$schemaOrCallable null)
  43.     {
  44.         if (!\extension_loaded('dom')) {
  45.             throw new \LogicException('Extension DOM is required.');
  46.         }
  47.         $internalErrors libxml_use_internal_errors(true);
  48.         if (\LIBXML_VERSION 20900) {
  49.             $disableEntities libxml_disable_entity_loader(true);
  50.         }
  51.         libxml_clear_errors();
  52.         $dom = new \DOMDocument();
  53.         $dom->validateOnParse true;
  54.         if (!$dom->loadXML($content\LIBXML_NONET | (\defined('LIBXML_COMPACT') ? \LIBXML_COMPACT 0))) {
  55.             if (\LIBXML_VERSION 20900) {
  56.                 libxml_disable_entity_loader($disableEntities);
  57.             }
  58.             throw new XmlParsingException(implode("\n", static::getXmlErrors($internalErrors)));
  59.         }
  60.         $dom->normalizeDocument();
  61.         libxml_use_internal_errors($internalErrors);
  62.         if (\LIBXML_VERSION 20900) {
  63.             libxml_disable_entity_loader($disableEntities);
  64.         }
  65.         foreach ($dom->childNodes as $child) {
  66.             if (\XML_DOCUMENT_TYPE_NODE === $child->nodeType) {
  67.                 throw new XmlParsingException('Document types are not allowed.');
  68.             }
  69.         }
  70.         if (null !== $schemaOrCallable) {
  71.             $internalErrors libxml_use_internal_errors(true);
  72.             libxml_clear_errors();
  73.             $e null;
  74.             if (\is_callable($schemaOrCallable)) {
  75.                 try {
  76.                     $valid $schemaOrCallable($dom$internalErrors);
  77.                 } catch (\Exception $e) {
  78.                     $valid false;
  79.                 }
  80.             } elseif (!\is_array($schemaOrCallable) && is_file((string) $schemaOrCallable)) {
  81.                 $schemaSource file_get_contents((string) $schemaOrCallable);
  82.                 $valid = @$dom->schemaValidateSource($schemaSource);
  83.             } else {
  84.                 libxml_use_internal_errors($internalErrors);
  85.                 throw new XmlParsingException('The schemaOrCallable argument has to be a valid path to XSD file or callable.');
  86.             }
  87.             if (!$valid) {
  88.                 $messages = static::getXmlErrors($internalErrors);
  89.                 if (empty($messages)) {
  90.                     throw new InvalidXmlException('The XML is not valid.'0$e);
  91.                 }
  92.                 throw new XmlParsingException(implode("\n"$messages), 0$e);
  93.             }
  94.         }
  95.         libxml_clear_errors();
  96.         libxml_use_internal_errors($internalErrors);
  97.         return $dom;
  98.     }
  99.     /**
  100.      * Loads an XML file.
  101.      *
  102.      * @param string               $file             An XML file path
  103.      * @param string|callable|null $schemaOrCallable An XSD schema file path, a callable, or null to disable validation
  104.      *
  105.      * @return \DOMDocument
  106.      *
  107.      * @throws \InvalidArgumentException When loading of XML file returns error
  108.      * @throws XmlParsingException       When XML parsing returns any errors
  109.      * @throws \RuntimeException         When DOM extension is missing
  110.      */
  111.     public static function loadFile(string $file$schemaOrCallable null)
  112.     {
  113.         if (!is_file($file)) {
  114.             throw new \InvalidArgumentException(sprintf('Resource "%s" is not a file.'$file));
  115.         }
  116.         if (!is_readable($file)) {
  117.             throw new \InvalidArgumentException(sprintf('File "%s" is not readable.'$file));
  118.         }
  119.         $content = @file_get_contents($file);
  120.         if ('' === trim($content)) {
  121.             throw new \InvalidArgumentException(sprintf('File "%s" does not contain valid XML, it is empty.'$file));
  122.         }
  123.         try {
  124.             return static::parse($content$schemaOrCallable);
  125.         } catch (InvalidXmlException $e) {
  126.             throw new XmlParsingException(sprintf('The XML file "%s" is not valid.'$file), 0$e->getPrevious());
  127.         }
  128.     }
  129.     /**
  130.      * Converts a \DOMElement object to a PHP array.
  131.      *
  132.      * The following rules applies during the conversion:
  133.      *
  134.      *  * Each tag is converted to a key value or an array
  135.      *    if there is more than one "value"
  136.      *
  137.      *  * The content of a tag is set under a "value" key (<foo>bar</foo>)
  138.      *    if the tag also has some nested tags
  139.      *
  140.      *  * The attributes are converted to keys (<foo foo="bar"/>)
  141.      *
  142.      *  * The nested-tags are converted to keys (<foo><foo>bar</foo></foo>)
  143.      *
  144.      * @param \DOMElement $element     A \DOMElement instance
  145.      * @param bool        $checkPrefix Check prefix in an element or an attribute name
  146.      *
  147.      * @return mixed
  148.      */
  149.     public static function convertDomElementToArray(\DOMElement $elementbool $checkPrefix true)
  150.     {
  151.         $prefix = (string) $element->prefix;
  152.         $empty true;
  153.         $config = [];
  154.         foreach ($element->attributes as $name => $node) {
  155.             if ($checkPrefix && !\in_array((string) $node->prefix, [''$prefix], true)) {
  156.                 continue;
  157.             }
  158.             $config[$name] = static::phpize($node->value);
  159.             $empty false;
  160.         }
  161.         $nodeValue false;
  162.         foreach ($element->childNodes as $node) {
  163.             if ($node instanceof \DOMText) {
  164.                 if ('' !== trim($node->nodeValue)) {
  165.                     $nodeValue trim($node->nodeValue);
  166.                     $empty false;
  167.                 }
  168.             } elseif ($checkPrefix && $prefix != (string) $node->prefix) {
  169.                 continue;
  170.             } elseif (!$node instanceof \DOMComment) {
  171.                 $value = static::convertDomElementToArray($node$checkPrefix);
  172.                 $key $node->localName;
  173.                 if (isset($config[$key])) {
  174.                     if (!\is_array($config[$key]) || !\is_int(key($config[$key]))) {
  175.                         $config[$key] = [$config[$key]];
  176.                     }
  177.                     $config[$key][] = $value;
  178.                 } else {
  179.                     $config[$key] = $value;
  180.                 }
  181.                 $empty false;
  182.             }
  183.         }
  184.         if (false !== $nodeValue) {
  185.             $value = static::phpize($nodeValue);
  186.             if (\count($config)) {
  187.                 $config['value'] = $value;
  188.             } else {
  189.                 $config $value;
  190.             }
  191.         }
  192.         return !$empty $config null;
  193.     }
  194.     /**
  195.      * Converts an xml value to a PHP type.
  196.      *
  197.      * @param mixed $value
  198.      *
  199.      * @return mixed
  200.      */
  201.     public static function phpize($value)
  202.     {
  203.         $value = (string) $value;
  204.         $lowercaseValue strtolower($value);
  205.         switch (true) {
  206.             case 'null' === $lowercaseValue:
  207.                 return null;
  208.             case ctype_digit($value):
  209.             case isset($value[1]) && '-' === $value[0] && ctype_digit(substr($value1)):
  210.                 $raw $value;
  211.                 $cast = (int) $value;
  212.                 return self::isOctal($value) ? \intval($value8) : (($raw === (string) $cast) ? $cast $raw);
  213.             case 'true' === $lowercaseValue:
  214.                 return true;
  215.             case 'false' === $lowercaseValue:
  216.                 return false;
  217.             case isset($value[1]) && '0b' == $value[0].$value[1] && preg_match('/^0b[01]*$/'$value):
  218.                 return bindec($value);
  219.             case is_numeric($value):
  220.                 return '0x' === $value[0].$value[1] ? hexdec($value) : (float) $value;
  221.             case preg_match('/^0x[0-9a-f]++$/i'$value):
  222.                 return hexdec($value);
  223.             case preg_match('/^[+-]?[0-9]+(\.[0-9]+)?$/'$value):
  224.                 return (float) $value;
  225.             default:
  226.                 return $value;
  227.         }
  228.     }
  229.     protected static function getXmlErrors(bool $internalErrors)
  230.     {
  231.         $errors = [];
  232.         foreach (libxml_get_errors() as $error) {
  233.             $errors[] = sprintf('[%s %s] %s (in %s - line %d, column %d)',
  234.                 \LIBXML_ERR_WARNING == $error->level 'WARNING' 'ERROR',
  235.                 $error->code,
  236.                 trim($error->message),
  237.                 $error->file ?: 'n/a',
  238.                 $error->line,
  239.                 $error->column
  240.             );
  241.         }
  242.         libxml_clear_errors();
  243.         libxml_use_internal_errors($internalErrors);
  244.         return $errors;
  245.     }
  246.     private static function isOctal(string $str): bool
  247.     {
  248.         if ('-' === $str[0]) {
  249.             $str substr($str1);
  250.         }
  251.         return $str === '0'.decoct(\intval($str8));
  252.     }
  253. }