merged branch hason/config (PR #5875)

This PR was merged into the master branch.

Commits
-------

45aab0f [Config] Added parameter to check prefix in an element or an attribute name in XmlUtils::convertDomElementToArray
7f5a7fd [DependencyInjection] Using class from Config component to loading XML files
d6f0880 [Validator] Added dependency to Config component to loading XML files
34471a6 [Routing] Using class from Config component to loading XML files
fa8b0d8 [Config] Added a utils class for XML manipulations

Discussion
----------

[Config] Added a utils class for XML manipulations

Bug fix: no
Feature addition: yes
Backwards compatibility break: no
Symfony2 tests pass: yes
License of the code: MIT

---------------------------------------------------------------------------

by fabpot at 2012-12-14T13:59:59Z

Looks good to me. Can you rebase on master as there is a conflict right now? Thanks.

---------------------------------------------------------------------------

by hason at 2012-12-14T14:44:11Z

Done.
This commit is contained in:
Fabien Potencier 2012-12-14 21:11:29 +01:00
commit f68ae0e57b
13 changed files with 397 additions and 255 deletions

View File

@ -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
-----

View File

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE scan [<!ENTITY test SYSTEM "php://filter/read=convert.base64-encode/resource={{ resource }}">]>
<scan></scan>

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<root>

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<root2 xmlns="http://example.com/schema" />

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns="http://example.com/schema"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://example.com/schema"
elementFormDefault="qualified">
<xsd:element name="root" />
</xsd:schema>

View File

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<root xmlns="http://example.com/schema">
</root>

View File

@ -0,0 +1,129 @@
<?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\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, $root = false, $checkPrefix = true)
{
$dom = new \DOMDocument();
$dom->loadXML($root ? $xml : '<root>'.$xml.'</root>');
$this->assertSame($expected, XmlUtils::convertDomElementToArray($dom->documentElement, $checkPrefix));
}
public function getDataForConvertDomToArray()
{
return array(
array(null, ''),
array('bar', 'bar'),
array(array('bar' => 'foobar'), '<foo bar="foobar" />', true),
array(array('foo' => null), '<foo />'),
array(array('foo' => 'bar'), '<foo>bar</foo>'),
array(array('foo' => array('foo' => 'bar')), '<foo foo="bar"/>'),
array(array('foo' => array('foo' => 'bar')), '<foo><foo>bar</foo></foo>'),
array(array('foo' => array('foo' => 'bar', 'value' => 'text')), '<foo foo="bar">text</foo>'),
array(array('foo' => array('attr' => 'bar', 'foo' => 'text')), '<foo attr="bar"><foo>text</foo></foo>'),
array(array('foo' => array('bar', 'text')), '<foo>bar</foo><foo>text</foo>'),
array(array('foo' => array(array('foo' => 'bar'), array('foo' => 'text'))), '<foo foo="bar"/><foo foo="text" />'),
array(array('foo' => array('foo' => array('bar', 'text'))), '<foo foo="bar"><foo>text</foo></foo>'),
array(array('foo' => 'bar'), '<foo><!-- Comment -->bar</foo>'),
array(array('foo' => 'text'), '<foo xmlns:h="http://www.example.org/bar" h:bar="bar">text</foo>'),
array(array('foo' => array('bar' => 'bar', 'value' => 'text')), '<foo xmlns:h="http://www.example.org/bar" h:bar="bar">text</foo>', false, false),
array(array('attr' => 1, 'b' => 'hello'), '<foo:a xmlns:foo="http://www.example.org/foo" xmlns:h="http://www.example.org/bar" attr="1" h:bar="bar"><foo:b>hello</foo:b><h:c>2</h:c></foo:a>', true),
);
}
/**
* @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();
}

View File

@ -0,0 +1,222 @@
<?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;
/**
* 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>
*/
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 (<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 Boolean $checkPrefix Check prefix in an element or an attribute name
*
* @return array A PHP array
*/
public static function convertDomElementToArray(\DomElement $element, $checkPrefix = true)
{
$prefix = (string) $element->prefix;
$empty = true;
$config = array();
foreach ($element->attributes as $name => $node) {
if ($checkPrefix && !in_array((string) $node->prefix, array('', $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] = 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;
}
}

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\DependencyInjection\Loader;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\Config\Util\XmlUtils;
use Symfony\Component\DependencyInjection\DefinitionDecorator;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Alias;
@ -202,33 +203,17 @@ class XmlFileLoader extends FileLoader
*
* @return SimpleXMLElement
*
* @throws \InvalidArgumentException When loading of XML file returns error
* @throws InvalidArgumentException When loading of XML file returns error
*/
protected function parseFile($file)
{
$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", $this->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.');
}
try {
$dom = XmlUtils::loadFile($file, array($this, 'validateSchema'));
} catch (\InvalidArgumentException $e) {
throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
}
$this->validate($dom, $file);
$this->validateExtensions($dom, $file);
return simplexml_import_dom($dom, 'Symfony\\Component\\DependencyInjection\\SimpleXMLElement');
}
@ -285,28 +270,14 @@ class XmlFileLoader extends FileLoader
}
}
/**
* Validates an XML document.
*
* @param \DOMDocument $dom
* @param string $file
*/
private function validate(\DOMDocument $dom, $file)
{
$this->validateSchema($dom, $file);
$this->validateExtensions($dom, $file);
}
/**
* Validates a documents XML schema.
*
* @param \DOMDocument $dom
* @param string $file
*
* @throws RuntimeException When extension references a non-existent XSD file
* @throws InvalidArgumentException When xml doesn't validate its xsd schema
* @throws RuntimeException When extension references a non-existent XSD file
*/
private function validateSchema(\DOMDocument $dom, $file)
public function validateSchema(\DOMDocument $dom)
{
$schemaLocations = array('http://symfony.com/schema/dic/services' => str_replace('\\', '/', __DIR__.'/schema/dic/services/services-1.0.xsd'));
@ -360,18 +331,13 @@ $imports
EOF
;
$current = libxml_use_internal_errors(true);
libxml_clear_errors();
$valid = @$dom->schemaValidateSource($source);
foreach ($tmpfiles as $tmpfile) {
@unlink($tmpfile);
}
if (!$valid) {
throw new InvalidArgumentException(implode("\n", $this->getXmlErrors($current)));
}
libxml_use_internal_errors($current);
return $valid;
}
/**
@ -403,33 +369,6 @@ EOF
}
}
/**
* Returns an array of XML errors.
*
* @param Boolean $internalErrors
*
* @return array
*/
private 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;
}
/**
* Loads from an extension.
*
@ -472,50 +411,6 @@ EOF
*/
public static function convertDomElementToArray(\DomElement $element)
{
$empty = true;
$config = array();
foreach ($element->attributes as $name => $node) {
$config[$name] = SimpleXMLElement::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) {
if ($node instanceof \DOMElement && '_services' === $node->nodeName) {
$value = new Reference($node->getAttribute('id'));
} else {
$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 = SimpleXMLElement::phpize($nodeValue);
if (count($config)) {
$config['value'] = $value;
} else {
$config = $value;
}
}
return !$empty ? $config : null;
return XmlUtils::convertDomElementToArray($element);
}
}

