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