Completed
Push — master ( 01e9a3...b0156c )
by Kamil
18:54
created

XmlFileLoader::validateExtensions()   B

Complexity

Conditions 6
Paths 4

Size

Total Lines 20
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 20
rs 8.8571
cc 6
eloc 12
nc 4
nop 2
1
<?php // @codingStandardsIgnoreStart
2
3
/*
4
 * This file is part of the Symfony package.
5
 *
6
 * (c) Fabien Potencier <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Sylius\Behat\MultiContainerExtension\Loader;
13
14
use Sylius\Behat\MultiContainerExtension\ContainerConfiguration;
15
use Symfony\Component\Config\FileLocatorInterface;
16
use Symfony\Component\Config\Resource\FileResource;
17
use Symfony\Component\Config\Util\XmlUtils;
18
use Symfony\Component\DependencyInjection\ContainerBuilder;
19
use Symfony\Component\DependencyInjection\DefinitionDecorator;
20
use Symfony\Component\DependencyInjection\ContainerInterface;
21
use Symfony\Component\DependencyInjection\Alias;
22
use Symfony\Component\DependencyInjection\Definition;
23
use Symfony\Component\DependencyInjection\Loader\FileLoader;
24
use Symfony\Component\DependencyInjection\Reference;
25
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
26
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
27
use Symfony\Component\ExpressionLanguage\Expression;
28
29
/**
30
 * XmlFileLoader loads XML files service definitions.
31
 *
32
 * @author Fabien Potencier <[email protected]>
33
 * @author Kamil Kokot <[email protected]>
34
 */
