Passed
Pull Request — master (#1549)
by
unknown
02:36
created

XmlDeserializationVisitor::setCurrentObject()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 2
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 4
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
    /**
58 69
     * THIS IS ONLY USED FOR UNION DESERIALIZATION WHICH IS NOT SUPPORTED IN XML
59
     *
60 69
     * @var bool
61 1
     */
62
    private $requireAllRequiredProperties = false;
63
64 68
    public function __construct(
65
        bool $disableExternalEntities = true,
66
        array $doctypeAllowList = [],
67 71
        int $options = 0
68
    ) {
69 71
        $this->objectStack = new \SplStack();
70
        $this->metadataStack = new \SplStack();
71
        $this->objectMetadataStack = new \SplStack();
72 13
        $this->disableExternalEntities = $disableExternalEntities;
73
        $this->doctypeAllowList = $doctypeAllowList;
74
        $this->options = $options;
75 13
    }
76
77 25
    public function setRequireAllRequiredProperties(bool $requireAllRequiredProperties): void
78
    {
79 25
        $this->requireAllRequiredProperties = $requireAllRequiredProperties;
80
    }
81
82 8
    public function getRequireAllRequiredProperties(): bool
83
    {
84 8
        return $this->requireAllRequiredProperties;
85
    }
86 8
87 4
    /**
88 5
     * {@inheritdoc}
89 5
     */
90
    public function prepare($data)
91
    {
92
        $data = $this->emptyStringToSpaceCharacter($data);
93
94
        $previous = libxml_use_internal_errors(true);
95 8
        libxml_clear_errors();
96
97 8
        $previousEntityLoaderState = null;
98
        if (\LIBXML_VERSION < 20900) {
99
            // phpcs:ignore Generic.PHP.DeprecatedFunctions.Deprecated
100 10
            $previousEntityLoaderState = libxml_disable_entity_loader($this->disableExternalEntities);
101
        }
102 10
103
        if (false !== stripos($data, '<!doctype')) {
104
            $internalSubset = $this->getDomDocumentTypeEntitySubset($data);
105 18
            if (!in_array($internalSubset, $this->doctypeAllowList, true)) {
106
                throw new InvalidArgumentException(sprintf(
107
                    'The document type "%s" is not allowed. If it is safe, you may add it to the allowlist configuration.',
108 18
                    $internalSubset,
109 2
                ));
110
            }
111
        }
112 2
113
        $doc = simplexml_load_string($data, 'SimpleXMLElement', $this->options);
114 2
115
        libxml_use_internal_errors($previous);
116 2
117 2
        if (\LIBXML_VERSION < 20900) {
118 2
            // phpcs:ignore Generic.PHP.DeprecatedFunctions.Deprecated
119 2
            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

119
            libxml_disable_entity_loader(/** @scrutinizer ignore-type */ $previousEntityLoaderState);
Loading history...
120
        }
121
122 2
        if (false === $doc) {
123
            throw new XmlErrorException(libxml_get_last_error());
124
        }
125 18
126 18
        return $doc;
127
    }
128 18
129 13
    /**
130 13
     * @param mixed $data
131 13
     */
132 10
    private function emptyStringToSpaceCharacter($data): string
133 10
    {
134 1
        return '' === $data ? ' ' : (string) $data;
135
    }
136
137
    /**
138
     * {@inheritdoc}
139 18
     */
140 5
    public function visitNull($data, array $type)
141 5
    {
142 5
        return null;
143
    }
144 14
145
    /**
146
     * {@inheritdoc}
147 18
     */
148 4
    public function visitString($data, array $type): string
149
    {
150
        $this->assertValueCanBeCastToString($data);
151 18
152 18
        return (string) $data;
153
    }
154
155 18
    /**
156 18
     * {@inheritdoc}
157
     */
158 18
    public function visitBoolean($data, array $type): bool
159 18
    {
160
        $this->assertValueCanBeCastToString($data);
161
162 18
        $data = (string) $data;
163
164 4
        if ('true' === $data || '1' === $data) {
165 4
            return true;
166
        } elseif ('false' === $data || '0' === $data) {
167
            return false;
168
        } else {
169 4
            throw new RuntimeException(sprintf('Could not convert data to boolean. Expected "true", "false", "1" or "0", but got %s.', json_encode($data)));
170 4
        }
171
    }
172 4
173 4
    /**
174 4
     * {@inheritdoc}
175 4
     */
176
    public function visitInteger($data, array $type): int
177
    {
178
        $this->assertValueCanBeCastToInt($data);
179 4
180 4
        return (int) $data;
181
    }
182
183 4
    /**
184
     * {@inheritdoc}
185
     */
186
    public function visitDouble($data, array $type): float
187
    {
188
        $this->assertValueCanCastToFloat($data);
189
190 8
        return (float) $data;
191
    }
192
193
    /**
194 8
     * {@inheritdoc}
195 1
     */
196
    public function visitArray($data, array $type): array
197
    {
198 7
        // handle key-value-pairs
199 1
        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...
200
            if (2 !== count($type['params'])) {
201
                throw new RuntimeException('The array type must be specified as "array<K,V>" for Key-Value-Pairs.');
202 6
            }
203 1
204
            $this->revertCurrentMetadata();
205 5
206 4
            [$keyType, $entryType] = $type['params'];
207
208
            $result = [];
209 1
            foreach ($data as $key => $v) {
210 1
                $k = $this->navigator->accept($key, $keyType);
211 1
                $result[$k] = $this->navigator->accept($v, $entryType);
212 1
            }
213
214
            return $result;
215
        }
216
217 34
        $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...
218
        $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...
219 34
220 34
        if (null === $namespace && $this->objectMetadataStack->count()) {
221 34
            $classMetadata = $this->objectMetadataStack->top();
222
            $namespace = $classMetadata->xmlNamespaces[''] ?? $namespace;
223 30
            if (null === $namespace) {
224
                $namespaces = $data->getDocNamespaces();
225 30
                if (isset($namespaces[''])) {
226
                    $namespace = $namespaces[''];
227 30
                }
228
            }
229
        }
230
231 30
        if (null !== $namespace) {
232
            $prefix = uniqid('ns-');
233 6
            $data->registerXPathNamespace($prefix, $namespace);
234 6
            $nodes = $data->xpath(sprintf('%s:%s', $prefix, $entryName));
235 6
        } else {
236
            $nodes = $data->xpath($entryName);
237
        }
238
239
        if (null === $nodes || !\count($nodes)) {
240
            return [];
241 30
        }
242 7
243
        switch (\count($type['params'])) {
244
            case 0:
245 28
                throw new RuntimeException(sprintf('The array type must be specified either as "array<T>", or "array<K,V>".'));
246 7
247 7
            case 1:
248 6
                $result = [];
249
250
                foreach ($nodes as $v) {
251 7
                    $result[] = $this->navigator->accept($v, $type['params'][0]);
252 7
                }
253 7
254 7
                return $result;
255
256
            case 2:
257 27
                if (null === $this->currentMetadata) {
258 4
                    throw new RuntimeException('Maps are not supported on top-level without metadata.');
259 4
                }
260 4
261
                [$keyType, $entryType] = $type['params'];
262
                $result = [];
263
264 24
                $nodes = $data->children($namespace)->$entryName;
265
                foreach ($nodes as $v) {
266 24
                    $attrs = $v->attributes();
267 2
                    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...
268 2
                        throw new RuntimeException(sprintf('The key attribute "%s" must be set for each entry of the map.', $this->currentMetadata->xmlKeyAttribute));
269 2
                    }
270
271 22
                    $k = $this->navigator->accept($attrs[$this->currentMetadata->xmlKeyAttribute], $keyType);
272
                    $result[$k] = $this->navigator->accept($v, $entryType);
273 24
                }
274 2
275
                return $result;
276 24
277
            default:
278
                throw new LogicException(sprintf('The array type does not support more than 2 parameters, but got %s.', json_encode($type['params'])));
279 26
        }
