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 (#34)
by Chris
02:23
created

JsonSerializer::isSplList()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
c 0
b 0
f 0
rs 10
cc 1
eloc 2
nc 1
nop 1
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
     * Undefined Attribute Mode
81
     *
82
     * @var false|array
83
     */
84
    protected $supportedClasses = false;
85
86
    /**
87
     * Constructor.
88
     *
89
     * @param ClosureSerializerInterface $closureSerializer
90
     * @param array $customObjectSerializerMap
91
     */
92
    public function __construct(ClosureSerializerInterface $closureSerializer = null, $customObjectSerializerMap = array())
93
    {
94
        $this->preserveZeroFractionSupport = defined('JSON_PRESERVE_ZERO_FRACTION');
95
        $this->closureSerializer = $closureSerializer;
96
        $this->customObjectSerializerMap = (array)$customObjectSerializerMap;
97
    }
98
99
    public function withSupportedClasses($supportedClasses)
100
    {
101
        $this->supportedClasses = $supportedClasses;
102
        return $this;
103
    }
104
105
    public function isClassSupported($className)
106
    {
107
        if(false === $this->supportedClasses){
108
            return true;
109
        }
110
111
        return in_array($className, $this->supportedClasses);
112
    }
113
114
    /**
115
     * Serialize the value in JSON
116
     *
117
     * @param mixed $value
118
     * @return string JSON encoded
119
     * @throws JsonSerializerException
120
     */
121
    public function serialize($value)
122
    {
123
        $this->reset();
124
        $serializedData = $this->serializeData($value);
125
        $encoded = json_encode($serializedData, $this->calculateEncodeOptions());
126
        if ($encoded === false || json_last_error() != JSON_ERROR_NONE) {
127
            if (json_last_error() != JSON_ERROR_UTF8) {
128
                throw new JsonSerializerException('Invalid data to encode to JSON. Error: ' . json_last_error());
129
            }
130
131
            $serializedData = $this->encodeNonUtf8ToUtf8($serializedData);
132
            $encoded = json_encode($serializedData, $this->calculateEncodeOptions());
133
134
            if ($encoded === false || json_last_error() != JSON_ERROR_NONE) {
135
                throw new JsonSerializerException('Invalid data to encode to JSON. Error: ' . json_last_error());
136
            }
137
        }
138
        return $this->processEncodedValue($encoded);
139
    }
140
141
    /**
142
     * Calculate encoding options
143
     *
144
     * @return integer
145
     */
146
    protected function calculateEncodeOptions()
147
    {
148
        $options = JSON_UNESCAPED_UNICODE;
149
        if ($this->preserveZeroFractionSupport) {
150
            $options |= JSON_PRESERVE_ZERO_FRACTION;
151
        }
152
        return $options;
153
    }
154
155
    /**
156
     * @param mixed $serializedData
157
     *
158
     * @return array
159
     */
160
    protected function encodeNonUtf8ToUtf8($serializedData)
161
    {
162
        if (is_string($serializedData)) {
163
            if (!mb_check_encoding($serializedData, 'UTF-8')) {
164
                $serializedData = [
165
                    static::SCALAR_IDENTIFIER_KEY => mb_convert_encoding($serializedData, 'UTF-8', '8bit'),
166
                    static::UTF8ENCODED_IDENTIFIER_KEY => static::VALUE_UTF8ENCODED,
167
                ];
168
            }
169
170
            return $serializedData;
171
        }
172
173
        $encodedKeys = [];
174
        $encodedData = [];
175
        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...
176
            if (is_array($value)) {
177
                $value = $this->encodeNonUtf8ToUtf8($value);
178
            }
179
180
            if (!mb_check_encoding($key, 'UTF-8')) {
181
                $key = mb_convert_encoding($key, 'UTF-8', '8bit');
182
                $encodedKeys[$key] = (isset($encodedKeys[$key]) ? $encodedKeys[$key] : 0) | static::KEY_UTF8ENCODED;
183
            }
184
185
            if (is_string($value)) {
186
                if (!mb_check_encoding($value, 'UTF-8')) {
187
                    $value = mb_convert_encoding($value, 'UTF-8', '8bit');
188
                    $encodedKeys[$key] = (isset($encodedKeys[$key]) ? $encodedKeys[$key] : 0) | static::VALUE_UTF8ENCODED;
189
                }
190
            }
191
192
            $encodedData[$key] = $value;
193
        }
194
195
        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...
196
            $encodedData[self::UTF8ENCODED_IDENTIFIER_KEY] = $encodedKeys;
197
        }
