Completed
Pull Request — master (#851)
by
unknown
18:18
created

XmlDeserializationVisitor::visitDouble()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

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