Commit 21f2602c by Carsten Brandt

Merge pull request #2916 from yiisoft/refactor-errorhandler

Refactor errorhandler
parents c23b671f f6ce9ead
......@@ -10,7 +10,7 @@ use Yii;
try {
10/0;
} catch (ErrorException) {
} catch (ErrorException $e) {
Yii::warning("Tried dividing by zero.");
}
......@@ -19,38 +19,39 @@ try {
As demonstrated above you may handle errors using `try`-`catch`.
Second, even fatal errors in Yii are rendered in a nice way. This means that in debugging mode, you can trace the causes
of fatal errors in order to more quickly identify the cause of the problem.
Rendering errors in a dedicated controller action
-------------------------------------------------
The default Yii error page is great when developing a site, and is acceptable for production sites if `YII_DEBUG`
is turned off in your bootstrap index.php file. But you may want to customize the default error page to make it
is turned off in your bootstrap `index.php` file. But you may want to customize the default error page to make it
more suitable for your project.
The easiest way to create a custom error page it is to use a dedicated controller action for error rendering. First,
you'll need to configure the `errorHandler` component in the application's configuration:
```php
return [
// ...
'components' => [
// ...
'components' => [
// ...
'errorHandler' => [
'errorAction' => 'site/error',
],
]
```
With that configuration in place, whenever an error occurs, Yii will execute the "error" action of the "Site" controller.
With that configuration in place, whenever an error occurs, Yii will execute the `error`-action of the `site`-controller.
That action should look for an exception and, if present, render the proper view file, passing along the exception:
```php
public function actionError()
{
if (\Yii::$app->exception !== null) {
return $this->render('error', ['exception' => \Yii::$app->exception]);
$exception = \Yii::$app->errorHandler->exception;
if ($exception !== null) {
return $this->render('error', ['exception' => $exception]);
}
}
```
......@@ -58,14 +59,13 @@ public function actionError()
Next, you would create the `views/site/error.php` file, which would make use of the exception. The exception object has
the following properties:
- `statusCode`: the HTTP status code (e.g. 403, 500). Available for HTTP exceptions only.
- `statusCode`: the HTTP status code (e.g. 403, 500). Available for [[yii\web\HttpException|HTTP exceptions]] only.
- `code`: the code of the exception.
- `type`: the error type (e.g. HttpException, PHP Error).
- `message`: the error message.
- `file`: the name of the PHP script file where the error occurs.
- `line`: the line number of the code where the error occurs.
- `trace`: the call stack of the error.
- `source`: the context source code where the error occurs.
Rendering errors without a dedicated controller action
------------------------------------------------------
......@@ -91,4 +91,4 @@ automatically be used. The view will be passed three variables:
- `$message`: the error message
- `$exception`: the exception being handled
The `$exception` object will have the same properties outlined above.
The `$exception` object will have the same properties as outlined above.
......@@ -8,6 +8,7 @@
namespace yii\mongodb;
use Yii;
use yii\base\ErrorHandler;
use yii\base\InvalidConfigException;
use yii\di\Instance;
......@@ -49,6 +50,7 @@ class Session extends \yii\web\Session
*/
public $sessionCollection = 'session';
/**
* Initializes the Session component.
* This method will initialize the [[db]] property to make sure it refers to a valid MongoDB connection.
......@@ -148,10 +150,11 @@ class Session extends \yii\web\Session
['upsert' => true]
);
} catch (\Exception $e) {
if (YII_DEBUG) {
echo $e->getMessage();
}
// it is too late to log an error message here
$exception = ErrorHandler::convertExceptionToString($e);
// its too late to use Yii logging here
error_log($exception);
echo $exception;
return false;
}
......
......@@ -219,6 +219,9 @@ Yii Framework 2 Change Log
- Chg #2281: Renamed `ActiveRecord::create()` to `populateRecord()` and changed signature. This method will not call instantiate() anymore (cebe)
- Chg #2405: The CSS class of `MaskedInput` now defaults to `form-control` (qiangxue)
- Chg #2426: Changed URL creation method signatures to be consistent (samdark)
- Chg #2516: Moved error handling from application to ErrorHandler class and fixed problems with HTTP Exception response code (cebe)
- `Yii::$app->exception` has now moved to `Yii::$app->errorHandler->exception`
- `yii\base\ErrorHandler` was split into `yii\web\ErrorHandler` and `yii\console\ErrorHandler`
- Chg #2544: Changed `DetailView`'s `name:format:label` to `attribute:format:label` to match `GridView` (samdark)
- Chg #2603: `yii\base\ErrorException` now extends `\ErrorException` (samdark)
- Chg #2629: `Module::controllerPath` is now read only, and all controller classes must be namespaced under `Module::controllerNamespace`. (qiangxue)
......
......@@ -126,13 +126,6 @@ abstract class Application extends Module
*/
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
*/
public $requestedRoute;
......@@ -168,20 +161,11 @@ abstract class Application extends Module
*/
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 integer the current application state during a request handling life cycle.
* This property is managed by the application. Do not modify this property.
*/
public $state;
/**
* @var string Used to reserve memory for fatal error handler.
*/
private $_memoryReserve;
/**
* Constructor.
......@@ -195,8 +179,8 @@ abstract class Application extends Module
$this->state = self::STATE_BEGIN;
$this->registerErrorHandler($config);
$this->preInit($config);
$this->registerErrorHandlers();
Component::__construct($config);
}
......@@ -212,13 +196,13 @@ abstract class Application extends Module
public function preInit(&$config)
{
if (!isset($config['id'])) {
throw new InvalidConfigException('The "id" configuration is required.');
throw new InvalidConfigException('The "id" configuration for the Application is required.');
}
if (isset($config['basePath'])) {
$this->setBasePath($config['basePath']);
unset($config['basePath']);
} else {
throw new InvalidConfigException('The "basePath" configuration is required.');
throw new InvalidConfigException('The "basePath" configuration for the Application is required.');
}
if (isset($config['vendorPath'])) {
......@@ -301,18 +285,18 @@ abstract class Application extends Module
}
/**
* Registers error handlers.
* Registers the errorHandler component as a PHP error handler.
*/
public function registerErrorHandlers()
protected function registerErrorHandler(&$config)
{
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);
if (!isset($config['components']['errorHandler']['class'])) {
echo "Error: no errorHandler component is configured.\n";
exit(1);
}
register_shutdown_function([$this, 'handleFatalError']);
$this->set('errorHandler', $config['components']['errorHandler']);
unset($config['components']['errorHandler']);
$this->getErrorHandler()->register();
}
}
......@@ -480,7 +464,7 @@ abstract class Application extends Module
/**
* Returns the error handler component.
* @return ErrorHandler the error handler application component.
* @return \yii\web\ErrorHandler|\yii\console\ErrorHandler the error handler application component.
*/
public function getErrorHandler()
{
......@@ -586,7 +570,6 @@ abstract class Application extends Module
{
return [
'log' => ['class' => 'yii\log\Dispatcher'],
'errorHandler' => ['class' => 'yii\base\ErrorHandler'],
'view' => ['class' => 'yii\web\View'],
'formatter' => ['class' => 'yii\base\Formatter'],
'i18n' => ['class' => 'yii\i18n\I18N'],
......@@ -623,160 +606,4 @@ abstract class Application extends Module
exit($status);
}
}
/**
* 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)
{
if ($exception instanceof ExitException) {
return;
}
$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);
}
}
......@@ -29,10 +29,7 @@ class ErrorException extends \ErrorException
*/
public function __construct($message = '', $code = 0, $severity = 1, $filename = __FILE__, $lineno = __LINE__, \Exception $previous = null)
{
parent::__construct($message, $code, $previous);
$this->severity = $severity;
$this->file = $filename;
$this->line = $lineno;
parent::__construct($message, $code, $severity, $filename, $lineno, $previous);
if (function_exists('xdebug_get_function_stack')) {
$trace = array_slice(array_reverse(xdebug_get_function_stack()), 3, -1);
......
......@@ -13,343 +13,234 @@ 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>
* @author Alexander Makarov <sam@rmcreative.ru>
* @author Carsten Brandt <mail@cebe.cc>
* @since 2.0
*/
class ErrorHandler extends Component
abstract 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.
*/
public $discardExistingOutput = true;
/**
* @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.
* @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 $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';
public $memoryReserveSize = 262144;
/**
* @var \Exception the exception that is being handled currently.
*/
public $exception;
/**
* Handles exception.
* @param \Exception $exception to be handled.
* @var string Used to reserve memory for fatal error handler.
*/
public function handle($exception)
private $_memoryReserve;
/**
* Register this errorhandler
*/
public function register()
{
$this->exception = $exception;
if ($this->discardExistingOutput) {
$this->clearOutput();
ini_set('display_errors', false);
set_exception_handler([$this, 'handleException']);
set_error_handler([$this, 'handleError']);
if ($this->memoryReserveSize > 0) {
$this->_memoryReserve = str_repeat('x', $this->memoryReserveSize);
}
$this->renderException($exception);
register_shutdown_function([$this, 'handleFatalError']);
}
/**
* Renders the exception.
* @param \Exception $exception the exception to be handled.
* Handles uncaught PHP exceptions.
*
* This method is implemented as a PHP exception handler.
*
* @param \Exception $exception the exception that is not caught
*/
protected function renderException($exception)
public function handleException($exception)
{
if (Yii::$app instanceof \yii\console\Application || YII_ENV_TEST) {
echo Yii::$app->renderException($exception);
if (!YII_ENV_TEST) {
exit(1);
}
if ($exception instanceof ExitException) {
return;
}
$response = Yii::$app->getResponse();
$useErrorView = $response->format === \yii\web\Response::FORMAT_HTML && (!YII_DEBUG || $exception instanceof UserException);
$this->exception = $exception;
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);
// 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();
}
$file = $useErrorView ? $this->errorView : $this->exceptionView;
$response->data = $this->renderFile($file, [
'exception' => $exception,
]);
$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 {
$response->data = $this->convertExceptionToArray($exception);
echo '<pre>' . htmlspecialchars($msg, ENT_QUOTES, Yii::$app->charset) . '</pre>';
}
if ($exception instanceof HttpException) {
$response->setStatusCode($exception->statusCode);
} else {
$response->setStatusCode(500);
}
$msg .= "\n\$_SERVER = " . var_export($_SERVER, true);
error_log($msg);
exit(1);
}
$response->send();
$this->exception = null;
}
/**
* Converts an exception into an array.
* @param \Exception $exception the exception being converted
* @return array the array representation of the exception.
* 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
*/
protected function convertExceptionToArray($exception)
public function handleError($code, $message, $file, $line)
{
$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 (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\\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);
}
if (($prev = $exception->getPrevious()) !== null) {
$array['previous'] = $this->convertExceptionToArray($prev);
}
return $array;
throw $exception;
}
/**
* 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);
}
/**
* Removes all output echoed before calling this method.
* Handles fatal PHP errors
*/
public function clearOutput()
public function handleFatalError()
{
// 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();
}
}
}
unset($this->_memoryReserve);
/**
* 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);
// 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');
}
return $html;
}
$error = error_get_last();
/**
* 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_));
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);
return ob_get_clean();
} else {
return Yii::$app->getView()->renderFile($_file_, $_params_, $this);
if ($this->discardExistingOutput) {
$this->clearOutput();
}
$this->renderException($exception);
exit(1);
}
}
/**
* 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.
* Renders the exception.
* @param \Exception $exception the exception to be rendered.
*/
public function renderPreviousExceptions($exception)
{
if (($previous = $exception->getPrevious()) !== null) {
return $this->renderFile($this->previousExceptionView, ['exception' => $previous]);
} else {
return '';
}
}
protected abstract function renderException($exception);
/**
* 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.
* Logs the given exception
* @param \Exception $exception the exception to be logged
*/
public function renderCallStackItem($file, $line, $class, $method, $index)
protected function logException($exception)
{
$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;
$category = get_class($exception);
if ($exception instanceof HttpException) {
$category = 'yii\\web\\HttpException:' . $exception->statusCode;
} elseif ($exception instanceof \ErrorException) {
$category .= ':' . $exception->getSeverity();
}
return $this->renderFile($this->callStackItemView, [
'file' => $file,
'line' => $line,
'class' => $class,
'method' => $method,
'index' => $index,
'lines' => $lines,
'begin' => $begin,
'end' => $end,
]);
Yii::error((string) $exception, $category);
}
/**
* Renders the request information.
* @return string the rendering result
* Removes all output echoed before calling this method.
*/
public function renderRequest()
public function clearOutput()
{
$request = '';
foreach (['_GET', '_POST', '_SERVER', '_FILES', '_COOKIE', '_SESSION', '_ENV'] as $name) {
if (!empty($GLOBALS[$name])) {
$request .= '$' . $name . ' = ' . var_export($GLOBALS[$name], true) . ";\n\n";
}
// 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();
}
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.
* 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 function createHttpStatusLink($statusCode, $statusDescription)
public static function convertExceptionToError($exception)
{
return '<a href="http://en.wikipedia.org/wiki/List_of_HTTP_status_codes#' . (int) $statusCode . '" target="_blank">HTTP ' . (int) $statusCode . ' &ndash; ' . $statusDescription . '</a>';
trigger_error(static::convertExceptionToString($exception), E_USER_ERROR);
}
/**
* 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.
* Converts an exception into a simple string.
* @param \Exception $exception the exception being converted
* @return string the string representation of the exception.
*/
public function createServerInformationLink()
public static function convertExceptionToString($exception)
{
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>';
}
}
}
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';
}
return '';
$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();
}
/**
* 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>';
return $message;
}
}
......@@ -188,9 +188,20 @@ class Application extends \yii\base\Application
*/
public function coreComponents()
{
return array_merge([
return array_merge(parent::coreComponents(), [
'request' => ['class' => 'yii\console\Request'],
'response' => ['class' => 'yii\console\Response'],
], parent::coreComponents());
]);
}
/**
* Registers the errorHandler component as a PHP error handler.
*/
protected function registerErrorHandler(&$config)
{
if (!isset($config['components']['errorHandler']['class'])) {
$config['components']['errorHandler']['class'] = 'yii\\console\\ErrorHandler';
}
parent::registerErrorHandler($config);
}
}
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\console;
use Yii;
use yii\base\ErrorException;
use yii\base\UserException;
use yii\helpers\Console;
/**
* ErrorHandler handles uncaught PHP errors and exceptions.
*
* ErrorHandler is configured as an application component in [[\yii\base\Application]] by default.
* You can access that instance via `Yii::$app->errorHandler`.
*
* @author Carsten Brandt <mail@cebe.cc>
* @since 2.0
*/
class ErrorHandler extends \yii\base\ErrorHandler
{
/**
* Renders an exception using ansi format for console output.
* @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;
}
}
\ No newline at end of file
......@@ -7,6 +7,7 @@
namespace yii\mail;
use yii\base\ErrorHandler;
use yii\base\Object;
use Yii;
......@@ -58,7 +59,7 @@ abstract class BaseMessage extends Object implements MessageInterface
try {
return $this->toString();
} catch (\Exception $e) {
trigger_error($e->getMessage() . "\n\n" . $e->getTraceAsString());
ErrorHandler::convertExceptionToError($e);
return '';
}
}
......
......@@ -146,11 +146,22 @@ class Application extends \yii\base\Application
*/
public function coreComponents()
{
return array_merge([
return array_merge(parent::coreComponents(), [
'request' => ['class' => 'yii\web\Request'],
'response' => ['class' => 'yii\web\Response'],
'session' => ['class' => 'yii\web\Session'],
'user' => ['class' => 'yii\web\User'],
], parent::coreComponents());
]);
}
/**
* Registers the errorHandler component as a PHP error handler.
*/
protected function registerErrorHandler(&$config)
{
if (!isset($config['components']['errorHandler']['class'])) {
$config['components']['errorHandler']['class'] = 'yii\\web\\ErrorHandler';
}
parent::registerErrorHandler($config);
}
}
......@@ -105,10 +105,9 @@ class Controller extends \yii\base\Controller
public function 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.'));
}
return true;
} else {
return false;
......
......@@ -182,10 +182,11 @@ class DbSession extends Session
->execute();
}
} catch (\Exception $e) {
if (YII_DEBUG) {
echo $e->getMessage();
}
// it is too late to log an error message here
$exception = ErrorHandler::convertExceptionToString($e);
// its too late to use Yii logging here
error_log($exception);
echo $exception;
return false;
}
......
......@@ -68,7 +68,7 @@ class ErrorAction extends Action
public function run()
{
if (($exception = Yii::$app->exception) === null) {
if (($exception = Yii::$app->errorHandler->exception) === null) {
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\Exception;
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 19.
*/
public $maxSourceLines = 19;
/**
* @var integer maximum number of trace source code lines to be displayed. Defaults to 13.
*/
public $maxTraceSourceLines = 13;
/**
* @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
* using `Yii::$app->errorHandler->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 rendered.
*/
protected function renderException($exception)
{
if (Yii::$app->has('response')) {
$response = Yii::$app->getResponse();
} else {
$response = new Response();
}
$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' || YII_ENV_TEST) {
// AJAX request
$response->data = '<pre>' . $this->htmlEncode($this->convertExceptionToString($exception)) . '</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 Exception || $exception instanceof ErrorException) ? $exception->getName() : 'Exception',
'message' => $exception->getMessage(),
'code' => $exception->getCode(),
];
if ($exception instanceof HttpException) {
$array['status'] = $exception->statusCode;
}
if (YII_DEBUG) {
$array['stack-trace'] = explode("\n", $exception->getTraceAsString());
}
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 || !Yii::$app->has('view')) {
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 == 1 ? $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()
{
$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>';
}
}
......@@ -8,6 +8,7 @@ namespace yii\widgets;
use Yii;
use yii\base\Component;
use yii\base\ErrorHandler;
use yii\helpers\ArrayHelper;
use yii\helpers\Html;
use yii\base\Model;
......@@ -140,8 +141,7 @@ class ActiveField extends Component
try {
return $this->render();
} catch (\Exception $e) {
trigger_error($e->getMessage() . "\n\n" . $e->getTraceAsString());
ErrorHandler::convertExceptionToError($e);
return '';
}
}
......
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