Completed
Pull Request — master (#840)
by
unknown
10:27
created

XmlDeserializationVisitor::revertCurrentObject()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
c 0
b 0
f 0
ccs 2
cts 2
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 0
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\InvalidArgumentException;
22
use JMS\Serializer\Exception\LogicException;
23
use JMS\Serializer\Exception\RuntimeException;
24
use JMS\Serializer\Exception\XmlErrorException;
25
use JMS\Serializer\Metadata\ClassMetadata;
26
use JMS\Serializer\Metadata\PropertyMetadata;
27
28
class XmlDeserializationVisitor extends AbstractVisitor implements NullAwareVisitorInterface
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 59
    public function setNavigator(GraphNavigator $navigator)
46
    {
47 59
        $this->navigator = $navigator;
48 59
        $this->objectStack = new \SplStack;
49 59
        $this->metadataStack = new \SplStack;
50 59
        $this->objectMetadataStack = new \SplStack;
51 59
        $this->result = null;
52 59
    }
53
54 1
    public function getNavigator()
55
    {
56 1
        return $this->navigator;
57
    }
58
59 62
    public function prepare($data)
60
    {
61 62
        $data = $this->emptyStringToSpaceCharacter($data);
62
63 62
        $previous = libxml_use_internal_errors(true);
64 62
        libxml_clear_errors();
65
66 62
        $previousEntityLoaderState = libxml_disable_entity_loader($this->disableExternalEntities);
67
68 62
        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 60
        $doc = simplexml_load_string($data);
79
80 60
        libxml_use_internal_errors($previous);
81 60
        libxml_disable_entity_loader($previousEntityLoaderState);
82
83 60
        if (false === $doc) {
84 1
            throw new XmlErrorException(libxml_get_last_error());
85
        }
86
87 59
        return $doc;
88
    }
89
90 62
    private function emptyStringToSpaceCharacter($data)
91
    {
92 62
        return $data === '' ? ' ' : (string)$data;
93
    }
94
95 7
    public function visitNull($data, array $type, Context $context)
96
    {
97 7
        return null;
98
    }
99
100 21
    public function visitString($data, array $type, Context $context)
101
    {
102 21
        $data = (string)$data;
103
104 21
        if (null === $this->result) {
105 2
            $this->result = $data;
106 2
        }
107
108 21
        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 7
    public function visitInteger($data, array $type, Context $context)
131
    {
132 7
        $data = (integer)$data;
133
134 7
        if (null === $this->result) {
135 1
            $this->result = $data;
136 1
        }
137
138 7
        return $data;
139
    }
140
141 10
    public function visitDouble($data, array $type, Context $context)
142
    {
143 10
        $data = (double)$data;
144
145 10
        if (null === $this->result) {
146 4
            $this->result = $data;
147 4
        }
148
149 10
        return $data;
150
    }
151
152 15
    public function visitArray($data, array $type, Context $context)
153
    {
154 15
        if (null === $this->currentMetadata && $context->getMetadataStack()->count()) {
155 15
            $this->setCurrentMetadata($context->getMetadataStack()->top());
156
        }
157 15
158 10
        // handle key-value-pairs
159 10
        if (null !== $this->currentMetadata && $this->currentMetadata->xmlKeyValuePairs) {
160 10
            if (2 !== count($type['params'])) {
161
                throw new RuntimeException('The array type must be specified as "array<K,V>" for Key-Value-Pairs.');
162 15
            }
163 4
164 4
            list($keyType, $entryType) = $type['params'];
165 4
166 4
            $result = [];
167 12
            foreach ($data as $key => $v) {
168
                $k = $this->navigator->accept($key, $keyType, $context);
169
                $result[$k] = $this->navigator->accept($v, $entryType, $context);
170 15
            }
171 2
172
            return $result;
173
        }
174
175 2
        $entryName = null !== $this->currentMetadata && $this->currentMetadata->xmlEntryName ? $this->currentMetadata->xmlEntryName : 'entry';
176
        $namespace = null !== $this->currentMetadata && $this->currentMetadata->xmlEntryNamespace ? $this->currentMetadata->xmlEntryNamespace : null;
177
178 15
        if ($namespace === null && $this->objectMetadataStack->count()) {
179 15
            $classMetadata = $this->objectMetadataStack->top();
180
            $namespace = isset($classMetadata->xmlNamespaces['']) ? $classMetadata->xmlNamespaces[''] : $namespace;
181
        }
182 15
183 15
        if (null !== $namespace) {
184
            $prefix = uniqid('ns-');
185 15
            $data->registerXPathNamespace($prefix, $namespace);
186 5
            $nodes = $data->xpath("$prefix:$entryName");
187 5
        } else {
188
            $nodes = $data->xpath($entryName);
189 15
        }
190 15
191 15
        if (!count($nodes)) {
192
            if (null === $this->result) {
193 15
                return $this->result = array();
194
            }
195 4
196 4
            return array();
197
        }
198
199
        switch (count($type['params'])) {
200 4
            case 0:
201 4
                throw new RuntimeException(sprintf('The array type must be specified either as "array<T>", or "array<K,V>".'));
202 4
203
            case 1:
204
                $result = array();
205
206 4
                if (null === $this->result) {
207 4
                    $this->result = &$result;
208 4
                }
209 4
210
                foreach ($nodes as $v) {
211
                    $result[] = $this->navigator->accept($v, $type['params'][0], $context);
212
                }
213 4
214 4
                return $result;
215 4
216
            case 2:
217 4
                if (null === $this->currentMetadata) {
218
                    throw new RuntimeException('Maps are not supported on top-level without metadata.');
219
                }
220
221
                list($keyType, $entryType) = $type['params'];
222
                $result = array();
223
                if (null === $this->result) {
224 30
                    $this->result = &$result;
225
                }
226 30
227 30
                $nodes = $data->children($namespace)->$entryName;
228 30
                foreach ($nodes as $v) {
229 29
                    $attrs = $v->attributes();
230 29
                    if (!isset($attrs[$this->currentMetadata->xmlKeyAttribute])) {
231 30
                        throw new RuntimeException(sprintf('The key attribute "%s" must be set for each entry of the map.', $this->currentMetadata->xmlKeyAttribute));
232
                    }
233 26
234
                    $k = $this->navigator->accept($attrs[$this->currentMetadata->xmlKeyAttribute], $keyType, $context);
235 26
                    $result[$k] = $this->navigator->accept($v, $entryType, $context);
236
                }
237 26
238
                return $result;
239
240
            default:
241 26
                throw new LogicException(sprintf('The array type does not support more than 2 parameters, but got %s.', json_encode($type['params'])));
242
        }
243 5
    }
244 5
245 5
    public function startVisitingObject(ClassMetadata $metadata, $object, array $type, Context $context)
246 5
    {
247 5
        $this->setCurrentObject($object);
248
        $this->objectMetadataStack->push($metadata);
249 5
        if (null === $this->result) {
250
            $this->result = $this->currentObject;
251
        }
252 26
    }
253 6
254 6
    public function visitProperty(PropertyMetadata $metadata, $data, Context $context)
255
    {
256 6
        $name = $this->namingStrategy->translateName($metadata);
257
258
        if (!$metadata->type) {
259 24
            throw new RuntimeException(sprintf('You must define a type for %s::$%s.', $metadata->reflection->class, $metadata->name));
260 6
        }
261 6
262 6
        if ($metadata->xmlAttribute) {
263 6
264
            $attributes = $data->attributes($metadata->xmlNamespace);
265 6
            if (isset($attributes[$name])) {
266 6
                $v = $this->navigator->accept($attributes[$name], $metadata->type, $context);
267 6
                $this->accessor->setValue($this->currentObject, $v, $metadata);
268 6
            }
269
270 6
            return;
271
        }
272
273 23
        if ($metadata->xmlValue) {
274 4
            $v = $this->navigator->accept($data, $metadata->type, $context);
275 4
            $this->accessor->setValue($this->currentObject, $v, $metadata);
276 1
277
            return;
278 3
        }
279
280 20
        if ($metadata->xmlCollection) {
281
            $enclosingElem = $data;
282 20
            if (!$metadata->xmlCollectionInline) {
283 1
                $enclosingElem = $data->children($metadata->xmlNamespace)->$name;
284 1
            }
285 1
286 1
            $this->setCurrentMetadata($metadata);
287 19
            $v = $this->navigator->accept($enclosingElem, $metadata->type, $context);
288
            $this->revertCurrentMetadata();
289 20
            $this->accessor->setValue($this->currentObject, $v, $metadata);
290 1
291
            return;
292 20
        }
293
294
        if ($metadata->xmlNamespace) {
295 22
            $node = $data->children($metadata->xmlNamespace)->$name;
296
            if (!$node->count()) {
297 22
                return;
298 22
            }
299
        } else {
300 30
301
            $namespaces = $data->getDocNamespaces();
302 30
303 30
            if (isset($namespaces[''])) {
304 30
                $prefix = uniqid('ns-');
305
                $data->registerXPathNamespace($prefix, $namespaces['']);
306 30
                $nodes = $data->xpath('./' . $prefix . ':' . $name);
307
            } else {
308
                $nodes = $data->xpath('./' . $name);
309 30
            }
310
            if (empty($nodes)) {
311 30
                return;
312 30
            }
313 30
            $node = reset($nodes);
314
        }
315
316
        $v = $this->navigator->accept($node, $metadata->type, $context);
317
318
        $this->accessor->setValue($this->currentObject, $v, $metadata);
319
    }
320 30
321
    public function endVisitingObject(ClassMetadata $metadata, $data, array $type, Context $context)
322 30
    {
323
        $rs = $this->currentObject;
324
        $this->objectMetadataStack->pop();
325 6
        $this->revertCurrentObject();
326
327 6
        return $rs;
328 6
    }
329 6
330
    public function setCurrentObject($object)
331
    {
332
        $this->objectStack->push($this->currentObject);
333
        $this->currentObject = $object;
334
    }
335
336 6
    public function getCurrentObject()
337
    {
338 6
        return $this->currentObject;
339
    }
340
341 58
    public function revertCurrentObject()
342
    {
343 58
        return $this->currentObject = $this->objectStack->pop();
344
    }
345
346
    public function setCurrentMetadata(PropertyMetadata $metadata)
347
    {
348
        $this->metadataStack->push($this->currentMetadata);
349 1
        $this->currentMetadata = $metadata;
350
    }
351 1
352 1
    public function getCurrentMetadata()
353
    {
354
        return $this->currentMetadata;
355
    }
356
357
    public function revertCurrentMetadata()
358
    {
359
        return $this->currentMetadata = $this->metadataStack->pop();
360
    }
361
362
    public function getResult()
363
    {
364
        return $this->result;
365
    }
366
367
    /**
368
     * @param array <string> $doctypeWhitelist
369 3
     */
370
    public function setDoctypeWhitelist(array $doctypeWhitelist)
371 3
    {
372 3
        $this->doctypeWhitelist = $doctypeWhitelist;
373
    }
374 3
375 3
    /**
376 3
     * @return array<string>
377 3
     */
378 3
    public function getDoctypeWhitelist()
379 3
    {
380 3
        return $this->doctypeWhitelist;
381 3
    }
382
383 3
    /**
384 3
     * Retrieves internalSubset even in bugfixed php versions
385 3
     *
386 3
     * @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...
387
     * @param string $data
388 3
     * @return string
389
     */
390
    private function getDomDocumentTypeEntitySubset($data)
391
    {
392
        $startPos = $endPos = stripos($data, '<!doctype');
393
        $braces = 0;
394
        do {
395
            $char = $data[$endPos++];
396 60
            if ($char === '<') {
397
                ++$braces;
398 60
            }
399
            if ($char === '>') {
400
                --$braces;
401
            }
402 60
        } while ($braces > 0);
403
404 2
        $internalSubset = substr($data, $startPos, $endPos - $startPos);
405
        $internalSubset = str_replace(array("\n", "\r"), '', $internalSubset);
406
        $internalSubset = preg_replace('/\s{2,}/', ' ', $internalSubset);
407 60
        $internalSubset = str_replace(array("[ <!", "> ]>"), array('[<!', '>]>'), $internalSubset);
408 60
409 60
        return $internalSubset;
410 60
    }
411 8
412
    /**
413 53
     * @param mixed $value
414
     *
415 54
     * @return bool
416
     */
417
    public function isNull($value)
418
    {
419
        if ($value instanceof \SimpleXMLElement) {
420
            // Workaround for https://bugs.php.net/bug.php?id=75168 and https://github.com/schmittjoh/serializer/issues/817
421
            // If the "name" is empty means that we are on an not-existent node and subsequent operations on the object will trigger the warning:
422
            // "Node no longer exists"
423
            if ($value->getName() === "") {
424
                // @todo should be "true", but for collections needs a default collection value. maybe something for the 2.0
425
                return false;
426
            }
427
428
            $xsiAttributes = $value->attributes('http://www.w3.org/2001/XMLSchema-instance');
429
            if (isset($xsiAttributes['nil'])
430
                && ((string) $xsiAttributes['nil'] === 'true' || (string) $xsiAttributes['nil'] === '1')
431
            ) {
432
                return true;
433
            }
434
        }
435
436
        return $value === null;
437
    }
438
}
439