This repository has been archived on 2023-08-20. You can view files and clone it, but cannot push or open issues or pull requests.
symfony/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php
Fabien Potencier a6bc12c9c1 Merge branch '2.0'
* 2.0:
  updated VERSION for 2.0.17
  updated CHANGELOG for 2.0.17
  updated vendors for 2.0.17
  fixed XML decoding attack vector through external entities
  prevents injection of malicious doc types
  disabled network access when loading XML documents
  refined previous commit
  prevents injection of malicious doc types
  standardized the way we handle XML errors
  Redirects are now absolute

Conflicts:
	CHANGELOG-2.0.md
	src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php
	src/Symfony/Component/DomCrawler/Crawler.php
	src/Symfony/Component/HttpKernel/Kernel.php
	tests/Symfony/Tests/Component/DependencyInjection/Loader/XmlFileLoaderTest.php
	tests/Symfony/Tests/Component/Routing/Loader/XmlFileLoaderTest.php
	tests/Symfony/Tests/Component/Serializer/Encoder/XmlEncoderTest.php
	tests/Symfony/Tests/Component/Translation/Loader/XliffFileLoaderTest.php
	tests/Symfony/Tests/Component/Validator/Mapping/Loader/XmlFileLoaderTest.php
	vendors.php
2012-08-28 09:54:42 +02:00

518 lines
17 KiB
PHP

