| 1 |
<?php |
| 2 |
|
| 3 |
declare (strict_types=1); |
| 4 |
namespace WCPOS\Vendor\Sentry; |
| 5 |
|
| 6 |
use WCPOS\Vendor\Sentry\Exception\FatalErrorException; |
| 7 |
use WCPOS\Vendor\Sentry\Exception\SilencedErrorException; |
| 8 |
/** |
| 9 |
* This class implements a simple error handler that catches all configured |
| 10 |
* error types and relays them to all configured listeners. Registering this |
| 11 |
* error handler more than once is not supported and will lead to nasty |
| 12 |
* problems. The code is based on the Symfony ErrorHandler component. |
| 13 |
* |
| 14 |
* @phpstan-import-type StacktraceFrame from FrameBuilder |
| 15 |
*/ |
| 16 |
final class ErrorHandler |
| 17 |
{ |
| 18 |
/** |
| 19 |
* The default amount of bytes of memory to reserve for the fatal error handler. |
| 20 |
* |
| 21 |
* @internal |
| 22 |
*/ |
| 23 |
public const DEFAULT_RESERVED_MEMORY_SIZE = 16 * 1024; |
| 24 |
// 16 KiB |
| 25 |
/** |
| 26 |
* The regular expression used to match the message of an out of memory error. |
| 27 |
* |
| 28 |
* Regex inspired by https://github.com/php/php-src/blob/524b13460752fba908f88e3c4428b91fa66c083a/Zend/tests/new_oom.phpt#L15 |
| 29 |
*/ |
| 30 |
private const OOM_MESSAGE_MATCHER = '/^Allowed memory size of (?<memory_limit>\\d+) bytes exhausted[^\\r\\n]* \\(tried to allocate \\d+ bytes\\)/'; |
| 31 |
/** |
| 32 |
* The fatal error types that cannot be silenced using the @ operator in PHP 8+. |
| 33 |
*/ |
| 34 |
private const PHP8_UNSILENCEABLE_FATAL_ERRORS = \E_ERROR | \E_PARSE | \E_CORE_ERROR | \E_COMPILE_ERROR | \E_USER_ERROR | \E_RECOVERABLE_ERROR; |
| 35 |
/** |
| 36 |
* @var self|null The current registered handler (this class is a singleton) |
| 37 |
*/ |
| 38 |
private static $handlerInstance; |
| 39 |
/** |
| 40 |
* @var callable[] List of listeners that will act on each captured error |
| 41 |
* |
| 42 |
* @phpstan-var (callable(\ErrorException): void)[] |
| 43 |
*/ |
| 44 |
private $errorListeners = []; |
| 45 |
/** |
| 46 |
* @var callable[] List of listeners that will act of each captured fatal error |
| 47 |
* |
| 48 |
* @phpstan-var (callable(FatalErrorException): void)[] |
| 49 |
*/ |
| 50 |
private $fatalErrorListeners = []; |
| 51 |
/** |
| 52 |
* @var callable[] List of listeners that will act on each captured exception |
| 53 |
* |
| 54 |
* @phpstan-var (callable(\Throwable): void)[] |
| 55 |
*/ |
| 56 |
private $exceptionListeners = []; |
| 57 |
/** |
| 58 |
* @var \ReflectionProperty A reflection cached instance that points to the |
| 59 |
* trace property of the exception objects |
| 60 |
*/ |
| 61 |
private $exceptionReflection; |
| 62 |
/** |
| 63 |
* @var callable|null The previous error handler, if any |
| 64 |
*/ |
| 65 |
private $previousErrorHandler; |
| 66 |
/** |
| 67 |
* @var callable|null The previous exception handler, if any |
| 68 |
* |
| 69 |
* @phpstan-var (callable(\Throwable): void)|null |
| 70 |
*/ |
| 71 |
private $previousExceptionHandler; |
| 72 |
/** |
| 73 |
* @var bool Whether the error handler has been registered |
| 74 |
*/ |
| 75 |
private $isErrorHandlerRegistered = \false; |
| 76 |
/** |
| 77 |
* @var bool Whether the exception handler has been registered |
| 78 |
*/ |
| 79 |
private $isExceptionHandlerRegistered = \false; |
| 80 |
/** |
| 81 |
* @var bool Whether the fatal error handler has been registered |
| 82 |
*/ |
| 83 |
private $isFatalErrorHandlerRegistered = \false; |
| 84 |
/** |
| 85 |
* @var int|null the amount of bytes of memory to increase the memory limit by when we are capturing a out of memory error, set to null to not increase the memory limit |
| 86 |
*/ |
| 87 |
private $memoryLimitIncreaseOnOutOfMemoryErrorValue = 5 * 1024 * 1024; |
| 88 |
// 5 MiB |
| 89 |
/** |
| 90 |
* @var Options|null The SDK options |
| 91 |
*/ |
| 92 |
private $options; |
| 93 |
/** |
| 94 |
* @var bool Whether the memory limit has been increased |
| 95 |
*/ |
| 96 |
private static $didIncreaseMemoryLimit = \false; |
| 97 |
/** |
| 98 |
* @var string|null A portion of pre-allocated memory data that will be reclaimed in case a fatal error occurs to handle it |
| 99 |
*/ |
| 100 |
private static $reservedMemory; |
| 101 |
/** |
| 102 |
* @var int The amount of memory to reserve for the fatal error handler |
| 103 |
*/ |
| 104 |
private static $reservedMemorySize = self::DEFAULT_RESERVED_MEMORY_SIZE; |
| 105 |
/** |
| 106 |
* @var bool Whether the fatal error handler should be disabled |
| 107 |
*/ |
| 108 |
private static $disableFatalErrorHandler = \false; |
| 109 |
/** |
| 110 |
* @var string[] List of error levels and their description |
| 111 |
*/ |
| 112 |
private const ERROR_LEVELS_DESCRIPTION = [ |
| 113 |
\E_DEPRECATED => 'Deprecated', |
| 114 |
\E_USER_DEPRECATED => 'User Deprecated', |
| 115 |
\E_NOTICE => 'Notice', |
| 116 |
\E_USER_NOTICE => 'User Notice', |
| 117 |
// This is \E_STRICT which has been deprecated in PHP 8.4 so we should not reference it directly to prevent deprecation notices |
| 118 |
2048 => 'Runtime Notice', |
| 119 |
\E_WARNING => 'Warning', |
| 120 |
\E_USER_WARNING => 'User Warning', |
| 121 |
\E_COMPILE_WARNING => 'Compile Warning', |
| 122 |
\E_CORE_WARNING => 'Core Warning', |
| 123 |
\E_USER_ERROR => 'User Error', |
| 124 |
\E_RECOVERABLE_ERROR => 'Catchable Fatal Error', |
| 125 |
\E_COMPILE_ERROR => 'Compile Error', |
| 126 |
\E_PARSE => 'Parse Error', |
| 127 |
\E_ERROR => 'Error', |
| 128 |
\E_CORE_ERROR => 'Core Error', |
| 129 |
]; |
| 130 |
/** |
| 131 |
* Constructor. |
| 132 |
* |
| 133 |
* @throws \ReflectionException If hooking into the \Exception class to |
| 134 |
* make the `trace` property accessible fails |
| 135 |
*/ |
| 136 |
private function __construct() |
| 137 |
{ |
| 138 |
$this->exceptionReflection = new \ReflectionProperty(\Exception::class, 'trace'); |
| 139 |
if (\PHP_VERSION_ID < 80100) { |
| 140 |
$this->exceptionReflection->setAccessible(\true); |
| 141 |
} |
| 142 |
} |
| 143 |
/** |
| 144 |
* Registers the error handler once and returns its instance. |
| 145 |
*/ |
| 146 |
public static function registerOnceErrorHandler(?Options $options = null) : self |
| 147 |
{ |
| 148 |
if (self::$handlerInstance === null) { |
| 149 |
self::$handlerInstance = new self(); |
| 150 |
} |
| 151 |
self::$handlerInstance->options = $options; |
| 152 |
if (self::$handlerInstance->isErrorHandlerRegistered) { |
| 153 |
return self::$handlerInstance; |
| 154 |
} |
| 155 |
$errorHandlerCallback = \Closure::fromCallable([self::$handlerInstance, 'handleError']); |
| 156 |
self::$handlerInstance->isErrorHandlerRegistered = \true; |
| 157 |
self::$handlerInstance->previousErrorHandler = \set_error_handler($errorHandlerCallback); |
| 158 |
if (self::$handlerInstance->previousErrorHandler === null) { |
| 159 |
\restore_error_handler(); |
| 160 |
// Specifying the error types caught by the error handler with the |
| 161 |
// first call to the set_error_handler method would cause the PHP |
| 162 |
// bug https://bugs.php.net/63206 if the handler is not the first |
| 163 |
// one in the chain of handlers |
| 164 |
\set_error_handler($errorHandlerCallback, \E_ALL); |
| 165 |
} |
| 166 |
return self::$handlerInstance; |
| 167 |
} |
| 168 |
/** |
| 169 |
* Registers the fatal error handler and reserves a certain amount of memory |
| 170 |
* that will be reclaimed to handle the errors (to prevent out of memory |
| 171 |
* issues while handling them) and returns its instance. |
| 172 |
* |
| 173 |
* @param int $reservedMemorySize The amount of memory to reserve for the fatal |
| 174 |
* error handler expressed in bytes |
| 175 |
*/ |
| 176 |
public static function registerOnceFatalErrorHandler(int $reservedMemorySize = self::DEFAULT_RESERVED_MEMORY_SIZE) : self |
| 177 |
{ |
| 178 |
if ($reservedMemorySize <= 0) { |
| 179 |
throw new \InvalidArgumentException('The $reservedMemorySize argument must be greater than 0.'); |
| 180 |
} |
| 181 |
if (self::$handlerInstance === null) { |
| 182 |
self::$handlerInstance = new self(); |
| 183 |
} |
| 184 |
if (self::$handlerInstance->isFatalErrorHandlerRegistered) { |
| 185 |
return self::$handlerInstance; |
| 186 |
} |
| 187 |
self::$handlerInstance->isFatalErrorHandlerRegistered = \true; |
| 188 |
self::$reservedMemorySize = $reservedMemorySize; |
| 189 |
self::$reservedMemory = \str_repeat('x', $reservedMemorySize); |
| 190 |
\register_shutdown_function(\Closure::fromCallable([self::$handlerInstance, 'handleFatalError'])); |
| 191 |
return self::$handlerInstance; |
| 192 |
} |
| 193 |
/** |
| 194 |
* Registers the exception handler, effectively replacing the current one |
| 195 |
* and returns its instance. The previous one will be saved anyway and |
| 196 |
* called when appropriate. |
| 197 |
*/ |
| 198 |
public static function registerOnceExceptionHandler() : self |
| 199 |
{ |
| 200 |
if (self::$handlerInstance === null) { |
| 201 |
self::$handlerInstance = new self(); |
| 202 |
} |
| 203 |
if (self::$handlerInstance->isExceptionHandlerRegistered) { |
| 204 |
return self::$handlerInstance; |
| 205 |
} |
| 206 |
self::$handlerInstance->isExceptionHandlerRegistered = \true; |
| 207 |
self::$handlerInstance->previousExceptionHandler = \set_exception_handler(\Closure::fromCallable([self::$handlerInstance, 'handleException'])); |
| 208 |
return self::$handlerInstance; |
| 209 |
} |
| 210 |
/** |
| 211 |
* Adds a listener to the current error handler that will be called every |
| 212 |
* time an error is captured. |
| 213 |
* |
| 214 |
* @param callable $listener A callable that will act as a listener |
| 215 |
* and that must accept a single argument |
| 216 |
* of type \ErrorException |
| 217 |
* |
| 218 |
* @phpstan-param callable(\ErrorException): void $listener |
| 219 |
*/ |
| 220 |
public function addErrorHandlerListener(callable $listener) : void |
| 221 |
{ |
| 222 |
$this->errorListeners[] = $listener; |
| 223 |
} |
| 224 |
/** |
| 225 |
* Adds a listener to the current error handler that will be called every |
| 226 |
* time a fatal error handler is captured. |
| 227 |
* |
| 228 |
* @param callable $listener A callable that will act as a listener |
| 229 |
* and that must accept a single argument |
| 230 |
* of type \Sentry\Exception\FatalErrorException |
| 231 |
* |
| 232 |
* @phpstan-param callable(FatalErrorException): void $listener |
| 233 |
*/ |
| 234 |
public function addFatalErrorHandlerListener(callable $listener) : void |
| 235 |
{ |
| 236 |
$this->fatalErrorListeners[] = $listener; |
| 237 |
} |
| 238 |
/** |
| 239 |
* Adds a listener to the current error handler that will be called every |
| 240 |
* time an exception is captured. |
| 241 |
* |
| 242 |
* @param callable $listener A callable that will act as a listener |
| 243 |
* and that must accept a single argument |
| 244 |
* of type \Throwable |
| 245 |
* |
| 246 |
* @phpstan-param callable(\Throwable): void $listener |
| 247 |
*/ |
| 248 |
public function addExceptionHandlerListener(callable $listener) : void |
| 249 |
{ |
| 250 |
$this->exceptionListeners[] = $listener; |
| 251 |
} |
| 252 |
/** |
| 253 |
* Sets the amount of memory to increase the memory limit by when we are capturing a out of memory error. |
| 254 |
* |
| 255 |
* @param int|null $valueInBytes the number of bytes to increase the memory limit by, or null to not increase the memory limit |
| 256 |
*/ |
| 257 |
public function setMemoryLimitIncreaseOnOutOfMemoryErrorInBytes(?int $valueInBytes) : void |
| 258 |
{ |
| 259 |
if ($valueInBytes !== null && $valueInBytes <= 0) { |
| 260 |
throw new \InvalidArgumentException('The $valueInBytes argument must be greater than 0 or null.'); |
| 261 |
} |
| 262 |
$this->memoryLimitIncreaseOnOutOfMemoryErrorValue = $valueInBytes; |
| 263 |
} |
| 264 |
/** |
| 265 |
* @internal |
| 266 |
*/ |
| 267 |
public static function resetFatalErrorHandlerState() : void |
| 268 |
{ |
| 269 |
self::$disableFatalErrorHandler = \false; |
| 270 |
self::$didIncreaseMemoryLimit = \false; |
| 271 |
if (self::$handlerInstance !== null && self::$handlerInstance->isFatalErrorHandlerRegistered && self::$reservedMemory === null) { |
| 272 |
self::$reservedMemory = \str_repeat('x', self::$reservedMemorySize); |
| 273 |
} |
| 274 |
} |
| 275 |
/** |
| 276 |
* Handles errors by capturing them through the client according to the |
| 277 |
* configured bit field. |
| 278 |
* |
| 279 |
* @param int $level The level of the error raised, represented by |
| 280 |
* one of the E_* constants |
| 281 |
* @param string $message The error message |
| 282 |
* @param string $file The filename the error was raised in |
| 283 |
* @param int $line The line number the error was raised at |
| 284 |
* @param array<string, mixed>|null $errcontext The error context (deprecated since PHP 7.2) |
| 285 |
* |
| 286 |
* @return bool If the function returns `false` then the PHP native error |
| 287 |
* handler will be called |
| 288 |
* |
| 289 |
* @throws \Throwable |
| 290 |
*/ |
| 291 |
private function handleError(int $level, string $message, string $file, int $line, ?array $errcontext = []) : bool |
| 292 |
{ |
| 293 |
$isSilencedError = \error_reporting() === 0; |
| 294 |
if (\PHP_MAJOR_VERSION >= 8) { |
| 295 |
// Starting from PHP8, when a silenced error occurs the `error_reporting()` |
| 296 |
// function will return a bitmask of fatal errors that are unsilenceable. |
| 297 |
// If by subtracting from this value those errors the result is 0, we can |
| 298 |
// conclude that the error was silenced. |
| 299 |
$isSilencedError = 0 === (\error_reporting() & ~self::PHP8_UNSILENCEABLE_FATAL_ERRORS); |
| 300 |
// However, starting from PHP8 some fatal errors are unsilenceable, |
| 301 |
// so we have to check for them to avoid reporting any of them as |
| 302 |
// silenced instead |
| 303 |
if ($level === (self::PHP8_UNSILENCEABLE_FATAL_ERRORS & $level)) { |
| 304 |
$isSilencedError = \false; |
| 305 |
} |
| 306 |
} |
| 307 |
if ($this->shouldHandleError($level, $isSilencedError)) { |
| 308 |
if ($isSilencedError) { |
| 309 |
$errorAsException = new SilencedErrorException(self::ERROR_LEVELS_DESCRIPTION[$level] . ': ' . $message, 0, $level, $file, $line); |
| 310 |
} else { |
| 311 |
$errorAsException = new \ErrorException(self::ERROR_LEVELS_DESCRIPTION[$level] . ': ' . $message, 0, $level, $file, $line); |
| 312 |
} |
| 313 |
$backtrace = $this->cleanBacktraceFromErrorHandlerFrames($errorAsException->getTrace(), $errorAsException->getFile(), $errorAsException->getLine()); |
| 314 |
$this->exceptionReflection->setValue($errorAsException, $backtrace); |
| 315 |
$this->invokeListeners($this->errorListeners, $errorAsException); |
| 316 |
} |
| 317 |
if ($this->previousErrorHandler !== null) { |
| 318 |
return \false !== ($this->previousErrorHandler)($level, $message, $file, $line, $errcontext); |
| 319 |
} |
| 320 |
return \false; |
| 321 |
} |
| 322 |
private function shouldHandleError(int $level, bool $silenced) : bool |
| 323 |
{ |
| 324 |
// If we were not given any options, we should handle all errors |
| 325 |
if ($this->options === null) { |
| 326 |
return \true; |
| 327 |
} |
| 328 |
if ($silenced) { |
| 329 |
return $this->options->shouldCaptureSilencedErrors(); |
| 330 |
} |
| 331 |
return ($this->options->getErrorTypes() & $level) !== 0; |
| 332 |
} |
| 333 |
/** |
| 334 |
* Tries to handle a fatal error if any and relay them to the listeners. |
| 335 |
* It only tries to do this if we still have some reserved memory at |
| 336 |
* disposal. This method is used as callback of a shutdown function. |
| 337 |
*/ |
| 338 |
private function handleFatalError() : void |
| 339 |
{ |
| 340 |
if (self::$disableFatalErrorHandler) { |
| 341 |
return; |
| 342 |
} |
| 343 |
// Free the reserved memory that allows us to potentially handle OOM errors |
| 344 |
self::$reservedMemory = null; |
| 345 |
$error = \error_get_last(); |
| 346 |
if (!empty($error) && $error['type'] & (\E_ERROR | \E_PARSE | \E_CORE_ERROR | \E_CORE_WARNING | \E_COMPILE_ERROR | \E_COMPILE_WARNING)) { |
| 347 |
// If we did not do so already and we are allowed to increase the memory limit, we do so when we detect an OOM error |
| 348 |
if (self::$didIncreaseMemoryLimit === \false && $this->memoryLimitIncreaseOnOutOfMemoryErrorValue !== null && \preg_match(self::OOM_MESSAGE_MATCHER, $error['message'], $matches) === 1) { |
| 349 |
$currentMemoryLimit = (int) $matches['memory_limit']; |
| 350 |
$newMemoryLimit = $currentMemoryLimit + $this->memoryLimitIncreaseOnOutOfMemoryErrorValue; |
| 351 |
// It can happen that the memory limit + increase is still lower than |
| 352 |
// the memory that is currently being used. This produces warnings |
| 353 |
// that may end up in Sentry. To prevent this, we can check the real |
| 354 |
// usage before. |
| 355 |
if ($newMemoryLimit > \memory_get_usage(\true)) { |
| 356 |
$this->setMemoryLimitWithoutHandlingWarnings($newMemoryLimit); |
| 357 |
} |
| 358 |
self::$didIncreaseMemoryLimit = \true; |
| 359 |
} |
| 360 |
$errorAsException = new FatalErrorException(self::ERROR_LEVELS_DESCRIPTION[$error['type']] . ': ' . $error['message'], 0, $error['type'], $error['file'], $error['line']); |
| 361 |
$this->exceptionReflection->setValue($errorAsException, []); |
| 362 |
$this->invokeListeners($this->fatalErrorListeners, $errorAsException); |
| 363 |
} |
| 364 |
} |
| 365 |
/** |
| 366 |
* Handles the given exception by passing it to all the listeners, |
| 367 |
* then forwarding it to another handler. |
| 368 |
* |
| 369 |
* @param \Throwable $exception The exception to handle |
| 370 |
* |
| 371 |
* @throws \Throwable |
| 372 |
*/ |
| 373 |
private function handleException(\Throwable $exception) : void |
| 374 |
{ |
| 375 |
$this->invokeListeners($this->exceptionListeners, $exception); |
| 376 |
$previousExceptionHandlerException = $exception; |
| 377 |
// Unset the previous exception handler to prevent infinite loop in case |
| 378 |
// we need to handle an exception thrown from it |
| 379 |
$previousExceptionHandler = $this->previousExceptionHandler; |
| 380 |
$this->previousExceptionHandler = null; |
| 381 |
try { |
| 382 |
if ($previousExceptionHandler !== null) { |
| 383 |
$previousExceptionHandler($exception); |
| 384 |
return; |
| 385 |
} |
| 386 |
} catch (\Throwable $previousExceptionHandlerException) { |
| 387 |
// This `catch` statement is here to forcefully override the |
| 388 |
// $previousExceptionHandlerException variable with the exception |
| 389 |
// we just caught |
| 390 |
} |
| 391 |
// If the instance of the exception we're handling is the same as the one |
| 392 |
// caught from the previous exception handler then we give it back to the |
| 393 |
// native PHP handler to prevent an infinite loop |
| 394 |
if ($exception === $previousExceptionHandlerException) { |
| 395 |
// Disable the fatal error handler or the error will be reported twice |
| 396 |
self::$disableFatalErrorHandler = \true; |
| 397 |
throw $exception; |
| 398 |
} |
| 399 |
$this->handleException($previousExceptionHandlerException); |
| 400 |
} |
| 401 |
/** |
| 402 |
* Set the memory_limit while having no real error handler so that a warning emitted |
| 403 |
* will not get reported. |
| 404 |
*/ |
| 405 |
private function setMemoryLimitWithoutHandlingWarnings(int $memoryLimit) : void |
| 406 |
{ |
| 407 |
\set_error_handler(static function () : bool { |
| 408 |
return \true; |
| 409 |
}, \E_WARNING); |
| 410 |
try { |
| 411 |
\ini_set('memory_limit', (string) $memoryLimit); |
| 412 |
} finally { |
| 413 |
\restore_error_handler(); |
| 414 |
} |
| 415 |
} |
| 416 |
/** |
| 417 |
* Cleans and returns the backtrace without the first frames that belong to |
| 418 |
* this error handler. |
| 419 |
* |
| 420 |
* @param array<int, array<string, mixed>> $backtrace The backtrace to clear |
| 421 |
* @param string $file The filename the backtrace was raised in |
| 422 |
* @param int $line The line number the backtrace was raised at |
| 423 |
* |
| 424 |
* @phpstan-param list<StacktraceFrame> $backtrace |
| 425 |
* |
| 426 |
* @return array<int, mixed> |
| 427 |
*/ |
| 428 |
private function cleanBacktraceFromErrorHandlerFrames(array $backtrace, string $file, int $line) : array |
| 429 |
{ |
| 430 |
$cleanedBacktrace = $backtrace; |
| 431 |
$index = 0; |
| 432 |
while ($index < \count($backtrace)) { |
| 433 |
if (isset($backtrace[$index]['file'], $backtrace[$index]['line']) && $backtrace[$index]['line'] === $line && $backtrace[$index]['file'] === $file) { |
| 434 |
$cleanedBacktrace = \array_slice($cleanedBacktrace, 1 + $index); |
| 435 |
break; |
| 436 |
} |
| 437 |
++$index; |
| 438 |
} |
| 439 |
return $cleanedBacktrace; |
| 440 |
} |
| 441 |
/** |
| 442 |
* Invokes all the listeners and pass the exception to all of them. |
| 443 |
* |
| 444 |
* @param callable[] $listeners The array of listeners to be called |
| 445 |
* @param \Throwable $throwable The exception to be passed onto listeners |
| 446 |
*/ |
| 447 |
private function invokeListeners(array $listeners, \Throwable $throwable) : void |
| 448 |
{ |
| 449 |
foreach ($listeners as $listener) { |
| 450 |
try { |
| 451 |
$listener($throwable); |
| 452 |
} catch (\Throwable $exception) { |
| 453 |
// Do nothing as this should be as transparent as possible |
| 454 |
} |
| 455 |
} |
| 456 |
} |
| 457 |
} |
| 458 |
|