FlattenException.php
332 lines
| 1 | <?php |
| 2 | |
| 3 | /* |
| 4 | * This file is part of the Symfony package. |
| 5 | * |
| 6 | * (c) Fabien Potencier <fabien@symfony.com> |
| 7 | * |
| 8 | * For the full copyright and license information, please view the LICENSE |
| 9 | * file that was distributed with this source code. |
| 10 | */ |
| 11 | namespace Matomo\Dependencies\Symfony\Component\ErrorHandler\Exception; |
| 12 | |
| 13 | use Matomo\Dependencies\Symfony\Component\HttpFoundation\Exception\RequestExceptionInterface; |
| 14 | use Matomo\Dependencies\Symfony\Component\HttpFoundation\Response; |
| 15 | use Matomo\Dependencies\Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; |
| 16 | /** |
| 17 | * FlattenException wraps a PHP Error or Exception to be able to serialize it. |
| 18 | * |
| 19 | * Basically, this class removes all objects from the trace. |
| 20 | * |
| 21 | * @author Fabien Potencier <fabien@symfony.com> |
| 22 | */ |
| 23 | class FlattenException |
| 24 | { |
| 25 | /** @var string */ |
| 26 | private $message; |
| 27 | /** @var int|string */ |
| 28 | private $code; |
| 29 | /** @var self|null */ |
| 30 | private $previous; |
| 31 | /** @var array */ |
| 32 | private $trace; |
| 33 | /** @var string */ |
| 34 | private $traceAsString; |
| 35 | /** @var string */ |
| 36 | private $class; |
| 37 | /** @var int */ |
| 38 | private $statusCode; |
| 39 | /** @var string */ |
| 40 | private $statusText; |
| 41 | /** @var array */ |
| 42 | private $headers; |
| 43 | /** @var string */ |
| 44 | private $file; |
| 45 | /** @var int */ |
| 46 | private $line; |
| 47 | /** @var string|null */ |
| 48 | private $asString; |
| 49 | /** |
| 50 | * @return static |
| 51 | */ |
| 52 | public static function create(\Exception $exception, ?int $statusCode = null, array $headers = []) : self |
| 53 | { |
| 54 | return static::createFromThrowable($exception, $statusCode, $headers); |
| 55 | } |
| 56 | /** |
| 57 | * @return static |
| 58 | */ |
| 59 | public static function createFromThrowable(\Throwable $exception, ?int $statusCode = null, array $headers = []) : self |
| 60 | { |
| 61 | $e = new static(); |
| 62 | $e->setMessage($exception->getMessage()); |
| 63 | $e->setCode($exception->getCode()); |
| 64 | if ($exception instanceof HttpExceptionInterface) { |
| 65 | $statusCode = $exception->getStatusCode(); |
| 66 | $headers = array_merge($headers, $exception->getHeaders()); |
| 67 | } elseif ($exception instanceof RequestExceptionInterface) { |
| 68 | $statusCode = 400; |
| 69 | } |
| 70 | if (null === $statusCode) { |
| 71 | $statusCode = 500; |
| 72 | } |
| 73 | if (class_exists(Response::class) && isset(Response::$statusTexts[$statusCode])) { |
| 74 | $statusText = Response::$statusTexts[$statusCode]; |
| 75 | } else { |
| 76 | $statusText = 'Whoops, looks like something went wrong.'; |
| 77 | } |
| 78 | $e->setStatusText($statusText); |
| 79 | $e->setStatusCode($statusCode); |
| 80 | $e->setHeaders($headers); |
| 81 | $e->setTraceFromThrowable($exception); |
| 82 | $e->setClass(\get_class($exception)); |
| 83 | $e->setFile($exception->getFile()); |
| 84 | $e->setLine($exception->getLine()); |
| 85 | $previous = $exception->getPrevious(); |
| 86 | if ($previous instanceof \Throwable) { |
| 87 | $e->setPrevious(static::createFromThrowable($previous)); |
| 88 | } |
| 89 | return $e; |
| 90 | } |
| 91 | public function toArray() : array |
| 92 | { |
| 93 | $exceptions = []; |
| 94 | foreach (array_merge([$this], $this->getAllPrevious()) as $exception) { |
| 95 | $exceptions[] = ['message' => $exception->getMessage(), 'class' => $exception->getClass(), 'trace' => $exception->getTrace()]; |
| 96 | } |
| 97 | return $exceptions; |
| 98 | } |
| 99 | public function getStatusCode() : int |
| 100 | { |
| 101 | return $this->statusCode; |
| 102 | } |
| 103 | /** |
| 104 | * @return $this |
| 105 | */ |
| 106 | public function setStatusCode(int $code) : self |
| 107 | { |
| 108 | $this->statusCode = $code; |
| 109 | return $this; |
| 110 | } |
| 111 | public function getHeaders() : array |
| 112 | { |
| 113 | return $this->headers; |
| 114 | } |
| 115 | /** |
| 116 | * @return $this |
| 117 | */ |
| 118 | public function setHeaders(array $headers) : self |
| 119 | { |
| 120 | $this->headers = $headers; |
| 121 | return $this; |
| 122 | } |
| 123 | public function getClass() : string |
| 124 | { |
| 125 | return $this->class; |
| 126 | } |
| 127 | /** |
| 128 | * @return $this |
| 129 | */ |
| 130 | public function setClass(string $class) : self |
| 131 | { |
| 132 | $this->class = \false !== strpos($class, "@anonymous\x00") ? ((get_parent_class($class) ?: key(class_implements($class))) ?: 'class') . '@anonymous' : $class; |
| 133 | return $this; |
| 134 | } |
| 135 | public function getFile() : string |
| 136 | { |
| 137 | return $this->file; |
| 138 | } |
| 139 | /** |
| 140 | * @return $this |
| 141 | */ |
| 142 | public function setFile(string $file) : self |
| 143 | { |
| 144 | $this->file = $file; |
| 145 | return $this; |
| 146 | } |
| 147 | public function getLine() : int |
| 148 | { |
| 149 | return $this->line; |
| 150 | } |
| 151 | /** |
| 152 | * @return $this |
| 153 | */ |
| 154 | public function setLine(int $line) : self |
| 155 | { |
| 156 | $this->line = $line; |
| 157 | return $this; |
| 158 | } |
| 159 | public function getStatusText() : string |
| 160 | { |
| 161 | return $this->statusText; |
| 162 | } |
| 163 | /** |
| 164 | * @return $this |
| 165 | */ |
| 166 | public function setStatusText(string $statusText) : self |
| 167 | { |
| 168 | $this->statusText = $statusText; |
| 169 | return $this; |
| 170 | } |
| 171 | public function getMessage() : string |
| 172 | { |
| 173 | return $this->message; |
| 174 | } |
| 175 | /** |
| 176 | * @return $this |
| 177 | */ |
| 178 | public function setMessage(string $message) : self |
| 179 | { |
| 180 | if (\false !== strpos($message, "@anonymous\x00")) { |
| 181 | $message = preg_replace_callback('/[a-zA-Z_\\x7f-\\xff][\\\\a-zA-Z0-9_\\x7f-\\xff]*+@anonymous\\x00.*?\\.php(?:0x?|:[0-9]++\\$)?[0-9a-fA-F]++/', function ($m) { |
| 182 | return class_exists($m[0], \false) ? ((get_parent_class($m[0]) ?: key(class_implements($m[0]))) ?: 'class') . '@anonymous' : $m[0]; |
| 183 | }, $message); |
| 184 | } |
| 185 | $this->message = $message; |
| 186 | return $this; |
| 187 | } |
| 188 | /** |
| 189 | * @return int|string int most of the time (might be a string with PDOException) |
| 190 | */ |
| 191 | public function getCode() |
| 192 | { |
| 193 | return $this->code; |
| 194 | } |
| 195 | /** |
| 196 | * @param int|string $code |
| 197 | * |
| 198 | * @return $this |
| 199 | */ |
| 200 | public function setCode($code) : self |
| 201 | { |
| 202 | $this->code = $code; |
| 203 | return $this; |
| 204 | } |
| 205 | public function getPrevious() : ?self |
| 206 | { |
| 207 | return $this->previous; |
| 208 | } |
| 209 | /** |
| 210 | * @return $this |
| 211 | */ |
| 212 | public function setPrevious(?self $previous) : self |
| 213 | { |
| 214 | $this->previous = $previous; |
| 215 | return $this; |
| 216 | } |
| 217 | /** |
| 218 | * @return self[] |
| 219 | */ |
| 220 | public function getAllPrevious() : array |
| 221 | { |
| 222 | $exceptions = []; |
| 223 | $e = $this; |
| 224 | while ($e = $e->getPrevious()) { |
| 225 | $exceptions[] = $e; |
| 226 | } |
| 227 | return $exceptions; |
| 228 | } |
| 229 | public function getTrace() : array |
| 230 | { |
| 231 | return $this->trace; |
| 232 | } |
| 233 | /** |
| 234 | * @return $this |
| 235 | */ |
| 236 | public function setTraceFromThrowable(\Throwable $throwable) : self |
| 237 | { |
| 238 | $this->traceAsString = $throwable->getTraceAsString(); |
| 239 | return $this->setTrace($throwable->getTrace(), $throwable->getFile(), $throwable->getLine()); |
| 240 | } |
| 241 | /** |
| 242 | * @return $this |
| 243 | */ |
| 244 | public function setTrace(array $trace, ?string $file, ?int $line) : self |
| 245 | { |
| 246 | $this->trace = []; |
| 247 | $this->trace[] = ['namespace' => '', 'short_class' => '', 'class' => '', 'type' => '', 'function' => '', 'file' => $file, 'line' => $line, 'args' => []]; |
| 248 | foreach ($trace as $entry) { |
| 249 | $class = ''; |
| 250 | $namespace = ''; |
| 251 | if (isset($entry['class'])) { |
| 252 | $parts = explode('\\', $entry['class']); |
| 253 | $class = array_pop($parts); |
| 254 | $namespace = implode('\\', $parts); |
| 255 | } |
| 256 | $this->trace[] = ['namespace' => $namespace, 'short_class' => $class, 'class' => $entry['class'] ?? '', 'type' => $entry['type'] ?? '', 'function' => $entry['function'] ?? null, 'file' => $entry['file'] ?? null, 'line' => $entry['line'] ?? null, 'args' => isset($entry['args']) ? $this->flattenArgs($entry['args']) : []]; |
| 257 | } |
| 258 | return $this; |
| 259 | } |
| 260 | private function flattenArgs(array $args, int $level = 0, int &$count = 0) : array |
| 261 | { |
| 262 | $result = []; |
| 263 | foreach ($args as $key => $value) { |
| 264 | if (++$count > 10000.0) { |
| 265 | return ['array', '*SKIPPED over 10000 entries*']; |
| 266 | } |
| 267 | if ($value instanceof \__PHP_Incomplete_Class) { |
| 268 | $result[$key] = ['incomplete-object', $this->getClassNameFromIncomplete($value)]; |
| 269 | } elseif (\is_object($value)) { |
| 270 | $result[$key] = ['object', \get_class($value)]; |
| 271 | } elseif (\is_array($value)) { |
| 272 | if ($level > 10) { |
| 273 | $result[$key] = ['array', '*DEEP NESTED ARRAY*']; |
| 274 | } else { |
| 275 | $result[$key] = ['array', $this->flattenArgs($value, $level + 1, $count)]; |
| 276 | } |
| 277 | } elseif (null === $value) { |
| 278 | $result[$key] = ['null', null]; |
| 279 | } elseif (\is_bool($value)) { |
| 280 | $result[$key] = ['boolean', $value]; |
| 281 | } elseif (\is_int($value)) { |
| 282 | $result[$key] = ['integer', $value]; |
| 283 | } elseif (\is_float($value)) { |
| 284 | $result[$key] = ['float', $value]; |
| 285 | } elseif (\is_resource($value)) { |
| 286 | $result[$key] = ['resource', get_resource_type($value)]; |
| 287 | } else { |
| 288 | $result[$key] = ['string', (string) $value]; |
| 289 | } |
| 290 | } |
| 291 | return $result; |
| 292 | } |
| 293 | private function getClassNameFromIncomplete(\__PHP_Incomplete_Class $value) : string |
| 294 | { |
| 295 | $array = new \ArrayObject($value); |
| 296 | return $array['__PHP_Incomplete_Class_Name']; |
| 297 | } |
| 298 | public function getTraceAsString() : string |
| 299 | { |
| 300 | return $this->traceAsString; |
| 301 | } |
| 302 | /** |
| 303 | * @return $this |
| 304 | */ |
| 305 | public function setAsString(?string $asString) : self |
| 306 | { |
| 307 | $this->asString = $asString; |
| 308 | return $this; |
| 309 | } |
| 310 | public function getAsString() : string |
| 311 | { |
| 312 | if (null !== $this->asString) { |
| 313 | return $this->asString; |
| 314 | } |
| 315 | $message = ''; |
| 316 | $next = \false; |
| 317 | foreach (array_reverse(array_merge([$this], $this->getAllPrevious())) as $exception) { |
| 318 | if ($next) { |
| 319 | $message .= 'Next '; |
| 320 | } else { |
| 321 | $next = \true; |
| 322 | } |
| 323 | $message .= $exception->getClass(); |
| 324 | if ('' != $exception->getMessage()) { |
| 325 | $message .= ': ' . $exception->getMessage(); |
| 326 | } |
| 327 | $message .= ' in ' . $exception->getFile() . ':' . $exception->getLine() . "\nStack trace:\n" . $exception->getTraceAsString() . "\n\n"; |
| 328 | } |
| 329 | return rtrim($message); |
| 330 | } |
| 331 | } |
| 332 |