198
199
        return $encodedData;
200
    }
201
202
    /**
203
     * Execute post-encoding actions
204
     *
205
     * @param string $encoded
206
     * @return string
207
     */
208
    protected function processEncodedValue($encoded)
209
    {
210
        if (!$this->preserveZeroFractionSupport) {
211
            $encoded = preg_replace('/"' . static::FLOAT_ADAPTER . '\((.*?)\)"/', '\1', $encoded);
212
        }
213
        return $encoded;
214
    }
215
216
    /**
217
     * Unserialize the value from JSON
218
     *
219
     * @param string $value
220
     * @return mixed
221
     */
222
    public function unserialize($value)
223
    {
224
        $this->reset();
225
        $data = json_decode($value, true);
226
        if ($data === null && json_last_error() != JSON_ERROR_NONE) {
227
            throw new JsonSerializerException('Invalid JSON to unserialize.');
228
        }
229
230
        if (mb_strpos($value, static::UTF8ENCODED_IDENTIFIER_KEY) !== false) {
231
            $data = $this->decodeNonUtf8FromUtf8($data);
232
        }
233
234
        return $this->unserializeData($data);
235
    }
236
237
    /**
238
     * Set unserialization mode for undeclared class properties
239
     *
240
     * @param integer $value One of the JsonSerializer::UNDECLARED_PROPERTY_MODE_*
241
     * @return self
242
     * @throws InvalidArgumentException When the value is not one of the UNDECLARED_PROPERTY_MODE_* options
243
     */
244
    public function setUnserializeUndeclaredPropertyMode($value)
245
    {
246
        $availableOptions = [
247
            static::UNDECLARED_PROPERTY_MODE_SET,
248
            static::UNDECLARED_PROPERTY_MODE_IGNORE,
249
            static::UNDECLARED_PROPERTY_MODE_EXCEPTION
250
        ];
251
        if (!in_array($value, $availableOptions)) {
252
            throw new InvalidArgumentException('Invalid value.');
253
        }
254
        $this->undefinedAttributeMode = $value;
255
        return $this;
256
    }
257
258
    /**
259
     * Parse the data to be json encoded
260
     *
261
     * @param mixed $value
262
     * @return mixed
263
     * @throws JsonSerializerException
264
     */
265
    protected function serializeData($value)
266
    {
267
        if (is_scalar($value) || $value === null) {
268
            if (!$this->preserveZeroFractionSupport && is_float($value) && ctype_digit((string)$value)) {
269
                // Because the PHP bug #50224, the float numbers with no
270
                // precision numbers are converted to integers when encoded
271
                $value = static::FLOAT_ADAPTER . '(' . $value . '.0)';
272
            }
273
            return $value;
274
        }
275
        if (is_resource($value)) {
276
            throw new JsonSerializerException('Resource is not supported in JsonSerializer');
277
        }
278
        if (is_array($value)) {
279
            return array_map(array($this, __FUNCTION__), $value);
280
        }
281
        if ($value instanceof \Closure) {
282
            if (!$this->closureSerializer) {
283
                throw new JsonSerializerException('Closure serializer not given. Unable to serialize closure.');
284
            }
285
            return array(
286
                static::CLOSURE_IDENTIFIER_KEY => true,
287
                'value' => $this->closureSerializer->serialize($value)
288
            );
289
        }
290
        return $this->serializeObject($value);
291
    }
292
293
    /**
294
     * Extract the data from an object
295
     *
296
     * @param object $value
297
     * @return array
298
     */
299
    protected function serializeObject($value)