<?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\DependencyInjection\Loader;
use Symfony\Component\DependencyInjection\DefinitionDecorator;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\SimpleXMLElement;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
/**
* XmlFileLoader loads XML files service definitions.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class XmlFileLoader extends FileLoader
{
/**
* Loads an XML file.
*
* @param mixed $file The resource
* @param string $type The resource type
*/
public function load($file, $type = null)
{
$path = $this->locator->locate($file);
$xml = $this->parseFile($path);
$xml->registerXPathNamespace('container', 'http://symfony.com/schema/dic/services');
$this->container->addResource(new FileResource($path));
// anonymous services
$this->processAnonymousServices($xml, $path);
// imports
$this->parseImports($xml, $path);
// parameters
$this->parseParameters($xml, $path);
// extensions
$this->loadFromExtensions($xml);
// services
$this->parseDefinitions($xml, $path);
}
/**
* Returns true if this class supports the given resource.
*
* @param mixed $resource A resource
* @param string $type The resource type
*
* @return Boolean true if this class supports the given resource, false otherwise
*/
public function supports($resource, $type = null)
{
return is_string($resource) && 'xml' === pathinfo($resource, PATHINFO_EXTENSION);
}
/**
* Parses parameters
*
* @param SimpleXMLElement $xml
* @param string $file
*/
private function parseParameters(SimpleXMLElement $xml, $file)
{
if (!$xml->parameters) {
return;
}
$this->container->getParameterBag()->add($xml->parameters->getArgumentsAsPhp('parameter'));
}
/**
* Parses imports
*
* @param SimpleXMLElement $xml
* @param string $file
*/
private function parseImports(SimpleXMLElement $xml, $file)
{
if (false === $imports = $xml->xpath('//container:imports/container:import')) {
return;
}
foreach ($imports as $import) {
$this->setCurrentDir(dirname($file));
$this->import((string) $import['resource'], null, (Boolean) $import->getAttributeAsPhp('ignore-errors'), $file);
}
}
/**
* Parses multiple definitions
*
* @param SimpleXMLElement $xml
* @param string $file
*/
private function parseDefinitions(SimpleXMLElement $xml, $file)
{
if (false === $services = $xml->xpath('//container:services/container:service')) {
return;
}
foreach ($services as $service) {
$this->parseDefinition((string) $service['id'], $service, $file);
}
}
/**
* Parses an individual Definition
*
* @param string $id
* @param SimpleXMLElement $service
* @param string $file
*/
private function parseDefinition($id, $service, $file)
{
if ((string) $service['alias']) {
$public = true;
if (isset($service['public'])) {
$public = $service->getAttributeAsPhp('public');
}
$this->container->setAlias($id, new Alias((string) $service['alias'], $public));
return;
}
if (isset($service['parent'])) {
$definition = new DefinitionDecorator((string) $service['parent']);
} else {
$definition = new Definition();
}
foreach (array('class', 'scope', 'public', 'factory-class', 'factory-method', 'factory-service', 'synthetic', 'abstract') as $key) {
if (isset($service[$key])) {
$method = 'set'.str_replace('-', '', $key);
$definition->$method((string) $service->getAttributeAsPhp($key));
}
}
if ($service->file) {
$definition->setFile((string) $service->file);
}
$definition->setArguments($service->getArgumentsAsPhp('argument'));
$definition->setProperties($service->getArgumentsAsPhp('property'));
if (isset($service->configurator)) {
if (isset($service->configurator['function'])) {
$definition->setConfigurator((string) $service->configurator['function']);
} else {
if (isset($service->configurator['service'])) {
$class = new Reference((string) $service->configurator['service'], ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, false);
} else {
$class = (string) $service->configurator['class'];
}
$definition->setConfigurator(array($class, (string) $service->configurator['method']));
}
}
foreach ($service->call as $call) {
$definition->addMethodCall((string) $call['method'], $call->getArgumentsAsPhp('argument'));
}
foreach ($service->tag as $tag) {
$parameters = array();
foreach ($tag->attributes() as $name => $value) {
if ('name' === $name) {
continue;
}
$parameters[$name] = SimpleXMLElement::phpize($value);
}
$definition->addTag((string) $tag['name'], $parameters);
}
$this->container->setDefinition($id, $definition);
}
/**
* Parses a XML file.
*
* @param string $file Path to a file
*
* @throws InvalidArgumentException When loading of XML file returns error
*/
private 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.');
}
}
$this->validate($dom, $file);
return simplexml_import_dom($dom, 'Symfony\\Component\\DependencyInjection\\SimpleXMLElement');
}
/**
* Processes anonymous services
*
* @param SimpleXMLElement $xml
* @param string $file
*/
private function processAnonymousServices(SimpleXMLElement $xml, $file)
{
$definitions = array();
$count = 0;
// anonymous services as arguments/properties
if (false !== $nodes = $xml->xpath('//container:argument[@type="service"][not(@id)]|//container:property[@type="service"][not(@id)]')) {
foreach ($nodes as $node) {
// give it a unique name
$node['id'] = sprintf('%s_%d', md5($file), ++$count);
$definitions[(string) $node['id']] = array($node->service, $file, false);
$node->service['id'] = (string) $node['id'];
}
}
// anonymous services "in the wild"
if (false !== $nodes = $xml->xpath('//container:services/container:service[not(@id)]')) {
foreach ($nodes as $node) {
// give it a unique name
$node['id'] = sprintf('%s_%d', md5($file), ++$count);
$definitions[(string) $node['id']] = array($node, $file, true);
$node->service['id'] = (string) $node['id'];
}
}
// resolve definitions
krsort($definitions);
foreach ($definitions as $id => $def) {
// anonymous services are always private
$def[0]['public'] = false;
$this->parseDefinition($id, $def[0], $def[1]);
$oNode = dom_import_simplexml($def[0]);
if (true === $def[2]) {
$nNode = new \DOMElement('_services');
$oNode->parentNode->replaceChild($nNode, $oNode);
$nNode->setAttribute('id', $id);
} else {
$oNode->parentNode->removeChild($oNode);
}
}
}
/**
* 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
*/
private function validateSchema(\DOMDocument $dom, $file)
{
$schemaLocations = array('http://symfony.com/schema/dic/services' => str_replace('\\', '/', __DIR__.'/schema/dic/services/services-1.0.xsd'));
if ($element = $dom->documentElement->getAttributeNS('http://www.w3.org/2001/XMLSchema-instance', 'schemaLocation')) {
$items = preg_split('/\s+/', $element);
for ($i = 0, $nb = count($items); $i < $nb; $i += 2) {
if (!$this->container->hasExtension($items[$i])) {
continue;
}
if (($extension = $this->container->getExtension($items[$i])) && false !== $extension->getXsdValidationBasePath()) {
$path = str_replace($extension->getNamespace(), str_replace('\\', '/', $extension->getXsdValidationBasePath()).'/', $items[$i + 1]);
if (!is_file($path)) {
throw new RuntimeException(sprintf('Extension "%s" references a non-existent XSD file "%s"', get_class($extension), $path));
}
$schemaLocations[$items[$i]] = $path;
}
}
}
$tmpfiles = array();
$imports = '';
foreach ($schemaLocations as $namespace => $location) {
$parts = explode('/', $location);
if (0 === stripos($location, 'phar://')) {
$tmpfile = tempnam(sys_get_temp_dir(), 'sf2');
if ($tmpfile) {
copy($location, $tmpfile);
$tmpfiles[] = $tmpfile;
$parts = explode('/', str_replace('\\', '/', $tmpfile));
}
}
$drive = '\\' === DIRECTORY_SEPARATOR ? array_shift($parts).'/' : '';
$location = 'file:///'.$drive.implode('/', array_map('rawurlencode', $parts));
$imports .= sprintf(' <xsd:import namespace="%s" schemaLocation="%s" />'."\n", $namespace, $location);
}
$source = <<<EOF
<?xml version="1.0" encoding="utf-8" ?>
<xsd:schema xmlns="http://symfony.com/schema"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://symfony.com/schema"
elementFormDefault="qualified">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
$imports
</xsd:schema>
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);
}
/**
* Validates an extension.
*
* @param \DOMDocument $dom
* @param string $file
*
* @throws InvalidArgumentException When no extension is found corresponding to a tag
*/
private function validateExtensions(\DOMDocument $dom, $file)
{
foreach ($dom->documentElement->childNodes as $node) {
if (!$node instanceof \DOMElement || 'http://symfony.com/schema/dic/services' === $node->namespaceURI) {
continue;
}
// can it be handled by an extension?
if (!$this->container->hasExtension($node->namespaceURI)) {
$extensionNamespaces = array_filter(array_map(function ($ext) { return $ext->getNamespace(); }, $this->container->getExtensions()));
throw new InvalidArgumentException(sprintf(
'There is no extension able to load the configuration for "%s" (in %s). Looked for namespace "%s", found %s',
$node->tagName,
$file,
$node->namespaceURI,
$extensionNamespaces ? sprintf('"%s"', implode('", "', $extensionNamespaces)) : 'none'
));
}
}
}
/**
* Returns an array of XML errors.
*
* @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.
*
* @param SimpleXMLElement $xml
*/
private function loadFromExtensions(SimpleXMLElement $xml)
{
foreach (dom_import_simplexml($xml)->childNodes as $node) {
if (!$node instanceof \DOMElement || $node->namespaceURI === 'http://symfony.com/schema/dic/services') {
continue;
}
$values = static::convertDomElementToArray($node);
if (!is_array($values)) {
$values = array();
}
$this->container->loadFromExtension($node->namespaceURI, $values);
}
}
/**
* 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
*
* @return array A PHP array
*/
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;
}
}