1<?php 2/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */ 3 4namespace Icinga\Util; 5 6use Icinga\Exception\Json\JsonDecodeException; 7use Icinga\Exception\Json\JsonEncodeException; 8 9/** 10 * Wrap {@link json_encode()} and {@link json_decode()} with error handling 11 */ 12class Json 13{ 14 /** 15 * {@link json_encode()} wrapper 16 * 17 * @param mixed $value 18 * @param int $options 19 * @param int $depth 20 * 21 * @return string 22 * @throws JsonEncodeException 23 */ 24 public static function encode($value, $options = 0, $depth = 512) 25 { 26 return static::encodeAndSanitize($value, $options, $depth, false); 27 } 28 29 /** 30 * {@link json_encode()} wrapper, automatically sanitizes bad UTF-8 31 * 32 * @param mixed $value 33 * @param int $options 34 * @param int $depth 35 * 36 * @return string 37 * @throws JsonEncodeException 38 */ 39 public static function sanitize($value, $options = 0, $depth = 512) 40 { 41 return static::encodeAndSanitize($value, $options, $depth, true); 42 } 43 44 /** 45 * {@link json_encode()} wrapper, sanitizes bad UTF-8 46 * 47 * @param mixed $value 48 * @param int $options 49 * @param int $depth 50 * @param bool $autoSanitize Automatically sanitize invalid UTF-8 (if any) 51 * 52 * @return string 53 * @throws JsonEncodeException 54 */ 55 protected static function encodeAndSanitize($value, $options, $depth, $autoSanitize) 56 { 57 $encoded = json_encode($value, $options, $depth); 58 59 switch (json_last_error()) { 60 case JSON_ERROR_NONE: 61 return $encoded; 62 63 /** @noinspection PhpMissingBreakStatementInspection */ 64 case JSON_ERROR_UTF8: 65 if ($autoSanitize) { 66 return static::encode(static::sanitizeUtf8Recursive($value), $options, $depth); 67 } 68 // Fallthrough 69 70 default: 71 throw new JsonEncodeException('%s: %s', json_last_error_msg(), var_export($value, true)); 72 } 73 } 74 75 /** 76 * {@link json_decode()} wrapper 77 * 78 * @param string $json 79 * @param bool $assoc 80 * @param int $depth 81 * @param int $options 82 * 83 * @return mixed 84 * @throws JsonDecodeException 85 */ 86 public static function decode($json, $assoc = false, $depth = 512, $options = 0) 87 { 88 $decoded = json_decode($json, $assoc, $depth, $options); 89 90 if (json_last_error() !== JSON_ERROR_NONE) { 91 throw new JsonDecodeException('%s: %s', json_last_error_msg(), var_export($json, true)); 92 } 93 return $decoded; 94 } 95 96 /** 97 * Replace bad byte sequences in UTF-8 strings inside the given JSON-encodable structure with question marks 98 * 99 * @param mixed $value 100 * 101 * @return mixed 102 */ 103 protected static function sanitizeUtf8Recursive($value) 104 { 105 switch (gettype($value)) { 106 case 'string': 107 return static::sanitizeUtf8String($value); 108 109 case 'array': 110 $sanitized = array(); 111 112 foreach ($value as $key => $val) { 113 if (is_string($key)) { 114 $key = static::sanitizeUtf8String($key); 115 } 116 117 $sanitized[$key] = static::sanitizeUtf8Recursive($val); 118 } 119 120 return $sanitized; 121 122 case 'object': 123 $sanitized = array(); 124 125 foreach ($value as $key => $val) { 126 if (is_string($key)) { 127 $key = static::sanitizeUtf8String($key); 128 } 129 130 $sanitized[$key] = static::sanitizeUtf8Recursive($val); 131 } 132 133 return (object) $sanitized; 134 135 default: 136 return $value; 137 } 138 } 139 140 /** 141 * Replace bad byte sequences in the given UTF-8 string with question marks 142 * 143 * @param string $string 144 * 145 * @return string 146 */ 147 protected static function sanitizeUtf8String($string) 148 { 149 return mb_convert_encoding($string, 'UTF-8', 'UTF-8'); 150 } 151} 152