Subversion Repositories php-qbpwcf

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
3 liveuser 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
 
12
namespace Symfony\Component\Routing\Loader;
13
 
14
use Symfony\Component\Config\Loader\FileLoader;
15
use Symfony\Component\Config\Resource\FileResource;
16
use Symfony\Component\Config\Util\XmlUtils;
17
use Symfony\Component\Routing\Loader\Configurator\Traits\HostTrait;
18
use Symfony\Component\Routing\Loader\Configurator\Traits\LocalizedRouteTrait;
19
use Symfony\Component\Routing\Loader\Configurator\Traits\PrefixTrait;
20
use Symfony\Component\Routing\RouteCollection;
21
 
22
/**
23
 * XmlFileLoader loads XML routing files.
24
 *
25
 * @author Fabien Potencier <fabien@symfony.com>
26
 * @author Tobias Schultze <http://tobion.de>
27
 */
28
class XmlFileLoader extends FileLoader
29
{
30
    use HostTrait;
31
    use LocalizedRouteTrait;
32
    use PrefixTrait;
33
 
34
    const NAMESPACE_URI = 'http://symfony.com/schema/routing';
35
    const SCHEME_PATH = '/schema/routing/routing-1.0.xsd';
36
 
37
    /**
38
     * Loads an XML file.
39
     *
40
     * @param string      $file An XML file path
41
     * @param string|null $type The resource type
42
     *
43
     * @return RouteCollection A RouteCollection instance
44
     *
45
     * @throws \InvalidArgumentException when the file cannot be loaded or when the XML cannot be
46
     *                                   parsed because it does not validate against the scheme
47
     */
48
    public function load($file, string $type = null)
49
    {
50
        $path = $this->locator->locate($file);
51
 
52
        $xml = $this->loadFile($path);
53
 
54
        $collection = new RouteCollection();
55
        $collection->addResource(new FileResource($path));
56
 
57
        // process routes and imports
58
        foreach ($xml->documentElement->childNodes as $node) {
59
            if (!$node instanceof \DOMElement) {
60
                continue;
61
            }
62
 
63
            $this->parseNode($collection, $node, $path, $file);
64
        }
65
 
66
        return $collection;
67
    }
68
 
69
    /**
70
     * Parses a node from a loaded XML file.
71
     *
72
     * @param \DOMElement $node Element to parse
73
     * @param string      $path Full path of the XML file being processed
74
     * @param string      $file Loaded file name
75
     *
76
     * @throws \InvalidArgumentException When the XML is invalid
77
     */
78
    protected function parseNode(RouteCollection $collection, \DOMElement $node, string $path, string $file)
79
    {
80
        if (self::NAMESPACE_URI !== $node->namespaceURI) {
81
            return;
82
        }
83
 
84
        switch ($node->localName) {
85
            case 'route':
86
                $this->parseRoute($collection, $node, $path);
87
                break;
88
            case 'import':
89
                $this->parseImport($collection, $node, $path, $file);
90
                break;
91
            default:
92
                throw new \InvalidArgumentException(sprintf('Unknown tag "%s" used in file "%s". Expected "route" or "import".', $node->localName, $path));
93
        }
94
    }
95
 
96
    /**
97
     * {@inheritdoc}
98
     */
99
    public function supports($resource, string $type = null)
100
    {
101
        return \is_string($resource) && 'xml' === pathinfo($resource, \PATHINFO_EXTENSION) && (!$type || 'xml' === $type);
102
    }
103
 
104
    /**
105
     * Parses a route and adds it to the RouteCollection.
106
     *
107
     * @param \DOMElement $node Element to parse that represents a Route
108
     * @param string      $path Full path of the XML file being processed
109
     *
110
     * @throws \InvalidArgumentException When the XML is invalid
111
     */
112
    protected function parseRoute(RouteCollection $collection, \DOMElement $node, string $path)
113
    {
114
        if ('' === $id = $node->getAttribute('id')) {
115
            throw new \InvalidArgumentException(sprintf('The <route> element in file "%s" must have an "id" attribute.', $path));
116
        }
117
 
118
        $schemes = preg_split('/[\s,\|]++/', $node->getAttribute('schemes'), -1, \PREG_SPLIT_NO_EMPTY);
119
        $methods = preg_split('/[\s,\|]++/', $node->getAttribute('methods'), -1, \PREG_SPLIT_NO_EMPTY);
120
 
121
        list($defaults, $requirements, $options, $condition, $paths, /* $prefixes */, $hosts) = $this->parseConfigs($node, $path);
122
 
123
        if (!$paths && '' === $node->getAttribute('path')) {
124
            throw new \InvalidArgumentException(sprintf('The <route> element in file "%s" must have a "path" attribute or <path> child nodes.', $path));
125
        }
126
 
127
        if ($paths && '' !== $node->getAttribute('path')) {
128
            throw new \InvalidArgumentException(sprintf('The <route> element in file "%s" must not have both a "path" attribute and <path> child nodes.', $path));
129
        }
130
 
131
        $routes = $this->createLocalizedRoute($collection, $id, $paths ?: $node->getAttribute('path'));
132
        $routes->addDefaults($defaults);
133
        $routes->addRequirements($requirements);
134
        $routes->addOptions($options);
135
        $routes->setSchemes($schemes);
136
        $routes->setMethods($methods);
137
        $routes->setCondition($condition);
138
 
139
        if (null !== $hosts) {
140
            $this->addHost($routes, $hosts);
141
        }
142
    }
143
 
144
    /**
145
     * Parses an import and adds the routes in the resource to the RouteCollection.
146
     *
147
     * @param \DOMElement $node Element to parse that represents a Route
148
     * @param string      $path Full path of the XML file being processed
149
     * @param string      $file Loaded file name
150
     *
151
     * @throws \InvalidArgumentException When the XML is invalid
152
     */
153
    protected function parseImport(RouteCollection $collection, \DOMElement $node, string $path, string $file)
154
    {
155
        if ('' === $resource = $node->getAttribute('resource')) {
156
            throw new \InvalidArgumentException(sprintf('The <import> element in file "%s" must have a "resource" attribute.', $path));
157
        }
158
 
159
        $type = $node->getAttribute('type');
160
        $prefix = $node->getAttribute('prefix');
161
        $schemes = $node->hasAttribute('schemes') ? preg_split('/[\s,\|]++/', $node->getAttribute('schemes'), -1, \PREG_SPLIT_NO_EMPTY) : null;
162
        $methods = $node->hasAttribute('methods') ? preg_split('/[\s,\|]++/', $node->getAttribute('methods'), -1, \PREG_SPLIT_NO_EMPTY) : null;
163
        $trailingSlashOnRoot = $node->hasAttribute('trailing-slash-on-root') ? XmlUtils::phpize($node->getAttribute('trailing-slash-on-root')) : true;
164
        $namePrefix = $node->getAttribute('name-prefix') ?: null;
165
 
166
        list($defaults, $requirements, $options, $condition, /* $paths */, $prefixes, $hosts) = $this->parseConfigs($node, $path);
167
 
168
        if ('' !== $prefix && $prefixes) {
169
            throw new \InvalidArgumentException(sprintf('The <route> element in file "%s" must not have both a "prefix" attribute and <prefix> child nodes.', $path));
170
        }
171
 
172
        $exclude = [];
173
        foreach ($node->childNodes as $child) {
174
            if ($child instanceof \DOMElement && $child->localName === $exclude && self::NAMESPACE_URI === $child->namespaceURI) {
175
                $exclude[] = $child->nodeValue;
176
            }
177
        }
178
 
179
        if ($node->hasAttribute('exclude')) {
180
            if ($exclude) {
181
                throw new \InvalidArgumentException('You cannot use both the attribute "exclude" and <exclude> tags at the same time.');
182
            }
183
            $exclude = [$node->getAttribute('exclude')];
184
        }
185
 
186
        $this->setCurrentDir(\dirname($path));
187
 
188
        /** @var RouteCollection[] $imported */
189
        $imported = $this->import($resource, ('' !== $type ? $type : null), false, $file, $exclude) ?: [];
190
 
191
        if (!\is_array($imported)) {
192
            $imported = [$imported];
193
        }
194
 
195
        foreach ($imported as $subCollection) {
196
            $this->addPrefix($subCollection, $prefixes ?: $prefix, $trailingSlashOnRoot);
197
 
198
            if (null !== $hosts) {
199
                $this->addHost($subCollection, $hosts);
200
            }
201
 
202
            if (null !== $condition) {
203
                $subCollection->setCondition($condition);
204
            }
205
            if (null !== $schemes) {
206
                $subCollection->setSchemes($schemes);
207
            }
208
            if (null !== $methods) {
209
                $subCollection->setMethods($methods);
210
            }
211
            if (null !== $namePrefix) {
212
                $subCollection->addNamePrefix($namePrefix);
213
            }
214
            $subCollection->addDefaults($defaults);
215
            $subCollection->addRequirements($requirements);
216
            $subCollection->addOptions($options);
217
 
218
            $collection->addCollection($subCollection);
219
        }
220
    }
221
 
222
    /**
223
     * Loads an XML file.
224
     *
225
     * @param string $file An XML file path
226
     *
227
     * @return \DOMDocument
228
     *
229
     * @throws \InvalidArgumentException When loading of XML file fails because of syntax errors
230
     *                                   or when the XML structure is not as expected by the scheme -
231
     *                                   see validate()
232
     */
233
    protected function loadFile(string $file)
234
    {
235
        return XmlUtils::loadFile($file, __DIR__.static::SCHEME_PATH);
236
    }
237
 
238
    /**
239
     * Parses the config elements (default, requirement, option).
240
     *
241
     * @throws \InvalidArgumentException When the XML is invalid
242
     */
243
    private function parseConfigs(\DOMElement $node, string $path): array
244
    {
245
        $defaults = [];
246
        $requirements = [];
247
        $options = [];
248
        $condition = null;
249
        $prefixes = [];
250
        $paths = [];
251
        $hosts = [];
252
 
253
        /** @var \DOMElement $n */
254
        foreach ($node->getElementsByTagNameNS(self::NAMESPACE_URI, '*') as $n) {
255
            if ($node !== $n->parentNode) {
256
                continue;
257
            }
258
 
259
            switch ($n->localName) {
260
                case 'path':
261
                    $paths[$n->getAttribute('locale')] = trim($n->textContent);
262
                    break;
263
                case 'host':
264
                    $hosts[$n->getAttribute('locale')] = trim($n->textContent);
265
                    break;
266
                case 'prefix':
267
                    $prefixes[$n->getAttribute('locale')] = trim($n->textContent);
268
                    break;
269
                case 'default':
270
                    if ($this->isElementValueNull($n)) {
271
                        $defaults[$n->getAttribute('key')] = null;
272
                    } else {
273
                        $defaults[$n->getAttribute('key')] = $this->parseDefaultsConfig($n, $path);
274
                    }
275
 
276
                    break;
277
                case 'requirement':
278
                    $requirements[$n->getAttribute('key')] = trim($n->textContent);
279
                    break;
280
                case 'option':
281
                    $options[$n->getAttribute('key')] = XmlUtils::phpize(trim($n->textContent));
282
                    break;
283
                case 'condition':
284
                    $condition = trim($n->textContent);
285
                    break;
286
                default:
287
                    throw new \InvalidArgumentException(sprintf('Unknown tag "%s" used in file "%s". Expected "default", "requirement", "option" or "condition".', $n->localName, $path));
288
            }
289
        }
290
 
291
        if ($controller = $node->getAttribute('controller')) {
292
            if (isset($defaults['_controller'])) {
293
                $name = $node->hasAttribute('id') ? sprintf('"%s".', $node->getAttribute('id')) : sprintf('the "%s" tag.', $node->tagName);
294
 
295
                throw new \InvalidArgumentException(sprintf('The routing file "%s" must not specify both the "controller" attribute and the defaults key "_controller" for ', $path).$name);
296
            }
297
 
298
            $defaults['_controller'] = $controller;
299
        }
300
        if ($node->hasAttribute('locale')) {
301
            $defaults['_locale'] = $node->getAttribute('locale');
302
        }
303
        if ($node->hasAttribute('format')) {
304
            $defaults['_format'] = $node->getAttribute('format');
305
        }
306
        if ($node->hasAttribute('utf8')) {
307
            $options['utf8'] = XmlUtils::phpize($node->getAttribute('utf8'));
308
        }
309
        if ($stateless = $node->getAttribute('stateless')) {
310
            if (isset($defaults['_stateless'])) {
311
                $name = $node->hasAttribute('id') ? sprintf('"%s".', $node->getAttribute('id')) : sprintf('the "%s" tag.', $node->tagName);
312
 
313
                throw new \InvalidArgumentException(sprintf('The routing file "%s" must not specify both the "stateless" attribute and the defaults key "_stateless" for ', $path).$name);
314
            }
315
 
316
            $defaults['_stateless'] = XmlUtils::phpize($stateless);
317
        }
318
 
319
        if (!$hosts) {
320
            $hosts = $node->hasAttribute('host') ? $node->getAttribute('host') : null;
321
        }
322
 
323
        return [$defaults, $requirements, $options, $condition, $paths, $prefixes, $hosts];
324
    }
325
 
326
    /**
327
     * Parses the "default" elements.
328
     *
329
     * @return array|bool|float|int|string|null The parsed value of the "default" element
330
     */
331
    private function parseDefaultsConfig(\DOMElement $element, string $path)
332
    {
333
        if ($this->isElementValueNull($element)) {
334
            return null;
335
        }
336
 
337
        // Check for existing element nodes in the default element. There can
338
        // only be a single element inside a default element. So this element
339
        // (if one was found) can safely be returned.
340
        foreach ($element->childNodes as $child) {
341
            if (!$child instanceof \DOMElement) {
342
                continue;
343
            }
344
 
345
            if (self::NAMESPACE_URI !== $child->namespaceURI) {
346
                continue;
347
            }
348
 
349
            return $this->parseDefaultNode($child, $path);
350
        }
351
 
352
        // If the default element doesn't contain a nested "bool", "int", "float",
353
        // "string", "list", or "map" element, the element contents will be treated
354
        // as the string value of the associated default option.
355
        return trim($element->textContent);
356
    }
357
 
358
    /**
359
     * Recursively parses the value of a "default" element.
360
     *
361
     * @return array|bool|float|int|string The parsed value
362
     *
363
     * @throws \InvalidArgumentException when the XML is invalid
364
     */
365
    private function parseDefaultNode(\DOMElement $node, string $path)
366
    {
367
        if ($this->isElementValueNull($node)) {
368
            return null;
369
        }
370
 
371
        switch ($node->localName) {
372
            case 'bool':
373
                return 'true' === trim($node->nodeValue) || '1' === trim($node->nodeValue);
374
            case 'int':
375
                return (int) trim($node->nodeValue);
376
            case 'float':
377
                return (float) trim($node->nodeValue);
378
            case 'string':
379
                return trim($node->nodeValue);
380
            case 'list':
381
                $list = [];
382
 
383
                foreach ($node->childNodes as $element) {
384
                    if (!$element instanceof \DOMElement) {
385
                        continue;
386
                    }
387
 
388
                    if (self::NAMESPACE_URI !== $element->namespaceURI) {
389
                        continue;
390
                    }
391
 
392
                    $list[] = $this->parseDefaultNode($element, $path);
393
                }
394
 
395
                return $list;
396
            case 'map':
397
                $map = [];
398
 
399
                foreach ($node->childNodes as $element) {
400
                    if (!$element instanceof \DOMElement) {
401
                        continue;
402
                    }
403
 
404
                    if (self::NAMESPACE_URI !== $element->namespaceURI) {
405
                        continue;
406
                    }
407
 
408
                    $map[$element->getAttribute('key')] = $this->parseDefaultNode($element, $path);
409
                }
410
 
411
                return $map;
412
            default:
413
                throw new \InvalidArgumentException(sprintf('Unknown tag "%s" used in file "%s". Expected "bool", "int", "float", "string", "list", or "map".', $node->localName, $path));
414
        }
415
    }
416
 
417
    private function isElementValueNull(\DOMElement $element): bool
418
    {
419
        $namespaceUri = 'http://www.w3.org/2001/XMLSchema-instance';
420
 
421
        if (!$element->hasAttributeNS($namespaceUri, 'nil')) {
422
            return false;
423
        }
424
 
425
        return 'true' === $element->getAttributeNS($namespaceUri, 'nil') || '1' === $element->getAttributeNS($namespaceUri, 'nil');
426
    }
427
}