diff --git a/src/Symfony/Component/Config/CHANGELOG.md b/src/Symfony/Component/Config/CHANGELOG.md index 8426abd57b..63c244078f 100644 --- a/src/Symfony/Component/Config/CHANGELOG.md +++ b/src/Symfony/Component/Config/CHANGELOG.md @@ -6,6 +6,7 @@ CHANGELOG * added numerical type handling for config definitions * added convenience methods for optional configuration sections to ArrayNodeDefinition + * added a utils class for XML manipulations 2.1.0 ----- diff --git a/src/Symfony/Component/Config/Tests/Fixtures/Util/document_type.xml b/src/Symfony/Component/Config/Tests/Fixtures/Util/document_type.xml new file mode 100644 index 0000000000..4c25228263 --- /dev/null +++ b/src/Symfony/Component/Config/Tests/Fixtures/Util/document_type.xml @@ -0,0 +1,3 @@ + +]> + diff --git a/src/Symfony/Component/Config/Tests/Fixtures/Util/invalid.xml b/src/Symfony/Component/Config/Tests/Fixtures/Util/invalid.xml new file mode 100644 index 0000000000..a07af9fd85 --- /dev/null +++ b/src/Symfony/Component/Config/Tests/Fixtures/Util/invalid.xml @@ -0,0 +1,2 @@ + + diff --git a/src/Symfony/Component/Config/Tests/Fixtures/Util/invalid_schema.xml b/src/Symfony/Component/Config/Tests/Fixtures/Util/invalid_schema.xml new file mode 100644 index 0000000000..e2725a2c2a --- /dev/null +++ b/src/Symfony/Component/Config/Tests/Fixtures/Util/invalid_schema.xml @@ -0,0 +1,2 @@ + + diff --git a/src/Symfony/Component/Config/Tests/Fixtures/Util/schema.xsd b/src/Symfony/Component/Config/Tests/Fixtures/Util/schema.xsd new file mode 100644 index 0000000000..e56820f691 --- /dev/null +++ b/src/Symfony/Component/Config/Tests/Fixtures/Util/schema.xsd @@ -0,0 +1,9 @@ + + + + + + diff --git a/src/Symfony/Component/Config/Tests/Fixtures/Util/valid.xml b/src/Symfony/Component/Config/Tests/Fixtures/Util/valid.xml new file mode 100644 index 0000000000..a96bb38267 --- /dev/null +++ b/src/Symfony/Component/Config/Tests/Fixtures/Util/valid.xml @@ -0,0 +1,3 @@ + + + diff --git a/src/Symfony/Component/Config/Tests/Util/XmlUtilsTest.php b/src/Symfony/Component/Config/Tests/Util/XmlUtilsTest.php new file mode 100644 index 0000000000..0ad0b19576 --- /dev/null +++ b/src/Symfony/Component/Config/Tests/Util/XmlUtilsTest.php @@ -0,0 +1,124 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Tests\Loader; + +use Symfony\Component\Config\Util\XmlUtils; + +class XmlUtilsTest extends \PHPUnit_Framework_TestCase +{ + public function testLoadFile() + { + $fixtures = __DIR__.'/../Fixtures/Util/'; + + try { + XmlUtils::loadFile($fixtures.'invalid.xml'); + $this->fail(); + } catch (\InvalidArgumentException $e) { + $this->assertContains('ERROR 77', $e->getMessage()); + } + + try { + XmlUtils::loadFile($fixtures.'document_type.xml'); + $this->fail(); + } catch (\InvalidArgumentException $e) { + $this->assertContains('Document types are not allowed', $e->getMessage()); + } + + try { + XmlUtils::loadFile($fixtures.'invalid_schema.xml', $fixtures.'schema.xsd'); + $this->fail(); + } catch (\InvalidArgumentException $e) { + $this->assertContains('ERROR 1845', $e->getMessage()); + } + + try { + XmlUtils::loadFile($fixtures.'invalid_schema.xml', 'invalid_callback_or_file'); + $this->fail(); + } catch (\InvalidArgumentException $e) { + $this->assertContains('XSD file or callable', $e->getMessage()); + } + + $mock = $this->getMock(__NAMESPACE__.'\Validator'); + $mock->expects($this->exactly(2))->method('validate')->will($this->onConsecutiveCalls(false, true)); + + try { + XmlUtils::loadFile($fixtures.'valid.xml', array($mock, 'validate')); + $this->fail(); + } catch (\InvalidArgumentException $e) { + $this->assertContains('is not valid', $e->getMessage()); + } + + $this->assertInstanceOf('DOMDocument', XmlUtils::loadFile($fixtures.'valid.xml', array($mock, 'validate'))); + } + + /** + * @dataProvider getDataForConvertDomToArray + */ + public function testConvertDomToArray($expected, $xml) + { + $dom = new \DOMDocument(); + $dom->loadXML(''.$xml.''); + + $this->assertSame($expected, XmlUtils::convertDomElementToArray($dom->documentElement)); + } + + public function getDataForConvertDomToArray() + { + return array( + array(null, ''), + array(array('foo' => null), ''), + array(array('foo' => 'bar'), 'bar'), + array(array('foo' => array('foo' => 'bar')), ''), + array(array('foo' => array('foo' => 'bar')), 'bar'), + array(array('foo' => array('foo' => 'bar', 'value' => 'text')), 'text'), + array(array('foo' => array('attr' => 'bar', 'foo' => 'text')), 'text'), + array(array('foo' => array('bar', 'text')), 'bartext'), + array(array('foo' => array(array('foo' => 'bar'), array('foo' => 'text'))), ''), + array(array('foo' => array('foo' => array('bar', 'text'))), 'text'), + array(array('foo' => 'bar'), 'bar'), + ); + } + + /** + * @dataProvider getDataForPhpize + */ + public function testPhpize($expected, $value) + { + $this->assertSame($expected, XmlUtils::phpize($value)); + } + + public function getDataForPhpize() + { + return array( + array(null, 'null'), + array(true, 'true'), + array(false, 'false'), + array(null, 'Null'), + array(true, 'True'), + array(false, 'False'), + array(0, '0'), + array(1, '1'), + array(0777, '0777'), + array(255, '0xFF'), + array(100.0, '1e2'), + array(-120.0, '-1.2E2'), + array(-10100.1, '-10100.1'), + array(-10100.1, '-10,100.1'), + array('foo', 'foo'), + ); + } +} + +interface Validator +{ + public function validate(); +} diff --git a/src/Symfony/Component/Config/Util/XmlUtils.php b/src/Symfony/Component/Config/Util/XmlUtils.php new file mode 100644 index 0000000000..8ab8225df5 --- /dev/null +++ b/src/Symfony/Component/Config/Util/XmlUtils.php @@ -0,0 +1,215 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Util; + +/** + * 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 + * @author Martin HasoĊˆ + */ +class XmlUtils +{ + /** + * This class should not be instantiated + */ + private function __construct() + { + } + + /** + * Loads an XML file. + * + * @param string $file An XML file path + * @param string|callable $schemaOrCallable An XSD schema file path or callable + * + * @return \DOMDocument + * + * @throws \InvalidArgumentException When loading of XML file returns error + */ + public static function loadFile($file, $schemaOrCallable = null) + { + $internalErrors = libxml_use_internal_errors(true); + $disableEntities = libxml_disable_entity_loader(true); + libxml_clear_errors(); + + $dom = new \DOMDocument(); + $dom->validateOnParse = true; + if (!$dom->loadXML(file_get_contents($file), LIBXML_NONET | (defined('LIBXML_COMPACT') ? LIBXML_COMPACT : 0))) { + libxml_disable_entity_loader($disableEntities); + + throw new \InvalidArgumentException(implode("\n", static::getXmlErrors($internalErrors))); + } + + $dom->normalizeDocument(); + + libxml_use_internal_errors($internalErrors); + libxml_disable_entity_loader($disableEntities); + + foreach ($dom->childNodes as $child) { + if ($child->nodeType === XML_DOCUMENT_TYPE_NODE) { + throw new \InvalidArgumentException('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 = call_user_func($schemaOrCallable, $dom, $internalErrors); + } catch (\Exception $e) { + $valid = false; + } + } elseif (!is_array($schemaOrCallable) && is_file((string) $schemaOrCallable)) { + $valid = @$dom->schemaValidate($schemaOrCallable); + } else { + libxml_use_internal_errors($internalErrors); + + throw new \InvalidArgumentException('The schemaOrCallable argument has to be a valid path to XSD file or callable.'); + } + + if (!$valid) { + $messages = static::getXmlErrors($internalErrors); + if (empty($messages)) { + $messages = array(sprintf('The XML file "%s" is not valid.', $file)); + } + throw new \InvalidArgumentException(implode("\n", $messages), 0, $e); + } + + libxml_use_internal_errors($internalErrors); + } + + return $dom; + } + + /** + * 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 (bar) + * if the tag also has some nested tags + * + * * The attributes are converted to keys () + * + * * The nested-tags are converted to keys (bar) + * + * @param \DomElement $element A \DomElement instance + * + * @return array A PHP array + */ + public static function convertDomElementToArray(\DomElement $element) + { + $empty = true; + $config = array(); + foreach ($element->attributes as $name => $node) { + $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 (!$node instanceof \DOMComment) { + $value = static::convertDomElementToArray($node); + + $key = $node->localName; + if (isset($config[$key])) { + if (!is_array($config[$key]) || !is_int(key($config[$key]))) { + $config[$key] = array($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): + $raw = $value; + $cast = intval($value); + + return '0' == $value[0] ? octdec($value) : (((string) $raw == (string) $cast) ? $cast : $raw); + case 'true' === $lowercaseValue: + return true; + case 'false' === $lowercaseValue: + return false; + case is_numeric($value): + return '0x' == $value[0].$value[1] ? hexdec($value) : floatval($value); + case preg_match('/^(-|\+)?[0-9,]+(\.[0-9]+)?$/', $value): + return floatval(str_replace(',', '', $value)); + default: + return $value; + } + } + + protected static function getXmlErrors($internalErrors) + { + $errors = array(); + 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 ? $error->file : 'n/a', + $error->line, + $error->column + ); + } + + libxml_clear_errors(); + libxml_use_internal_errors($internalErrors); + + return $errors; + } +}