280 2
    }
281
282
    /**
283 26
     * {@inheritdoc}
284
     */
285
    public function visitDiscriminatorMapProperty($data, ClassMetadata $metadata): string
286
    {
287
        switch (true) {
288
            // Check XML attribute without namespace for discriminatorFieldName
289
            case $metadata->xmlDiscriminatorAttribute && null === $metadata->xmlDiscriminatorNamespace && isset($data->attributes()->{$metadata->discriminatorFieldName}):
290
                return (string) $data->attributes()->{$metadata->discriminatorFieldName};
291
292 34
            // Check XML attribute with namespace for discriminatorFieldName
293
            case $metadata->xmlDiscriminatorAttribute && null !== $metadata->xmlDiscriminatorNamespace && isset($data->attributes($metadata->xmlDiscriminatorNamespace)->{$metadata->discriminatorFieldName}):
294 34
                return (string) $data->attributes($metadata->xmlDiscriminatorNamespace)->{$metadata->discriminatorFieldName};
295 34
296 34
            // Check XML element with namespace for discriminatorFieldName
297
            case !$metadata->xmlDiscriminatorAttribute && null !== $metadata->xmlDiscriminatorNamespace && isset($data->children($metadata->xmlDiscriminatorNamespace)->{$metadata->discriminatorFieldName}):
298 34
                return (string) $data->children($metadata->xmlDiscriminatorNamespace)->{$metadata->discriminatorFieldName};
299
300
            // Check XML element for discriminatorFieldName
301 34
            case isset($data->{$metadata->discriminatorFieldName}):
302
                return (string) $data->{$metadata->discriminatorFieldName};
303 34
304 34
            default:
305 34
                throw new LogicException(sprintf(
306
                    'The discriminator field name "%s" for base-class "%s" was not found in input data.',
307
                    $metadata->discriminatorFieldName,
308
                    $metadata->name,
309
                ));
310
        }
311
    }
