Completed
Push — master ( 9dc44f...b715d6 )
by Asmir
03:39
created

XmlDeserializationVisitor::endVisitingObject()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

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