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