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