312 34
313
    public function startVisitingObject(ClassMetadata $metadata, object $object, array $type): void
314 34
    {
315
        $this->setCurrentObject($object);
316
        $this->objectMetadataStack->push($metadata);
317 9
    }
318
319 9
    /**
320 9
     * {@inheritdoc}
321 9
     */
322
    public function visitProperty(PropertyMetadata $metadata, $data)
323
    {
324
        $name = $metadata->serializedName;
325
326
        if (true === $metadata->inline) {
327
            if (!$metadata->type) {
328 9
                throw RuntimeException::noMetadataForProperty($metadata->class, $metadata->name);
329
            }
330 9
331
            return $this->navigator->accept($data, $metadata->type);
332
        }
333 67
334
        if ($metadata->xmlAttribute) {
335 67
            $attributes = $data->attributes($metadata->xmlNamespace);
336
            if (isset($attributes[$name])) {
337
                if (!$metadata->type) {
338
                    throw RuntimeException::noMetadataForProperty($metadata->class, $metadata->name);
339
                }
340
341
                return $this->navigator->accept($attributes[$name], $metadata->type);
342
            }
343
344 2
            throw new NotAcceptableException();
345
        }
346 2
347 2
        if ($metadata->xmlValue) {
348
            if (!$metadata->type) {
349 2
                throw RuntimeException::noMetadataForProperty($metadata->class, $metadata->name);
350 2
            }
351 2
352
            return $this->navigator->accept($data, $metadata->type);
353 2
        }
354 2
355
        if ($metadata->xmlCollection) {
356 2
            $enclosingElem = $data;
357
            if (!$metadata->xmlCollectionInline) {
358 2
                $enclosingElem = $data->children($metadata->xmlNamespace)->$name;
359 2
            }
360 2
361 2
            $this->setCurrentMetadata($metadata);
362
            if (!$metadata->type) {
363 2
                throw RuntimeException::noMetadataForProperty($metadata->class, $metadata->name);
364
            }
365
366
            $v = $this->navigator->accept($enclosingElem, $metadata->type);
367
            $this->revertCurrentMetadata();
368
369
            return $v;
370
        }
371 69
372
        if ($metadata->xmlNamespace) {
373 69
            $node = $data->children($metadata->xmlNamespace)->$name;
374
            if (!$node->count()) {
375
                throw new NotAcceptableException();
376
            }
377 69
        } elseif ('' === $metadata->xmlNamespace) {
378
            // See #1087 - element must be like: <element xmlns="" /> - https://www.w3.org/TR/REC-xml-names/#iri-use
379 2
            // Use of an empty string in a namespace declaration turns it into an "undeclaration".
380
            $nodes = $data->xpath('./' . $name);
381
            if (empty($nodes)) {
382 69
                throw new NotAcceptableException();
383 69
            }
384 69
385
            $node = reset($nodes);
386 14
        } else {
387
            $namespaces = $data->getDocNamespaces();
388
            if (isset($namespaces[''])) {
389
                $prefix = uniqid('ns-');
390 57
                $data->registerXPathNamespace($prefix, $namespaces['']);
391
                $nodes = $data->xpath('./' . $prefix . ':' . $name);
392
            } else {
393
                $nodes = $data->xpath('./' . $name);
394
            }
395
396
            if (empty($nodes)) {
397
                throw new NotAcceptableException();
398
            }
399
400
            $node = reset($nodes);
401
        }
402
403
        if ($metadata->xmlKeyValuePairs) {
404
            $this->setCurrentMetadata($metadata);
405
        }
406
407
        if (!$metadata->type) {
408
            throw RuntimeException::noMetadataForProperty($metadata->class, $metadata->name);
409
        }
410
411
        return $this->navigator->accept($node, $metadata->type);
412
    }
