GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Pull Request — master (#33)
by Juan
02:38 queued 01:26
created

JsonSerializer::restoreUsingUnserialize()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 6
rs 9.4285
cc 1
eloc 4
nc 1
nop 2
1
<?php
2
3
namespace Zumba\JsonSerializer;
4
5
use InvalidArgumentException;
6
use ReflectionClass;
7
use ReflectionException;
8
use SplObjectStorage;
9
use Zumba\JsonSerializer\Exception\JsonSerializerException;
10
use SuperClosure\SerializerInterface as ClosureSerializerInterface;
11
12
class JsonSerializer
13
{
14
15
    const CLASS_IDENTIFIER_KEY = '@type';
16
    const CLOSURE_IDENTIFIER_KEY = '@closure';
17
    const UTF8ENCODED_IDENTIFIER_KEY = '@utf8encoded';
18
    const SCALAR_IDENTIFIER_KEY = '@scalar';
19
    const FLOAT_ADAPTER = 'JsonSerializerFloatAdapter';
20
21
    const KEY_UTF8ENCODED = 1;
22
    const VALUE_UTF8ENCODED = 2;
23
24
    const UNDECLARED_PROPERTY_MODE_SET = 1;
25
    const UNDECLARED_PROPERTY_MODE_IGNORE = 2;
26
    const UNDECLARED_PROPERTY_MODE_EXCEPTION = 3;
27
28
    /**
29
     * Storage for object
30
     *
31
     * Used for recursion
32
     *
33
     * @var SplObjectStorage
34
     */
35
    protected $objectStorage;
36
37
    /**
38
     * Object mapping for recursion
39
     *
40
     * @var array
41
     */
42
    protected $objectMapping = array();
43
44
    /**
45
     * Object mapping index
46
     *
47
     * @var integer
48
     */
49
    protected $objectMappingIndex = 0;
50
51
    /**
52
     * Support PRESERVE_ZERO_FRACTION json option
53
     *
54
     * @var boolean
55
     */
56
    protected $preserveZeroFractionSupport;
57
58
    /**
59
     * Closure serializer instance
60
     *
61
     * @var ClosureSerializerInterface
62
     */
63
    protected $closureSerializer;
64
65
    /**
66
     * Map of custom object serializers
67
     *
68
     * @var array
69
     */
70
    protected $customObjectSerializerMap;
71
72
    /**
73
     * Undefined Attribute Mode
74
     *
75
     * @var integer
76
     */
77
    protected $undefinedAttributeMode = self::UNDECLARED_PROPERTY_MODE_SET;
78
79
    /**
80
     * Constructor.
81
     *
82
     * @param ClosureSerializerInterface $closureSerializer
83
     * @param array $customObjectSerializerMap
84
     */
85
    public function __construct(ClosureSerializerInterface $closureSerializer = null, $customObjectSerializerMap = array())
86
    {
87
        $this->preserveZeroFractionSupport = defined('JSON_PRESERVE_ZERO_FRACTION');
88
        $this->closureSerializer = $closureSerializer;
89
        $this->customObjectSerializerMap = (array)$customObjectSerializerMap;
90
    }
91
92
    /**
93
     * Serialize the value in JSON
94
     *
95
     * @param mixed $value
96
     * @return string JSON encoded
97
     * @throws JsonSerializerException
98
     */
99
    public function serialize($value)
100
    {
101
        $this->reset();
102
        $serializedData = $this->serializeData($value);
103
        $encoded = json_encode($serializedData, $this->calculateEncodeOptions());
104
        if ($encoded === false || json_last_error() != JSON_ERROR_NONE) {
105
            if (json_last_error() != JSON_ERROR_UTF8) {
106
                throw new JsonSerializerException('Invalid data to encode to JSON. Error: ' . json_last_error());
107
            }
108
109
            $serializedData = $this->encodeNonUtf8ToUtf8($serializedData);
110
            $encoded = json_encode($serializedData, $this->calculateEncodeOptions());
111
112
            if ($encoded === false || json_last_error() != JSON_ERROR_NONE) {
113
                throw new JsonSerializerException('Invalid data to encode to JSON. Error: ' . json_last_error());
114
            }
115
        }
116
        return $this->processEncodedValue($encoded);
117
    }
118
119
    /**
120
     * Calculate encoding options
121
     *
122
     * @return integer
123
     */
124
    protected function calculateEncodeOptions()
125
    {
126
        $options = JSON_UNESCAPED_UNICODE;
127
        if ($this->preserveZeroFractionSupport) {
128
            $options |= JSON_PRESERVE_ZERO_FRACTION;
129
        }
130
        return $options;
131
    }
132
133
    /**
134
     * @param mixed $serializedData
135
     *
136
     * @return array
137
     */
138
    protected function encodeNonUtf8ToUtf8($serializedData)
139
    {
140
        if (is_string($serializedData)) {
141
            if (!mb_check_encoding($serializedData, 'UTF-8')) {
142
                $serializedData = [
143
                    static::SCALAR_IDENTIFIER_KEY => mb_convert_encoding($serializedData, 'UTF-8', '8bit'),
144
                    static::UTF8ENCODED_IDENTIFIER_KEY => static::VALUE_UTF8ENCODED,
145
                ];
146
            }
147
148
            return $serializedData;
149
        }
150
151
        $encodedKeys = [];
152
        $encodedData = [];
153
        foreach ($serializedData as $key => $value) {
0 ignored issues
show
Bug introduced by
The expression $serializedData of type object|integer|double|null|array|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
154
            if (is_array($value)) {
155
                $value = $this->encodeNonUtf8ToUtf8($value);
156
            }
157
158
            if (!mb_check_encoding($key, 'UTF-8')) {
159
                $key = mb_convert_encoding($key, 'UTF-8', '8bit');
160
                $encodedKeys[$key] = (isset($encodedKeys[$key]) ? $encodedKeys[$key] : 0) | static::KEY_UTF8ENCODED;
161
            }
162
163
            if (is_string($value)) {
164
                if (!mb_check_encoding($value, 'UTF-8')) {
165
                    $value = mb_convert_encoding($value, 'UTF-8', '8bit');
166
                    $encodedKeys[$key] = (isset($encodedKeys[$key]) ? $encodedKeys[$key] : 0) | static::VALUE_UTF8ENCODED;
167
                }
168
            }
169
170
            $encodedData[$key] = $value;
171
        }
172
173
        if ($encodedKeys) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $encodedKeys of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
174
            $encodedData[self::UTF8ENCODED_IDENTIFIER_KEY] = $encodedKeys;
175
        }
176
177
        return $encodedData;
178
    }
179
180
    /**
181
     * Execute post-encoding actions
182
     *
183
     * @param string $encoded
184
     * @return string
185
     */
186
    protected function processEncodedValue($encoded)
187
    {
188
        if (!$this->preserveZeroFractionSupport) {
189
            $encoded = preg_replace('/"' . static::FLOAT_ADAPTER . '\((.*?)\)"/', '\1', $encoded);
190
        }
191
        return $encoded;
192
    }
193
194
    /**
195
     * Unserialize the value from JSON
196
     *
197
     * @param string $value
198
     * @return mixed
199
     */
200
    public function unserialize($value)
201
    {
202
        $this->reset();
203
        $data = json_decode($value, true);
204
        if ($data === null && json_last_error() != JSON_ERROR_NONE) {
205
            throw new JsonSerializerException('Invalid JSON to unserialize.');
206
        }
207
208
        if (mb_strpos($value, static::UTF8ENCODED_IDENTIFIER_KEY) !== false) {
209
            $data = $this->decodeNonUtf8FromUtf8($data);
210
        }
211
212
        return $this->unserializeData($data);
213
    }
214
215
    /**
216
     * Set unserialization mode for undeclared class properties
217
     *
218
     * @param integer $value One of the JsonSerializer::UNDECLARED_PROPERTY_MODE_*
219
     * @return self
220
     * @throws InvalidArgumentException When the value is not one of the UNDECLARED_PROPERTY_MODE_* options
221
     */
222
    public function setUnserializeUndeclaredPropertyMode($value)
223
    {
224
        $availableOptions = [
225
            static::UNDECLARED_PROPERTY_MODE_SET,
226
            static::UNDECLARED_PROPERTY_MODE_IGNORE,
227
            static::UNDECLARED_PROPERTY_MODE_EXCEPTION
228
        ];
229
        if (!in_array($value, $availableOptions)) {
230
            throw new InvalidArgumentException('Invalid value.');
231
        }
232
        $this->undefinedAttributeMode = $value;
233
        return $this;
234
    }
235
236
    /**
237
     * Parse the data to be json encoded
238
     *
239
     * @param mixed $value
240
     * @return mixed
241
     * @throws JsonSerializerException
242
     */
243
    protected function serializeData($value)
244
    {
245
        if (is_scalar($value) || $value === null) {
246
            if (!$this->preserveZeroFractionSupport && is_float($value) && ctype_digit((string)$value)) {
247
                // Because the PHP bug #50224, the float numbers with no
248
                // precision numbers are converted to integers when encoded
249
                $value = static::FLOAT_ADAPTER . '(' . $value . '.0)';
250
            }
251
            return $value;
252
        }
253
        if (is_resource($value)) {
254
            throw new JsonSerializerException('Resource is not supported in JsonSerializer');
255
        }
256
        if (is_array($value)) {
257
            return array_map(array($this, __FUNCTION__), $value);
258
        }
259
        if ($value instanceof \Closure) {
260
            if (!$this->closureSerializer) {
261
                throw new JsonSerializerException('Closure serializer not given. Unable to serialize closure.');
262
            }
263
            return array(
264
                static::CLOSURE_IDENTIFIER_KEY => true,
265
                'value' => $this->closureSerializer->serialize($value)
266
            );
267
        }
268
        return $this->serializeObject($value);
269
    }
270
271
    /**
272
     * Extract the data from an object
273
     *
274
     * @param object $value
275
     * @return array
276
     */
277
    protected function serializeObject($value)
278
    {
279
        if ($this->objectStorage->contains($value)) {
280
            return array(static::CLASS_IDENTIFIER_KEY => '@' . $this->objectStorage[$value]);
281
        }
282
        $this->objectStorage->attach($value, $this->objectMappingIndex++);
283
284
        $ref = new ReflectionClass($value);
285
        $className = $ref->getName();
286
        if (array_key_exists($className, $this->customObjectSerializerMap)) {
287
            $data = array(static::CLASS_IDENTIFIER_KEY => $className);
288
            $data += $this->customObjectSerializerMap[$className]->serialize($value);
289
            return $data;
290
        }
291
292
        $paramsToSerialize = $this->getObjectProperties($ref, $value);
293
        $data = array(static::CLASS_IDENTIFIER_KEY => $className);
294
295
        if($value instanceof \SplDoublyLinkedList){
296
            return $data + array('value' => $value->serialize());
297
        }
298
299
        $data += array_map(array($this, 'serializeData'), $this->extractObjectData($value, $ref, $paramsToSerialize));
300
        return $data;
301
    }
302
303
    /**
304
     * Return the list of properties to be serialized
305
     *
306
     * @param ReflectionClass $ref
307
     * @param object $value
308
     * @return array
309
     */
310
    protected function getObjectProperties($ref, $value)
311
    {
312
        if (method_exists($value, '__sleep')) {
313
            return $value->__sleep();
314
        }
315
316
        $props = array();
317
        foreach ($ref->getProperties() as $prop) {
318
            $props[] = $prop->getName();
319
        }
320
        return array_unique(array_merge($props, array_keys(get_object_vars($value))));
321
    }
322
323
    /**
324
     * Extract the object data
325
     *
326
     * @param object $value
327
     * @param ReflectionClass $ref
328
     * @param array $properties
329
     * @return array
330
     */
331
    protected function extractObjectData($value, $ref, $properties)
332
    {
333
        $data = array();
334
        foreach ($properties as $property) {
335
            try {
336
                $propRef = $ref->getProperty($property);
337
                $propRef->setAccessible(true);
338
                $data[$property] = $propRef->getValue($value);
339
            } catch (ReflectionException $e) {
340
                $data[$property] = $value->$property;
341
            }
342
        }
343
        return $data;
344
    }
345
346
    /**
347
     * Parse the json decode to convert to objects again
348
     *
349
     * @param mixed $value
350
     * @return mixed
351
     */
352
    protected function unserializeData($value)
353
    {
354
        if (is_scalar($value) || $value === null) {
355
            return $value;
356
        }
357
358
        if (isset($value[static::CLASS_IDENTIFIER_KEY])) {
359
            return $this->unserializeObject($value);
0 ignored issues
show
Bug introduced by
It seems like $value defined by parameter $value on line 352 can also be of type object; however, Zumba\JsonSerializer\Jso...er::unserializeObject() does only seem to accept array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
360
        }
361
362
        if (!empty($value[static::CLOSURE_IDENTIFIER_KEY])) {
363
            if (!$this->closureSerializer) {
364
                throw new JsonSerializerException('Closure serializer not provided to unserialize closure');
365
            }
366
            return $this->closureSerializer->unserialize($value['value']);
367
        }
368
369
        return array_map(array($this, __FUNCTION__), $value);
370
    }
371
372
    /**
373
     * @param mixed $serializedData
374
     *
375
     * @return mixed
376
     */
377
    protected function decodeNonUtf8FromUtf8($serializedData)
378
    {
379
        if (is_array($serializedData) && isset($serializedData[static::SCALAR_IDENTIFIER_KEY])) {
380
            $serializedData = mb_convert_encoding($serializedData[static::SCALAR_IDENTIFIER_KEY], '8bit', 'UTF-8');
381
            return $serializedData;
382
        } elseif (is_scalar($serializedData) || $serializedData === null) {
383
            return $serializedData;
384
        }
385
386
        $encodedKeys = [];
387
        if (isset($serializedData[static::UTF8ENCODED_IDENTIFIER_KEY])) {
388
            $encodedKeys = $serializedData[static::UTF8ENCODED_IDENTIFIER_KEY];
389
            unset($serializedData[static::UTF8ENCODED_IDENTIFIER_KEY]);
390
        }
391
392
        $decodedData = [];
393
        foreach ($serializedData as $key => $value) {
394
            if (is_array($value)) {
395
                $value = $this->decodeNonUtf8FromUtf8($value);
396
            }
397
398
            if (isset($encodedKeys[$key])) {
399
                $originalKey = $key;
400
                if ($encodedKeys[$key] & static::KEY_UTF8ENCODED) {
401
                    $key = mb_convert_encoding($key, '8bit', 'UTF-8');
402
                }
403
                if ($encodedKeys[$originalKey] & static::VALUE_UTF8ENCODED) {
404
                    $value = mb_convert_encoding($value, '8bit', 'UTF-8');
405
                }
406
            }
407
408
            $decodedData[$key] = $value;
409
        }
410
411
        return $decodedData;
412
    }
413
414
    /**
415
     * Convert the serialized array into an object
416
     *
417
     * @param array $value
418
     * @return object
419
     * @throws JsonSerializerException
420
     */
421
    protected function unserializeObject($value)
422
    {
423
        $className = $value[static::CLASS_IDENTIFIER_KEY];
424
        unset($value[static::CLASS_IDENTIFIER_KEY]);
425
426
        if ($className[0] === '@') {
427
            $index = substr($className, 1);
428
            return $this->objectMapping[$index];
429
        }
430
431
        if (array_key_exists($className, $this->customObjectSerializerMap)) {
432
            $obj = $this->customObjectSerializerMap[$className]->unserialize($value);
433
            $this->objectMapping[$this->objectMappingIndex++] = $obj;
434
            return $obj;
435
        }
436
437
        if (!class_exists($className)) {
438
            throw new JsonSerializerException('Unable to find class ' . $className);
439
        }
440
441
        if ($className === 'DateTime') {
442
            $obj = $this->restoreUsingUnserialize($className, $value);
443
            $this->objectMapping[$this->objectMappingIndex++] = $obj;
444
            return $obj;
445
        }
446
447
        if (!$this->isSplList($className)) {
448
            $ref = new ReflectionClass($className);
449
            $obj = $ref->newInstanceWithoutConstructor();
450
        } else {
451
            $obj = new $className();
452
        }
453
454
        if ($obj instanceof \SplDoublyLinkedList ) {
455
            $obj->unserialize($value['value']);
456
            $this->objectMapping[$this->objectMappingIndex++] = $obj;
457
            return $obj;
458
        }
459
460
        $this->objectMapping[$this->objectMappingIndex++] = $obj;
461
        foreach ($value as $property => $propertyValue) {
462
            try {
463
                $propRef = $ref->getProperty($property);
0 ignored issues
show
Bug introduced by
The variable $ref does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
464
                $propRef->setAccessible(true);
465
                $propRef->setValue($obj, $this->unserializeData($propertyValue));
466
            } catch (ReflectionException $e) {
467
                switch ($this->undefinedAttributeMode) {
468
                case static::UNDECLARED_PROPERTY_MODE_SET:
469
                    $obj->$property = $this->unserializeData($propertyValue);
470
                    break;
471
                case static::UNDECLARED_PROPERTY_MODE_IGNORE:
472
                    break;
473
                case static::UNDECLARED_PROPERTY_MODE_EXCEPTION:
474
                    throw new JsonSerializerException('Undefined attribute detected during unserialization');
475
                    break;
0 ignored issues
show
Unused Code introduced by
break; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
476
                }
477
            }
478
        }
479
        if (method_exists($obj, '__wakeup')) {
480
            $obj->__wakeup();
481
        }
482
        return $obj;
483
    }
484
485
    /**
486
     * @return boolean
487
     */
488
    protected function isSplList($className)
489
    {
490
        return in_array($className, array('SplQueue', 'SplDoublyLinkedList', 'SplStack'));
491
    }
492
493
    protected function restoreUsingUnserialize($className, $attributes)
494
    {
495
        $obj = (object)$attributes;
496
        $serialized = preg_replace('|^O:\d+:"\w+":|', 'O:' . strlen($className) . ':"' . $className . '":', serialize($obj));
497
        return unserialize($serialized);
498
    }
499
500
    /**
501
     * Reset variables
502
     *
503
     * @return void
504
     */
505
    protected function reset()
506
    {
507
        $this->objectStorage = new SplObjectStorage();
508
        $this->objectMapping = array();
509
        $this->objectMappingIndex = 0;
510
    }
511
}
512