vendor/sentry/sentry/src/ErrorHandler.php line 357

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace Sentry;
  4. use Sentry\Exception\FatalErrorException;
  5. use Sentry\Exception\SilencedErrorException;
  6. /**
  7.  * This class implements a simple error handler that catches all configured
  8.  * error types and relays them to all configured listeners. Registering this
  9.  * error handler more than once is not supported and will lead to nasty
  10.  * problems. The code is based on the Symfony ErrorHandler component.
  11.  *
  12.  * @psalm-import-type StacktraceFrame from FrameBuilder
  13.  */
  14. final class ErrorHandler
  15. {
  16.     /**
  17.      * The default amount of bytes of memory to reserve for the fatal error handler.
  18.      *
  19.      * @internal
  20.      */
  21.     public const DEFAULT_RESERVED_MEMORY_SIZE 10240;
  22.     /**
  23.      * The fatal error types that cannot be silenced using the @ operator in PHP 8+.
  24.      */
  25.     private const PHP8_UNSILENCEABLE_FATAL_ERRORS = \E_ERROR | \E_PARSE | \E_CORE_ERROR | \E_COMPILE_ERROR | \E_USER_ERROR | \E_RECOVERABLE_ERROR;
  26.     /**
  27.      * @var self|null The current registered handler (this class is a singleton)
  28.      */
  29.     private static $handlerInstance;
  30.     /**
  31.      * @var callable[] List of listeners that will act on each captured error
  32.      *
  33.      * @psalm-var (callable(\ErrorException): void)[]
  34.      */
  35.     private $errorListeners = [];
  36.     /**
  37.      * @var callable[] List of listeners that will act of each captured fatal error
  38.      *
  39.      * @psalm-var (callable(FatalErrorException): void)[]
  40.      */
  41.     private $fatalErrorListeners = [];
  42.     /**
  43.      * @var callable[] List of listeners that will act on each captured exception
  44.      *
  45.      * @psalm-var (callable(\Throwable): void)[]
  46.      */
  47.     private $exceptionListeners = [];
  48.     /**
  49.      * @var \ReflectionProperty A reflection cached instance that points to the
  50.      *                          trace property of the exception objects
  51.      */
  52.     private $exceptionReflection;
  53.     /**
  54.      * @var callable|null The previous error handler, if any
  55.      */
  56.     private $previousErrorHandler;
  57.     /**
  58.      * @var callable|null The previous exception handler, if any
  59.      *
  60.      * @psalm-var null|callable(\Throwable): void
  61.      */
  62.     private $previousExceptionHandler;
  63.     /**
  64.      * @var bool Whether the error handler has been registered
  65.      */
  66.     private $isErrorHandlerRegistered false;
  67.     /**
  68.      * @var bool Whether the exception handler has been registered
  69.      */
  70.     private $isExceptionHandlerRegistered false;
  71.     /**
  72.      * @var bool Whether the fatal error handler has been registered
  73.      */
  74.     private $isFatalErrorHandlerRegistered false;
  75.     /**
  76.      * @var string|null A portion of pre-allocated memory data that will be reclaimed
  77.      *                  in case a fatal error occurs to handle it
  78.      */
  79.     private static $reservedMemory;
  80.     /**
  81.      * @var string[] List of error levels and their description
  82.      */
  83.     private const ERROR_LEVELS_DESCRIPTION = [
  84.         \E_DEPRECATED => 'Deprecated',
  85.         \E_USER_DEPRECATED => 'User Deprecated',
  86.         \E_NOTICE => 'Notice',
  87.         \E_USER_NOTICE => 'User Notice',
  88.         \E_STRICT => 'Runtime Notice',
  89.         \E_WARNING => 'Warning',
  90.         \E_USER_WARNING => 'User Warning',
  91.         \E_COMPILE_WARNING => 'Compile Warning',
  92.         \E_CORE_WARNING => 'Core Warning',
  93.         \E_USER_ERROR => 'User Error',
  94.         \E_RECOVERABLE_ERROR => 'Catchable Fatal Error',
  95.         \E_COMPILE_ERROR => 'Compile Error',
  96.         \E_PARSE => 'Parse Error',
  97.         \E_ERROR => 'Error',
  98.         \E_CORE_ERROR => 'Core Error',
  99.     ];
  100.     /**
  101.      * Constructor.
  102.      *
  103.      * @throws \ReflectionException If hooking into the \Exception class to
  104.      *                              make the `trace` property accessible fails
  105.      */
  106.     private function __construct()
  107.     {
  108.         $this->exceptionReflection = new \ReflectionProperty(\Exception::class, 'trace');
  109.         $this->exceptionReflection->setAccessible(true);
  110.     }
  111.     /**
  112.      * Registers the error handler once and returns its instance.
  113.      */
  114.     public static function registerOnceErrorHandler(): self
  115.     {
  116.         if (null === self::$handlerInstance) {
  117.             self::$handlerInstance = new self();
  118.         }
  119.         if (self::$handlerInstance->isErrorHandlerRegistered) {
  120.             return self::$handlerInstance;
  121.         }
  122.         $errorHandlerCallback = \Closure::fromCallable([self::$handlerInstance'handleError']);
  123.         self::$handlerInstance->isErrorHandlerRegistered true;
  124.         self::$handlerInstance->previousErrorHandler set_error_handler($errorHandlerCallback);
  125.         if (null === self::$handlerInstance->previousErrorHandler) {
  126.             restore_error_handler();
  127.             // Specifying the error types caught by the error handler with the
  128.             // first call to the set_error_handler method would cause the PHP
  129.             // bug https://bugs.php.net/63206 if the handler is not the first
  130.             // one in the chain of handlers
  131.             set_error_handler($errorHandlerCallback, \E_ALL);
  132.         }
  133.         return self::$handlerInstance;
  134.     }
  135.     /**
  136.      * Registers the fatal error handler and reserves a certain amount of memory
  137.      * that will be reclaimed to handle the errors (to prevent out of memory
  138.      * issues while handling them) and returns its instance.
  139.      *
  140.      * @param int $reservedMemorySize The amount of memory to reserve for the fatal
  141.      *                                error handler expressed in bytes
  142.      */
  143.     public static function registerOnceFatalErrorHandler(int $reservedMemorySize self::DEFAULT_RESERVED_MEMORY_SIZE): self
  144.     {
  145.         if ($reservedMemorySize <= 0) {
  146.             throw new \InvalidArgumentException('The $reservedMemorySize argument must be greater than 0.');
  147.         }
  148.         if (null === self::$handlerInstance) {
  149.             self::$handlerInstance = new self();
  150.         }
  151.         if (self::$handlerInstance->isFatalErrorHandlerRegistered) {
  152.             return self::$handlerInstance;
  153.         }
  154.         self::$handlerInstance->isFatalErrorHandlerRegistered true;
  155.         self::$reservedMemory str_repeat('x'$reservedMemorySize);
  156.         register_shutdown_function(\Closure::fromCallable([self::$handlerInstance'handleFatalError']));
  157.         return self::$handlerInstance;
  158.     }
  159.     /**
  160.      * Registers the exception handler, effectively replacing the current one
  161.      * and returns its instance. The previous one will be saved anyway and
  162.      * called when appropriate.
  163.      */
  164.     public static function registerOnceExceptionHandler(): self
  165.     {
  166.         if (null === self::$handlerInstance) {
  167.             self::$handlerInstance = new self();
  168.         }
  169.         if (self::$handlerInstance->isExceptionHandlerRegistered) {
  170.             return self::$handlerInstance;
  171.         }
  172.         self::$handlerInstance->isExceptionHandlerRegistered true;
  173.         self::$handlerInstance->previousExceptionHandler set_exception_handler(\Closure::fromCallable([self::$handlerInstance'handleException']));
  174.         return self::$handlerInstance;
  175.     }
  176.     /**
  177.      * Adds a listener to the current error handler that will be called every
  178.      * time an error is captured.
  179.      *
  180.      * @param callable $listener A callable that will act as a listener
  181.      *                           and that must accept a single argument
  182.      *                           of type \ErrorException
  183.      *
  184.      * @psalm-param callable(\ErrorException): void $listener
  185.      */
  186.     public function addErrorHandlerListener(callable $listener): void
  187.     {
  188.         $this->errorListeners[] = $listener;
  189.     }
  190.     /**
  191.      * Adds a listener to the current error handler that will be called every
  192.      * time a fatal error handler is captured.
  193.      *
  194.      * @param callable $listener A callable that will act as a listener
  195.      *                           and that must accept a single argument
  196.      *                           of type \Sentry\Exception\FatalErrorException
  197.      *
  198.      * @psalm-param callable(FatalErrorException): void $listener
  199.      */
  200.     public function addFatalErrorHandlerListener(callable $listener): void
  201.     {
  202.         $this->fatalErrorListeners[] = $listener;
  203.     }
  204.     /**
  205.      * Adds a listener to the current error handler that will be called every
  206.      * time an exception is captured.
  207.      *
  208.      * @param callable $listener A callable that will act as a listener
  209.      *                           and that must accept a single argument
  210.      *                           of type \Throwable
  211.      *
  212.      * @psalm-param callable(\Throwable): void $listener
  213.      */
  214.     public function addExceptionHandlerListener(callable $listener): void
  215.     {
  216.         $this->exceptionListeners[] = $listener;
  217.     }
  218.     /**
  219.      * Handles errors by capturing them through the client according to the
  220.      * configured bit field.
  221.      *
  222.      * @param int                       $level      The level of the error raised, represented by
  223.      *                                              one of the E_* constants
  224.      * @param string                    $message    The error message
  225.      * @param string                    $file       The filename the error was raised in
  226.      * @param int                       $line       The line number the error was raised at
  227.      * @param array<string, mixed>|null $errcontext The error context (deprecated since PHP 7.2)
  228.      *
  229.      * @return bool If the function returns `false` then the PHP native error
  230.      *              handler will be called
  231.      *
  232.      * @throws \Throwable
  233.      */
  234.     private function handleError(int $levelstring $messagestring $fileint $line, ?array $errcontext = []): bool
  235.     {
  236.         $isSilencedError === error_reporting();
  237.         if (\PHP_MAJOR_VERSION >= 8) {
  238.             // Starting from PHP8, when a silenced error occurs the `error_reporting()`
  239.             // function will return a bitmask of fatal errors that are unsilenceable.
  240.             // If by subtracting from this value those errors the result is 0, we can
  241.             // conclude that the error was silenced.
  242.             $isSilencedError === (error_reporting() & ~self::PHP8_UNSILENCEABLE_FATAL_ERRORS);
  243.             // However, starting from PHP8 some fatal errors are unsilenceable,
  244.             // so we have to check for them to avoid reporting any of them as
  245.             // silenced instead
  246.             if ($level === (self::PHP8_UNSILENCEABLE_FATAL_ERRORS $level)) {
  247.                 $isSilencedError false;
  248.             }
  249.         }
  250.         if ($isSilencedError) {
  251.             $errorAsException = new SilencedErrorException(self::ERROR_LEVELS_DESCRIPTION[$level] . ': ' $message0$level$file$line);
  252.         } else {
  253.             $errorAsException = new \ErrorException(self::ERROR_LEVELS_DESCRIPTION[$level] . ': ' $message0$level$file$line);
  254.         }
  255.         $backtrace $this->cleanBacktraceFromErrorHandlerFrames($errorAsException->getTrace(), $errorAsException->getFile(), $errorAsException->getLine());
  256.         $this->exceptionReflection->setValue($errorAsException$backtrace);
  257.         $this->invokeListeners($this->errorListeners$errorAsException);
  258.         if (null !== $this->previousErrorHandler) {
  259.             return false !== ($this->previousErrorHandler)($level$message$file$line$errcontext);
  260.         }
  261.         return false;
  262.     }
  263.     /**
  264.      * Tries to handle a fatal error if any and relay them to the listeners.
  265.      * It only tries to do this if we still have some reserved memory at
  266.      * disposal. This method is used as callback of a shutdown function.
  267.      */
  268.     private function handleFatalError(): void
  269.     {
  270.         // If there is not enough memory that can be used to handle the error
  271.         // do nothing
  272.         if (null === self::$reservedMemory) {
  273.             return;
  274.         }
  275.         self::$reservedMemory null;
  276.         $error error_get_last();
  277.         if (!empty($error) && $error['type'] & (\E_ERROR | \E_PARSE | \E_CORE_ERROR | \E_CORE_WARNING | \E_COMPILE_ERROR | \E_COMPILE_WARNING)) {
  278.             $errorAsException = new FatalErrorException(self::ERROR_LEVELS_DESCRIPTION[$error['type']] . ': ' $error['message'], 0$error['type'], $error['file'], $error['line']);
  279.             $this->exceptionReflection->setValue($errorAsException, []);
  280.             $this->invokeListeners($this->fatalErrorListeners$errorAsException);
  281.         }
  282.     }
  283.     /**
  284.      * Handles the given exception by passing it to all the listeners,
  285.      * then forwarding it to another handler.
  286.      *
  287.      * @param \Throwable $exception The exception to handle
  288.      *
  289.      * @throws \Throwable
  290.      */
  291.     private function handleException(\Throwable $exception): void
  292.     {
  293.         $this->invokeListeners($this->exceptionListeners$exception);
  294.         $previousExceptionHandlerException $exception;
  295.         // Unset the previous exception handler to prevent infinite loop in case
  296.         // we need to handle an exception thrown from it
  297.         $previousExceptionHandler $this->previousExceptionHandler;
  298.         $this->previousExceptionHandler null;
  299.         try {
  300.             if (null !== $previousExceptionHandler) {
  301.                 $previousExceptionHandler($exception);
  302.                 return;
  303.             }
  304.         } catch (\Throwable $previousExceptionHandlerException) {
  305.             // This `catch` statement is here to forcefully override the
  306.             // $previousExceptionHandlerException variable with the exception
  307.             // we just caught
  308.         }
  309.         // If the instance of the exception we're handling is the same as the one
  310.         // caught from the previous exception handler then we give it back to the
  311.         // native PHP handler to prevent an infinite loop
  312.         if ($exception === $previousExceptionHandlerException) {
  313.             // Disable the fatal error handler or the error will be reported twice
  314.             self::$reservedMemory null;
  315.             throw $exception;
  316.         }
  317.         $this->handleException($previousExceptionHandlerException);
  318.     }
  319.     /**
  320.      * Cleans and returns the backtrace without the first frames that belong to
  321.      * this error handler.
  322.      *
  323.      * @param array<int, array<string, mixed>> $backtrace The backtrace to clear
  324.      * @param string                           $file      The filename the backtrace was raised in
  325.      * @param int                              $line      The line number the backtrace was raised at
  326.      *
  327.      * @return array<int, mixed>
  328.      *
  329.      * @psalm-param list<StacktraceFrame> $backtrace
  330.      */
  331.     private function cleanBacktraceFromErrorHandlerFrames(array $backtracestring $fileint $line): array
  332.     {
  333.         $cleanedBacktrace $backtrace;
  334.         $index 0;
  335.         while ($index < \count($backtrace)) {
  336.             if (isset($backtrace[$index]['file'], $backtrace[$index]['line']) && $backtrace[$index]['line'] === $line && $backtrace[$index]['file'] === $file) {
  337.                 $cleanedBacktrace = \array_slice($cleanedBacktrace$index);
  338.                 break;
  339.             }
  340.             ++$index;
  341.         }
  342.         return $cleanedBacktrace;
  343.     }
  344.     /**
  345.      * Invokes all the listeners and pass the exception to all of them.
  346.      *
  347.      * @param callable[] $listeners The array of listeners to be called
  348.      * @param \Throwable $throwable The exception to be passed onto listeners
  349.      */
  350.     private function invokeListeners(array $listeners, \Throwable $throwable): void
  351.     {
  352.         foreach ($listeners as $listener) {
  353.             try {
  354.                 $listener($throwable);
  355.             } catch (\Throwable $exception) {
  356.                 // Do nothing as this should be as transparent as possible
  357.             }
  358.         }
  359.     }
  360. }