300
    {
301
        if ($this->objectStorage->contains($value)) {
302
            return array(static::CLASS_IDENTIFIER_KEY => '@' . $this->objectStorage[$value]);
303
        }
304
        $this->objectStorage->attach($value, $this->objectMappingIndex++);
305
306
        $ref = new ReflectionClass($value);
307
        $className = $ref->getName();
308
309
        if(!$this->isClassSupported($className)){
310
            throw new JsonSerializerException('The supplied class was not on the explicitly allowed list of supported classes. Unable to serialize object.');
311
        }
312
313
        if (array_key_exists($className, $this->customObjectSerializerMap)) {
314
            $data = array(static::CLASS_IDENTIFIER_KEY => $className);
315
            $data += $this->customObjectSerializerMap[$className]->serialize($value);
316
            return $data;
317
        }
318
319
        $paramsToSerialize = $this->getObjectProperties($ref, $value);
320
        $data = array(static::CLASS_IDENTIFIER_KEY => $className);
321
322
        if($value instanceof \SplDoublyLinkedList){
323
            return $data + array('value' => $value->serialize());
324
        }
325
326
        $data += array_map(array($this, 'serializeData'), $this->extractObjectData($value, $ref, $paramsToSerialize));
327
        return $data;
328
    }
329
330
    /**
331
     * Return the list of properties to be serialized
332
     *
333
     * @param ReflectionClass $ref
334
     * @param object $value
335
     * @return array
336
     */
337
    protected function getObjectProperties($ref, $value)
338
    {
339
        if (method_exists($value, '__sleep')) {
340
            return $value->__sleep();
341
        }
342
343
        $props = array();
344
        foreach ($ref->getProperties() as $prop) {
345
            $props[] = $prop->getName();
346
        }
347
        return array_unique(array_merge($props, array_keys(get_object_vars($value))));
348
    }
349
350
    /**
351
     * Extract the object data
352
     *
353
     * @param object $value
354
     * @param ReflectionClass $ref
355
     * @param array $properties
356
     * @return array
357
     */
358
    protected function extractObjectData($value, $ref, $properties)
359
    {
360
        $data = array();
361
        foreach ($properties as $property) {
362
            try {
363
                $propRef = $ref->getProperty($property);
364
                $propRef->setAccessible(true);
365
                $data[$property] = $propRef->getValue($value);
366
            } catch (ReflectionException $e) {
367
                $data[$property] = $value->$property;
368
            }
369
        }
370
        return $data;
371
    }
372
373
    /**
374
     * Parse the json decode to convert to objects again
375
     *
376
     * @param mixed $value
377
     * @return mixed
378
     */
379
    protected function unserializeData($value)
380
    {
381
        if (is_scalar($value) || $value === null) {
382
            return $value;
383
        }
384
385
        if (isset($value[static::CLASS_IDENTIFIER_KEY])) {
386
            return $this->unserializeObject($value);
0 ignored issues
show
Bug introduced by
It seems like $value defined by parameter $value on line 379 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...
387
        }
388
389
        if (!empty($value[static::CLOSURE_IDENTIFIER_KEY])) {
390
            if (!$this->closureSerializer) {
391
                throw new JsonSerializerException('Closure serializer not provided to unserialize closure');
392
            }
393
            return $this->closureSerializer->unserialize($value['value']);
394
        }
395
396
        return array_map(array($this, __FUNCTION__), $value);
397
    }
398
399
    /**
400
     * @param mixed $serializedData
401
     *
402
     * @return mixed
403
     */
404
    protected function decodeNonUtf8FromUtf8($serializedData)
405
    {
406
        if (is_array($serializedData) && isset($serializedData[static::SCALAR_IDENTIFIER_KEY])) {
407
            $serializedData = mb_convert_encoding($serializedData[static::SCALAR_IDENTIFIER_KEY], '8bit', 'UTF-8');
408
            return $serializedData;
409
        } elseif (is_scalar($serializedData) || $serializedData === null) {
410
            return $serializedData;
411
        }
412
413
        $encodedKeys = [];
414
        if (isset($serializedData[static::UTF8ENCODED_IDENTIFIER_KEY])) {
415
            $encodedKeys = $serializedData[static::UTF8ENCODED_IDENTIFIER_KEY];
416
            unset($serializedData[static::UTF8ENCODED_IDENTIFIER_KEY]);
417
        }
418
419
        $decodedData = [];
420
        foreach ($serializedData as $key => $value) {
421
            if (is_array($value)) {
422
                $value = $this->decodeNonUtf8FromUtf8($value);
423
            }
424
425
            if (isset($encodedKeys[$key])) {
426
                $originalKey = $key;
427
                if ($encodedKeys[$key] & static::KEY_UTF8ENCODED) {
428
                    $key = mb_convert_encoding($key, '8bit', 'UTF-8');
429
                }
430
                if ($encodedKeys[$originalKey] & static::VALUE_UTF8ENCODED) {
431
                    $value = mb_convert_encoding($value, '8bit', 'UTF-8');
432
                }
433
            }
434
435
            $decodedData[$key] = $value;
436
        }
437
438
        return $decodedData;
439
    }
