Completed
Pull Request — master (#868)
by Asmir
02:13
created

enableExternalEntities()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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