<?php /** * @link http://www.yiiframework.com/ * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ namespace yii\base; use Yii; use yii\helpers\Console; use yii\web\HttpException; /** * ErrorHandler handles uncaught PHP errors and exceptions. * * ErrorHandler displays these errors using appropriate views based on the * nature of the errors and the mode the application runs at. * * ErrorHandler is configured as an application component in [[\yii\base\Application]] by default. * You can access that instance via `Yii::$app->errorHandler`. * * @author Qiang Xue <qiang.xue@gmail.com> * @author Timur Ruziev <resurtm@gmail.com> * @since 2.0 */ class ErrorHandler extends Component { /** * @var boolean whether to discard any existing page output before error display. Defaults to true. */ public $discardExistingOutput = true; /** * @var integer the size of the reserved memory. A portion of memory is pre-allocated so that * when an out-of-memory issue occurs, the error handler is able to handle the error with * the help of this reserved memory. If you set this value to be 0, no memory will be reserved. * Defaults to 256KB. */ public $memoryReserveSize = 262144; /** * @var \Exception the exception that is being handled currently. */ public $exception; /** * @var string Used to reserve memory for fatal error handler. */ private $_memoryReserve; /** * Register this errorhandler */ public function register() { ini_set('display_errors', false); set_exception_handler([$this, 'handleException']); set_error_handler([$this, 'handleError']); // TODO care for errorReporting set to deprecated or not if ($this->memoryReserveSize > 0) { $this->_memoryReserve = str_repeat('x', $this->memoryReserveSize); } register_shutdown_function([$this, 'handleFatalError']); } /** * Handles uncaught PHP exceptions. * * This method is implemented as a PHP exception handler. * * @param \Exception $exception the exception that is not caught */ public function handleException($exception) { $this->exception = $exception; // disable error capturing to avoid recursive errors while handling exceptions restore_error_handler(); restore_exception_handler(); try { $this->logException($exception); if ($this->discardExistingOutput) { $this->clearOutput(); } $this->renderException($exception); if (!YII_ENV_TEST) { exit(1); } } catch (\Exception $e) { // an other exception could be thrown while displaying the exception $msg = (string) $e; $msg .= "\nPrevious exception:\n"; $msg .= (string) $exception; if (YII_DEBUG) { if (PHP_SAPI === 'cli') { echo $msg . "\n"; } else { echo '<pre>' . htmlspecialchars($msg, ENT_QUOTES, $this->charset) . '</pre>'; } } $msg .= "\n\$_SERVER = " . var_export($_SERVER, true); error_log($msg); exit(1); } } /** * Handles PHP execution errors such as warnings and notices. * * This method is used as a PHP error handler. It will simply raise an [[ErrorException]]. * * @param integer $code the level of the error raised. * @param string $message the error message. * @param string $file the filename that the error was raised in. * @param integer $line the line number the error was raised at. * * @throws ErrorException */ public function handleError($code, $message, $file, $line) { if (error_reporting() & $code) { // TODO care for errorReporting set to deprecated or not // load ErrorException manually here because autoloading them will not work // when error occurs while autoloading a class if (!class_exists('\\yii\\base\\ErrorException', false)) { require_once(__DIR__ . '/ErrorException.php'); } $exception = new ErrorException($message, $code, $code, $file, $line); // in case error appeared in __toString method we can't throw any exception $trace = debug_backtrace(0); array_shift($trace); foreach ($trace as $frame) { if ($frame['function'] == '__toString') { $this->handleException($exception); exit(1); } } throw $exception; } } /** * Handles fatal PHP errors */ public function handleFatalError() { unset($this->_memoryReserve); // load ErrorException manually here because autoloading them will not work // when error occurs while autoloading a class if (!class_exists('\\yii\\base\\ErrorException', false)) { require_once(__DIR__ . '/ErrorException.php'); } $error = error_get_last(); if (ErrorException::isFatalError($error)) { $exception = new ErrorException($error['message'], $error['type'], $error['type'], $error['file'], $error['line']); $this->exception = $exception; // use error_log because it's too late to use Yii log // also do not log when on CLI SAPI because message will be sent to STDERR which has already been done by PHP PHP_SAPI === 'cli' or error_log($exception); if ($this->discardExistingOutput) { $this->clearOutput(); } $this->renderException($exception); exit(1); } } /** * Renders an exception without using rich format. * @param \Exception $exception the exception to be rendered. */ protected function renderException($exception) { if ($exception instanceof Exception && ($exception instanceof UserException || !YII_DEBUG)) { $message = $this->formatMessage($exception->getName() . ': ') . $exception->getMessage(); } elseif (YII_DEBUG) { if ($exception instanceof Exception) { $message = $this->formatMessage("Exception ({$exception->getName()})"); } elseif ($exception instanceof ErrorException) { $message = $this->formatMessage($exception->getName()); } else { $message = $this->formatMessage('Exception'); } $message .= $this->formatMessage(" '" . get_class($exception) . "'", [Console::BOLD, Console::FG_BLUE]) . " with message " . $this->formatMessage("'{$exception->getMessage()}'", [Console::BOLD]) //. "\n" . "\n\nin " . dirname($exception->getFile()) . DIRECTORY_SEPARATOR . $this->formatMessage(basename($exception->getFile()), [Console::BOLD]) . ':' . $this->formatMessage($exception->getLine(), [Console::BOLD, Console::FG_YELLOW]) . "\n\n" . $this->formatMessage("Stack trace:\n", [Console::BOLD]) . $exception->getTraceAsString(); } else { $message = $this->formatMessage('Error: ') . $exception->getMessage(); } if (PHP_SAPI === 'cli') { Console::stderr($message . "\n"); } else { echo $message . "\n"; } } /** * Colorizes a message for console output. * @param string $message the message to colorize. * @param array $format the message format. * @return string the colorized message. * @see Console::ansiFormat() for details on how to specify the message format. */ protected function formatMessage($message, $format = [Console::FG_RED, Console::BOLD]) { $stream = (PHP_SAPI === 'cli') ? STDERR : STDOUT; // try controller first to allow check for --color switch if (Yii::$app->controller instanceof \yii\console\Controller && Yii::$app->controller->isColorEnabled($stream) || Yii::$app instanceof \yii\console\Application && Console::streamSupportsAnsiColors($stream)) { $message = Console::ansiFormat($message, $format); } return $message; } /** * Logs the given exception * @param \Exception $exception the exception to be logged */ protected function logException($exception) { $category = get_class($exception); if ($exception instanceof HttpException) { $category = 'yii\\web\\HttpException:' . $exception->statusCode; } elseif ($exception instanceof \ErrorException) { $category .= ':' . $exception->getSeverity(); } Yii::error((string) $exception, $category); } /** * Removes all output echoed before calling this method. */ public function clearOutput() { // the following manual level counting is to deal with zlib.output_compression set to On for ($level = ob_get_level(); $level > 0; --$level) { if (!@ob_end_clean()) { ob_clean(); } } } /** * Converts an exception into a PHP error. * * This method can be used to convert exceptions inside of methods like `__toString()` * to PHP errors because exceptions cannot be thrown inside of them. * @param \Exception $exception the exception to convert to a PHP error. */ public static function convertExceptionToError($exception) { trigger_error(static::convertExceptionToString($exception), E_USER_ERROR); } /** * Converts an exception into a simple string. * @param \Exception $exception the exception being converted * @return string the string representation of the exception. */ public static function convertExceptionToString($exception) { if ($exception instanceof Exception && ($exception instanceof UserException || !YII_DEBUG)) { $message = "{$exception->getName()}: {$exception->getMessage()}"; } elseif (YII_DEBUG) { if ($exception instanceof Exception) { $message = "Exception ({$exception->getName()})"; } elseif ($exception instanceof ErrorException) { $message = "{$exception->getName()}"; } else { $message = 'Exception'; } $message .= " '" . get_class($exception) . "' with message '{$exception->getMessage()}' \n\nin " . $exception->getFile() . ':' . $exception->getLine() . "\n\n" . "Stack trace:\n" . $exception->getTraceAsString(); } else { $message = 'Error: ' . $exception->getMessage(); } return $message; } }