440
441
    /**
442
     * Convert the serialized array into an object
443
     *
444
     * @param array $value
445
     * @return object
446
     * @throws JsonSerializerException
447
     */
448
    protected function unserializeObject($value)
449
    {
450
        $className = $value[static::CLASS_IDENTIFIER_KEY];
451
        unset($value[static::CLASS_IDENTIFIER_KEY]);
452
453
        if(!$this->isClassSupported($className)){
454
            throw new JsonSerializerException('The supplied class was not on the explicitly allowed list of supported classes. Unable to unserialize object.');
455
        }
456
457
        if ($className[0] === '@') {
458
            $index = substr($className, 1);
459
            return $this->objectMapping[$index];
460
        }
461
462
        if (array_key_exists($className, $this->customObjectSerializerMap)) {
463
            $obj = $this->customObjectSerializerMap[$className]->unserialize($value);
464
            $this->objectMapping[$this->objectMappingIndex++] = $obj;
465
            return $obj;
466
        }
467
468
        if (!class_exists($className)) {
469
            throw new JsonSerializerException('Unable to find class ' . $className);
470
        }
471
472
        if ($className === 'DateTime') {
473
            $obj = $this->restoreUsingUnserialize($className, $value);
474
            $this->objectMapping[$this->objectMappingIndex++] = $obj;
475
            return $obj;
476
        }
477
478
        if (!$this->isSplList($className)) {
479
            $ref = new ReflectionClass($className);
480
            $obj = $ref->newInstanceWithoutConstructor();
481
        } else {
482
            $obj = new $className();
483
        }
484
485
        if ($obj instanceof \SplDoublyLinkedList ) {
486
            $obj->unserialize($value['value']);
487
            $this->objectMapping[$this->objectMappingIndex++] = $obj;
488
            return $obj;
489
        }
490
491
        $this->objectMapping[$this->objectMappingIndex++] = $obj;
492
        foreach ($value as $property => $propertyValue) {
493
            try {
494
                $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...
495
                $propRef->setAccessible(true);
496
                $propRef->setValue($obj, $this->unserializeData($propertyValue));
497
            } catch (ReflectionException $e) {
498
                switch ($this->undefinedAttributeMode) {
499
                case static::UNDECLARED_PROPERTY_MODE_SET:
500
                    $obj->$property = $this->unserializeData($propertyValue);
501
                    break;
502
                case static::UNDECLARED_PROPERTY_MODE_IGNORE:
503
                    break;
504
                case static::UNDECLARED_PROPERTY_MODE_EXCEPTION:
505
                    throw new JsonSerializerException('Undefined attribute detected during unserialization');
506
                    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...
507
                }
508
            }
509
        }
510
        if (method_exists($obj, '__wakeup')) {
511
            $obj->__wakeup();
512
        }
513
        return $obj;
514
    }
515
516
    /**
517
     * @return boolean
518
     */
519
    protected function isSplList($className)
520
    {
521
        return in_array($className, array('SplQueue', 'SplDoublyLinkedList', 'SplStack'));
522
    }
523
524
    protected function restoreUsingUnserialize($className, $attributes)
525
    {
526
        $obj = (object)$attributes;
527
        $serialized = preg_replace('|^O:\d+:"\w+":|', 'O:' . strlen($className) . ':"' . $className . '":', serialize($obj));
528
        return unserialize($serialized);
529
    }
530
531
    /**
532
     * Reset variables
533
     *
534
     * @return void
535
     */
536
    protected function reset()
537
    {
538
        $this->objectStorage = new SplObjectStorage();
539
        $this->objectMapping = array();
540
        $this->objectMappingIndex = 0;
541
    }
542
}
543