Completed
Push — master ( 0c820c...9912c8 )
by Asmir
9s
created

XmlDeserializationVisitor::getNavigator()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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