XmlDeserializationVisitor::getCurrentObject()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 3
ccs 0
cts 0
cp 0
crap 2
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace JMS\Serializer;
6
7
use JMS\Serializer\Exception\InvalidArgumentException;
8
use JMS\Serializer\Exception\LogicException;
9
use JMS\Serializer\Exception\NotAcceptableException;
10
use JMS\Serializer\Exception\RuntimeException;
11
use JMS\Serializer\Exception\XmlErrorException;
12
use JMS\Serializer\Metadata\ClassMetadata;
13
use JMS\Serializer\Metadata\PropertyMetadata;
14
use JMS\Serializer\Visitor\DeserializationVisitorInterface;
15
16
final class XmlDeserializationVisitor extends AbstractVisitor implements NullAwareVisitorInterface, DeserializationVisitorInterface
17
{
18
    /**
19
     * @var \SplStack
20
     */
21
    private $objectStack;
22
23
    /**
24
     * @var \SplStack
25
     */
26 72
    private $metadataStack;
27
28
    /**
29 72
     * @var \SplStack
30 72
     */
31 72
    private $objectMetadataStack;
32 72
33 72
    /**
34 72
     * @var object|null
35
     */
36 71
    private $currentObject;
37
38 71
    /**
39
     * @var ClassMetadata|PropertyMetadata|null
40 71
     */
41 71
    private $currentMetadata;
42
43 71
    /**
44
     * @var bool
45 71
     */
46 2
    private $disableExternalEntities;
47 2
48 2
    /**
49 2
     * @var string[]
50 2
     */
51
    private $doctypeAllowList;
52
    /**
53
     * @var int
54
     */
55 69
    private $options;
56
57 69
    public function __construct(
58 69
        bool $disableExternalEntities = true,
59
        array $doctypeAllowList = [],
60 69
        int $options = 0
61 1
    ) {
62
        $this->objectStack = new \SplStack();
63
        $this->metadataStack = new \SplStack();
64 68
        $this->objectMetadataStack = new \SplStack();
65
        $this->disableExternalEntities = $disableExternalEntities;
66
        $this->doctypeAllowList = $doctypeAllowList;
67 71
        $this->options = $options;
68
    }
69 71
70
    /**
71
     * {@inheritdoc}
72 13
     */
73
    public function prepare($data)
74
    {
75 13
        $data = $this->emptyStringToSpaceCharacter($data);
76
77 25
        $previous = libxml_use_internal_errors(true);
78
        libxml_clear_errors();
79 25
80
        $previousEntityLoaderState = null;
81
        if (\LIBXML_VERSION < 20900) {
82 8
            // phpcs:ignore Generic.PHP.DeprecatedFunctions.Deprecated
83
            $previousEntityLoaderState = libxml_disable_entity_loader($this->disableExternalEntities);
84 8
        }
85
86 8
        if (false !== stripos($data, '<!doctype')) {
87 4
            $internalSubset = $this->getDomDocumentTypeEntitySubset($data);
88 5
            if (!in_array($internalSubset, $this->doctypeAllowList, true)) {
89 5
                throw new InvalidArgumentException(sprintf(
90
                    'The document type "%s" is not allowed. If it is safe, you may add it to the allowlist configuration.',
91
                    $internalSubset,
92
                ));
93
            }
94
        }
95 8
96
        $doc = simplexml_load_string($data, 'SimpleXMLElement', $this->options);
97 8
98
        libxml_use_internal_errors($previous);
99
100 10
        if (\LIBXML_VERSION < 20900) {
101
            // phpcs:ignore Generic.PHP.DeprecatedFunctions.Deprecated
102 10
            libxml_disable_entity_loader($previousEntityLoaderState);
0 ignored issues
show
Bug introduced by
It seems like $previousEntityLoaderState can also be of type null; however, parameter $disable of libxml_disable_entity_loader() does only seem to accept boolean, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

102
            libxml_disable_entity_loader(/** @scrutinizer ignore-type */ $previousEntityLoaderState);
Loading history...
103
        }
104
105 18
        if (false === $doc) {
106
            throw new XmlErrorException(libxml_get_last_error());
107
        }
108 18
109 2
        return $doc;
110
    }
111
112 2
    /**
113
     * @param mixed $data
114 2
     */
115
    private function emptyStringToSpaceCharacter($data): string
116 2
    {
117 2
        return '' === $data ? ' ' : (string) $data;
118 2
    }
119 2
120
    /**
121
     * {@inheritdoc}
122 2
     */
123
    public function visitNull($data, array $type)
124
    {
125 18
        return null;
126 18
    }
127
128 18
    /**
129 13
     * {@inheritdoc}
130 13
     */
131 13
    public function visitString($data, array $type): string
132 10
    {
133 10
        $this->assertValueCanBeCastToString($data);
134 1
135
        return (string) $data;
136
    }
137
138
    /**
139 18
     * {@inheritdoc}
140 5
     */
141 5
    public function visitBoolean($data, array $type): bool
142 5
    {
143
        $this->assertValueCanBeCastToString($data);
144 14
145
        $data = (string) $data;
146
147 18
        if ('true' === $data || '1' === $data) {
148 4
            return true;
149
        } elseif ('false' === $data || '0' === $data) {
150
            return false;
151 18
        } else {
152 18
            throw new RuntimeException(sprintf('Could not convert data to boolean. Expected "true", "false", "1" or "0", but got %s.', json_encode($data)));
153
        }
154
    }
155 18
156 18
    /**
157
     * {@inheritdoc}
158 18
     */
159 18
    public function visitInteger($data, array $type): int
160
    {
161
        $this->assertValueCanBeCastToInt($data);
162 18
163
        return (int) $data;
164 4
    }
165 4
166
    /**
167
     * {@inheritdoc}
168
     */
169 4
    public function visitDouble($data, array $type): float
170 4
    {
171
        $this->assertValueCanCastToFloat($data);
172 4
173 4
        return (float) $data;
174 4
    }
175 4
176
    /**
177
     * {@inheritdoc}
178
     */
179 4
    public function visitArray($data, array $type): array
180 4
    {
181
        // handle key-value-pairs
182
        if (null !== $this->currentMetadata && $this->currentMetadata->xmlKeyValuePairs) {
0 ignored issues
show
Bug introduced by
The property xmlKeyValuePairs does not seem to exist on JMS\Serializer\Metadata\ClassMetadata.
Loading history...
183 4
            if (2 !== count($type['params'])) {
184
                throw new RuntimeException('The array type must be specified as "array<K,V>" for Key-Value-Pairs.');
185
            }
186
187
            $this->revertCurrentMetadata();
188
189
            [$keyType, $entryType] = $type['params'];
190 8
191
            $result = [];
192
            foreach ($data as $key => $v) {
193
                $k = $this->navigator->accept($key, $keyType);
0 ignored issues
show
Bug introduced by
The method accept() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

193
                /** @scrutinizer ignore-call */ 
194
                $k = $this->navigator->accept($key, $keyType);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
194 8
                $result[$k] = $this->navigator->accept($v, $entryType);
195 1
            }
196
197
            return $result;
198 7
        }
199 1
200
        $entryName = null !== $this->currentMetadata && $this->currentMetadata->xmlEntryName ? $this->currentMetadata->xmlEntryName : 'entry';
0 ignored issues
show
Bug introduced by
The property xmlEntryName does not seem to exist on JMS\Serializer\Metadata\ClassMetadata.
Loading history...
201
        $namespace = null !== $this->currentMetadata && $this->currentMetadata->xmlEntryNamespace ? $this->currentMetadata->xmlEntryNamespace : null;
0 ignored issues
show
Bug introduced by
The property xmlEntryNamespace does not exist on JMS\Serializer\Metadata\ClassMetadata. Did you mean xmlNamespace?
Loading history...
202 6
203 1
        if (null === $namespace && $this->objectMetadataStack->count()) {
204
            $classMetadata = $this->objectMetadataStack->top();
205 5
            $namespace = $classMetadata->xmlNamespaces[''] ?? $namespace;
206 4
            if (null === $namespace) {
207
                $namespaces = $data->getDocNamespaces();
208
                if (isset($namespaces[''])) {
209 1
                    $namespace = $namespaces[''];
210 1
                }
211 1
            }
212 1
        }
213
214
        if (null !== $namespace) {
215
            $prefix = uniqid('ns-');
216
            $data->registerXPathNamespace($prefix, $namespace);
217 34
            $nodes = $data->xpath(sprintf('%s:%s', $prefix, $entryName));
218
        } else {
219 34
            $nodes = $data->xpath($entryName);
220 34
        }
221 34
222
        if (null === $nodes || !\count($nodes)) {
223 30
            return [];
224
        }
225 30
226
        switch (\count($type['params'])) {
227 30
            case 0:
228
                throw new RuntimeException(sprintf('The array type must be specified either as "array<T>", or "array<K,V>".'));
229
230
            case 1:
231 30
                $result = [];
232
233 6
                foreach ($nodes as $v) {
234 6
                    $result[] = $this->navigator->accept($v, $type['params'][0]);
235 6
                }
236
237
                return $result;
238
239
            case 2:
240
                if (null === $this->currentMetadata) {
241 30
                    throw new RuntimeException('Maps are not supported on top-level without metadata.');
242 7
                }
243
244
                [$keyType, $entryType] = $type['params'];
245 28
                $result = [];
246 7
247 7
                $nodes = $data->children($namespace)->$entryName;
248 6
                foreach ($nodes as $v) {
249
                    $attrs = $v->attributes();
250
                    if (!isset($attrs[$this->currentMetadata->xmlKeyAttribute])) {
0 ignored issues
show
Bug introduced by
The property xmlKeyAttribute does not exist on JMS\Serializer\Metadata\ClassMetadata. Did you mean xmlAttribute?
Loading history...
251 7
                        throw new RuntimeException(sprintf('The key attribute "%s" must be set for each entry of the map.', $this->currentMetadata->xmlKeyAttribute));
252 7
                    }
253 7
254 7
                    $k = $this->navigator->accept($attrs[$this->currentMetadata->xmlKeyAttribute], $keyType);
255
                    $result[$k] = $this->navigator->accept($v, $entryType);
256
                }
257 27
258 4
                return $result;
259 4
260 4
            default:
261
                throw new LogicException(sprintf('The array type does not support more than 2 parameters, but got %s.', json_encode($type['params'])));
262
        }
263
    }
264 24
265
    /**
266 24
     * {@inheritdoc}
267 2
     */
268 2
    public function visitDiscriminatorMapProperty($data, ClassMetadata $metadata): string
269 2
    {
270
        switch (true) {
271 22
            // Check XML attribute without namespace for discriminatorFieldName
272
            case $metadata->xmlDiscriminatorAttribute && null === $metadata->xmlDiscriminatorNamespace && isset($data->attributes()->{$metadata->discriminatorFieldName}):
273 24
                return (string) $data->attributes()->{$metadata->discriminatorFieldName};
274 2
275
            // Check XML attribute with namespace for discriminatorFieldName
276 24
            case $metadata->xmlDiscriminatorAttribute && null !== $metadata->xmlDiscriminatorNamespace && isset($data->attributes($metadata->xmlDiscriminatorNamespace)->{$metadata->discriminatorFieldName}):
277
                return (string) $data->attributes($metadata->xmlDiscriminatorNamespace)->{$metadata->discriminatorFieldName};
278
279 26
            // Check XML element with namespace for discriminatorFieldName
280 2
            case !$metadata->xmlDiscriminatorAttribute && null !== $metadata->xmlDiscriminatorNamespace && isset($data->children($metadata->xmlDiscriminatorNamespace)->{$metadata->discriminatorFieldName}):
281
                return (string) $data->children($metadata->xmlDiscriminatorNamespace)->{$metadata->discriminatorFieldName};
282
283 26
            // Check XML element for discriminatorFieldName
284
            case isset($data->{$metadata->discriminatorFieldName}):
285
                return (string) $data->{$metadata->discriminatorFieldName};
286
287
            default:
288
                throw new LogicException(sprintf(
289
                    'The discriminator field name "%s" for base-class "%s" was not found in input data.',
290
                    $metadata->discriminatorFieldName,
291
                    $metadata->name,
292 34
                ));
293
        }
294 34
    }
295 34
296 34
    public function startVisitingObject(ClassMetadata $metadata, object $object, array $type): void
297
    {
298 34
        $this->setCurrentObject($object);
299
        $this->objectMetadataStack->push($metadata);
300
    }
301 34
302
    /**
303 34
     * {@inheritdoc}
304 34
     */
305 34
    public function visitProperty(PropertyMetadata $metadata, $data)
306
    {
307
        $name = $metadata->serializedName;
308
309
        if (true === $metadata->inline) {
310
            if (!$metadata->type) {
311
                throw RuntimeException::noMetadataForProperty($metadata->class, $metadata->name);
312 34
            }
313
314 34
            return $this->navigator->accept($data, $metadata->type);
0 ignored issues
show
Bug introduced by
$metadata->type of type JMS\Serializer\Metadata\TypeArray is incompatible with the type array|null expected by parameter $type of JMS\Serializer\GraphNavigatorInterface::accept(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

314
            return $this->navigator->accept($data, /** @scrutinizer ignore-type */ $metadata->type);
Loading history...
315
        }
316
317 9
        if ($metadata->xmlAttribute) {
318
            $attributes = $data->attributes($metadata->xmlNamespace);
319 9
            if (isset($attributes[$name])) {
320 9
                if (!$metadata->type) {
321 9
                    throw RuntimeException::noMetadataForProperty($metadata->class, $metadata->name);
322
                }
323
324
                return $this->navigator->accept($attributes[$name], $metadata->type);
325
            }
326
327
            throw new NotAcceptableException();
328 9
        }
329
330 9
        if ($metadata->xmlValue) {
331
            if (!$metadata->type) {
332
                throw RuntimeException::noMetadataForProperty($metadata->class, $metadata->name);
333 67
            }
334
335 67
            return $this->navigator->accept($data, $metadata->type);
336
        }
337
338
        if ($metadata->xmlCollection) {
339
            $enclosingElem = $data;
340
            if (!$metadata->xmlCollectionInline) {
341
                $enclosingElem = $data->children($metadata->xmlNamespace)->$name;
342
            }
343
344 2
            $this->setCurrentMetadata($metadata);
345
            if (!$metadata->type) {
346 2
                throw RuntimeException::noMetadataForProperty($metadata->class, $metadata->name);
347 2
            }
348
349 2
            $v = $this->navigator->accept($enclosingElem, $metadata->type);
350 2
            $this->revertCurrentMetadata();
351 2
352
            return $v;
353 2
        }
354 2
355
        if ($metadata->xmlNamespace) {
356 2
            $node = $data->children($metadata->xmlNamespace)->$name;
357
            if (!$node->count()) {
358 2
                throw new NotAcceptableException();
359 2
            }
360 2
        } elseif ('' === $metadata->xmlNamespace) {
361 2
            // See #1087 - element must be like: <element xmlns="" /> - https://www.w3.org/TR/REC-xml-names/#iri-use
362
            // Use of an empty string in a namespace declaration turns it into an "undeclaration".
363 2
            $nodes = $data->xpath('./' . $name);
364
            if (empty($nodes)) {
365
                throw new NotAcceptableException();
366
            }
367
368
            $node = reset($nodes);
369
        } else {
370
            $namespaces = $data->getDocNamespaces();
371 69
            if (isset($namespaces[''])) {
372
                $prefix = uniqid('ns-');
373 69
                $data->registerXPathNamespace($prefix, $namespaces['']);
374
                $nodes = $data->xpath('./' . $prefix . ':' . $name);
375
            } else {
376
                $nodes = $data->xpath('./' . $name);
377 69
            }
378
379 2
            if (empty($nodes)) {
380
                throw new NotAcceptableException();
381
            }
382 69
383 69
            $node = reset($nodes);
384 69
        }
385
386 14
        if ($metadata->xmlKeyValuePairs) {
387
            $this->setCurrentMetadata($metadata);
388
        }
389
390 57
        if (!$metadata->type) {
391
            throw RuntimeException::noMetadataForProperty($metadata->class, $metadata->name);
392
        }
393
394
        return $this->navigator->accept($node, $metadata->type);
395
    }
396
397
    /**
398
     * {@inheritdoc}
399
     */
400
    public function endVisitingObject(ClassMetadata $metadata, $data, array $type): object
401
    {
402
        $rs = $this->currentObject;
403
        $this->objectMetadataStack->pop();
404
        $this->revertCurrentObject();
405
406
        return $rs;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $rs could return the type null which is incompatible with the type-hinted return object. Consider adding an additional type-check to rule them out.
Loading history...
407
    }
408
409
    public function setCurrentObject(object $object): void
410
    {
411
        $this->objectStack->push($this->currentObject);
412
        $this->currentObject = $object;
413
    }
414
415
    public function getCurrentObject(): ?object
416
    {
417
        return $this->currentObject;
418
    }
419
420
    public function revertCurrentObject(): ?object
421
    {
422
        return $this->currentObject = $this->objectStack->pop();
423
    }
424
425
    public function setCurrentMetadata(PropertyMetadata $metadata): void
426
    {
427
        $this->metadataStack->push($this->currentMetadata);
428
        $this->currentMetadata = $metadata;
429
    }
430
431
    /**
432
     * @return ClassMetadata|PropertyMetadata|null
433
     */
434
    public function getCurrentMetadata()
435
    {
436
        return $this->currentMetadata;
437
    }
438
439
    /**
440
     * @return ClassMetadata|PropertyMetadata|null
441
     */
442
    public function revertCurrentMetadata()
443
    {
444
        return $this->currentMetadata = $this->metadataStack->pop();
445
    }
446
447
    /**
448
     * {@inheritdoc}
449
     */
450
    public function getResult($data)
451
    {
452
        $this->navigator = null;
453
454
        return $data;
455
    }
456
457
    /**
458
     * Retrieves internalSubset even in bugfixed php versions
459
     */
460
    private function getDomDocumentTypeEntitySubset(string $data): string
461
    {
462
        $startPos = $endPos = stripos($data, '<!doctype');
463
        $braces = 0;
464
        do {
465
            $char = $data[$endPos++];
466
            if ('<' === $char) {
467
                ++$braces;
468
            }
469
470
            if ('>' === $char) {
471
                --$braces;
472
            }
473
        } while ($braces > 0);
474
475
        $internalSubset = substr($data, $startPos, $endPos - $startPos);
476
        $internalSubset = str_replace(["\n", "\r"], '', $internalSubset);
477
        $internalSubset = preg_replace('/\s{2,}/', ' ', $internalSubset);
478
        $internalSubset = str_replace(['[ <!', '> ]>'], ['[<!', '>]>'], $internalSubset);
479
480
        return $internalSubset;
481
    }
482
483
    /**
484
     * {@inheritdoc}
485
     */
486
    public function isNull($value): bool
487
    {
488
        if ($value instanceof \SimpleXMLElement) {
489
            // Workaround for https://bugs.php.net/bug.php?id=75168 and https://github.com/schmittjoh/serializer/issues/817
490
            // If the "name" is empty means that we are on an not-existent node and subsequent operations on the object will trigger the warning:
491
            // "Node no longer exists"
492
            if ('' === $value->getName()) {
493
                // @todo should be "true", but for collections needs a default collection value. maybe something for the 2.0
494
                return false;
495
            }
496
497
            $xsiAttributes = $value->attributes('http://www.w3.org/2001/XMLSchema-instance');
498
            if (
499
                isset($xsiAttributes['nil'])
500
                && ('true' === (string) $xsiAttributes['nil'] || '1' === (string) $xsiAttributes['nil'])
501
            ) {
502
                return true;
503
            }
504
        }
505
506
        return null === $value;
507
    }
508
}
509