View File

@ -11,6 +11,8 @@
namespace Symfony\Component\DependencyInjection;
use Symfony\Component\Config\Util\XmlUtils;
/**
* SimpleXMLElement class.
*
@ -101,27 +103,6 @@ class SimpleXMLElement extends \SimpleXMLElement
*/
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;
}
return XmlUtils::phpize($value);
}
}

View File

@ -15,6 +15,7 @@ use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\Config\Loader\FileLoader;
use Symfony\Component\Config\Util\XmlUtils;
/**
* XmlFileLoader loads XML routing files.
@ -172,74 +173,7 @@ class XmlFileLoader extends FileLoader
*/
protected function loadFile($file)
{
$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", $this->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.');
}
}
$this->validate($dom);
return $dom;
}
/**
* Validates a loaded XML file.
*
* @param \DOMDocument $dom A loaded XML file
*
* @throws \InvalidArgumentException When XML doesn't validate its XSD schema
*/
protected function validate(\DOMDocument $dom)
{
$current = libxml_use_internal_errors(true);
libxml_clear_errors();
if (!$dom->schemaValidate(__DIR__ . static::SCHEME_PATH)) {
throw new \InvalidArgumentException(implode("\n", $this->getXmlErrors($current)));
}
libxml_use_internal_errors($current);
}
/**
* Retrieves libxml errors and clears them.
*
* @return array An array of libxml error strings
*/
private 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;
return XmlUtils::loadFile($file, __DIR__ . static::SCHEME_PATH);
}
/**

View File

@ -13,6 +13,7 @@ namespace Symfony\Component\Validator\Mapping\Loader;
use Symfony\Component\Validator\Exception\MappingException;
use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Config\Util\XmlUtils;
class XmlFileLoader extends FileLoader
{
@ -185,54 +186,12 @@ class XmlFileLoader extends FileLoader
*/
protected function parseFile($file)
{
$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 MappingException(implode("\n", $this->getXmlErrors($internalErrors)));
}
libxml_disable_entity_loader($disableEntities);
if (!$dom->schemaValidate(__DIR__.'/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd')) {
throw new MappingException(implode("\n", $this->getXmlErrors($internalErrors)));
}
$dom->normalizeDocument();
libxml_use_internal_errors($internalErrors);
foreach ($dom->childNodes as $child) {
if ($child->nodeType === XML_DOCUMENT_TYPE_NODE) {
throw new MappingException('Document types are not allowed.');
}
try {
$dom = XmlUtils::loadFile($file, __DIR__.'/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd');
} catch (\Exception $e) {
throw new MappingException($e->getMessage(), $e->getCode(), $e);
}
return simplexml_import_dom($dom);
}
protected 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;
}
}

View File

@ -21,12 +21,14 @@
"require-dev": {
"symfony/http-foundation": "2.2.*",
"symfony/locale": "2.2.*",
"symfony/yaml": "2.2.*"
"symfony/yaml": "2.2.*",
"symfony/config": "2.2.*"
},
"suggest": {
"doctrine/common": ">=2.1,<2.4-dev",
"symfony/http-foundation": "2.2.*",
"symfony/yaml": "2.2.*"
"symfony/yaml": "2.2.*",
"symfony/config": "2.2.*"
},
"autoload": {
"psr-0": { "Symfony\\Component\\Validator\\": "" }