1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\DependencyInjection\Loader;
13
14use Symfony\Component\Config\Util\XmlUtils;
15use Symfony\Component\DependencyInjection\Alias;
16use Symfony\Component\DependencyInjection\Argument\BoundArgument;
17use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
18use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
19use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
20use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
21use Symfony\Component\DependencyInjection\ChildDefinition;
22use Symfony\Component\DependencyInjection\ContainerBuilder;
23use Symfony\Component\DependencyInjection\ContainerInterface;
24use Symfony\Component\DependencyInjection\Definition;
25use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
26use Symfony\Component\DependencyInjection\Exception\RuntimeException;
27use Symfony\Component\DependencyInjection\Extension\ExtensionInterface;
28use Symfony\Component\DependencyInjection\Reference;
29use Symfony\Component\ExpressionLanguage\Expression;
30
31/**
32 * XmlFileLoader loads XML files service definitions.
33 *
34 * @author Fabien Potencier <fabien@symfony.com>
35 */
36class XmlFileLoader extends FileLoader
37{
38    public const NS = 'http://symfony.com/schema/dic/services';
39
40    protected $autoRegisterAliasesForSinglyImplementedInterfaces = false;
41
42    /**
43     * {@inheritdoc}
44     */
45    public function load($resource, $type = null)
46    {
47        $path = $this->locator->locate($resource);
48
49        $xml = $this->parseFileToDOM($path);
50
51        $this->container->fileExists($path);
52
53        $defaults = $this->getServiceDefaults($xml, $path);
54
55        // anonymous services
56        $this->processAnonymousServices($xml, $path);
57
58        // imports
59        $this->parseImports($xml, $path);
60
61        // parameters
62        $this->parseParameters($xml, $path);
63
64        // extensions
65        $this->loadFromExtensions($xml);
66
67        // services
68        try {
69            $this->parseDefinitions($xml, $path, $defaults);
70        } finally {
71            $this->instanceof = [];
72            $this->registerAliasesForSinglyImplementedInterfaces();
73        }
74    }
75
76    /**
77     * {@inheritdoc}
78     */
79    public function supports($resource, $type = null)
80    {
81        if (!\is_string($resource)) {
82            return false;
83        }
84
85        if (null === $type && 'xml' === pathinfo($resource, \PATHINFO_EXTENSION)) {
86            return true;
87        }
88
89        return 'xml' === $type;
90    }
91
92    private function parseParameters(\DOMDocument $xml, string $file)
93    {
94        if ($parameters = $this->getChildren($xml->documentElement, 'parameters')) {
95            $this->container->getParameterBag()->add($this->getArgumentsAsPhp($parameters[0], 'parameter', $file));
96        }
97    }
98
99    private function parseImports(\DOMDocument $xml, string $file)
100    {
101        $xpath = new \DOMXPath($xml);
102        $xpath->registerNamespace('container', self::NS);
103
104        if (false === $imports = $xpath->query('//container:imports/container:import')) {
105            return;
106        }
107
108        $defaultDirectory = \dirname($file);
109        foreach ($imports as $import) {
110            $this->setCurrentDir($defaultDirectory);
111            $this->import($import->getAttribute('resource'), XmlUtils::phpize($import->getAttribute('type')) ?: null, XmlUtils::phpize($import->getAttribute('ignore-errors')) ?: false, $file);
112        }
113    }
114
115    private function parseDefinitions(\DOMDocument $xml, string $file, array $defaults)
116    {
117        $xpath = new \DOMXPath($xml);
118        $xpath->registerNamespace('container', self::NS);
119
120        if (false === $services = $xpath->query('//container:services/container:service|//container:services/container:prototype')) {
121            return;
122        }
123        $this->setCurrentDir(\dirname($file));
124
125        $this->instanceof = [];
126        $this->isLoadingInstanceof = true;
127        $instanceof = $xpath->query('//container:services/container:instanceof');
128        foreach ($instanceof as $service) {
129            $this->setDefinition((string) $service->getAttribute('id'), $this->parseDefinition($service, $file, []));
130        }
131
132        $this->isLoadingInstanceof = false;
133        foreach ($services as $service) {
134            if (null !== $definition = $this->parseDefinition($service, $file, $defaults)) {
135                if ('prototype' === $service->tagName) {
136                    $excludes = array_column($this->getChildren($service, 'exclude'), 'nodeValue');
137                    if ($service->hasAttribute('exclude')) {
138                        if (\count($excludes) > 0) {
139                            throw new InvalidArgumentException('You cannot use both the attribute "exclude" and <exclude> tags at the same time.');
140                        }
141                        $excludes = [$service->getAttribute('exclude')];
142                    }
143                    $this->registerClasses($definition, (string) $service->getAttribute('namespace'), (string) $service->getAttribute('resource'), $excludes);
144                } else {
145                    $this->setDefinition((string) $service->getAttribute('id'), $definition);
146                }
147            }
148        }
149    }
150
151    /**
152     * Get service defaults.
153     */
154    private function getServiceDefaults(\DOMDocument $xml, string $file): array
155    {
156        $xpath = new \DOMXPath($xml);
157        $xpath->registerNamespace('container', self::NS);
158
159        if (null === $defaultsNode = $xpath->query('//container:services/container:defaults')->item(0)) {
160            return [];
161        }
162
163        $bindings = [];
164        foreach ($this->getArgumentsAsPhp($defaultsNode, 'bind', $file) as $argument => $value) {
165            $bindings[$argument] = new BoundArgument($value, true, BoundArgument::DEFAULTS_BINDING, $file);
166        }
167
168        $defaults = [
169            'tags' => $this->getChildren($defaultsNode, 'tag'),
170            'bind' => $bindings,
171        ];
172
173        foreach ($defaults['tags'] as $tag) {
174            if ('' === $tag->getAttribute('name')) {
175                throw new InvalidArgumentException(sprintf('The tag name for tag "<defaults>" in "%s" must be a non-empty string.', $file));
176            }
177        }
178
179        if ($defaultsNode->hasAttribute('autowire')) {
180            $defaults['autowire'] = XmlUtils::phpize($defaultsNode->getAttribute('autowire'));
181        }
182        if ($defaultsNode->hasAttribute('public')) {
183            $defaults['public'] = XmlUtils::phpize($defaultsNode->getAttribute('public'));
184        }
185        if ($defaultsNode->hasAttribute('autoconfigure')) {
186            $defaults['autoconfigure'] = XmlUtils::phpize($defaultsNode->getAttribute('autoconfigure'));
187        }
188
189        return $defaults;
190    }
191
192    /**
193     * Parses an individual Definition.
194     */
195    private function parseDefinition(\DOMElement $service, string $file, array $defaults): ?Definition
196    {
197        if ($alias = $service->getAttribute('alias')) {
198            $this->validateAlias($service, $file);
199
200            $this->container->setAlias((string) $service->getAttribute('id'), $alias = new Alias($alias));
201            if ($publicAttr = $service->getAttribute('public')) {
202                $alias->setPublic(XmlUtils::phpize($publicAttr));
203            } elseif (isset($defaults['public'])) {
204                $alias->setPublic($defaults['public']);
205            }
206
207            if ($deprecated = $this->getChildren($service, 'deprecated')) {
208                $alias->setDeprecated(true, $deprecated[0]->nodeValue ?: null);
209            }
210
211            return null;
212        }
213
214        if ($this->isLoadingInstanceof) {
215            $definition = new ChildDefinition('');
216        } elseif ($parent = $service->getAttribute('parent')) {
217            if (!empty($this->instanceof)) {
218                throw new InvalidArgumentException(sprintf('The service "%s" cannot use the "parent" option in the same file where "instanceof" configuration is defined as using both is not supported. Move your child definitions to a separate file.', $service->getAttribute('id')));
219            }
220
221            foreach ($defaults as $k => $v) {
222                if ('tags' === $k) {
223                    // since tags are never inherited from parents, there is no confusion
224                    // thus we can safely add them as defaults to ChildDefinition
225                    continue;
226                }
227                if ('bind' === $k) {
228                    if ($defaults['bind']) {
229                        throw new InvalidArgumentException(sprintf('Bound values on service "%s" cannot be inherited from "defaults" when a "parent" is set. Move your child definitions to a separate file.', $service->getAttribute('id')));
230                    }
231
232                    continue;
233                }
234                if (!$service->hasAttribute($k)) {
235                    throw new InvalidArgumentException(sprintf('Attribute "%s" on service "%s" cannot be inherited from "defaults" when a "parent" is set. Move your child definitions to a separate file or define this attribute explicitly.', $k, $service->getAttribute('id')));
236                }
237            }
238
239            $definition = new ChildDefinition($parent);
240        } else {
241            $definition = new Definition();
242
243            if (isset($defaults['public'])) {
244                $definition->setPublic($defaults['public']);
245            }
246            if (isset($defaults['autowire'])) {
247                $definition->setAutowired($defaults['autowire']);
248            }
249            if (isset($defaults['autoconfigure'])) {
250                $definition->setAutoconfigured($defaults['autoconfigure']);
251            }
252
253            $definition->setChanges([]);
254        }
255
256        foreach (['class', 'public', 'shared', 'synthetic', 'abstract'] as $key) {
257            if ($value = $service->getAttribute($key)) {
258                $method = 'set'.$key;
259                $definition->$method($value = XmlUtils::phpize($value));
260            }
261        }
262
263        if ($value = $service->getAttribute('lazy')) {
264            $definition->setLazy((bool) $value = XmlUtils::phpize($value));
265            if (\is_string($value)) {
266                $definition->addTag('proxy', ['interface' => $value]);
267            }
268        }
269
270        if ($value = $service->getAttribute('autowire')) {
271            $definition->setAutowired(XmlUtils::phpize($value));
272        }
273
274        if ($value = $service->getAttribute('autoconfigure')) {
275            if (!$definition instanceof ChildDefinition) {
276                $definition->setAutoconfigured(XmlUtils::phpize($value));
277            } elseif ($value = XmlUtils::phpize($value)) {
278                throw new InvalidArgumentException(sprintf('The service "%s" cannot have a "parent" and also have "autoconfigure". Try setting autoconfigure="false" for the service.', $service->getAttribute('id')));
279            }
280        }
281
282        if ($files = $this->getChildren($service, 'file')) {
283            $definition->setFile($files[0]->nodeValue);
284        }
285
286        if ($deprecated = $this->getChildren($service, 'deprecated')) {
287            $definition->setDeprecated(true, $deprecated[0]->nodeValue ?: null);
288        }
289
290        $definition->setArguments($this->getArgumentsAsPhp($service, 'argument', $file, $definition instanceof ChildDefinition));
291        $definition->setProperties($this->getArgumentsAsPhp($service, 'property', $file));
292
293        if ($factories = $this->getChildren($service, 'factory')) {
294            $factory = $factories[0];
295            if ($function = $factory->getAttribute('function')) {
296                $definition->setFactory($function);
297            } else {
298                if ($childService = $factory->getAttribute('service')) {
299                    $class = new Reference($childService, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE);
300                } else {
301                    $class = $factory->hasAttribute('class') ? $factory->getAttribute('class') : null;
302                }
303
304                $definition->setFactory([$class, $factory->getAttribute('method') ?: '__invoke']);
305            }
306        }
307
308        if ($configurators = $this->getChildren($service, 'configurator')) {
309            $configurator = $configurators[0];
310            if ($function = $configurator->getAttribute('function')) {
311                $definition->setConfigurator($function);
312            } else {
313                if ($childService = $configurator->getAttribute('service')) {
314                    $class = new Reference($childService, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE);
315                } else {
316                    $class = $configurator->getAttribute('class');
317                }
318
319                $definition->setConfigurator([$class, $configurator->getAttribute('method') ?: '__invoke']);
320            }
321        }
322
323        foreach ($this->getChildren($service, 'call') as $call) {
324            $definition->addMethodCall($call->getAttribute('method'), $this->getArgumentsAsPhp($call, 'argument', $file), XmlUtils::phpize($call->getAttribute('returns-clone')));
325        }
326
327        $tags = $this->getChildren($service, 'tag');
328
329        if (!empty($defaults['tags'])) {
330            $tags = array_merge($tags, $defaults['tags']);
331        }
332
333        foreach ($tags as $tag) {
334            $parameters = [];
335            foreach ($tag->attributes as $name => $node) {
336                if ('name' === $name) {
337                    continue;
338                }
339
340                if (false !== strpos($name, '-') && false === strpos($name, '_') && !\array_key_exists($normalizedName = str_replace('-', '_', $name), $parameters)) {
341                    $parameters[$normalizedName] = XmlUtils::phpize($node->nodeValue);
342                }
343                // keep not normalized key
344                $parameters[$name] = XmlUtils::phpize($node->nodeValue);
345            }
346
347            if ('' === $tag->getAttribute('name')) {
348                throw new InvalidArgumentException(sprintf('The tag name for service "%s" in "%s" must be a non-empty string.', (string) $service->getAttribute('id'), $file));
349            }
350
351            $definition->addTag($tag->getAttribute('name'), $parameters);
352        }
353
354        $bindings = $this->getArgumentsAsPhp($service, 'bind', $file);
355        $bindingType = $this->isLoadingInstanceof ? BoundArgument::INSTANCEOF_BINDING : BoundArgument::SERVICE_BINDING;
356        foreach ($bindings as $argument => $value) {
357            $bindings[$argument] = new BoundArgument($value, true, $bindingType, $file);
358        }
359
360        if (isset($defaults['bind'])) {
361            // deep clone, to avoid multiple process of the same instance in the passes
362            $bindings = array_merge(unserialize(serialize($defaults['bind'])), $bindings);
363        }
364        if ($bindings) {
365            $definition->setBindings($bindings);
366        }
367
368        if ($decorates = $service->getAttribute('decorates')) {
369            $decorationOnInvalid = $service->getAttribute('decoration-on-invalid') ?: 'exception';
370            if ('exception' === $decorationOnInvalid) {
371                $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE;
372            } elseif ('ignore' === $decorationOnInvalid) {
373                $invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE;
374            } elseif ('null' === $decorationOnInvalid) {
375                $invalidBehavior = ContainerInterface::NULL_ON_INVALID_REFERENCE;
376            } else {
377                throw new InvalidArgumentException(sprintf('Invalid value "%s" for attribute "decoration-on-invalid" on service "%s". Did you mean "exception", "ignore" or "null" in "%s"?', $decorationOnInvalid, (string) $service->getAttribute('id'), $file));
378            }
379
380            $renameId = $service->hasAttribute('decoration-inner-name') ? $service->getAttribute('decoration-inner-name') : null;
381            $priority = $service->hasAttribute('decoration-priority') ? $service->getAttribute('decoration-priority') : 0;
382
383            $definition->setDecoratedService($decorates, $renameId, $priority, $invalidBehavior);
384        }
385
386        return $definition;
387    }
388
389    /**
390     * Parses an XML file to a \DOMDocument.
391     *
392     * @throws InvalidArgumentException When loading of XML file returns error
393     */
394    private function parseFileToDOM(string $file): \DOMDocument
395    {
396        try {
397            $dom = XmlUtils::loadFile($file, [$this, 'validateSchema']);
398        } catch (\InvalidArgumentException $e) {
399            throw new InvalidArgumentException(sprintf('Unable to parse file "%s": ', $file).$e->getMessage(), $e->getCode(), $e);
400        }
401
402        $this->validateExtensions($dom, $file);
403
404        return $dom;
405    }
406
407    /**
408     * Processes anonymous services.
409     */
410    private function processAnonymousServices(\DOMDocument $xml, string $file)
411    {
412        $definitions = [];
413        $count = 0;
414        $suffix = '~'.ContainerBuilder::hash($file);
415
416        $xpath = new \DOMXPath($xml);
417        $xpath->registerNamespace('container', self::NS);
418
419        // anonymous services as arguments/properties
420        if (false !== $nodes = $xpath->query('//container:argument[@type="service"][not(@id)]|//container:property[@type="service"][not(@id)]|//container:bind[not(@id)]|//container:factory[not(@service)]|//container:configurator[not(@service)]')) {
421            foreach ($nodes as $node) {
422                if ($services = $this->getChildren($node, 'service')) {
423                    // give it a unique name
424                    $id = sprintf('.%d_%s', ++$count, preg_replace('/^.*\\\\/', '', $services[0]->getAttribute('class')).$suffix);
425                    $node->setAttribute('id', $id);
426                    $node->setAttribute('service', $id);
427
428                    $definitions[$id] = [$services[0], $file];
429                    $services[0]->setAttribute('id', $id);
430
431                    // anonymous services are always private
432                    // we could not use the constant false here, because of XML parsing
433                    $services[0]->setAttribute('public', 'false');
434                }
435            }
436        }
437
438        // anonymous services "in the wild"
439        if (false !== $nodes = $xpath->query('//container:services/container:service[not(@id)]')) {
440            foreach ($nodes as $node) {
441                throw new InvalidArgumentException(sprintf('Top-level services must have "id" attribute, none found in "%s" at line %d.', $file, $node->getLineNo()));
442            }
443        }
444
445        // resolve definitions
446        uksort($definitions, 'strnatcmp');
447        foreach (array_reverse($definitions) as $id => [$domElement, $file]) {
448            if (null !== $definition = $this->parseDefinition($domElement, $file, [])) {
449                $this->setDefinition($id, $definition);
450            }
451        }
452    }
453
454    private function getArgumentsAsPhp(\DOMElement $node, string $name, string $file, bool $isChildDefinition = false): array
455    {
456        $arguments = [];
457        foreach ($this->getChildren($node, $name) as $arg) {
458            if ($arg->hasAttribute('name')) {
459                $arg->setAttribute('key', $arg->getAttribute('name'));
460            }
461
462            // this is used by ChildDefinition to overwrite a specific
463            // argument of the parent definition
464            if ($arg->hasAttribute('index')) {
465                $key = ($isChildDefinition ? 'index_' : '').$arg->getAttribute('index');
466            } elseif (!$arg->hasAttribute('key')) {
467                // Append an empty argument, then fetch its key to overwrite it later
468                $arguments[] = null;
469                $keys = array_keys($arguments);
470                $key = array_pop($keys);
471            } else {
472                $key = $arg->getAttribute('key');
473            }
474
475            $onInvalid = $arg->getAttribute('on-invalid');
476            $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE;
477            if ('ignore' == $onInvalid) {
478                $invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE;
479            } elseif ('ignore_uninitialized' == $onInvalid) {
480                $invalidBehavior = ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE;
481            } elseif ('null' == $onInvalid) {
482                $invalidBehavior = ContainerInterface::NULL_ON_INVALID_REFERENCE;
483            }
484
485            switch ($arg->getAttribute('type')) {
486                case 'service':
487                    if ('' === $arg->getAttribute('id')) {
488                        throw new InvalidArgumentException(sprintf('Tag "<%s>" with type="service" has no or empty "id" attribute in "%s".', $name, $file));
489                    }
490
491                    $arguments[$key] = new Reference($arg->getAttribute('id'), $invalidBehavior);
492                    break;
493                case 'expression':
494                    if (!class_exists(Expression::class)) {
495                        throw new \LogicException('The type="expression" attribute cannot be used without the ExpressionLanguage component. Try running "composer require symfony/expression-language".');
496                    }
497
498                    $arguments[$key] = new Expression($arg->nodeValue);
499                    break;
500                case 'collection':
501                    $arguments[$key] = $this->getArgumentsAsPhp($arg, $name, $file);
502                    break;
503                case 'iterator':
504                    $arg = $this->getArgumentsAsPhp($arg, $name, $file);
505                    try {
506                        $arguments[$key] = new IteratorArgument($arg);
507                    } catch (InvalidArgumentException $e) {
508                        throw new InvalidArgumentException(sprintf('Tag "<%s>" with type="iterator" only accepts collections of type="service" references in "%s".', $name, $file));
509                    }
510                    break;
511                case 'service_closure':
512                    if ('' === $arg->getAttribute('id')) {
513                        throw new InvalidArgumentException(sprintf('Tag "<%s>" with type="service_closure" has no or empty "id" attribute in "%s".', $name, $file));
514                    }
515
516                    $arguments[$key] = new ServiceClosureArgument(new Reference($arg->getAttribute('id'), $invalidBehavior));
517                    break;
518                case 'service_locator':
519                    $arg = $this->getArgumentsAsPhp($arg, $name, $file);
520                    try {
521                        $arguments[$key] = new ServiceLocatorArgument($arg);
522                    } catch (InvalidArgumentException $e) {
523                        throw new InvalidArgumentException(sprintf('Tag "<%s>" with type="service_locator" only accepts maps of type="service" references in "%s".', $name, $file));
524                    }
525                    break;
526                case 'tagged':
527                case 'tagged_iterator':
528                case 'tagged_locator':
529                    $type = $arg->getAttribute('type');
530                    $forLocator = 'tagged_locator' === $type;
531
532                    if (!$arg->getAttribute('tag')) {
533                        throw new InvalidArgumentException(sprintf('Tag "<%s>" with type="%s" has no or empty "tag" attribute in "%s".', $name, $type, $file));
534                    }
535
536                    $arguments[$key] = new TaggedIteratorArgument($arg->getAttribute('tag'), $arg->getAttribute('index-by') ?: null, $arg->getAttribute('default-index-method') ?: null, $forLocator, $arg->getAttribute('default-priority-method') ?: null);
537
538                    if ($forLocator) {
539                        $arguments[$key] = new ServiceLocatorArgument($arguments[$key]);
540                    }
541                    break;
542                case 'binary':
543                    if (false === $value = base64_decode($arg->nodeValue)) {
544                        throw new InvalidArgumentException(sprintf('Tag "<%s>" with type="binary" is not a valid base64 encoded string.', $name));
545                    }
546                    $arguments[$key] = $value;
547                    break;
548                case 'string':
549                    $arguments[$key] = $arg->nodeValue;
550                    break;
551                case 'constant':
552                    $arguments[$key] = \constant(trim($arg->nodeValue));
553                    break;
554                default:
555                    $arguments[$key] = XmlUtils::phpize($arg->nodeValue);
556            }
557        }
558
559        return $arguments;
560    }
561
562    /**
563     * Get child elements by name.
564     *
565     * @return \DOMElement[]
566     */
567    private function getChildren(\DOMNode $node, string $name): array
568    {
569        $children = [];
570        foreach ($node->childNodes as $child) {
571            if ($child instanceof \DOMElement && $child->localName === $name && self::NS === $child->namespaceURI) {
572                $children[] = $child;
573            }
574        }
575
576        return $children;
577    }
578
579    /**
580     * Validates a documents XML schema.
581     *
582     * @return bool
583     *
584     * @throws RuntimeException When extension references a non-existent XSD file
585     */
586    public function validateSchema(\DOMDocument $dom)
587    {
588        $schemaLocations = ['http://symfony.com/schema/dic/services' => str_replace('\\', '/', __DIR__.'/schema/dic/services/services-1.0.xsd')];
589
590        if ($element = $dom->documentElement->getAttributeNS('http://www.w3.org/2001/XMLSchema-instance', 'schemaLocation')) {
591            $items = preg_split('/\s+/', $element);
592            for ($i = 0, $nb = \count($items); $i < $nb; $i += 2) {
593                if (!$this->container->hasExtension($items[$i])) {
594                    continue;
595                }
596
597                if (($extension = $this->container->getExtension($items[$i])) && false !== $extension->getXsdValidationBasePath()) {
598                    $ns = $extension->getNamespace();
599                    $path = str_replace([$ns, str_replace('http://', 'https://', $ns)], str_replace('\\', '/', $extension->getXsdValidationBasePath()).'/', $items[$i + 1]);
600
601                    if (!is_file($path)) {
602                        throw new RuntimeException(sprintf('Extension "%s" references a non-existent XSD file "%s".', \get_class($extension), $path));
603                    }
604
605                    $schemaLocations[$items[$i]] = $path;
606                }
607            }
608        }
609
610        $tmpfiles = [];
611        $imports = '';
612        foreach ($schemaLocations as $namespace => $location) {
613            $parts = explode('/', $location);
614            $locationstart = 'file:///';
615            if (0 === stripos($location, 'phar://')) {
616                $tmpfile = tempnam(sys_get_temp_dir(), 'symfony');
617                if ($tmpfile) {
618                    copy($location, $tmpfile);
619                    $tmpfiles[] = $tmpfile;
620                    $parts = explode('/', str_replace('\\', '/', $tmpfile));
621                } else {
622                    array_shift($parts);
623                    $locationstart = 'phar:///';
624                }
625            } elseif ('\\' === \DIRECTORY_SEPARATOR && 0 === strpos($location, '\\\\')) {
626                $locationstart = '';
627            }
628            $drive = '\\' === \DIRECTORY_SEPARATOR ? array_shift($parts).'/' : '';
629            $location = $locationstart.$drive.implode('/', array_map('rawurlencode', $parts));
630
631            $imports .= sprintf('  <xsd:import namespace="%s" schemaLocation="%s" />'."\n", $namespace, $location);
632        }
633
634        $source = <<<EOF
635<?xml version="1.0" encoding="utf-8" ?>
636<xsd:schema xmlns="http://symfony.com/schema"
637    xmlns:xsd="http://www.w3.org/2001/XMLSchema"
638    targetNamespace="http://symfony.com/schema"
639    elementFormDefault="qualified">
640
641    <xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
642$imports
643</xsd:schema>
644EOF
645        ;
646
647        if ($this->shouldEnableEntityLoader()) {
648            $disableEntities = libxml_disable_entity_loader(false);
649            $valid = @$dom->schemaValidateSource($source);
650            libxml_disable_entity_loader($disableEntities);
651        } else {
652            $valid = @$dom->schemaValidateSource($source);
653        }
654        foreach ($tmpfiles as $tmpfile) {
655            @unlink($tmpfile);
656        }
657
658        return $valid;
659    }
660
661    private function shouldEnableEntityLoader(): bool
662    {
663        // Version prior to 8.0 can be enabled without deprecation
664        if (\PHP_VERSION_ID < 80000) {
665            return true;
666        }
667
668        static $dom, $schema;
669        if (null === $dom) {
670            $dom = new \DOMDocument();
671            $dom->loadXML('<?xml version="1.0"?><test/>');
672
673            $tmpfile = tempnam(sys_get_temp_dir(), 'symfony');
674            register_shutdown_function(static function () use ($tmpfile) {
675                @unlink($tmpfile);
676            });
677            $schema = '<?xml version="1.0" encoding="utf-8"?>
678<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
679  <xsd:include schemaLocation="file:///'.str_replace('\\', '/', $tmpfile).'" />
680</xsd:schema>';
681            file_put_contents($tmpfile, '<?xml version="1.0" encoding="utf-8"?>
682<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
683  <xsd:element name="test" type="testType" />
684  <xsd:complexType name="testType"/>
685</xsd:schema>');
686        }
687
688        return !@$dom->schemaValidateSource($schema);
689    }
690
691    private function validateAlias(\DOMElement $alias, string $file)
692    {
693        foreach ($alias->attributes as $name => $node) {
694            if (!\in_array($name, ['alias', 'id', 'public'])) {
695                throw new InvalidArgumentException(sprintf('Invalid attribute "%s" defined for alias "%s" in "%s".', $name, $alias->getAttribute('id'), $file));
696            }
697        }
698
699        foreach ($alias->childNodes as $child) {
700            if (!$child instanceof \DOMElement || self::NS !== $child->namespaceURI) {
701                continue;
702            }
703            if (!\in_array($child->localName, ['deprecated'], true)) {
704                throw new InvalidArgumentException(sprintf('Invalid child element "%s" defined for alias "%s" in "%s".', $child->localName, $alias->getAttribute('id'), $file));
705            }
706        }
707    }
708
709    /**
710     * Validates an extension.
711     *
712     * @throws InvalidArgumentException When no extension is found corresponding to a tag
713     */
714    private function validateExtensions(\DOMDocument $dom, string $file)
715    {
716        foreach ($dom->documentElement->childNodes as $node) {
717            if (!$node instanceof \DOMElement || 'http://symfony.com/schema/dic/services' === $node->namespaceURI) {
718                continue;
719            }
720
721            // can it be handled by an extension?
722            if (!$this->container->hasExtension($node->namespaceURI)) {
723                $extensionNamespaces = array_filter(array_map(function (ExtensionInterface $ext) { return $ext->getNamespace(); }, $this->container->getExtensions()));
724                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 ? implode('", "', $extensionNamespaces) : 'none'));
725            }
726        }
727    }
728
729    /**
730     * Loads from an extension.
731     */
732    private function loadFromExtensions(\DOMDocument $xml)
733    {
734        foreach ($xml->documentElement->childNodes as $node) {
735            if (!$node instanceof \DOMElement || self::NS === $node->namespaceURI) {
736                continue;
737            }
738
739            $values = static::convertDomElementToArray($node);
740            if (!\is_array($values)) {
741                $values = [];
742            }
743
744            $this->container->loadFromExtension($node->namespaceURI, $values);
745        }
746    }
747
748    /**
749     * Converts a \DOMElement object to a PHP array.
750     *
751     * The following rules applies during the conversion:
752     *
753     *  * Each tag is converted to a key value or an array
754     *    if there is more than one "value"
755     *
756     *  * The content of a tag is set under a "value" key (<foo>bar</foo>)
757     *    if the tag also has some nested tags
758     *
759     *  * The attributes are converted to keys (<foo foo="bar"/>)
760     *
761     *  * The nested-tags are converted to keys (<foo><foo>bar</foo></foo>)
762     *
763     * @param \DOMElement $element A \DOMElement instance
764     *
765     * @return mixed
766     */
767    public static function convertDomElementToArray(\DOMElement $element)
768    {
769        return XmlUtils::convertDomElementToArray($element);
770    }
771}
772