413
414
    /**
415
     * {@inheritdoc}
416
     */
417
    public function endVisitingObject(ClassMetadata $metadata, $data, array $type): object
418
    {
419
        $rs = $this->currentObject;
420
        $this->objectMetadataStack->pop();
421
        $this->revertCurrentObject();
422
423
        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...
424
    }
425
426
    public function setCurrentObject(object $object): void
427
    {
428
        $this->objectStack->push($this->currentObject);
429
        $this->currentObject = $object;
430
    }
431
432
    public function getCurrentObject(): ?object
433
    {
434
        return $this->currentObject;
435
    }
436
437
    public function revertCurrentObject(): ?object
438
    {
439
        return $this->currentObject = $this->objectStack->pop();
440
    }
441
442
    public function setCurrentMetadata(PropertyMetadata $metadata): void
443
    {
444
        $this->metadataStack->push($this->currentMetadata);
445
        $this->currentMetadata = $metadata;
446
    }
447
448
    /**
449
     * @return ClassMetadata|PropertyMetadata|null
450
     */
451
    public function getCurrentMetadata()
452
    {
453
        return $this->currentMetadata;
454
    }
455
456
    /**
457
     * @return ClassMetadata|PropertyMetadata|null
458
     */
459
    public function revertCurrentMetadata()
460
    {
461
        return $this->currentMetadata = $this->metadataStack->pop();
462
    }
463
464
    /**
465
     * {@inheritdoc}
466
     */
467
    public function getResult($data)
468
    {
469
        unset($this->navigator);
470
471
        return $data;
472
    }
473
474
    /**
475
     * Retrieves internalSubset even in bugfixed php versions
476
     */
477
    private function getDomDocumentTypeEntitySubset(string $data): string
478
    {
479
        $startPos = $endPos = stripos($data, '<!doctype');
480
        $braces = 0;
481
        do {
482
            $char = $data[$endPos++];
483
            if ('<' === $char) {
484
                ++$braces;
485
            }
486
487
            if ('>' === $char) {
488
                --$braces;
489
            }
490
        } while ($braces > 0);
491
492
        $internalSubset = substr($data, $startPos, $endPos - $startPos);
493
        $internalSubset = str_replace(["\n", "\r"], '', $internalSubset);
494
        $internalSubset = preg_replace('/\s{2,}/', ' ', $internalSubset);
495
        $internalSubset = str_replace(['[ <!', '> ]>'], ['[<!', '>]>'], $internalSubset);
496
497
        return $internalSubset;
498
    }
499
500
    /**
501
     * {@inheritdoc}
502
     */
503
    public function isNull($value): bool
504
    {
505
        if ($value instanceof \SimpleXMLElement) {
506
            // Workaround for https://bugs.php.net/bug.php?id=75168 and https://github.com/schmittjoh/serializer/issues/817
507
            // If the "name" is empty means that we are on an not-existent node and subsequent operations on the object will trigger the warning:
508
            // "Node no longer exists"
509
            if ('' === $value->getName()) {
510
                // @todo should be "true", but for collections needs a default collection value. maybe something for the 2.0
511
                return false;
512
            }
513
514
            $xsiAttributes = $value->attributes('http://www.w3.org/2001/XMLSchema-instance');
515
            if (
516
                isset($xsiAttributes['nil'])
517
                && ('true' === (string) $xsiAttributes['nil'] || '1' === (string) $xsiAttributes['nil'])
518
            ) {
519
                return true;
520
            }
521
        }
522
523
        return null === $value;
524
    }
525
}
526