1<?php declare(strict_types=1); 2 3/* 4 * This file is part of the Monolog package. 5 * 6 * (c) Jordi Boggiano <j.boggiano@seld.be> 7 * 8 * For the full copyright and license information, please view the LICENSE 9 * file that was distributed with this source code. 10 */ 11 12namespace Monolog; 13 14final class Utils 15{ 16 const DEFAULT_JSON_FLAGS = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRESERVE_ZERO_FRACTION | JSON_INVALID_UTF8_SUBSTITUTE | JSON_PARTIAL_OUTPUT_ON_ERROR; 17 18 public static function getClass(object $object): string 19 { 20 $class = \get_class($object); 21 22 if (false === ($pos = \strpos($class, "@anonymous\0"))) { 23 return $class; 24 } 25 26 if (false === ($parent = \get_parent_class($class))) { 27 return \substr($class, 0, $pos + 10); 28 } 29 30 return $parent . '@anonymous'; 31 } 32 33 public static function substr(string $string, int $start, ?int $length = null): string 34 { 35 if (extension_loaded('mbstring')) { 36 return mb_strcut($string, $start, $length); 37 } 38 39 return substr($string, $start, (null === $length) ? strlen($string) : $length); 40 } 41 42 /** 43 * Makes sure if a relative path is passed in it is turned into an absolute path 44 * 45 * @param string $streamUrl stream URL or path without protocol 46 */ 47 public static function canonicalizePath(string $streamUrl): string 48 { 49 $prefix = ''; 50 if ('file://' === substr($streamUrl, 0, 7)) { 51 $streamUrl = substr($streamUrl, 7); 52 $prefix = 'file://'; 53 } 54 55 // other type of stream, not supported 56 if (false !== strpos($streamUrl, '://')) { 57 return $streamUrl; 58 } 59 60 // already absolute 61 if (substr($streamUrl, 0, 1) === '/' || substr($streamUrl, 1, 1) === ':' || substr($streamUrl, 0, 2) === '\\\\') { 62 return $prefix.$streamUrl; 63 } 64 65 $streamUrl = getcwd() . '/' . $streamUrl; 66 67 return $prefix.$streamUrl; 68 } 69 70 /** 71 * Return the JSON representation of a value 72 * 73 * @param mixed $data 74 * @param int $encodeFlags flags to pass to json encode, defaults to DEFAULT_JSON_FLAGS 75 * @param bool $ignoreErrors whether to ignore encoding errors or to throw on error, when ignored and the encoding fails, "null" is returned which is valid json for null 76 * @throws \RuntimeException if encoding fails and errors are not ignored 77 * @return string when errors are ignored and the encoding fails, "null" is returned which is valid json for null 78 */ 79 public static function jsonEncode($data, ?int $encodeFlags = null, bool $ignoreErrors = false): string 80 { 81 if (null === $encodeFlags) { 82 $encodeFlags = self::DEFAULT_JSON_FLAGS; 83 } 84 85 if ($ignoreErrors) { 86 $json = @json_encode($data, $encodeFlags); 87 if (false === $json) { 88 return 'null'; 89 } 90 91 return $json; 92 } 93 94 $json = json_encode($data, $encodeFlags); 95 if (false === $json) { 96 $json = self::handleJsonError(json_last_error(), $data); 97 } 98 99 return $json; 100 } 101 102 /** 103 * Handle a json_encode failure. 104 * 105 * If the failure is due to invalid string encoding, try to clean the 106 * input and encode again. If the second encoding attempt fails, the 107 * initial error is not encoding related or the input can't be cleaned then 108 * raise a descriptive exception. 109 * 110 * @param int $code return code of json_last_error function 111 * @param mixed $data data that was meant to be encoded 112 * @param int $encodeFlags flags to pass to json encode, defaults to JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRESERVE_ZERO_FRACTION 113 * @throws \RuntimeException if failure can't be corrected 114 * @return string JSON encoded data after error correction 115 */ 116 public static function handleJsonError(int $code, $data, ?int $encodeFlags = null): string 117 { 118 if ($code !== JSON_ERROR_UTF8) { 119 self::throwEncodeError($code, $data); 120 } 121 122 if (is_string($data)) { 123 self::detectAndCleanUtf8($data); 124 } elseif (is_array($data)) { 125 array_walk_recursive($data, array('Monolog\Utils', 'detectAndCleanUtf8')); 126 } else { 127 self::throwEncodeError($code, $data); 128 } 129 130 if (null === $encodeFlags) { 131 $encodeFlags = self::DEFAULT_JSON_FLAGS; 132 } 133 134 $json = json_encode($data, $encodeFlags); 135 136 if ($json === false) { 137 self::throwEncodeError(json_last_error(), $data); 138 } 139 140 return $json; 141 } 142 143 /** 144 * @internal 145 */ 146 public static function pcreLastErrorMessage(int $code): string 147 { 148 if (PHP_VERSION_ID >= 80000) { 149 return preg_last_error_msg(); 150 } 151 152 $constants = (get_defined_constants(true))['pcre']; 153 $constants = array_filter($constants, function ($key) { 154 return substr($key, -6) == '_ERROR'; 155 }, ARRAY_FILTER_USE_KEY); 156 157 $constants = array_flip($constants); 158 159 return $constants[$code] ?? 'UNDEFINED_ERROR'; 160 } 161 162 /** 163 * Throws an exception according to a given code with a customized message 164 * 165 * @param int $code return code of json_last_error function 166 * @param mixed $data data that was meant to be encoded 167 * @throws \RuntimeException 168 * 169 * @return never 170 */ 171 private static function throwEncodeError(int $code, $data): void 172 { 173 switch ($code) { 174 case JSON_ERROR_DEPTH: 175 $msg = 'Maximum stack depth exceeded'; 176 break; 177 case JSON_ERROR_STATE_MISMATCH: 178 $msg = 'Underflow or the modes mismatch'; 179 break; 180 case JSON_ERROR_CTRL_CHAR: 181 $msg = 'Unexpected control character found'; 182 break; 183 case JSON_ERROR_UTF8: 184 $msg = 'Malformed UTF-8 characters, possibly incorrectly encoded'; 185 break; 186 default: 187 $msg = 'Unknown error'; 188 } 189 190 throw new \RuntimeException('JSON encoding failed: '.$msg.'. Encoding: '.var_export($data, true)); 191 } 192 193 /** 194 * Detect invalid UTF-8 string characters and convert to valid UTF-8. 195 * 196 * Valid UTF-8 input will be left unmodified, but strings containing 197 * invalid UTF-8 codepoints will be reencoded as UTF-8 with an assumed 198 * original encoding of ISO-8859-15. This conversion may result in 199 * incorrect output if the actual encoding was not ISO-8859-15, but it 200 * will be clean UTF-8 output and will not rely on expensive and fragile 201 * detection algorithms. 202 * 203 * Function converts the input in place in the passed variable so that it 204 * can be used as a callback for array_walk_recursive. 205 * 206 * @param mixed $data Input to check and convert if needed, passed by ref 207 */ 208 private static function detectAndCleanUtf8(&$data): void 209 { 210 if (is_string($data) && !preg_match('//u', $data)) { 211 $data = preg_replace_callback( 212 '/[\x80-\xFF]+/', 213 function ($m) { 214 return utf8_encode($m[0]); 215 }, 216 $data 217 ); 218 if (!is_string($data)) { 219 $pcreErrorCode = preg_last_error(); 220 throw new \RuntimeException('Failed to preg_replace_callback: ' . $pcreErrorCode . ' / ' . self::pcreLastErrorMessage($pcreErrorCode)); 221 } 222 $data = str_replace( 223 ['¤', '¦', '¨', '´', '¸', '¼', '½', '¾'], 224 ['€', 'Š', 'š', 'Ž', 'ž', 'Œ', 'œ', 'Ÿ'], 225 $data 226 ); 227 } 228 } 229 230 /** 231 * Converts a string with a valid 'memory_limit' format, to bytes. 232 * 233 * @param string|false $val 234 * @return int|false Returns an integer representing bytes. Returns FALSE in case of error. 235 */ 236 public static function expandIniShorthandBytes($val) 237 { 238 if (!is_string($val)) { 239 return false; 240 } 241 242 // support -1 243 if ((int) $val < 0) { 244 return (int) $val; 245 } 246 247 if (!preg_match('/^\s*(?<val>\d+)(?:\.\d+)?\s*(?<unit>[gmk]?)\s*$/i', $val, $match)) { 248 return false; 249 } 250 251 $val = (int) $match['val']; 252 switch (strtolower($match['unit'] ?? '')) { 253 case 'g': 254 $val *= 1024; 255 case 'm': 256 $val *= 1024; 257 case 'k': 258 $val *= 1024; 259 } 260 261 return $val; 262 } 263 264 /** 265 * @param array<mixed> $record 266 */ 267 public static function getRecordMessageForException(array $record): string 268 { 269 $context = ''; 270 $extra = ''; 271 try { 272 if ($record['context']) { 273 $context = "\nContext: " . json_encode($record['context']); 274 } 275 if ($record['extra']) { 276 $extra = "\nExtra: " . json_encode($record['extra']); 277 } 278 } catch (\Throwable $e) { 279 // noop 280 } 281 282 return "\nThe exception occurred while attempting to log: " . $record['message'] . $context . $extra; 283 } 284} 285