Completed
Pull Request — master (#724)
by Asmir
02:58
created

XmlDeserializationVisitor::setCurrentMetadata()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 5
c 0
b 0
f 0
ccs 4
cts 4
cp 1
rs 9.4285
cc 1
eloc 3
nc 1
nop 1
crap 1
1
<?php
2
3
/*
4
 * Copyright 2016 Johannes M. Schmitt <[email protected]>
5
 *
6
 * Licensed under the Apache License, Version 2.0 (the "License");
7
 * you may not use this file except in compliance with the License.
8
 * You may obtain a copy of the License at
9
 *
10
 *     http://www.apache.org/licenses/LICENSE-2.0
11
 *
12
 * Unless required by applicable law or agreed to in writing, software
13
 * distributed under the License is distributed on an "AS IS" BASIS,
14
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
 * See the License for the specific language governing permissions and
16
 * limitations under the License.
17
 */
18
19
namespace JMS\Serializer;
20
21
use JMS\Serializer\Exception\XmlErrorException;
22
use JMS\Serializer\Exception\LogicException;
23
use JMS\Serializer\Exception\InvalidArgumentException;
24
use JMS\Serializer\Exception\RuntimeException;
25
use JMS\Serializer\Metadata\PropertyMetadata;
26
use JMS\Serializer\Metadata\ClassMetadata;
27
28
class XmlDeserializationVisitor extends AbstractVisitor
29
{
30
    private $objectStack;
31
    private $metadataStack;
32
    private $objectMetadataStack;
33
    private $currentObject;
34
    private $currentMetadata;
35
    private $result;
36
    private $navigator;
37
    private $disableExternalEntities = true;
38
    private $doctypeWhitelist = array();
39
40
    public function enableExternalEntities()
41
    {
42
        $this->disableExternalEntities = false;
43
    }
44
45 57
    public function setNavigator(GraphNavigator $navigator)
46
    {
47 57
        $this->navigator = $navigator;
48 57
        $this->objectStack = new \SplStack;
49 57
        $this->metadataStack = new \SplStack;
50 57
        $this->objectMetadataStack = new \SplStack;
51 57
        $this->result = null;
52 57
    }
53
54 1
    public function getNavigator()
55
    {
56 1
        return $this->navigator;
57
    }
58
59 60
    public function prepare($data)
60
    {
61 60
        $data = $this->emptyStringToSpaceCharacter($data);
62
63 60
        $previous = libxml_use_internal_errors(true);
64 60
        libxml_clear_errors();
65
66 60
        $previousEntityLoaderState = libxml_disable_entity_loader($this->disableExternalEntities);
67
68 60
        if (false !== stripos($data, '<!doctype')) {
69 3
            $internalSubset = $this->getDomDocumentTypeEntitySubset($data);
70 3
            if (!in_array($internalSubset, $this->doctypeWhitelist, true)) {
71 2
                throw new InvalidArgumentException(sprintf(
72 2
                    'The document type "%s" is not allowed. If it is safe, you may add it to the whitelist configuration.',
73
                    $internalSubset
74 2
                ));
75
            }
76 1
        }
77
78 58
        $doc = simplexml_load_string($data);
79
80 58
        libxml_use_internal_errors($previous);
81 58
        libxml_disable_entity_loader($previousEntityLoaderState);
82
83 58
        if (false === $doc) {
84 1
            throw new XmlErrorException(libxml_get_last_error());
85
        }
86
87 57
        return $doc;
88
    }
89
90 60
    private function emptyStringToSpaceCharacter($data)
91
    {
92 60
        return $data === '' ? ' ' : $data;
93
    }
94
95 1
    public function visitNull($data, array $type, Context $context)
96
    {
97 1
        return null;
98
    }
99
100 20
    public function visitString($data, array $type, Context $context)
101
    {
102 20
        $data = (string) $data;
103
104 20
        if (null === $this->result) {
105 3
            $this->result = $data;
106 3
        }
107
108 20
        return $data;
109
    }
110
111 8
    public function visitBoolean($data, array $type, Context $context)
112
    {
113 8
        $data = (string) $data;
114
115 8
        if ('true' === $data || '1' === $data) {
116 4
            $data = true;
117 8
        } elseif ('false' === $data || '0' === $data) {
118 5
            $data = false;
119 5
        } else {
120
            throw new RuntimeException(sprintf('Could not convert data to boolean. Expected "true", "false", "1" or "0", but got %s.', json_encode($data)));
121
        }
122
123 8
        if (null === $this->result) {
124 6
            $this->result = $data;
125 6
        }
126
127 8
        return $data;
128
    }
129
130 9
    public function visitInteger($data, array $type, Context $context)
131
    {
132 9
        $data = (integer) $data;
133
134 9
        if (null === $this->result) {
135 2
            $this->result = $data;
136 2
        }
137
138 9
        return $data;
139
    }
140
141 12
    public function visitDouble($data, array $type, Context $context)
142
    {
143 12
        $data = (double) $data;
144
145 12
        if (null === $this->result) {
146 6
            $this->result = $data;
147 6
        }
148
149 12
        return $data;
150
    }
151
152 16
    public function visitArray($data, array $type, Context $context)
153
    {
154 16
        $entryName = null !== $this->currentMetadata && $this->currentMetadata->xmlEntryName ? $this->currentMetadata->xmlEntryName : 'entry';
155 16
        $namespace = null !== $this->currentMetadata && $this->currentMetadata->xmlEntryNamespace ? $this->currentMetadata->xmlEntryNamespace : null;
156
157 16
        if ($namespace === null && $this->objectMetadataStack->count()) {
158 11
            $classMetadata = $this->objectMetadataStack->top();
159 11
            $namespace = isset($classMetadata->xmlNamespaces[''])?$classMetadata->xmlNamespaces['']:$namespace;
160 11
        }
161
162 16
        if (null !== $namespace) {
163 4
            $prefix = uniqid('ns-');
164 4
            $data->registerXPathNamespace($prefix, $namespace);
165 4
            $nodes = $data->xpath("$prefix:$entryName");
166 4
        } else {
167 13
            $nodes = $data->xpath($entryName);
168
        }
169
170 16
        if (!count($nodes)) {
171 2
            if (null === $this->result) {
172
                return $this->result = array();
173
            }
174
175 2
            return array();
176
        }
177
178 16
        switch (count($type['params'])) {
179 16
            case 0:
180
                throw new RuntimeException(sprintf('The array type must be specified either as "array<T>", or "array<K,V>".'));
181
182 16
            case 1:
183 16
                $result = array();
184
185 16
                if (null === $this->result) {
186 5
                    $this->result = &$result;
187 5
                }
188
189 16
                foreach ($nodes as $v) {
190 16
                    $result[] = $this->navigator->accept($v, $type['params'][0], $context);
191 16
                }
192
193 16
                return $result;
194
195 4
            case 2:
196 4
                if (null === $this->currentMetadata) {
197
                    throw new RuntimeException('Maps are not supported on top-level without metadata.');
198
                }
199
200 4
                list($keyType, $entryType) = $type['params'];
201 4
                $result = array();
202 4
                if (null === $this->result) {
203
                    $this->result = &$result;
204
                }
205
206 4
                $nodes = $data->children($namespace)->$entryName;
207 4
                foreach ($nodes as $v) {
208 4
                    $attrs = $v->attributes();
209 4
                    if ( ! isset($attrs[$this->currentMetadata->xmlKeyAttribute])) {
210
                        throw new RuntimeException(sprintf('The key attribute "%s" must be set for each entry of the map.', $this->currentMetadata->xmlKeyAttribute));
211
                    }
212
213 4
                    $k = $this->navigator->accept($attrs[$this->currentMetadata->xmlKeyAttribute], $keyType, $context);
214 4
                    $result[$k] = $this->navigator->accept($v, $entryType, $context);
215 4
                }
216
217 4
                return $result;
218
219
            default:
220
                throw new LogicException(sprintf('The array type does not support more than 2 parameters, but got %s.', json_encode($type['params'])));
221
        }
222
    }
223
224 28
    public function startVisitingObject(ClassMetadata $metadata, $object, array $type, Context $context)
225
    {
226 28
        $this->setCurrentObject($object);
227 28
        $this->objectMetadataStack->push($metadata);
228 28
        if (null === $this->result) {
229 27
            $this->result = $this->currentObject;
230 27
        }
231 28
    }
232
233 25
    public function visitProperty(PropertyMetadata $metadata, $data, Context $context)
234
    {
235 25
        $name = $this->namingStrategy->translateName($metadata);
236
237 25
        if ( ! $metadata->type) {
238
            throw new RuntimeException(sprintf('You must define a type for %s::$%s.', $metadata->reflection->class, $metadata->name));
239
        }
240
241 25
       if ($metadata->xmlAttribute) {
242
243 5
            $attributes = $data->attributes($metadata->xmlNamespace);
244 5
            if (isset($attributes[$name])) {
245 5
                $v = $this->navigator->accept($attributes[$name], $metadata->type, $context);
246 5
                $metadata->setValue($this->currentObject, $v);
247 5
            }
248
249 5
            return;
250
        }
251
252 25
        if ($metadata->xmlValue) {
253 6
            $v = $this->navigator->accept($data, $metadata->type, $context);
254 6
            $metadata->setValue($this->currentObject, $v);
255
256 6
            return;
257
        }
258
259 23
        if ($metadata->xmlCollection) {
260 7
            $enclosingElem = $data;
261 7
            if (!$metadata->xmlCollectionInline) {
262 7
                $enclosingElem = $data->children($metadata->xmlNamespace)->$name;
263 7
            }
264
265 7
            $this->setCurrentMetadata($metadata);
266 7
            $v = $this->navigator->accept($enclosingElem, $metadata->type, $context);
267 7
            $this->revertCurrentMetadata();
268 7
            $metadata->setValue($this->currentObject, $v);
269
270 7
            return;
271
        }
272
273 22
        if ($metadata->xmlNamespace) {
274 4
            $node = $data->children($metadata->xmlNamespace)->$name;
275 4
            if (!$node->count()) {
276 1
                return;
277
            }
278 3
        } else {
279
280 19
            $namespaces = $data->getDocNamespaces();
281
282 19
            if (isset($namespaces[''])) {
283 1
                $prefix = uniqid('ns-');
284 1
                $data->registerXPathNamespace($prefix, $namespaces['']);
285 1
                $nodes = $data->xpath('./'.$prefix. ':'.$name );
286 1
            } else {
287 18
                $nodes = $data->xpath('./'. $name );
288
            }
289 19
            if (empty($nodes)) {
290 1
                return;
291
            }
292 19
            $node = reset($nodes);
293
        }
294
295 21
        $v = $this->navigator->accept($node, $metadata->type, $context);
296
297 21
        $metadata->setValue($this->currentObject, $v);
298 21
    }
299
300 28
    public function endVisitingObject(ClassMetadata $metadata, $data, array $type, Context $context)
301
    {
302 28
        $rs = $this->currentObject;
303 28
        $this->objectMetadataStack->pop();
304 28
        $this->revertCurrentObject();
305
306 28
        return $rs;
307
    }
308
309 28
    public function setCurrentObject($object)
310
    {
311 28
        $this->objectStack->push($this->currentObject);
312 28
        $this->currentObject = $object;
313 28
    }
314
315
    public function getCurrentObject()
316
    {
317
        return $this->currentObject;
318
    }
319
320 28
    public function revertCurrentObject()
321
    {
322 28
        return $this->currentObject = $this->objectStack->pop();
323
    }
324
325 7
    public function setCurrentMetadata(PropertyMetadata $metadata)
326
    {
327 7
        $this->metadataStack->push($this->currentMetadata);
328 7
        $this->currentMetadata = $metadata;
329 7
    }
330
331
    public function getCurrentMetadata()
332
    {
333
        return $this->currentMetadata;
334
    }
335
336 7
    public function revertCurrentMetadata()
337
    {
338 7
        return $this->currentMetadata = $this->metadataStack->pop();
339
    }
340
341 56
    public function getResult()
342
    {
343 56
        return $this->result;
344
    }
345
346
    /**
347
     * @param array<string> $doctypeWhitelist
348
     */
349 1
    public function setDoctypeWhitelist(array $doctypeWhitelist)
350
    {
351 1
        $this->doctypeWhitelist = $doctypeWhitelist;
352 1
    }
353
354
    /**
355
     * @return array<string>
356
     */
357
    public function getDoctypeWhitelist()
358
    {
359
        return $this->doctypeWhitelist;
360
    }
361
362
    /**
363
     * Retrieves internalSubset even in bugfixed php versions
364
     *
365
     * @param \DOMDocumentType $child
0 ignored issues
show
Bug introduced by
There is no parameter named $child. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
366
     * @param string $data
367
     * @return string
368
     */
369 3
    private function getDomDocumentTypeEntitySubset($data)
370
    {
371 3
        $startPos = $endPos = stripos($data, '<!doctype');
372 3
        $braces = 0;
373
        do {
374 3
            $char = $data[$endPos++];
375 3
            if ($char === '<') {
376 3
                ++$braces;
377 3
            }
378 3
            if ($char === '>') {
379 3
                --$braces;
380 3
            }
381 3
        } while ($braces > 0);
382
383 3
        $internalSubset = substr($data, $startPos, $endPos - $startPos);
384 3
        $internalSubset = str_replace(array("\n", "\r"), '', $internalSubset);
385 3
        $internalSubset = preg_replace('/\s{2,}/', ' ', $internalSubset);
386 3
        $internalSubset = str_replace(array("[ <!", "> ]>"), array('[<!', '>]>'), $internalSubset);
387
388 3
        return $internalSubset;
389
    }
390
}
391