XmlUtils.php 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  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 $element, bool $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. $raw = $value;
  210. $cast = (int) $value;
  211. return '0' == $value[0] ? octdec($value) : (((string) $raw === (string) $cast) ? $cast : $raw);
  212. case isset($value[1]) && '-' === $value[0] && ctype_digit(substr($value, 1)):
  213. $raw = $value;
  214. $cast = (int) $value;
  215. return '0' == $value[1] ? octdec($value) : (((string) $raw === (string) $cast) ? $cast : $raw);
  216. case 'true' === $lowercaseValue:
  217. return true;
  218. case 'false' === $lowercaseValue:
  219. return false;
  220. case isset($value[1]) && '0b' == $value[0].$value[1] && preg_match('/^0b[01]*$/', $value):
  221. return bindec($value);
  222. case is_numeric($value):
  223. return '0x' === $value[0].$value[1] ? hexdec($value) : (float) $value;
  224. case preg_match('/^0x[0-9a-f]++$/i', $value):
  225. return hexdec($value);
  226. case preg_match('/^[+-]?[0-9]+(\.[0-9]+)?$/', $value):
  227. return (float) $value;
  228. default:
  229. return $value;
  230. }
  231. }
  232. protected static function getXmlErrors(bool $internalErrors)
  233. {
  234. $errors = [];
  235. foreach (libxml_get_errors() as $error) {
  236. $errors[] = sprintf('[%s %s] %s (in %s - line %d, column %d)',
  237. \LIBXML_ERR_WARNING == $error->level ? 'WARNING' : 'ERROR',
  238. $error->code,
  239. trim($error->message),
  240. $error->file ?: 'n/a',
  241. $error->line,
  242. $error->column
  243. );
  244. }
  245. libxml_clear_errors();
  246. libxml_use_internal_errors($internalErrors);
  247. return $errors;
  248. }
  249. }