Completed
Pull Request — master (#871)
by
unknown
03:34
created

XmlDeserializationVisitor::isNull()   B

Complexity

Conditions 6
Paths 4

Size

Total Lines 21
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 6

Importance

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