35
class XmlFileLoader extends FileLoader
36
{
37
    const NS = 'http://symfony.com/schema/dic/services';
38
39
    /**
40
     * @var ContainerConfiguration
41
     */
42
    private $containerConfiguration;
43
44
    /**
45
     * {@inheritdoc}
46
     *
47
     * @param ContainerConfiguration $containerConfiguration
48
     */
49
    public function __construct(
50
        ContainerBuilder $container,
51
        FileLocatorInterface $locator,
52
        ContainerConfiguration $containerConfiguration
53
    ) {
54
        parent::__construct($container, $locator);
55
56
        $this->containerConfiguration = $containerConfiguration;
57
    }
58
59
    /**
60
     * {@inheritdoc}
61
     */
62
    public function load($resource, $type = null)
63
    {
64
        $path = $this->locator->locate($resource);
65
66
        $xml = $this->parseFileToDOM($path);
67
68
        $this->container->addResource(new FileResource($path));
69
70
        // anonymous services
71
        $this->processAnonymousServices($xml, $path);
72
73
        // imports
74
        $this->parseImports($xml, $path);
75
76
        // parameters
77
        $this->parseParameters($xml);
78
79
        // extensions
80
        $this->loadFromExtensions($xml);
81
82
        // services
83
        $this->parseDefinitions($xml, $path);
84
    }
85
86
    /**
87
     * {@inheritdoc}
88
     */
89
    public function supports($resource, $type = null)
90
    {
91
        return is_string($resource) && 'xml' === pathinfo($resource, PATHINFO_EXTENSION);
92
    }
93
94
    /**
95
     * Parses parameters.
96
     *
97
     * @param \DOMDocument $xml
98
     */
99
    private function parseParameters(\DOMDocument $xml)
100
    {
101
        if ($parameters = $this->getChildren($xml->documentElement, 'parameters')) {
102
            $this->container->getParameterBag()->add($this->getArgumentsAsPhp($parameters[0], 'parameter'));
103
        }
104
    }
105
106
    /**
107
     * Parses imports.
108
     *
109
     * @param \DOMDocument $xml
110
     * @param string       $file
111
     */
112
    private function parseImports(\DOMDocument $xml, $file)
113
    {
114
        $xpath = new \DOMXPath($xml);
115
        $xpath->registerNamespace('container', self::NS);
116
117
        if (false === $imports = $xpath->query('//container:imports/container:import')) {
118
            return;
119
        }
120
121
        foreach ($imports as $import) {
122
            $this->setCurrentDir(dirname($file));
123
            $this->import($import->getAttribute('resource'), null, (bool) XmlUtils::phpize($import->getAttribute('ignore-errors')), $file);
124
        }
125
    }
126
127
    /**
128
     * Parses multiple definitions.
129
     *
130
     * @param \DOMDocument $xml
131
     * @param string       $file
132
     */
133
    private function parseDefinitions(\DOMDocument $xml, $file)
134
    {
135
        $xpath = new \DOMXPath($xml);
136
        $xpath->registerNamespace('container', self::NS);
137
138
        if (false === $services = $xpath->query('//container:services/container:service')) {
139
            return;
140
        }
141
142
        foreach ($services as $service) {
143
            if (null !== $definition = $this->parseDefinition($service, $file)) {
144
                $this->container->setDefinition((string) $service->getAttribute('id'), $definition);
145
            }
146
        }
147
    }
148
149
    /**
150
     * Parses an individual Definition.
151
     *
152
     * @param \DOMElement $service
153
     * @param string      $file
154
     *
155
     * @return Definition|null
156
     */
157
    private function parseDefinition(\DOMElement $service, $file)
158
    {
159
        if ($alias = $service->getAttribute('alias')) {
160
            $public = true;
161
            if ($publicAttr = $service->getAttribute('public')) {
162
                $public = XmlUtils::phpize($publicAttr);
163
            }
164
            $this->container->setAlias((string) $service->getAttribute('id'), new Alias($alias, $public));
165
166
            return;
167
        }
168
169
        if ($parent = $service->getAttribute('parent')) {
170
            $definition = new DefinitionDecorator($parent);
171
        } else {
172
            $definition = new Definition();
173
        }
174
175
        foreach (array('class', 'scope', 'public', 'factory-class', 'factory-method', 'factory-service', 'synthetic', 'lazy', 'abstract') as $key) {
176
            if ($value = $service->getAttribute($key)) {
177
                if (in_array($key, array('factory-class', 'factory-method', 'factory-service'))) {
178
                    @trigger_error(sprintf('The "%s" attribute of service "%s" in file "%s" is deprecated since version 2.6 and will be removed in 3.0. Use the "factory" element instead.', $key, (string) $service->getAttribute('id'), $file), E_USER_DEPRECATED);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
179
                }
180
                $method = 'set'.str_replace('-', '', $key);
181
                $definition->$method(XmlUtils::phpize($value));
182
            }
183
        }
184
185
        if ($value = $service->getAttribute('synchronized')) {
186
            $triggerDeprecation = 'request' !== (string) $service->getAttribute('id');
187
188
            if ($triggerDeprecation) {
189
                @trigger_error(sprintf('The "synchronized" attribute of service "%s" in file "%s" is deprecated since version 2.7 and will be removed in 3.0.', (string) $service->getAttribute('id'), $file), E_USER_DEPRECATED);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
190
            }
191
192
            $definition->setSynchronized(XmlUtils::phpize($value), $triggerDeprecation);
0 ignored issues
show
Deprecated Code introduced by
The method Symfony\Component\Depend...tion::setSynchronized() has been deprecated with message: since version 2.7, will be removed in 3.0.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
193
        }
194
195
        if ($files = $this->getChildren($service, 'file')) {
196
            $definition->setFile($files[0]->nodeValue);
197
        }
198
199
        $definition->setArguments($this->getArgumentsAsPhp($service, 'argument'));
200
        $definition->setProperties($this->getArgumentsAsPhp($service, 'property'));
201
202
        if ($factories = $this->getChildren($service, 'factory')) {
203
            $factory = $factories[0];
204
            if ($function = $factory->getAttribute('function')) {
205
                $definition->setFactory($function);
206
            } else {
207
                $factoryService = $this->getChildren($factory, 'service');
208
209
                if (isset($factoryService[0])) {
210
                    $class = $this->parseDefinition($factoryService[0], $file);
211
                } elseif ($childService = $factory->getAttribute('service')) {
212
                    $class = new Reference($childService, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, false);
213
                } else {
214
                    $class = $factory->getAttribute('class');
215
                }
216
217
                $definition->setFactory(array($class, $factory->getAttribute('method')));
218
            }
219
        }
220
221
        if ($configurators = $this->getChildren($service, 'configurator')) {
222
            $configurator = $configurators[0];
223
            if ($function = $configurator->getAttribute('function')) {
224
                $definition->setConfigurator($function);
225
            } else {
226
                $configuratorService = $this->getChildren($configurator, 'service');
227
228
                if (isset($configuratorService[0])) {
229
                    $class = $this->parseDefinition($configuratorService[0], $file);
230
                } elseif ($childService = $configurator->getAttribute('service')) {
231
                    $class = new Reference($childService, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, false);
232
                } else {
233
                    $class = $configurator->getAttribute('class');
234
                }
235
236
                $definition->setConfigurator(array($class, $configurator->getAttribute('method')));
237
            }
238
        }
239
240
        foreach ($this->getChildren($service, 'call') as $call) {
241
            $definition->addMethodCall($call->getAttribute('method'), $this->getArgumentsAsPhp($call, 'argument'));
242
        }
243
244
        foreach ($this->getChildren($service, 'tag') as $tag) {
245
            $parameters = array();
246
            foreach ($tag->attributes as $name => $node) {
247
                if ('name' === $name) {
248
                    continue;
249
                }
250
251
                if (false !== strpos($name, '-') && false === strpos($name, '_') && !array_key_exists($normalizedName = str_replace('-', '_', $name), $parameters)) {
252
                    $parameters[$normalizedName] = XmlUtils::phpize($node->nodeValue);
253
                }
254
                // keep not normalized key for BC too
255
                $parameters[$name] = XmlUtils::phpize($node->nodeValue);
256
            }
257
258
            $definition->addTag($tag->getAttribute('name'), $parameters);
259
        }
260
261
        if ($value = $service->getAttribute('decorates')) {
262
            $renameId = $service->hasAttribute('decoration-inner-name') ? $service->getAttribute('decoration-inner-name') : null;
263
            $definition->setDecoratedService($value, $renameId);
264
        }
265
266
        return $definition;
267
    }
268
269
    /**
270
     * Parses a XML file to a \DOMDocument.
271
     *
272
     * @param string $file Path to a file
273
     *
274
     * @return \DOMDocument
275
     *
276
     * @throws InvalidArgumentException When loading of XML file returns error
277
     */
278
    private function parseFileToDOM($file)
279
    {
280
        try {
281
            $dom = XmlUtils::loadFile($file, array($this, 'validateSchema'));
282
        } catch (\InvalidArgumentException $e) {
283
            throw new InvalidArgumentException(sprintf('Unable to parse file "%s".', $file), $e->getCode(), $e);
284
        }
285
286
        $this->validateExtensions($dom, $file);
287
288
        return $dom;
289
    }
290
291
    /**
292
     * Processes anonymous services.
293
     *
294
     * @param \DOMDocument $xml
295
     * @param string       $file
296
     */
297
    private function processAnonymousServices(\DOMDocument $xml, $file)
298
    {
299
        $definitions = array();
300
        $count = 0;
301
302
        $xpath = new \DOMXPath($xml);
303
        $xpath->registerNamespace('container', self::NS);
304
305
        // anonymous services as arguments/properties
306
        if (false !== $nodes = $xpath->query('//container:argument[@type="service"][not(@id)]|//container:property[@type="service"][not(@id)]')) {
307
            foreach ($nodes as $node) {
308
                // give it a unique name
309
                $id = sprintf('%s_%d', hash('sha256', $file), ++$count);
310
                $node->setAttribute('id', $id);
311
312
                if ($services = $this->getChildren($node, 'service')) {
313
                    $definitions[$id] = array($services[0], $file, false);
314
                    $services[0]->setAttribute('id', $id);
315
                }
316
            }
317
        }
318
319
        // anonymous services "in the wild"
320
        if (false !== $nodes = $xpath->query('//container:services/container:service[not(@id)]')) {
321
            foreach ($nodes as $node) {
322
                // give it a unique name
323
                $id = sprintf('%s_%d', hash('sha256', $file), ++$count);
324
                $node->setAttribute('id', $id);
325
326
                if ($services = $this->getChildren($node, 'service')) {
327
                    $definitions[$id] = array($node, $file, true);
328
                    $services[0]->setAttribute('id', $id);
329
                }
330
            }
331
        }
332
333
        // resolve definitions
334
        krsort($definitions);
335
        foreach ($definitions as $id => $def) {
336
            list($domElement, $file, $wild) = $def;
337
338
            // anonymous services are always private
339
            // we could not use the constant false here, because of XML parsing
340
            $domElement->setAttribute('public', 'false');
341
342
            if (null !== $definition = $this->parseDefinition($domElement, $file)) {
343
                $this->container->setDefinition($id, $definition);
344
            }
345
346
            if (true === $wild) {
347
                $tmpDomElement = new \DOMElement('_services', null, self::NS);
348
                $domElement->parentNode->replaceChild($tmpDomElement, $domElement);
349
                $tmpDomElement->setAttribute('id', $id);
350
            } else {
351
                $domElement->parentNode->removeChild($domElement);
352
            }
353
        }
354
    }
355
356
    /**
357
     * Returns arguments as valid php types.
358
     *
359
     * @param \DOMElement $node
360
     * @param string      $name
361
     * @param bool        $lowercase
362
     *
363
     * @return mixed
364
     */
365
    private function getArgumentsAsPhp(\DOMElement $node, $name, $lowercase = true)
366
    {
367
        $arguments = array();
368
        foreach ($this->getChildren($node, $name) as $arg) {
369
            if ($arg->hasAttribute('name')) {
370
                $arg->setAttribute('key', $arg->getAttribute('name'));
371
            }
372
373
            if (!$arg->hasAttribute('key')) {
374
                $key = !$arguments ? 0 : max(array_keys($arguments)) + 1;
375
            } else {
376
                $key = $arg->getAttribute('key');
377
            }
378
379
            // parameter keys are case insensitive
380
            if ('parameter' == $name && $lowercase) {
381
                $key = strtolower($key);
382
            }
383
384
            // this is used by DefinitionDecorator to overwrite a specific
385
            // argument of the parent definition
386
            if ($arg->hasAttribute('index')) {
387
                $key = 'index_'.$arg->getAttribute('index');
388
            }
389
390
            switch ($arg->getAttribute('type')) {
391
                case 'service':
392
                    $arguments[$key] = $this->containerConfiguration->createReferenceFor($arg->getAttribute('id'), $arg->getAttribute('container') ?: null);
393
                    break;
394
                case 'expression':
395
                    $arguments[$key] = new Expression($arg->nodeValue);
396
                    break;
397
                case 'collection':
398
                    $arguments[$key] = $this->getArgumentsAsPhp($arg, $name, false);
399
                    break;
400
                case 'string':
401
                    $arguments[$key] = $arg->nodeValue;
402
                    break;
403
                case 'constant':
404
                    $arguments[$key] = constant($arg->nodeValue);
405
                    break;
406
                default:
407
                    $arguments[$key] = XmlUtils::phpize($arg->nodeValue);
408
            }
409
        }
410
411
        return $arguments;
412
    }
413
414
    /**
415
     * Get child elements by name.
416
     *
417
     * @param \DOMNode $node
418
     * @param mixed    $name
419
     *
420
     * @return array
421
     */
422
    private function getChildren(\DOMNode $node, $name)
423
    {
424
        $children = array();
425
        foreach ($node->childNodes as $child) {
426
            if ($child instanceof \DOMElement && $child->localName === $name && $child->namespaceURI === self::NS) {
427
                $children[] = $child;
428
            }
429
        }
430
431
        return $children;
432
    }
433
434
    /**
435
     * Validates a documents XML schema.
436
     *
437
     * @param \DOMDocument $dom
438
     *
439
     * @return bool
440
     *
441
     * @throws RuntimeException When extension references a non-existent XSD file
442
     */
443
    public function validateSchema(\DOMDocument $dom)
444
    {
445
        $schemaLocations = array('http://symfony.com/schema/dic/services' => str_replace('\\', '/', __DIR__.'/schema/dic/services/services-1.0.xsd'));
446
447
        if ($element = $dom->documentElement->getAttributeNS('http://www.w3.org/2001/XMLSchema-instance', 'schemaLocation')) {
448
            $items = preg_split('/\s+/', $element);
449
            for ($i = 0, $nb = count($items); $i < $nb; $i += 2) {
450
                if (!$this->container->hasExtension($items[$i])) {
451
                    continue;
452
                }
453
454
                if (($extension = $this->container->getExtension($items[$i])) && false !== $extension->getXsdValidationBasePath()) {
455
                    $path = str_replace($extension->getNamespace(), str_replace('\\', '/', $extension->getXsdValidationBasePath()).'/', $items[$i + 1]);
456
457
                    if (!is_file($path)) {
458
                        throw new RuntimeException(sprintf('Extension "%s" references a non-existent XSD file "%s"', get_class($extension), $path));
459
                    }
460
461
                    $schemaLocations[$items[$i]] = $path;
462
                }
463
            }
464
        }
465
466
        $tmpfiles = array();
467
        $imports = '';
468
        foreach ($schemaLocations as $namespace => $location) {
469
            $parts = explode('/', $location);
470
            if (0 === stripos($location, 'phar://')) {
471
                $tmpfile = tempnam(sys_get_temp_dir(), 'sf2');
472
                if ($tmpfile) {
473
                    copy($location, $tmpfile);
474
                    $tmpfiles[] = $tmpfile;
475
                    $parts = explode('/', str_replace('\\', '/', $tmpfile));
476
                }
477
            }
478
            $drive = '\\' === DIRECTORY_SEPARATOR ? array_shift($parts).'/' : '';
479
            $location = 'file:///'.$drive.implode('/', array_map('rawurlencode', $parts));
480
481
            $imports .= sprintf('  <xsd:import namespace="%s" schemaLocation="%s" />'."\n", $namespace, $location);
482
        }
483
484
        $source = <<<EOF
485
<?xml version="1.0" encoding="utf-8" ?>
486
<xsd:schema xmlns="http://symfony.com/schema"
487
    xmlns:xsd="http://www.w3.org/2001/XMLSchema"
488
    targetNamespace="http://symfony.com/schema"
489
    elementFormDefault="qualified">
490
491
    <xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
492
$imports
493
</xsd:schema>
494
EOF
495
        ;
496
497
        $valid = @$dom->schemaValidateSource($source);
498
499
        foreach ($tmpfiles as $tmpfile) {
500
            @unlink($tmpfile);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
501
        }
502
503
        return $valid;
504
    }
505
506
    /**
507
     * Validates an extension.
508
     *
509
     * @param \DOMDocument $dom
510
     * @param string       $file
511
     *
512
     * @throws InvalidArgumentException When no extension is found corresponding to a tag
513
     */
514
    private function validateExtensions(\DOMDocument $dom, $file)
515
    {
516
        foreach ($dom->documentElement->childNodes as $node) {
517
            if (!$node instanceof \DOMElement || 'http://symfony.com/schema/dic/services' === $node->namespaceURI) {
518
                continue;
519
            }
520
521
            // can it be handled by an extension?
522
            if (!$this->container->hasExtension($node->namespaceURI)) {
523
                $extensionNamespaces = array_filter(array_map(function ($ext) { return $ext->getNamespace(); }, $this->container->getExtensions()));
524
                throw new InvalidArgumentException(sprintf(
525
                    'There is no extension able to load the configuration for "%s" (in %s). Looked for namespace "%s", found %s',
526
                    $node->tagName,
527
                    $file,
528
                    $node->namespaceURI,
529
                    $extensionNamespaces ? sprintf('"%s"', implode('", "', $extensionNamespaces)) : 'none'
530
                ));
531
            }
532
        }
533
    }
534
535
    /**
536
     * Loads from an extension.
537
     *
538
     * @param \DOMDocument $xml
539
     */
540
    private function loadFromExtensions(\DOMDocument $xml)
541
    {
542
        foreach ($xml->documentElement->childNodes as $node) {
543
            if (!$node instanceof \DOMElement || $node->namespaceURI === self::NS) {
544
                continue;
545
            }
546
547
            $values = static::convertDomElementToArray($node);
548
            if (!is_array($values)) {
549
                $values = array();
550
            }
551
552
            $this->container->loadFromExtension($node->namespaceURI, $values);
553
        }
554
    }
555
556
    /**
557
     * Converts a \DomElement object to a PHP array.
558
     *
559
     * The following rules applies during the conversion:
560
     *
561
     *  * Each tag is converted to a key value or an array
562
     *    if there is more than one "value"
563
     *
564
     *  * The content of a tag is set under a "value" key (<foo>bar</foo>)
565
     *    if the tag also has some nested tags
566
     *
567
     *  * The attributes are converted to keys (<foo foo="bar"/>)
568
     *
569
     *  * The nested-tags are converted to keys (<foo><foo>bar</foo></foo>)
570
     *
571
     * @param \DomElement $element A \DomElement instance
572
     *
573
     * @return array A PHP array
574
     */
575
    public static function convertDomElementToArray(\DomElement $element)
576
    {
577
        return XmlUtils::convertDomElementToArray($element);
578
    }
579
}
580