You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
289 lines
9.7 KiB
289 lines
9.7 KiB
<?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\Config\Util; |
|
|
|
use Symfony\Component\Config\Util\Exception\InvalidXmlException; |
|
use 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|null $schemaOrCallable An XSD schema file path, a callable, or null to disable validation |
|
* |
|
* @return \DOMDocument |
|
* |
|
* @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) |
|
{ |
|
if (!\extension_loaded('dom')) { |
|
throw new \LogicException('Extension DOM is required.'); |
|
} |
|
|
|
$internalErrors = libxml_use_internal_errors(true); |
|
if (\LIBXML_VERSION < 20900) { |
|
$disableEntities = libxml_disable_entity_loader(true); |
|
} |
|
libxml_clear_errors(); |
|
|
|
$dom = new \DOMDocument(); |
|
$dom->validateOnParse = true; |
|
if (!$dom->loadXML($content, \LIBXML_NONET | (\defined('LIBXML_COMPACT') ? \LIBXML_COMPACT : 0))) { |
|
if (\LIBXML_VERSION < 20900) { |
|
libxml_disable_entity_loader($disableEntities); |
|
} |
|
|
|
throw new XmlParsingException(implode("\n", static::getXmlErrors($internalErrors))); |
|
} |
|
|
|
$dom->normalizeDocument(); |
|
|
|
libxml_use_internal_errors($internalErrors); |
|
if (\LIBXML_VERSION < 20900) { |
|
libxml_disable_entity_loader($disableEntities); |
|
} |
|
|
|
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_array($schemaOrCallable) && is_file((string) $schemaOrCallable)) { |
|
$schemaSource = file_get_contents((string) $schemaOrCallable); |
|
$valid = @$dom->schemaValidateSource($schemaSource); |
|
} else { |
|
libxml_use_internal_errors($internalErrors); |
|
|
|
throw new XmlParsingException('The schemaOrCallable argument has to be a valid path to XSD file or callable.'); |
|
} |
|
|
|
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|null $schemaOrCallable An XSD schema file path, a callable, or null to disable validation |
|
* |
|
* @return \DOMDocument |
|
* |
|
* @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) |
|
{ |
|
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 mixed $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)); |
|
} |
|
}
|
|
|