<?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 RectorPrefix202211\Symfony\Component\Config\Util;
use RectorPrefix202211\Symfony\Component\Config\Util\Exception\InvalidXmlException;
use RectorPrefix202211\Symfony\Component\Config\Util\Exception\XmlParsingException;
/**
* XMLUtils is a bunch of utility methods to XML operations.
*
* This class contains static methods only and is not meant to be instantiated.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Martin Hasoň <martin.hason@gmail.com>
* @author Ole Rößner <ole@roessner.it>
*/
class XmlUtils
{
/**
* This class should not be instantiated.
*/
private function __construct()
{
}
/**
* Parses an XML string.
*
* @param string $content An XML string
* @param string|callable $schemaOrCallable An XSD schema file path, a callable, or null to disable validation
*
* @throws XmlParsingException When parsing of XML file returns error
* @throws InvalidXmlException When parsing of XML with schema or callable produces any errors unrelated to the XML parsing itself
* @throws \RuntimeException When DOM extension is missing
*/
public static function parse(string $content, $schemaOrCallable = null) : \DOMDocument
{
if (!\extension_loaded('dom')) {
throw new \LogicException('Extension DOM is required.');
}
$internalErrors = \libxml_use_internal_errors(\true);
\libxml_clear_errors();
$dom = new \DOMDocument();
$dom->validateOnParse = \true;
if (!$dom->loadXML($content, \LIBXML_NONET | \LIBXML_COMPACT)) {
throw new XmlParsingException(\implode("\n", static::getXmlErrors($internalErrors)));
}
$dom->normalizeDocument();
\libxml_use_internal_errors($internalErrors);
foreach ($dom->childNodes as $child) {
if (\XML_DOCUMENT_TYPE_NODE === $child->nodeType) {
throw new XmlParsingException('Document types are not allowed.');
}
}
if (null !== $schemaOrCallable) {
$internalErrors = \libxml_use_internal_errors(\true);
\libxml_clear_errors();
$e = null;
if (\is_callable($schemaOrCallable)) {
try {
$valid = $schemaOrCallable($dom, $internalErrors);
} catch (\Exception $e) {
$valid = \false;
}
} elseif (\is_file($schemaOrCallable)) {
$schemaSource = \file_get_contents((string) $schemaOrCallable);
$valid = @$dom->schemaValidateSource($schemaSource);
} else {
\libxml_use_internal_errors($internalErrors);
throw new XmlParsingException(\sprintf('Invalid XSD file: "%s".', $schemaOrCallable));
}
if (!$valid) {
$messages = static::getXmlErrors($internalErrors);
if (empty($messages)) {
throw new InvalidXmlException('The XML is not valid.', 0, $e);
}
throw new XmlParsingException(\implode("\n", $messages), 0, $e);
}
}
\libxml_clear_errors();
\libxml_use_internal_errors($internalErrors);
return $dom;
}
/**
* Loads an XML file.
*
* @param string $file An XML file path
* @param string|callable $schemaOrCallable An XSD schema file path, a callable, or null to disable validation
*
* @throws \InvalidArgumentException When loading of XML file returns error
* @throws XmlParsingException When XML parsing returns any errors
* @throws \RuntimeException When DOM extension is missing
*/
public static function loadFile(string $file, $schemaOrCallable = null) : \DOMDocument
{
if (!\is_file($file)) {
throw new \InvalidArgumentException(\sprintf('Resource "%s" is not a file.', $file));
}
if (!\is_readable($file)) {
throw new \InvalidArgumentException(\sprintf('File "%s" is not readable.', $file));
}
$content = @\file_get_contents($file);
if ('' === \trim($content)) {
throw new \InvalidArgumentException(\sprintf('File "%s" does not contain valid XML, it is empty.', $file));
}
try {
return static::parse($content, $schemaOrCallable);
} catch (InvalidXmlException $e) {
throw new XmlParsingException(\sprintf('The XML file "%s" is not valid.', $file), 0, $e->getPrevious());
}
}
/**
* Converts a \DOMElement object to a PHP array.
*
* The following rules applies during the conversion:
*
* * Each tag is converted to a key value or an array
* if there is more than one "value"
*
* * The content of a tag is set under a "value" key (<foo>bar</foo>)
* if the tag also has some nested tags
*
* * The attributes are converted to keys (<foo foo="bar"/>)
*
* * The nested-tags are converted to keys (<foo><foo>bar</foo></foo>)
*
* @param \DOMElement $element A \DOMElement instance
* @param bool $checkPrefix Check prefix in an element or an attribute name
* @return mixed
*/
public static function convertDomElementToArray(\DOMElement $element, bool $checkPrefix = \true)
{
$prefix = (string) $element->prefix;
$empty = \true;
$config = [];
foreach ($element->attributes as $name => $node) {
if ($checkPrefix && !\in_array((string) $node->prefix, ['', $prefix], \true)) {
continue;
}
$config[$name] = static::phpize($node->value);
$empty = \false;
}
$nodeValue = \false;
foreach ($element->childNodes as $node) {
if ($node instanceof \DOMText) {
if ('' !== \trim($node->nodeValue)) {
$nodeValue = \trim($node->nodeValue);
$empty = \false;
}
} elseif ($checkPrefix && $prefix != (string) $node->prefix) {
continue;
} elseif (!$node instanceof \DOMComment) {
$value = static::convertDomElementToArray($node, $checkPrefix);
$key = $node->localName;
if (isset($config[$key])) {
if (!\is_array($config[$key]) || !\is_int(\key($config[$key]))) {
$config[$key] = [$config[$key]];
}
$config[$key][] = $value;
} else {
$config[$key] = $value;
}
$empty = \false;
}
}
if (\false !== $nodeValue) {
$value = static::phpize($nodeValue);
if (\count($config)) {
$config['value'] = $value;
} else {
$config = $value;
}
}
return !$empty ? $config : null;
}
/**
* Converts an xml value to a PHP type.
* @param string|\Stringable $value
* @return mixed
*/
public static function phpize($value)
{
$value = (string) $value;
$lowercaseValue = \strtolower($value);
switch (\true) {
case 'null' === $lowercaseValue:
return null;
case \ctype_digit($value):
case isset($value[1]) && '-' === $value[0] && \ctype_digit(\substr($value, 1)):
$raw = $value;
$cast = (int) $value;
return self::isOctal($value) ? \intval($value, 8) : ($raw === (string) $cast ? $cast : $raw);
case 'true' === $lowercaseValue:
return \true;
case 'false' === $lowercaseValue:
return \false;
case isset($value[1]) && '0b' == $value[0] . $value[1] && \preg_match('/^0b[01]*$/', $value):
return \bindec($value);
case \is_numeric($value):
return '0x' === $value[0] . $value[1] ? \hexdec($value) : (float) $value;
case \preg_match('/^0x[0-9a-f]++$/i', $value):
return \hexdec($value);
case \preg_match('/^[+-]?[0-9]+(\\.[0-9]+)?$/', $value):
return (float) $value;
default:
return $value;
}
}
protected static function getXmlErrors(bool $internalErrors)
{
$errors = [];
foreach (\libxml_get_errors() as $error) {
$errors[] = \sprintf('[%s %s] %s (in %s - line %d, column %d)', \LIBXML_ERR_WARNING == $error->level ? 'WARNING' : 'ERROR', $error->code, \trim($error->message), $error->file ?: 'n/a', $error->line, $error->column);
}
\libxml_clear_errors();
\libxml_use_internal_errors($internalErrors);
return $errors;
}
private static function isOctal(string $str) : bool
{
if ('-' === $str[0]) {
$str = \substr($str, 1);
}
return $str === '0' . \decoct(\intval($str, 8));
}
}