Commit d4ad533d by Carsten Brandt

moved error handling related methods from app to errorhandler

parent 33c8f703
...@@ -64,7 +64,6 @@ abstract class Application extends Module ...@@ -64,7 +64,6 @@ abstract class Application extends Module
* it will use the "app\controllers" namespace. * it will use the "app\controllers" namespace.
*/ */
public $controllerNamespace = 'app\\controllers'; public $controllerNamespace = 'app\\controllers';
/** /**
* @var string the application name. * @var string the application name.
*/ */
...@@ -98,13 +97,6 @@ abstract class Application extends Module ...@@ -98,13 +97,6 @@ abstract class Application extends Module
*/ */
public $layout = 'main'; public $layout = 'main';
/** /**
* @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 string the requested route * @var string the requested route
*/ */
public $requestedRoute; public $requestedRoute;
...@@ -139,16 +131,7 @@ abstract class Application extends Module ...@@ -139,16 +131,7 @@ abstract class Application extends Module
* will be invoked at the beginning of [[init()]]. * will be invoked at the beginning of [[init()]].
*/ */
public $bootstrap = []; public $bootstrap = [];
/**
* @var \Exception the exception that is being handled currently. When this is not null,
* it means the application is handling some exception and extra care should be taken.
*/
public $exception;
/**
* @var string Used to reserve memory for fatal error handler.
*/
private $_memoryReserve;
/** /**
* Constructor. * Constructor.
...@@ -160,8 +143,15 @@ abstract class Application extends Module ...@@ -160,8 +143,15 @@ abstract class Application extends Module
{ {
Yii::$app = $this; Yii::$app = $this;
// TODO how to deal with exceptions thrown in preInit()
$this->preInit($config); $this->preInit($config);
$this->registerErrorHandlers(); if (YII_ENABLE_ERROR_HANDLER) {
if (isset($config['components']['errorHandler'])) {
$this->set('errorHandler', $config['components']['errorHandler']);
unset($config['components']['errorHandler']);
}
$this->getErrorHandler()->register();
}
Component::__construct($config); Component::__construct($config);
} }
...@@ -264,22 +254,6 @@ abstract class Application extends Module ...@@ -264,22 +254,6 @@ abstract class Application extends Module
} }
/** /**
* Registers error handlers.
*/
public function registerErrorHandlers()
{
if (YII_ENABLE_ERROR_HANDLER) {
ini_set('display_errors', 0);
set_exception_handler([$this, 'handleException']);
set_error_handler([$this, 'handleError']);
if ($this->memoryReserveSize > 0) {
$this->_memoryReserve = str_repeat('x', $this->memoryReserveSize);
}
register_shutdown_function([$this, 'handleFatalError']);
}
}
/**
* Returns an ID that uniquely identifies this module among all modules within the current application. * Returns an ID that uniquely identifies this module among all modules within the current application.
* Since this is an application instance, it will always return an empty string. * Since this is an application instance, it will always return an empty string.
* @return string the unique ID of the module. * @return string the unique ID of the module.
...@@ -429,7 +403,7 @@ abstract class Application extends Module ...@@ -429,7 +403,7 @@ abstract class Application extends Module
*/ */
public function getErrorHandler() public function getErrorHandler()
{ {
return $this->get('errorHandler'); return $this->get('errorHandler', false);
} }
/** /**
...@@ -531,156 +505,4 @@ abstract class Application extends Module ...@@ -531,156 +505,4 @@ abstract class Application extends Module
'assetManager' => ['class' => 'yii\web\AssetManager'], 'assetManager' => ['class' => 'yii\web\AssetManager'],
]; ];
} }
/**
* 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 (($handler = $this->getErrorHandler()) !== null) {
$handler->handle($exception);
} else {
echo $this->renderException($exception);
if (PHP_SAPI === 'cli' && !YII_ENV_TEST) {
exit(1);
}
}
} catch (\Exception $e) {
// exception could be thrown in ErrorHandler::handle()
$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, 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) {
// load ErrorException manually here because autoloading them will not work
// when error occurs while autoloading a class
if (!class_exists('\\yii\\base\\Exception', false)) {
require_once(__DIR__ . '/Exception.php');
}
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(false);
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\\Exception', false)) {
require_once(__DIR__ . '/Exception.php');
}
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
error_log($exception);
if (($handler = $this->getErrorHandler()) !== null) {
$handler->handle($exception);
} else {
echo $this->renderException($exception);
}
exit(1);
}
}
/**
* Renders an exception without using rich format.
* @param \Exception $exception the exception to be rendered.
* @return string the rendering result
*/
public function renderException($exception)
{
if ($exception instanceof Exception && ($exception instanceof UserException || !YII_DEBUG)) {
$message = $exception->getName() . ': ' . $exception->getMessage();
if (Yii::$app->controller instanceof \yii\console\Controller) {
$message = Yii::$app->controller->ansiFormat($message, Console::FG_RED);
}
} else {
$message = YII_DEBUG ? (string) $exception : 'Error: ' . $exception->getMessage();
}
if (PHP_SAPI === 'cli') {
return $message . "\n";
} else {
return '<pre>' . htmlspecialchars($message, ENT_QUOTES, $this->charset) . '</pre>';
}
}
/**
* 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);
}
} }
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
namespace yii\base; namespace yii\base;
use Yii; use Yii;
use yii\helpers\Console;
use yii\web\HttpException; use yii\web\HttpException;
/** /**
...@@ -26,330 +27,193 @@ use yii\web\HttpException; ...@@ -26,330 +27,193 @@ use yii\web\HttpException;
class ErrorHandler extends Component class ErrorHandler extends Component
{ {
/** /**
* @var integer maximum number of source code lines to be displayed. Defaults to 25.
*/
public $maxSourceLines = 25;
/**
* @var integer maximum number of trace source code lines to be displayed. Defaults to 10.
*/
public $maxTraceSourceLines = 10;
/**
* @var boolean whether to discard any existing page output before error display. Defaults to true. * @var boolean whether to discard any existing page output before error display. Defaults to true.
*/ */
public $discardExistingOutput = true; public $discardExistingOutput = true;
/** /**
* @var string the route (e.g. 'site/error') to the controller action that will be used * @var integer the size of the reserved memory. A portion of memory is pre-allocated so that
* to display external errors. Inside the action, it can retrieve the error information * when an out-of-memory issue occurs, the error handler is able to handle the error with
* by Yii::$app->exception. This property defaults to null, meaning ErrorHandler * the help of this reserved memory. If you set this value to be 0, no memory will be reserved.
* will handle the error display. * Defaults to 256KB.
*/
public $errorAction;
/**
* @var string the path of the view file for rendering exceptions without call stack information.
*/
public $errorView = '@yii/views/errorHandler/error.php';
/**
* @var string the path of the view file for rendering exceptions.
*/ */
public $exceptionView = '@yii/views/errorHandler/exception.php'; public $memoryReserveSize = 262144;
/**
* @var string the path of the view file for rendering exceptions and errors call stack element.
*/
public $callStackItemView = '@yii/views/errorHandler/callStackItem.php';
/**
* @var string the path of the view file for rendering previous exceptions.
*/
public $previousExceptionView = '@yii/views/errorHandler/previousException.php';
/** /**
* @var \Exception the exception that is being handled currently. * @var \Exception the exception that is being handled currently.
*/ */
public $exception; public $exception;
/** /**
* Handles exception. * @var string Used to reserve memory for fatal error handler.
* @param \Exception $exception to be handled.
*/ */
public function handle($exception) private $_memoryReserve;
{
$this->exception = $exception;
if ($this->discardExistingOutput) {
$this->clearOutput();
}
$this->renderException($exception);
}
/**
* Renders the exception.
* @param \Exception $exception the exception to be handled.
*/
protected function renderException($exception)
{
if (Yii::$app instanceof \yii\console\Application || YII_ENV_TEST) {
echo Yii::$app->renderException($exception);
if (!YII_ENV_TEST) {
exit(1);
}
return;
}
$response = Yii::$app->getResponse();
$useErrorView = $response->format === \yii\web\Response::FORMAT_HTML && (!YII_DEBUG || $exception instanceof UserException);
if ($useErrorView && $this->errorAction !== null) {
$result = Yii::$app->runAction($this->errorAction);
if ($result instanceof Response) {
$response = $result;
} else {
$response->data = $result;
}
} elseif ($response->format === \yii\web\Response::FORMAT_HTML) {
if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest') {
// AJAX request
$response->data = Yii::$app->renderException($exception);
} else {
// if there is an error during error rendering it's useful to
// display PHP error in debug mode instead of a blank screen
if (YII_DEBUG) {
ini_set('display_errors', 1);
}
$file = $useErrorView ? $this->errorView : $this->exceptionView;
$response->data = $this->renderFile($file, [
'exception' => $exception,
]);
}
} else {
$response->data = $this->convertExceptionToArray($exception);
}
if ($exception instanceof HttpException) {
$response->setStatusCode($exception->statusCode);
} else {
$response->setStatusCode(500);
}
$response->send();
}
/** /**
* Converts an exception into an array. * Register this errorhandler
* @param \Exception $exception the exception being converted
* @return array the array representation of the exception.
*/ */
protected function convertExceptionToArray($exception) public function register()
{ {
$array = [ // TODO care for errorReporting set to deprecated or not
'type' => get_class($exception), ini_set('display_errors', 0);
'name' => $exception instanceof \yii\base\Exception || $exception instanceof \yii\base\ErrorException ? $exception->getName() : 'Exception', set_exception_handler([$this, 'handleException']);
'message' => $exception->getMessage(), set_error_handler([$this, 'handleError']);
'code' => $exception->getCode(), if ($this->memoryReserveSize > 0) {
]; $this->_memoryReserve = str_repeat('x', $this->memoryReserveSize);
if (YII_DEBUG) {
$array['stack-trace'] = explode("\n", $exception->getTraceAsString());
} }
if ($exception instanceof HttpException) { register_shutdown_function([$this, 'handleFatalError']);
$array['status'] = $exception->statusCode;
}
if (($prev = $exception->getPrevious()) !== null) {
$array['previous'] = $this->convertExceptionToArray($prev);
}
return $array;
} }
/** /**
* Converts special characters to HTML entities. * Handles uncaught PHP exceptions.
* @param string $text to encode. *
* @return string encoded original text. * This method is implemented as a PHP exception handler.
*
* @param \Exception $exception the exception that is not caught
*/ */
public function htmlEncode($text) public function handleException($exception)
{ {
return htmlspecialchars($text, ENT_QUOTES, Yii::$app->charset); $this->exception = $exception;
}
/** // disable error capturing to avoid recursive errors while handling exceptions
* Removes all output echoed before calling this method. restore_error_handler(); // TODO maybe not needed
*/ restore_exception_handler();
public function clearOutput() try {
{ $this->logException($exception);
// the following manual level counting is to deal with zlib.output_compression set to On // TODO set HTTP header when HTTP exception
for ($level = ob_get_level(); $level > 0; --$level) { if ($this->discardExistingOutput) {
if (!@ob_end_clean()) { $this->clearOutput();
ob_clean();
} }
echo $this->renderException($exception);
if (PHP_SAPI === 'cli' && !YII_ENV_TEST) { // TODO rethink condition
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>';
} }
/**
* Adds informational links to the given PHP type/class.
* @param string $code type/class name to be linkified.
* @return string linkified with HTML type/class name.
*/
public function addTypeLinks($code)
{
$html = '';
if (strpos($code, '\\') !== false) {
// namespaced class
foreach (explode('\\', $code) as $part) {
$html .= '<a href="http://yiiframework.com/doc/api/2.0/' . $this->htmlEncode($part) . '" target="_blank">' . $this->htmlEncode($part) . '</a>\\';
} }
$html = rtrim($html, '\\'); $msg .= "\n\$_SERVER = " . var_export($_SERVER, true);
} elseif (strpos($code, '()') !== false) { error_log($msg);
// method/function call exit(1);
$html = preg_replace_callback('/^(.*)\(\)$/', function ($matches) {
return '<a href="http://yiiframework.com/doc/api/2.0/' . $this->htmlEncode($matches[1]) . '" target="_blank">' .
$this->htmlEncode($matches[1]) . '</a>()';
}, $code);
} }
return $html;
} }
/** /**
* Renders a view file as a PHP script. * Handles PHP execution errors such as warnings and notices.
* @param string $_file_ the view file. *
* @param array $_params_ the parameters (name-value pairs) that will be extracted and made available in the view file. * This method is used as a PHP error handler. It will simply raise an [[ErrorException]].
* @return string the rendering result *
* @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 renderFile($_file_, $_params_) public function handleError($code, $message, $file, $line)
{ {
$_params_['handler'] = $this; if (error_reporting() & $code) {
if ($this->exception instanceof ErrorException) { // load ErrorException manually here because autoloading them will not work
ob_start(); // when error occurs while autoloading a class
ob_implicit_flush(false); if (!class_exists('\\yii\\base\\ErrorException', false)) {
extract($_params_, EXTR_OVERWRITE); require_once(__DIR__ . '/ErrorException.php');
require(Yii::getAlias($_file_)); }
$exception = new ErrorException($message, $code, $code, $file, $line);
return ob_get_clean();
} else { // in case error appeared in __toString method we can't throw any exception
return Yii::$app->getView()->renderFile($_file_, $_params_, $this); $trace = debug_backtrace(0);
array_shift($trace);
foreach ($trace as $frame) {
if ($frame['function'] == '__toString') {
$this->handleException($exception);
exit(1);
} }
} }
/** throw $exception;
* Renders the previous exception stack for a given Exception.
* @param \Exception $exception the exception whose precursors should be rendered.
* @return string HTML content of the rendered previous exceptions.
* Empty string if there are none.
*/
public function renderPreviousExceptions($exception)
{
if (($previous = $exception->getPrevious()) !== null) {
return $this->renderFile($this->previousExceptionView, ['exception' => $previous]);
} else {
return '';
} }
} }
/** /**
* Renders a single call stack element. * Handles fatal PHP errors
* @param string|null $file name where call has happened.
* @param integer|null $line number on which call has happened.
* @param string|null $class called class name.
* @param string|null $method called function/method name.
* @param integer $index number of the call stack element.
* @return string HTML content of the rendered call stack element.
*/ */
public function renderCallStackItem($file, $line, $class, $method, $index) public function handleFatalError()
{ {
$lines = []; unset($this->_memoryReserve);
$begin = $end = 0;
if ($file !== null && $line !== null) {
$line--; // adjust line number from one-based to zero-based
$lines = @file($file);
if ($line < 0 || $lines === false || ($lineCount = count($lines)) < $line + 1) {
return '';
}
$half = (int) (($index == 0 ? $this->maxSourceLines : $this->maxTraceSourceLines) / 2); // load ErrorException manually here because autoloading them will not work
$begin = $line - $half > 0 ? $line - $half : 0; // when error occurs while autoloading a class
$end = $line + $half < $lineCount ? $line + $half : $lineCount - 1; if (!class_exists('\\yii\\base\\ErrorException', false)) {
require_once(__DIR__ . '/ErrorException.php');
} }
return $this->renderFile($this->callStackItemView, [ $error = error_get_last();
'file' => $file,
'line' => $line, if (ErrorException::isFatalError($error)) {
'class' => $class, $exception = new ErrorException($error['message'], $error['type'], $error['type'], $error['file'], $error['line']);
'method' => $method, $this->exception = $exception;
'index' => $index, // use error_log because it's too late to use Yii log
'lines' => $lines, error_log($exception);
'begin' => $begin,
'end' => $end, if ($this->discardExistingOutput) {
]); $this->clearOutput();
}
echo $this->renderException($exception);
exit(1);
}
} }
/** /**
* Renders the request information. * Renders an exception without using rich format.
* @param \Exception $exception the exception to be rendered.
* @return string the rendering result * @return string the rendering result
*/ */
public function renderRequest() protected function renderException($exception)
{ {
$request = ''; if ($exception instanceof Exception && ($exception instanceof UserException || !YII_DEBUG)) {
foreach (['_GET', '_POST', '_SERVER', '_FILES', '_COOKIE', '_SESSION', '_ENV'] as $name) { $message = $exception->getName() . ': ' . $exception->getMessage();
if (!empty($GLOBALS[$name])) { if (Yii::$app->controller instanceof \yii\console\Controller) {
$request .= '$' . $name . ' = ' . var_export($GLOBALS[$name], true) . ";\n\n"; $message = Yii::$app->controller->ansiFormat($message, Console::FG_RED);
} }
} else {
$message = YII_DEBUG ? (string) $exception : 'Error: ' . $exception->getMessage(); // TODO improve output
} }
return $message . "\n";
return '<pre>' . rtrim($request, "\n") . '</pre>';
} }
/** /**
* Determines whether given name of the file belongs to the framework. * Logs the given exception
* @param string $file name to be checked. * @param \Exception $exception the exception to be logged
* @return boolean whether given name of the file belongs to the framework.
*/ */
public function isCoreFile($file) protected function logException($exception)
{ {
return $file === null || strpos(realpath($file), YII_PATH . DIRECTORY_SEPARATOR) === 0; // TODO logger may not be registered
$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);
/**
* Creates HTML containing link to the page with the information on given HTTP status code.
* @param integer $statusCode to be used to generate information link.
* @param string $statusDescription Description to display after the the status code.
* @return string generated HTML with HTTP status code information.
*/
public function createHttpStatusLink($statusCode, $statusDescription)
{
return '<a href="http://en.wikipedia.org/wiki/List_of_HTTP_status_codes#' . (int) $statusCode . '" target="_blank">HTTP ' . (int) $statusCode . ' &ndash; ' . $statusDescription . '</a>';
} }
/** /**
* Creates string containing HTML link which refers to the home page of determined web-server software * Removes all output echoed before calling this method.
* and its full name.
* @return string server software information hyperlink.
*/ */
public function createServerInformationLink() public function clearOutput()
{ {
static $serverUrls = [ // the following manual level counting is to deal with zlib.output_compression set to On
'http://httpd.apache.org/' => ['apache'], for ($level = ob_get_level(); $level > 0; --$level) {
'http://nginx.org/' => ['nginx'], if (!@ob_end_clean()) {
'http://lighttpd.net/' => ['lighttpd'], ob_clean();
'http://gwan.com/' => ['g-wan', 'gwan'],
'http://iis.net/' => ['iis', 'services'],
'http://php.net/manual/en/features.commandline.webserver.php' => ['development'],
];
if (isset($_SERVER['SERVER_SOFTWARE'])) {
foreach ($serverUrls as $url => $keywords) {
foreach ($keywords as $keyword) {
if (stripos($_SERVER['SERVER_SOFTWARE'], $keyword) !== false) {
return '<a href="' . $url . '" target="_blank">' . $this->htmlEncode($_SERVER['SERVER_SOFTWARE']) . '</a>';
}
}
}
} }
return '';
} }
/**
* Creates string containing HTML link which refers to the page with the current version
* of the framework and version number text.
* @return string framework version information hyperlink.
*/
public function createFrameworkVersionLink()
{
return '<a href="http://github.com/yiisoft/yii2/" target="_blank">' . $this->htmlEncode(Yii::getVersion()) . '</a>';
} }
} }
...@@ -105,10 +105,9 @@ class Controller extends \yii\base\Controller ...@@ -105,10 +105,9 @@ class Controller extends \yii\base\Controller
public function beforeAction($action) public function beforeAction($action)
{ {
if (parent::beforeAction($action)) { if (parent::beforeAction($action)) {
if ($this->enableCsrfValidation && Yii::$app->exception === null && !Yii::$app->getRequest()->validateCsrfToken()) { if ($this->enableCsrfValidation && Yii::$app->errorHandler->exception === null && !Yii::$app->getRequest()->validateCsrfToken()) {
throw new BadRequestHttpException(Yii::t('yii', 'Unable to verify your data submission.')); throw new BadRequestHttpException(Yii::t('yii', 'Unable to verify your data submission.'));
} }
return true; return true;
} else { } else {
return false; return false;
......
...@@ -68,7 +68,7 @@ class ErrorAction extends Action ...@@ -68,7 +68,7 @@ class ErrorAction extends Action
public function run() public function run()
{ {
if (($exception = Yii::$app->exception) === null) { if (($exception = Yii::$app->errorHandler->exception) === null) {
return ''; return '';
} }
......
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\web;
use Yii;
use yii\base\ErrorException;
use yii\base\UserException;
/**
* 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 \yii\base\ErrorHandler
{
/**
* @var integer maximum number of source code lines to be displayed. Defaults to 25.
*/
public $maxSourceLines = 25;
/**
* @var integer maximum number of trace source code lines to be displayed. Defaults to 10.
*/
public $maxTraceSourceLines = 10;
/**
* @var string the route (e.g. 'site/error') to the controller action that will be used
* to display external errors. Inside the action, it can retrieve the error information
* by Yii::$app->exception. This property defaults to null, meaning ErrorHandler
* will handle the error display.
*/
public $errorAction;
/**
* @var string the path of the view file for rendering exceptions without call stack information.
*/
public $errorView = '@yii/views/errorHandler/error.php';
/**
* @var string the path of the view file for rendering exceptions.
*/
public $exceptionView = '@yii/views/errorHandler/exception.php';
/**
* @var string the path of the view file for rendering exceptions and errors call stack element.
*/
public $callStackItemView = '@yii/views/errorHandler/callStackItem.php';
/**
* @var string the path of the view file for rendering previous exceptions.
*/
public $previousExceptionView = '@yii/views/errorHandler/previousException.php';
/**
* Renders the exception.
* @param \Exception $exception the exception to be handled.
*/
protected function renderException($exception)
{
// TODO what to do different in test env?
// parent::renderException($exception); + <pre>
$response = Yii::$app->getResponse();
$useErrorView = $response->format === \yii\web\Response::FORMAT_HTML && (!YII_DEBUG || $exception instanceof UserException);
if ($useErrorView && $this->errorAction !== null) {
$result = Yii::$app->runAction($this->errorAction);
if ($result instanceof Response) {
$response = $result;
} else {
$response->data = $result;
}
} elseif ($response->format === \yii\web\Response::FORMAT_HTML) {
if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest') {
// AJAX request
$response->data = '<pre>' . htmlspecialchars(parent::renderException($exception), ENT_QUOTES, $this->charset) . '</pre>';
} else {
// if there is an error during error rendering it's useful to
// display PHP error in debug mode instead of a blank screen
if (YII_DEBUG) {
ini_set('display_errors', 1);
}
$file = $useErrorView ? $this->errorView : $this->exceptionView;
$response->data = $this->renderFile($file, [
'exception' => $exception,
]);
}
} else {
$response->data = $this->convertExceptionToArray($exception);
}
if ($exception instanceof HttpException) {
$response->setStatusCode($exception->statusCode);
} else {
$response->setStatusCode(500);
}
$response->send();
}
/**
* Converts an exception into an array.
* @param \Exception $exception the exception being converted
* @return array the array representation of the exception.
*/
protected function convertExceptionToArray($exception)
{
$array = [
'type' => get_class($exception),
'name' => $exception instanceof \yii\base\Exception || $exception instanceof \yii\base\ErrorException ? $exception->getName() : 'Exception',
'message' => $exception->getMessage(),
'code' => $exception->getCode(),
];
if (YII_DEBUG) {
$array['stack-trace'] = explode("\n", $exception->getTraceAsString());
}
if ($exception instanceof HttpException) {
$array['status'] = $exception->statusCode;
}
if (($prev = $exception->getPrevious()) !== null) {
$array['previous'] = $this->convertExceptionToArray($prev);
}
return $array;
}
/**
* Converts special characters to HTML entities.
* @param string $text to encode.
* @return string encoded original text.
*/
public function htmlEncode($text)
{
return htmlspecialchars($text, ENT_QUOTES, Yii::$app->charset);
}
/**
* Adds informational links to the given PHP type/class.
* @param string $code type/class name to be linkified.
* @return string linkified with HTML type/class name.
*/
public function addTypeLinks($code)
{
$html = '';
if (strpos($code, '\\') !== false) {
// namespaced class
foreach (explode('\\', $code) as $part) {
$html .= '<a href="http://yiiframework.com/doc/api/2.0/' . $this->htmlEncode($part) . '" target="_blank">' . $this->htmlEncode($part) . '</a>\\';
}
$html = rtrim($html, '\\');
} elseif (strpos($code, '()') !== false) {
// method/function call
$html = preg_replace_callback('/^(.*)\(\)$/', function ($matches) {
return '<a href="http://yiiframework.com/doc/api/2.0/' . $this->htmlEncode($matches[1]) . '" target="_blank">' .
$this->htmlEncode($matches[1]) . '</a>()';
}, $code);
}
return $html;
}
/**
* Renders a view file as a PHP script.
* @param string $_file_ the view file.
* @param array $_params_ the parameters (name-value pairs) that will be extracted and made available in the view file.
* @return string the rendering result
*/
public function renderFile($_file_, $_params_)
{
$_params_['handler'] = $this;
if ($this->exception instanceof ErrorException) {
ob_start();
ob_implicit_flush(false);
extract($_params_, EXTR_OVERWRITE);
require(Yii::getAlias($_file_));
return ob_get_clean();
} else {
return Yii::$app->getView()->renderFile($_file_, $_params_, $this);
}
}
/**
* Renders the previous exception stack for a given Exception.
* @param \Exception $exception the exception whose precursors should be rendered.
* @return string HTML content of the rendered previous exceptions.
* Empty string if there are none.
*/
public function renderPreviousExceptions($exception)
{
if (($previous = $exception->getPrevious()) !== null) {
return $this->renderFile($this->previousExceptionView, ['exception' => $previous]);
} else {
return '';
}
}
/**
* Renders a single call stack element.
* @param string|null $file name where call has happened.
* @param integer|null $line number on which call has happened.
* @param string|null $class called class name.
* @param string|null $method called function/method name.
* @param integer $index number of the call stack element.
* @return string HTML content of the rendered call stack element.
*/
public function renderCallStackItem($file, $line, $class, $method, $index)
{
$lines = [];
$begin = $end = 0;
if ($file !== null && $line !== null) {
$line--; // adjust line number from one-based to zero-based
$lines = @file($file);
if ($line < 0 || $lines === false || ($lineCount = count($lines)) < $line + 1) {
return '';
}
$half = (int) (($index == 0 ? $this->maxSourceLines : $this->maxTraceSourceLines) / 2);
$begin = $line - $half > 0 ? $line - $half : 0;
$end = $line + $half < $lineCount ? $line + $half : $lineCount - 1;
}
return $this->renderFile($this->callStackItemView, [
'file' => $file,
'line' => $line,
'class' => $class,
'method' => $method,
'index' => $index,
'lines' => $lines,
'begin' => $begin,
'end' => $end,
]);
}
/**
* Renders the request information.
* @return string the rendering result
*/
public function renderRequest()
{
$request = '';
foreach (['_GET', '_POST', '_SERVER', '_FILES', '_COOKIE', '_SESSION', '_ENV'] as $name) {
if (!empty($GLOBALS[$name])) {
$request .= '$' . $name . ' = ' . var_export($GLOBALS[$name], true) . ";\n\n";
}
}
return '<pre>' . rtrim($request, "\n") . '</pre>';
}
/**
* Determines whether given name of the file belongs to the framework.
* @param string $file name to be checked.
* @return boolean whether given name of the file belongs to the framework.
*/
public function isCoreFile($file)
{
return $file === null || strpos(realpath($file), YII_PATH . DIRECTORY_SEPARATOR) === 0;
}
/**
* Creates HTML containing link to the page with the information on given HTTP status code.
* @param integer $statusCode to be used to generate information link.
* @param string $statusDescription Description to display after the the status code.
* @return string generated HTML with HTTP status code information.
*/
public function createHttpStatusLink($statusCode, $statusDescription)
{
return '<a href="http://en.wikipedia.org/wiki/List_of_HTTP_status_codes#' . (int) $statusCode . '" target="_blank">HTTP ' . (int) $statusCode . ' &ndash; ' . $statusDescription . '</a>';
}
/**
* Creates string containing HTML link which refers to the home page of determined web-server software
* and its full name.
* @return string server software information hyperlink.
*/
public function createServerInformationLink()
{
static $serverUrls = [
'http://httpd.apache.org/' => ['apache'],
'http://nginx.org/' => ['nginx'],
'http://lighttpd.net/' => ['lighttpd'],
'http://gwan.com/' => ['g-wan', 'gwan'],
'http://iis.net/' => ['iis', 'services'],
'http://php.net/manual/en/features.commandline.webserver.php' => ['development'],
];
if (isset($_SERVER['SERVER_SOFTWARE'])) {
foreach ($serverUrls as $url => $keywords) {
foreach ($keywords as $keyword) {
if (stripos($_SERVER['SERVER_SOFTWARE'], $keyword) !== false) {
return '<a href="' . $url . '" target="_blank">' . $this->htmlEncode($_SERVER['SERVER_SOFTWARE']) . '</a>';
}
}
}
}
return '';
}
/**
* Creates string containing HTML link which refers to the page with the current version
* of the framework and version number text.
* @return string framework version information hyperlink.
*/
public function createFrameworkVersionLink()
{
return '<a href="http://github.com/yiisoft/yii2/" target="_blank">' . $this->htmlEncode(Yii::getVersion()) . '</a>';
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment