Completed
Pull Request — master (#743)
by Asmir
07:41
created

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