Passed
Branch master (bf85d9)
by Johannes
05:40
created

XmlDeserializationVisitor::getCurrentObject()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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