From 7c06cc03d7cb72d3d7666dd9a55e9302897a3dd3 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sat, 21 Apr 2012 16:33:54 -0400 Subject: [PATCH] finished error handler. --- framework/base/ErrorHandler.php | 489 +++++++++++++--------------------------- framework/base/View.php | 38 +--- framework/views/error.php | 71 ++++++ framework/views/exception.php | 212 +++++++++++++++++ 4 files changed, 455 insertions(+), 355 deletions(-) create mode 100644 framework/views/error.php create mode 100644 framework/views/exception.php diff --git a/framework/base/ErrorHandler.php b/framework/base/ErrorHandler.php index d5df70e..8c3cd61 100644 --- a/framework/base/ErrorHandler.php +++ b/framework/base/ErrorHandler.php @@ -47,6 +47,7 @@ namespace yii\base; * {@link CApplication::getErrorHandler()}. * * @property array $error The error details. Null if there is no error. + * @property string $versionInfo * * @author Qiang Xue * @since 2.0 @@ -62,11 +63,6 @@ class ErrorHandler extends ApplicationComponent * @var integer maximum number of trace source code lines to be displayed. Defaults to 10. */ public $maxTraceSourceLines = 10; - - /** - * @var string the application administrator information (could be a name or email link). It is displayed in error pages to end users. Defaults to 'the webmaster'. - */ - public $adminInfo = 'the webmaster'; /** * @var boolean whether to discard any existing page output before error display. Defaults to true. */ @@ -78,9 +74,12 @@ class ErrorHandler extends ApplicationComponent */ public $errorAction; - private $_error; + public $exceptionView = '@yii/views/exception.php'; + public $errorView = '@yii/views/error.php'; + /** + * @var \Exception the exception that is being handled currently + */ public $exception; - public $compactOutput; public function init() { @@ -88,36 +87,26 @@ class ErrorHandler extends ApplicationComponent set_error_handler(array($this, 'handleError'), error_reporting()); } - protected function logException($exception) - { - $category = get_class($exception); - if ($exception instanceof HttpException) { - $category .= '\\' . $exception->statusCode; - } elseif ($exception instanceof \ErrorException) { - $category .= '\\' . $exception->getSeverity(); - } - \Yii::error((string)$exception, $category); - } - - protected 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) { - @ob_end_clean(); - } - } - - protected function simple() + /** + * Handles PHP execution errors such as warnings, notices. + * + * This method is implemented as a PHP error handler. It requires + * that constant YII_ENABLE_ERROR_HANDLER be defined true. + * + * This method will first raise an `error` event. + * If the error is not handled by any event handler, it will call + * {@link getErrorHandler errorHandler} to process the error. + * + * The application will be terminated by this method. + * + * @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 + */ + public function handleError($code, $message, $file, $line) { - if (YII_DEBUG) { - echo '

' . get_class($exception) . "

\n"; - echo '

' . $exception->getMessage() . ' (' . $exception->getFile() . ':' . $exception->getLine() . ')

'; - echo '
' . $exception->getTraceAsString() . '
'; - } else - { - echo '

' . get_class($exception) . "

\n"; - echo '

' . $exception->getMessage() . '

'; - } + throw new \ErrorException($message, 0, $code, $file, $line); } /** @@ -125,263 +114,50 @@ class ErrorHandler extends ApplicationComponent */ public function handleException($exception) { - $this->exception = $exception; - - // disable error capturing to avoid recursive errors + // disable error capturing to avoid recursive errors while handling exceptions restore_error_handler(); restore_exception_handler(); + $this->exception = $exception; $this->logException($exception); + if ($this->discardExistingOutput) { $this->clearOutput(); } - if ($this->compactOutput === null) { - // not in Web application, or not in AJAX request - $this->compactOutput = !(\Yii::$application instanceof \yii\web\Application) || isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH']==='XMLHttpRequest'; - } - - if ($this->compactOutput) { - $this->simple(); - return; - } - - if (($trace = $this->getExactTrace($exception)) === null) { - $fileName = $exception->getFile(); - $errorLine = $exception->getLine(); - } else { - $fileName = $trace['file']; - $errorLine = $trace['line']; - } - - $trace = $exception->getTrace(); - foreach ($trace as $i => $t) { - if (!isset($t['file'])) { - $trace[$i]['file'] = 'unknown'; - } - if (!isset($t['line'])) { - $trace[$i]['line'] = 0; - } - if (!isset($t['function'])) { - $trace[$i]['function'] = 'unknown'; - } - unset($trace[$i]['object']); - } - - $this->_error = $data = array( - 'code' => $exception instanceof HttpException ? $exception->statusCode : 500, - 'type' => get_class($exception), - 'errorCode' => $exception->getCode(), - 'message' => $exception->getMessage(), - 'file' => $fileName, - 'line' => $errorLine, - 'trace' => $exception->getTraceAsString(), - 'traces' => $trace, - ); - - if (!headers_sent()) { - header("HTTP/1.0 {$data['code']} " . get_class($exception)); - } - - if ($exception instanceof HttpException || !YII_DEBUG) { - $this->render('error', $data); - } else { - $this->render('exception', $data); - } - } - - /** - * Returns the details about the error that is currently being handled. - * The error is returned in terms of an array, with the following information: - * - * @return array the error details. Null if there is no error. - */ - public function getError() - { - return $this->_error; - } - - /** - * Handles the PHP error. - * @param CErrorEvent $event the PHP error event - */ - protected function handleError($event) - { - $trace = debug_backtrace(); - // skip the first 3 stacks as they do not tell the error position - if (count($trace) > 3) - $trace = array_slice($trace, 3); - $traceString = ''; - foreach ($trace as $i => $t) - { - if (!isset($t['file'])) - $trace[$i]['file'] = 'unknown'; - - if (!isset($t['line'])) - $trace[$i]['line'] = 0; - - if (!isset($t['function'])) - $trace[$i]['function'] = 'unknown'; - - $traceString .= "#$i {$trace[$i]['file']}({$trace[$i]['line']}): "; - if (isset($t['object']) && is_object($t['object'])) - $traceString .= get_class($t['object']) . '->'; - $traceString .= "{$trace[$i]['function']}()\n"; - - unset($trace[$i]['object']); - } - - $app = Yii::app(); - if ($app instanceof CWebApplication) { - switch ($event->code) - { - case E_WARNING: - $type = 'PHP warning'; - break; - case E_NOTICE: - $type = 'PHP notice'; - break; - case E_USER_ERROR: - $type = 'User error'; - break; - case E_USER_WARNING: - $type = 'User warning'; - break; - case E_USER_NOTICE: - $type = 'User notice'; - break; - case E_RECOVERABLE_ERROR: - $type = 'Recoverable error'; - break; - default: - $type = 'PHP error'; - } - $this->_error = $data = array( - 'code' => 500, - 'type' => $type, - 'message' => $event->message, - 'file' => $event->file, - 'line' => $event->line, - 'trace' => $traceString, - 'traces' => $trace, - ); - if (!headers_sent()) - header("HTTP/1.0 500 PHP Error"); - if ($this->isAjaxRequest()) - $app->displayError($event->code, $event->message, $event->file, $event->line); - else if (YII_DEBUG) - $this->render('exception', $data); - else - $this->render('error', $data); - } - else - $app->displayError($event->code, $event->message, $event->file, $event->line); - } - - /** - * Returns the exact trace where the problem occurs. - * @param \Exception $exception the uncaught exception - * @return array the exact trace where the problem occurs - */ - protected function getExactTrace($exception) - { - $traces = $exception->getTrace(); - foreach ($traces as $trace) { - // property access exception - if (isset($trace['function']) && ($trace['function'] === '__get' || $trace['function'] === '__set')) { - return $trace; - } - } - return null; - } - - /** - * Renders the view. - * @param string $view the view name (file name without extension). - * See {@link getViewFile} for how a view file is located given its name. - * @param array $data data to be passed to the view - */ - protected function render($view, $data) - { - if ($view === 'error' && $this->errorAction !== null) - Yii::app()->runController($this->errorAction); - else - { - // additional information to be passed to view - $data['version'] = $this->getVersionInfo(); - $data['time'] = time(); - $data['admin'] = $this->adminInfo; - include($this->getViewFile($view, $data['code'])); - } + $this->render($exception); } - /** - * Determines which view file should be used. - * @param string $view view name (either 'exception' or 'error') - * @param integer $code HTTP status code - * @return string view file path - */ - protected function getViewFile($view, $code) + protected function render($exception) { - $viewPaths = array( - Yii::app()->getTheme() === null ? null : Yii::app()->getTheme()->getSystemViewPath(), - Yii::app() instanceof CWebApplication ? Yii::app()->getSystemViewPath() : null, - YII_PATH . DIRECTORY_SEPARATOR . 'views', - ); - - foreach ($viewPaths as $i => $viewPath) - { - if ($viewPath !== null) { - $viewFile = $this->getViewFileInternal($viewPath, $view, $code, $i === 2 ? 'en_us' : null); - if (is_file($viewFile)) - return $viewFile; + if (\Yii::$application instanceof \yii\web\Application) { + if ($this->errorAction !== null) { + \Yii::$application->runController($this->errorAction); + } else { + if (!headers_sent()) { + $errorCode = $exception instanceof HttpException ? $exception->statusCode : 500; + header("HTTP/1.0 $errorCode " . get_class($exception)); + } + if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest') { + $this->renderAsText($exception); + } else { + $this->renderAsHtml($exception); + } } + } else { + $this->renderAsText($exception); } } /** - * Looks for the view under the specified directory. - * @param string $viewPath the directory containing the views - * @param string $view view name (either 'exception' or 'error') - * @param integer $code HTTP status code - * @param string $srcLanguage the language that the view file is in - * @return string view file path - */ - protected function getViewFileInternal($viewPath, $view, $code, $srcLanguage = null) - { - $app = Yii::app(); - if ($view === 'error') { - $viewFile = $app->findLocalizedFile($viewPath . DIRECTORY_SEPARATOR . "error{$code}.php", $srcLanguage); - if (!is_file($viewFile)) - $viewFile = $app->findLocalizedFile($viewPath . DIRECTORY_SEPARATOR . 'error.php', $srcLanguage); - } - else - $viewFile = $viewPath . DIRECTORY_SEPARATOR . "exception.php"; - return $viewFile; - } - - /** - * Returns server version information. - * If the application is in production mode, empty string is returned. - * @return string server version information. Empty if in production mode. + * Returns server and Yii version information. + * @return string server version information. */ - protected function getVersionInfo() + public function getVersionInfo() { - if (YII_DEBUG) { - $version = 'Yii Framework/' . \Yii::getVersion(); - if (isset($_SERVER['SERVER_SOFTWARE'])) { - $version = $_SERVER['SERVER_SOFTWARE'] . ' ' . $version; - } - } else { - $version = ''; + $version = 'Yii Framework/' . \Yii::getVersion(); + if (isset($_SERVER['SERVER_SOFTWARE'])) { + $version = $_SERVER['SERVER_SOFTWARE'] . ' ' . $version; } return $version; } @@ -392,50 +168,46 @@ class ErrorHandler extends ApplicationComponent * @param array $args arguments array to be converted * @return string string representation of the arguments array */ - protected function argumentsToString($args) + public function argumentsToString($args) { - $count = 0; - $isAssoc = $args !== array_values($args); - - foreach ($args as $key => $value) - { + $count = 0; + foreach ($args as $key => $value) { $count++; if ($count >= 5) { - if ($count > 5) + if ($count > 5) { unset($args[$key]); - else + } else { $args[$key] = '...'; + } continue; } - if (is_object($value)) + if (is_object($value)) { $args[$key] = get_class($value); - else if (is_bool($value)) + } elseif (is_bool($value)) { $args[$key] = $value ? 'true' : 'false'; - else if (is_string($value)) { - if (strlen($value) > 64) + } elseif (is_string($value)) { + if (strlen($value) > 64) { $args[$key] = '"' . substr($value, 0, 64) . '..."'; - else + } else { $args[$key] = '"' . $value . '"'; - } - else if (is_array($value)) + } + } elseif (is_array($value)) { $args[$key] = 'array(' . $this->argumentsToString($value) . ')'; - else if ($value === null) + } elseif ($value === null) { $args[$key] = 'null'; - else if (is_resource($value)) + } elseif (is_resource($value)) { $args[$key] = 'resource'; + } if (is_string($key)) { $args[$key] = '"' . $key . '" => ' . $args[$key]; - } - else if ($isAssoc) { + } elseif ($isAssoc) { $args[$key] = $key . ' => ' . $args[$key]; } } - $out = implode(", ", $args); - - return $out; + return implode(', ', $args); } /** @@ -443,11 +215,10 @@ class ErrorHandler extends ApplicationComponent * @param array $trace the trace data * @return boolean whether the call stack is from application code. */ - protected function isCoreCode($trace) + public function isCoreCode($trace) { if (isset($trace['file'])) { - $systemPath = realpath(dirname(__FILE__) . '/..'); - return $trace['file'] === 'unknown' || strpos(realpath($trace['file']), $systemPath . DIRECTORY_SEPARATOR) === 0; + return $trace['file'] === 'unknown' || strpos(realpath($trace['file']), YII_PATH . DIRECTORY_SEPARATOR) === 0; } return false; } @@ -457,13 +228,13 @@ class ErrorHandler extends ApplicationComponent * @param string $file source file path * @param integer $errorLine the error line number * @param integer $maxLines maximum number of lines to display - * @return string the rendering result */ - protected function renderSourceCode($file, $errorLine, $maxLines) + public function renderSourceCode($file, $errorLine, $maxLines) { $errorLine--; // adjust line number to 0-based from 1-based - if ($errorLine < 0 || ($lines = @file($file)) === false || ($lineCount = count($lines)) <= $errorLine) - return ''; + if ($errorLine < 0 || ($lines = @file($file)) === false || ($lineCount = count($lines)) <= $errorLine) { + return; + } $halfLines = (int)($maxLines / 2); $beginLine = $errorLine - $halfLines > 0 ? $errorLine - $halfLines : 0; @@ -471,37 +242,101 @@ class ErrorHandler extends ApplicationComponent $lineNumberWidth = strlen($endLine + 1); $output = ''; - for ($i = $beginLine; $i <= $endLine; ++$i) - { + for ($i = $beginLine; $i <= $endLine; ++$i) { $isErrorLine = $i === $errorLine; - $code = sprintf("%0{$lineNumberWidth}d %s", $i + 1, CHtml::encode(str_replace("\t", ' ', $lines[$i]))); - if (!$isErrorLine) + $code = sprintf("%0{$lineNumberWidth}d %s", $i + 1, $this->htmlEncode(str_replace("\t", ' ', $lines[$i]))); + if (!$isErrorLine) { $output .= $code; - else + } else { $output .= '' . $code . ''; + } + } + echo '
' . $output . '
'; + } + + public function renderTrace($trace) + { + $count = 0; + echo "\n"; + foreach ($trace as $n => $t) { + if ($this->isCoreCode($t)) { + $cssClass = 'core collapsed'; + } elseif (++$count > 3) { + $cssClass = 'app collapsed'; + } else { + $cssClass = 'app expanded'; + } + $hasCode = $t['file'] !== 'unknown' && is_file($t['file']); + echo "\n"; + } + echo '
#$n"; + echo '
'; + if ($hasCode) { + echo '
+
-
'; + } + echo ' '; + echo $this->htmlEncode($t['file']) . '(' . $t['line'] . '): '; + if (!empty($t['class'])) { + echo '' . $t['class'] . '' . $t['type']; + } + echo '' . $t['function'] . ''; + echo '(' . (empty($t['args']) ? '' : $this->htmlEncode($this->argumentsToString($t['args']))) . ')'; + echo '
'; + if ($hasCode) { + $this->renderSourceCode($t['file'], $t['line'], $this->maxTraceSourceLines); + } + echo "
'; + } + + public function htmlEncode($text) + { + return htmlspecialchars($text, ENT_QUOTES, \Yii::$application->charset); + } + + public function logException($exception) + { + $category = get_class($exception); + if ($exception instanceof HttpException) { + $category .= '\\' . $exception->statusCode; + } elseif ($exception instanceof \ErrorException) { + $category .= '\\' . $exception->getSeverity(); + } + \Yii::error((string)$exception, $category); + } + + 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) { + @ob_end_clean(); } - return '
' . $output . '
'; } /** - * Handles PHP execution errors such as warnings, notices. - * - * This method is implemented as a PHP error handler. It requires - * that constant YII_ENABLE_ERROR_HANDLER be defined true. - * - * This method will first raise an `error` event. - * If the error is not handled by any event handler, it will call - * {@link getErrorHandler errorHandler} to process the error. - * - * The application will be terminated by this method. - * - * @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 + * @param \Exception $exception */ - public function handleError($code, $message, $file, $line) + public function renderAsText($exception) { - throw new \ErrorException($message, 0, $code, $file, $line); + if (YII_DEBUG) { + echo get_class($exception) . "\n"; + echo $exception->getMessage() . ' (' . $exception->getFile() . ':' . $exception->getLine() . ")\n"; + echo $exception->getTraceAsString(); + } else { + echo get_class($exception) . "\n"; + echo $exception->getMessage(); + } + } + + /** + * @param \Exception $exception + */ + public function renderAsHtml($exception) + { + $view = new View; + $view->owner = $this; + $name = !YII_DEBUG || $exception instanceof HttpException ? $this->errorView : $this->exceptionView; + $view->render($name, array( + 'exception' => $exception, + )); } } diff --git a/framework/base/View.php b/framework/base/View.php index 122a12d..45f3da5 100644 --- a/framework/base/View.php +++ b/framework/base/View.php @@ -64,7 +64,7 @@ class View extends Component { $file = $this->findViewFile($view); if ($file !== false) { - return $this->renderFile($file, $params); + $this->renderFile($file, $params); } else { throw new Exception("Unable to find the view file for view '$view'."); } @@ -72,45 +72,30 @@ class View extends Component public function renderFile($file, $params = array()) { - return $this->renderFileInternal($file, $params); + $this->renderFileInternal($file, $params); } - public function widget($class, $properties = array(), $returnOutput = false) + public function widget($class, $properties = array()) { - if ($returnOutput) { - ob_start(); - ob_implicit_flush(false); - $widget = $this->createWidget($class, $properties); - $widget->run(); - return ob_get_clean(); - } else { - $widget = $this->createWidget($class, $properties); - $widget->run(); - return $widget; - } + $widget = $this->createWidget($class, $properties); + $widget->run(); + return $widget; } private $_widgetStack = array(); public function beginWidget($class, $properties = array()) { - ob_start(); - ob_implicit_flush(false); $widget = $this->createWidget($class, $properties); $this->_widgetStack[] = $widget; return $widget; } - public function endWidget($returnOutput = false) + public function endWidget() { if (($widget = array_pop($this->_widgetStack)) !== null) { $widget->run(); - if ($returnOutput) { - return ob_get_clean(); - } else { - ob_end_clean(); - return $widget; - } + return $widget; } else { throw new Exception("Unmatched beginWidget() and endWidget() calls."); } @@ -153,7 +138,7 @@ class View extends Component { $this->endWidget(); } - + /** * Begins fragment caching. * This method will display cached content if it is available. @@ -184,7 +169,7 @@ class View extends Component return true; } } - + /** * Ends fragment caching. * This is an alias to [[endWidget()]] @@ -228,10 +213,7 @@ class View extends Component protected function renderFileInternal($_file_, $_params_ = array()) { extract($_params_, EXTR_OVERWRITE); - ob_start(); - ob_implicit_flush(false); require($_file_); - return ob_get_clean(); } public function findViewFile($view) diff --git a/framework/views/error.php b/framework/views/error.php new file mode 100644 index 0000000..05b7a79 --- /dev/null +++ b/framework/views/error.php @@ -0,0 +1,71 @@ +owner; +?> + + + + + <?php echo get_class($exception); ?> + + + + + +

+

htmlEncode($exception->getMessage()))?>

+

+ The above error occurred while the Web server was processing your request. +

+

+ If you think this is a server error, please contact us. +

+

+ Thank you. +

+
+ + versionInfo : ''; ?> +
+ + \ No newline at end of file diff --git a/framework/views/exception.php b/framework/views/exception.php new file mode 100644 index 0000000..3f258f5 --- /dev/null +++ b/framework/views/exception.php @@ -0,0 +1,212 @@ +owner; +?> + + + + + <?php echo get_class($exception); ?> + + + + +
+

+ +

+ htmlEncode($exception->getMessage()))?> +

+ +
+

+ htmlEncode($exception->getFile()) . '(' . $exception->getLine() . ')'; ?> +

+ renderSourceCode($exception->getFile(), $exception->getLine(), $owner->maxSourceLines); ?> +
+ + +
+

Stack Trace

+ renderTrace($exception->getTrace()); ?> +
+ + +
+ + versionInfo : ''; ?> +
+
+ + + + +