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