1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/* |
4
|
|
|
* This file is part of the FOSRestBundle package. |
5
|
|
|
* |
6
|
|
|
* (c) FriendsOfSymfony <http://friendsofsymfony.github.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 FOS\RestBundle\Routing\Loader; |
13
|
|
|
|
14
|
|
|
use FOS\RestBundle\Routing\RestRouteCollection; |
15
|
|
|
use Symfony\Component\Config\FileLocatorInterface; |
16
|
|
|
use Symfony\Component\Config\Util\XmlUtils; |
17
|
|
|
use Symfony\Component\Routing\Loader\XmlFileLoader; |
18
|
|
|
use Symfony\Component\Routing\RouteCollection; |
19
|
|
|
|
20
|
|
|
/** |
21
|
|
|
* RestXmlCollectionLoader XML file collections loader. |
22
|
|
|
* |
23
|
|
|
* @author Donald Tyler <[email protected]> |
24
|
|
|
*/ |
25
|
|
|
class RestXmlCollectionLoader extends XmlFileLoader |
26
|
|
|
{ |
27
|
|
|
protected $collectionParents = []; |
28
|
|
|
private $processor; |
29
|
|
|
private $includeFormat; |
30
|
|
|
private $formats; |
31
|
|
|
private $defaultFormat; |
32
|
|
|
|
33
|
|
|
/** |
34
|
|
|
* Initializes xml loader. |
35
|
|
|
* |
36
|
|
|
* @param FileLocatorInterface $locator |
37
|
|
|
* @param RestRouteProcessor $processor |
38
|
|
|
* @param bool $includeFormat |
39
|
|
|
* @param string[] $formats |
40
|
|
|
* @param string $defaultFormat |
41
|
|
|
*/ |
42
|
22 |
|
public function __construct( |
43
|
|
|
FileLocatorInterface $locator, |
44
|
|
|
RestRouteProcessor $processor, |
45
|
|
|
$includeFormat = true, |
46
|
|
|
array $formats = [], |
47
|
|
|
$defaultFormat = null |
48
|
|
|
) { |
49
|
22 |
|
parent::__construct($locator); |
50
|
|
|
|
51
|
22 |
|
$this->processor = $processor; |
52
|
22 |
|
$this->includeFormat = $includeFormat; |
53
|
22 |
|
$this->formats = $formats; |
54
|
22 |
|
$this->defaultFormat = $defaultFormat; |
55
|
22 |
|
} |
56
|
|
|
|
57
|
|
|
/** |
58
|
|
|
* {@inheritdoc} |
59
|
|
|
*/ |
60
|
9 |
|
protected function parseNode(RouteCollection $collection, \DOMElement $node, $path, $file) |
61
|
|
|
{ |
62
|
9 |
|
switch ($node->tagName) { |
63
|
9 |
|
case 'route': |
64
|
6 |
|
$this->parseRoute($collection, $node, $path); |
65
|
6 |
|
|
66
|
3 |
|
break; |
67
|
3 |
|
case 'import': |
68
|
3 |
|
$name = (string) $node->getAttribute('id'); |
69
|
3 |
|
$resource = (string) $node->getAttribute('resource'); |
70
|
3 |
|
$prefix = (string) $node->getAttribute('prefix'); |
71
|
3 |
|
$namePrefix = (string) $node->getAttribute('name-prefix'); |
72
|
3 |
|
$parent = (string) $node->getAttribute('parent'); |
73
|
3 |
|
$type = (string) $node->getAttribute('type'); |
74
|
3 |
|
$host = isset($config['host']) ? $config['host'] : null; |
|
|
|
|
75
|
|
|
$currentDir = dirname($path); |
76
|
3 |
|
|
77
|
3 |
|
$parents = []; |
78
|
3 |
View Code Duplication |
if (!empty($parent)) { |
79
|
1 |
|
if (!isset($this->collectionParents[$parent])) { |
80
|
|
|
throw new \InvalidArgumentException(sprintf('Cannot find parent resource with name %s', $parent)); |
81
|
|
|
} |
82
|
2 |
|
|
83
|
2 |
|
$parents = $this->collectionParents[$parent]; |
84
|
|
|
} |
85
|
2 |
|
|
86
|
|
|
$imported = $this->processor->importResource($this, $resource, $parents, $prefix, $namePrefix, $type, $currentDir); |
87
|
2 |
|
|
88
|
2 |
|
if (!empty($name) && $imported instanceof RestRouteCollection) { |
89
|
2 |
|
$parents[] = (!empty($prefix) ? $prefix.'/' : '').$imported->getSingularName(); |
90
|
|
|
$prefix = null; |
91
|
2 |
|
|
92
|
2 |
|
$this->collectionParents[$name] = $parents; |
93
|
|
|
} |
94
|
2 |
|
|
95
|
|
|
if (!empty($host)) { |
96
|
|
|
$imported->setHost($host); |
97
|
|
|
} |
98
|
2 |
|
|
99
|
2 |
|
$imported->addPrefix($prefix); |
100
|
2 |
|
$collection->addCollection($imported); |
101
|
|
|
|
102
|
|
|
break; |
103
|
8 |
|
default: |
104
|
8 |
|
throw new \InvalidArgumentException(sprintf('Unable to parse tag "%s"', $node->tagName)); |
105
|
|
|
} |
106
|
|
|
} |
107
|
|
|
|
108
|
|
|
/** |
109
|
6 |
|
* {@inheritdoc} |
110
|
|
|
*/ |
111
|
6 |
|
protected function parseRoute(RouteCollection $collection, \DOMElement $node, $path) |
112
|
5 |
|
{ |
113
|
|
|
if ($this->includeFormat) { |
114
|
5 |
|
$path = $node->getAttribute('path'); |
115
|
5 |
|
// append format placeholder if not present |
116
|
5 |
|
if (false === strpos($path, '{_format}')) { |
117
|
|
|
$node->setAttribute('path', $path.'.{_format}'); |
118
|
|
|
} |
119
|
5 |
|
|
120
|
5 |
|
// set format requirement if configured globally |
121
|
5 |
|
$requirements = $node->getElementsByTagNameNS(self::NAMESPACE_URI, 'requirement'); |
122
|
|
|
$format = null; |
123
|
|
|
for ($i = 0; $i < $requirements->length; ++$i) { |
124
|
|
|
$item = $requirements->item($i); |
125
|
|
|
if ($item instanceof \DOMElement && $item->hasAttribute('_format')) { |
126
|
|
|
$format = $item->getAttribute('_format'); |
127
|
|
|
|
128
|
|
|
break; |
129
|
5 |
|
} |
130
|
5 |
|
} |
131
|
5 |
|
|
132
|
5 |
|
if (null === $format && !empty($this->formats)) { |
133
|
5 |
|
$requirement = $node->ownerDocument->createElementNs( |
134
|
5 |
|
self::NAMESPACE_URI, |
135
|
5 |
|
'requirement', |
136
|
5 |
|
implode('|', array_keys($this->formats)) |
137
|
5 |
|
); |
138
|
5 |
|
$requirement->setAttribute('key', '_format'); |
139
|
|
|
$node->appendChild($requirement); |
140
|
|
|
} |
141
|
6 |
|
} |
142
|
1 |
|
|
143
|
1 |
|
// set the default format if configured |
144
|
1 |
|
if (null !== $this->defaultFormat) { |
145
|
1 |
|
$defaultFormatNode = $node->ownerDocument->createElementNS( |
146
|
1 |
|
self::NAMESPACE_URI, |
147
|
1 |
|
'default', |
148
|
1 |
|
$this->defaultFormat |
149
|
1 |
|
); |
150
|
|
|
$defaultFormatNode->setAttribute('key', '_format'); |
151
|
6 |
|
$node->appendChild($defaultFormatNode); |
152
|
|
|
} |
153
|
6 |
|
|
154
|
1 |
|
$options = $this->getOptions($node); |
155
|
6 |
|
|
156
|
|
|
foreach ($options as $option) { |
157
|
6 |
|
$node->appendChild($option); |
158
|
6 |
|
} |
159
|
6 |
|
|
160
|
6 |
|
$length = $node->childNodes->length; |
161
|
6 |
|
for ($i = 0; $i < $length; ++$i) { |
162
|
|
|
$loopNode = $node->childNodes->item($i); |
163
|
|
|
if (XML_TEXT_NODE === $loopNode->nodeType) { |
164
|
6 |
|
continue; |
165
|
6 |
|
} |
166
|
6 |
|
|
167
|
6 |
|
$newNode = $node->ownerDocument->createElementNS( |
168
|
6 |
|
self::NAMESPACE_URI, |
169
|
|
|
$loopNode->nodeName, |
170
|
6 |
|
$loopNode->nodeValue |
171
|
6 |
|
); |
172
|
6 |
|
|
173
|
|
|
foreach ($loopNode->attributes as $value) { |
174
|
6 |
|
$newNode->setAttribute($value->name, $value->value); |
175
|
6 |
|
} |
176
|
|
|
|
177
|
6 |
|
$node->appendChild($newNode); |
178
|
6 |
|
} |
179
|
|
|
|
180
|
6 |
|
parent::parseRoute($collection, $node, $path); |
181
|
|
|
} |
182
|
6 |
|
|
183
|
6 |
|
private function getOptions(\DOMElement $node) |
184
|
6 |
|
{ |
185
|
1 |
|
$options = []; |
186
|
1 |
|
foreach ($node->childNodes as $child) { |
187
|
1 |
|
if ($child instanceof \DOMElement && 'option' === $child->tagName) { |
188
|
1 |
|
$option = $node->ownerDocument->createElementNs( |
189
|
1 |
|
self::NAMESPACE_URI, |
190
|
1 |
|
'option', |
191
|
1 |
|
$child->nodeValue |
192
|
1 |
|
); |
193
|
6 |
|
$option->setAttribute('key', $child->getAttribute('key')); |
194
|
|
|
$options[] = $option; |
195
|
6 |
|
} |
196
|
|
|
} |
197
|
|
|
|
198
|
|
|
return $options; |
199
|
|
|
} |
200
|
|
|
|
201
|
2 |
|
/** |
202
|
|
|
* {@inheritdoc} |
203
|
2 |
|
*/ |
204
|
2 |
|
public function supports($resource, $type = null) |
205
|
2 |
|
{ |
206
|
|
|
return is_string($resource) && |
207
|
|
|
'xml' === pathinfo($resource, PATHINFO_EXTENSION) && |
208
|
|
|
'rest' === $type; |
209
|
|
|
} |
210
|
|
|
|
211
|
|
|
/** |
212
|
|
|
* @param \DOMDocument $dom |
213
|
10 |
|
* |
214
|
|
|
* @throws \InvalidArgumentException When xml doesn't validate its xsd schema |
215
|
10 |
|
*/ |
216
|
10 |
|
protected function validate(\DOMDocument $dom) |
217
|
10 |
|
{ |
218
|
10 |
|
$restRoutinglocation = realpath(__DIR__.'/../../Resources/config/schema/routing/rest_routing-1.0.xsd'); |
219
|
|
|
$restRoutinglocation = rawurlencode(str_replace('\\', '/', $restRoutinglocation)); |
220
|
|
|
$routinglocation = realpath(__DIR__.'/../../Resources/config/schema/routing-1.0.xsd'); |
221
|
|
|
$routinglocation = rawurlencode(str_replace('\\', '/', $routinglocation)); |
222
|
|
|
$source = <<<EOF |
223
|
|
|
<?xml version="1.0" encoding="utf-8" ?> |
224
|
|
|
<xsd:schema xmlns="http://symfony.com/schema" |
225
|
|
|
xmlns:xsd="http://www.w3.org/2001/XMLSchema" |
226
|
|
|
targetNamespace="http://symfony.com/schema" |
227
|
|
|
elementFormDefault="qualified"> |
228
|
10 |
|
|
229
|
10 |
|
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> |
230
|
10 |
|
<xsd:import namespace="http://friendsofsymfony.github.com/schema/rest" schemaLocation="$restRoutinglocation" /> |
231
|
|
|
<xsd:import namespace="http://symfony.com/schema/routing" schemaLocation="$routinglocation" /> |
232
|
10 |
|
</xsd:schema> |
233
|
10 |
|
EOF; |
234
|
|
|
|
235
|
10 |
|
$current = libxml_use_internal_errors(true); |
236
|
1 |
|
libxml_clear_errors(); |
237
|
|
|
|
238
|
9 |
|
if (!$dom->schemaValidateSource($source)) { |
239
|
9 |
|
throw new \InvalidArgumentException(implode("\n", $this->getXmlErrors_($current))); |
240
|
|
|
} |
241
|
|
|
libxml_use_internal_errors($current); |
242
|
|
|
} |
243
|
|
|
|
244
|
|
|
/** |
245
|
|
|
* {@inheritdoc} |
246
|
10 |
|
* |
247
|
|
|
* @internal |
248
|
10 |
|
*/ |
249
|
10 |
|
protected function loadFile($file) |
250
|
10 |
|
{ |
251
|
|
|
if (class_exists('Symfony\Component\Config\Util\XmlUtils')) { |
252
|
9 |
|
$dom = XmlUtils::loadFile($file); |
253
|
|
|
$this->validate($dom); |
254
|
|
|
|
255
|
|
|
return $dom; |
256
|
|
|
} |
257
|
|
|
|
258
|
|
|
return parent::loadFile($file); |
259
|
|
|
} |
260
|
|
|
|
261
|
|
|
/** |
262
|
|
|
* Retrieves libxml errors and clears them. |
263
|
|
|
* |
264
|
|
|
* Note: The underscore postfix on the method name is to ensure compatibility with versions |
265
|
|
|
* before 2.0.16 while working around a bug in PHP https://bugs.php.net/bug.php?id=62956 |
266
|
|
|
* |
267
|
|
|
* @param bool $internalErrors The previous state of internal errors to reset it |
268
|
1 |
|
* |
269
|
|
|
* @return array An array of libxml error strings |
270
|
1 |
|
*/ |
271
|
1 |
|
private function getXmlErrors_($internalErrors) |
272
|
1 |
|
{ |
273
|
1 |
|
$errors = []; |
274
|
1 |
|
foreach (libxml_get_errors() as $error) { |
275
|
1 |
|
$errors[] = sprintf('[%s %s] %s (in %s - line %d, column %d)', |
276
|
1 |
|
LIBXML_ERR_WARNING === $error->level ? 'WARNING' : 'ERROR', |
277
|
1 |
|
$error->code, |
278
|
1 |
|
trim($error->message), |
279
|
1 |
|
$error->file ? $error->file : 'n/a', |
280
|
1 |
|
$error->line, |
281
|
|
|
$error->column |
282
|
1 |
|
); |
283
|
1 |
|
} |
284
|
|
|
|
285
|
1 |
|
libxml_clear_errors(); |
286
|
|
|
libxml_use_internal_errors($internalErrors); |
287
|
|
|
|
288
|
|
|
return $errors; |
289
|
|
|
} |
290
|
|
|
} |
291
|
|
|
|
This check looks for calls to
isset(...)
orempty()
on variables that are yet undefined. These calls will always produce the same result and can be removed.This is most likely caused by the renaming of a variable or the removal of a function/method parameter.