Completed
Pull Request — master (#840)
by
unknown
04:07
created

XmlDeserializationVisitor::startVisitingObject()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 8
ccs 7
cts 7
cp 1
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 5
nc 2
nop 4
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 7
    public function visitInteger($data, array $type, Context $context)
131
    {
132 7
        $data = (integer)$data;
133
134 7
        if (null === $this->result) {
135 1
            $this->result = $data;
136 1
        }
137
138 7
        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
        if (null === $this->currentMetadata && $context->getMetadataStack()->count()) {
155 5
            $this->setCurrentMetadata($context->getMetadataStack()->top());
156 5
        }
157
158
        // handle key-value-pairs
159 16
        if (null !== $this->currentMetadata && $this->currentMetadata->xmlKeyValuePairs) {
160 1
            if (2 !== count($type['params'])) {
161
                throw new RuntimeException('The array type must be specified as "array<K,V>" for Key-Value-Pairs.');
162
            }
163
164 1
            list($keyType, $entryType) = $type['params'];
165
166 1
            $result = [];
167 1
            foreach ($data as $key => $v) {
168 1
                $k = $this->navigator->accept($key, $keyType, $context);
169 1
                $result[$k] = $this->navigator->accept($v, $entryType, $context);
170 1
            }
171
172 1
            return $result;
173
        }
174
175 15
        $entryName = null !== $this->currentMetadata && $this->currentMetadata->xmlEntryName ? $this->currentMetadata->xmlEntryName : 'entry';
176 15
        $namespace = null !== $this->currentMetadata && $this->currentMetadata->xmlEntryNamespace ? $this->currentMetadata->xmlEntryNamespace : null;
177
178 15
        if ($namespace === null && $this->objectMetadataStack->count()) {
179 10
            $classMetadata = $this->objectMetadataStack->top();
180 10
            $namespace = isset($classMetadata->xmlNamespaces['']) ? $classMetadata->xmlNamespaces[''] : $namespace;
181 10
        }
182
183 15
        if (null !== $namespace) {
184 4
            $prefix = uniqid('ns-');
185 4
            $data->registerXPathNamespace($prefix, $namespace);
186 4
            $nodes = $data->xpath("$prefix:$entryName");
187 4
        } else {
188 12
            $nodes = $data->xpath($entryName);
189
        }
190
191 15
        if (!count($nodes)) {
192 2
            if (null === $this->result) {
193
                return $this->result = array();
194
            }
195
196 2
            return array();
197
        }
198
199 15
        switch (count($type['params'])) {
200 15
            case 0:
201
                throw new RuntimeException(sprintf('The array type must be specified either as "array<T>", or "array<K,V>".'));
202
203 15
            case 1:
204 15
                $result = array();
205
206 15
                if (null === $this->result) {
207 5
                    $this->result = &$result;
208 5
                }
209
210 15
                foreach ($nodes as $v) {
211 15
                    $result[] = $this->navigator->accept($v, $type['params'][0], $context);
212 15
                }
213
214 15
                return $result;
215
216 4
            case 2:
217 4
                if (null === $this->currentMetadata) {
218
                    throw new RuntimeException('Maps are not supported on top-level without metadata.');
219
                }
220
221 4
                list($keyType, $entryType) = $type['params'];
222 4
                $result = array();
223 4
                if (null === $this->result) {
224
                    $this->result = &$result;
225
                }
226
227 4
                $nodes = $data->children($namespace)->$entryName;
228 4
                foreach ($nodes as $v) {
229 4
                    $attrs = $v->attributes();
230 4
                    if (!isset($attrs[$this->currentMetadata->xmlKeyAttribute])) {
231
                        throw new RuntimeException(sprintf('The key attribute "%s" must be set for each entry of the map.', $this->currentMetadata->xmlKeyAttribute));
232
                    }
233
234 4
                    $k = $this->navigator->accept($attrs[$this->currentMetadata->xmlKeyAttribute], $keyType, $context);
235 4
                    $result[$k] = $this->navigator->accept($v, $entryType, $context);
236 4
                }
237
238 4
                return $result;
239
240
            default:
241
                throw new LogicException(sprintf('The array type does not support more than 2 parameters, but got %s.', json_encode($type['params'])));
242
        }
243
    }
244
245 31
    public function startVisitingObject(ClassMetadata $metadata, $object, array $type, Context $context)
246
    {
247 31
        $this->setCurrentObject($object);
248 31
        $this->objectMetadataStack->push($metadata);
249 31
        if (null === $this->result) {
250 30
            $this->result = $this->currentObject;
251 30
        }
252 31
    }
253
254 27
    public function visitProperty(PropertyMetadata $metadata, $data, Context $context)
255
    {
256 27
        $name = $this->namingStrategy->translateName($metadata);
257
258 27
        if (!$metadata->type) {
259
            throw new RuntimeException(sprintf('You must define a type for %s::$%s.', $metadata->reflection->class, $metadata->name));
260
        }
261
262 27
        if ($metadata->xmlAttribute) {
263
264 5
            $attributes = $data->attributes($metadata->xmlNamespace);
265 5
            if (isset($attributes[$name])) {
266 5
                $v = $this->navigator->accept($attributes[$name], $metadata->type, $context);
267 5
                $this->accessor->setValue($this->currentObject, $v, $metadata);
268 5
            }
269
270 5
            return;
271
        }
272
273 27
        if ($metadata->xmlValue) {
274 6
            $v = $this->navigator->accept($data, $metadata->type, $context);
275 6
            $this->accessor->setValue($this->currentObject, $v, $metadata);
276
277 6
            return;
278
        }
279
280 25
        if ($metadata->xmlCollection) {
281 6
            $enclosingElem = $data;
282 6
            if (!$metadata->xmlCollectionInline) {
283 6
                $enclosingElem = $data->children($metadata->xmlNamespace)->$name;
284 6
            }
285
286 6
            $this->setCurrentMetadata($metadata);
287 6
            $v = $this->navigator->accept($enclosingElem, $metadata->type, $context);
288 6
            $this->revertCurrentMetadata();
289 6
            $this->accessor->setValue($this->currentObject, $v, $metadata);
290
291 6
            return;
292
        }
293
294 24
        if ($metadata->xmlNamespace) {
295 4
            $node = $data->children($metadata->xmlNamespace)->$name;
296 4
            if (!$node->count()) {
297 1
                return;
298
            }
299 3
        } else {
300
301 21
            $namespaces = $data->getDocNamespaces();
302
303 21
            if (isset($namespaces[''])) {
304 1
                $prefix = uniqid('ns-');
305 1
                $data->registerXPathNamespace($prefix, $namespaces['']);
306 1
                $nodes = $data->xpath('./' . $prefix . ':' . $name);
307 1
            } else {
308 20
                $nodes = $data->xpath('./' . $name);
309
            }
310 21
            if (empty($nodes)) {
311 1
                return;
312
            }
313 21
            $node = reset($nodes);
314
        }
315
316 23
        $v = $this->navigator->accept($node, $metadata->type, $context);
317
318 23
        $this->accessor->setValue($this->currentObject, $v, $metadata);
319 23
    }
320
321 31
    public function endVisitingObject(ClassMetadata $metadata, $data, array $type, Context $context)
322
    {
323 31
        $rs = $this->currentObject;
324 31
        $this->objectMetadataStack->pop();
325 31
        $this->revertCurrentObject();
326
327 31
        return $rs;
328
    }
329
330 31
    public function setCurrentObject($object)
331
    {
332 31
        $this->objectStack->push($this->currentObject);
333 31
        $this->currentObject = $object;
334 31
    }
335
336
    public function getCurrentObject()
337
    {
338
        return $this->currentObject;
339
    }
340
341 31
    public function revertCurrentObject()
342
    {
343 31
        return $this->currentObject = $this->objectStack->pop();
344
    }
345
346 11
    public function setCurrentMetadata(PropertyMetadata $metadata)
347
    {
348 11
        $this->metadataStack->push($this->currentMetadata);
349 11
        $this->currentMetadata = $metadata;
350 11
    }
351
352
    public function getCurrentMetadata()
353
    {
354
        return $this->currentMetadata;
355
    }
356
357 6
    public function revertCurrentMetadata()
358
    {
359 6
        return $this->currentMetadata = $this->metadataStack->pop();
360
    }
361
362 59
    public function getResult()
363
    {
364 59
        return $this->result;
365
    }
366
367
    /**
368
     * @param array <string> $doctypeWhitelist
369
     */
370 1
    public function setDoctypeWhitelist(array $doctypeWhitelist)
371
    {
372 1
        $this->doctypeWhitelist = $doctypeWhitelist;
373 1
    }
374
375
    /**
376
     * @return array<string>
377
     */
378
    public function getDoctypeWhitelist()
379
    {
380
        return $this->doctypeWhitelist;
381
    }
382
383
    /**
384
     * Retrieves internalSubset even in bugfixed php versions
385
     *
386
     * @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...
387
     * @param string $data
388
     * @return string
389
     */
390 3
    private function getDomDocumentTypeEntitySubset($data)
391
    {
392 3
        $startPos = $endPos = stripos($data, '<!doctype');
393 3
        $braces = 0;
394
        do {
395 3
            $char = $data[$endPos++];
396 3
            if ($char === '<') {
397 3
                ++$braces;
398 3
            }
399 3
            if ($char === '>') {
400 3
                --$braces;
401 3
            }
402 3
        } while ($braces > 0);
403
404 3
        $internalSubset = substr($data, $startPos, $endPos - $startPos);
405 3
        $internalSubset = str_replace(array("\n", "\r"), '', $internalSubset);
406 3
        $internalSubset = preg_replace('/\s{2,}/', ' ', $internalSubset);
407 3
        $internalSubset = str_replace(array("[ <!", "> ]>"), array('[<!', '>]>'), $internalSubset);
408
409 3
        return $internalSubset;
410
    }
411
412
    /**
413
     * @param mixed $value
414
     *
415
     * @return bool
416
     */
417 61
    public function isNull($value)
418
    {
419 61
        if ($value instanceof \SimpleXMLElement) {
420
            // Workaround for https://bugs.php.net/bug.php?id=75168 and https://github.com/schmittjoh/serializer/issues/817
421
            // If the "name" is empty means that we are on an not-existent node and subsequent operations on the object will trigger the warning:
422
            // "Node no longer exists"
423 61
            if ($value->getName() === "") {
424
                // @todo should be "true", but for collections needs a default collection value. maybe something for the 2.0
425 2
                return false;
426
            }
427
428 61
            $xsiAttributes = $value->attributes('http://www.w3.org/2001/XMLSchema-instance');
429 61
            if (isset($xsiAttributes['nil'])
430 61
                && ((string) $xsiAttributes['nil'] === 'true' || (string) $xsiAttributes['nil'] === '1')
431 61
            ) {
432 8
                return true;
433
            }
434 54
        }
435
436 55
        return $value === null;
437
    }
438
}
439