Completed
Push — master ( 2b1686...37cc39 )
by Johannes
11s
created

XmlDeserializationVisitor::visitArray()   F

Complexity

Conditions 19
Paths 720

Size

Total Lines 64
Code Lines 38

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 64
rs 3.125
cc 19
eloc 38
nc 720
nop 3

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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