Passed
Pull Request — master (#920)
by Asmir
03:17
created

XmlDeserializationVisitor::visitString()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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