Browse Source

Merge pull request #408 from yiisoft/error-page

New error/exception page implemented
tags/2.0.0-beta
Qiang Xue 12 years ago
parent
commit
01f74b3cab
  1. 273
      framework/yii/base/ErrorHandler.php
  2. 67
      framework/yii/views/error.php
  3. 34
      framework/yii/views/errorHandler/callStackItem.php
  4. 416
      framework/yii/views/errorHandler/main.php
  5. 210
      framework/yii/views/exception.php

273
framework/yii/base/ErrorHandler.php

@ -7,6 +7,8 @@
namespace yii\base; namespace yii\base;
use Yii;
/** /**
* ErrorHandler handles uncaught PHP errors and exceptions. * ErrorHandler handles uncaught PHP errors and exceptions.
* *
@ -14,6 +16,7 @@ namespace yii\base;
* nature of the errors and the mode the application runs at. * nature of the errors and the mode the application runs at.
* *
* @author Qiang Xue <qiang.xue@gmail.com> * @author Qiang Xue <qiang.xue@gmail.com>
* @author Timur Ruziev <resurtm@gmail.com>
* @since 2.0 * @since 2.0
*/ */
class ErrorHandler extends Component class ErrorHandler extends Component
@ -31,49 +34,50 @@ class ErrorHandler extends Component
*/ */
public $discardExistingOutput = true; public $discardExistingOutput = true;
/** /**
* @var string the route (eg 'site/error') to the controller action that will be used to display external errors. * @var string the route (e.g. 'site/error') to the controller action that will be used
* Inside the action, it can retrieve the error information by \Yii::$app->errorHandler->error. * to display external errors. Inside the action, it can retrieve the error information
* This property defaults to null, meaning ErrorHandler will handle the error display. * by Yii::$app->errorHandler->error. This property defaults to null, meaning ErrorHandler
* will handle the error display.
*/ */
public $errorAction; public $errorAction;
/** /**
* @var string the path of the view file for rendering exceptions * @var string the path of the view file for rendering exceptions and errors.
*/ */
public $exceptionView = '@yii/views/exception.php'; public $mainView = '@yii/views/errorHandler/main.php';
/** /**
* @var string the path of the view file for rendering errors * @var string the path of the view file for rendering exceptions and errors call stack element.
*/ */
public $errorView = '@yii/views/error.php'; public $callStackItemView = '@yii/views/errorHandler/callStackItem.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 * Handles exception.
* @param \Exception $exception * @param \Exception $exception to be handled.
*/ */
public function handle($exception) public function handle($exception)
{ {
$this->exception = $exception; $this->exception = $exception;
if ($this->discardExistingOutput) { if ($this->discardExistingOutput) {
$this->clearOutput(); $this->clearOutput();
} }
$this->renderException($exception); $this->renderException($exception);
} }
/** /**
* Renders exception * Renders exception.
* @param \Exception $exception * @param \Exception $exception to be handled.
*/ */
protected function renderException($exception) protected function renderException($exception)
{ {
if ($this->errorAction !== null) { if ($this->errorAction !== null) {
\Yii::$app->runAction($this->errorAction); Yii::$app->runAction($this->errorAction);
} elseif (\Yii::$app instanceof \yii\web\Application) { } elseif (!(Yii::$app instanceof \yii\web\Application)) {
Yii::$app->renderException($exception);
} else {
if (!headers_sent()) { if (!headers_sent()) {
if ($exception instanceof HttpException) { if ($exception instanceof HttpException) {
header('HTTP/1.0 ' . $exception->statusCode . ' ' . $exception->getName()); header('HTTP/1.0 ' . $exception->statusCode . ' ' . $exception->getName());
@ -82,7 +86,7 @@ class ErrorHandler extends Component
} }
} }
if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest') { if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest') {
\Yii::$app->renderException($exception); Yii::$app->renderException($exception);
} else { } else {
// if there is an error during error rendering it's useful to // if there is an error during error rendering it's useful to
// display PHP error in debug mode instead of a blank screen // display PHP error in debug mode instead of a blank screen
@ -90,194 +94,145 @@ class ErrorHandler extends Component
ini_set('display_errors', 1); ini_set('display_errors', 1);
} }
$view = new View; $view = new View();
if (!YII_DEBUG || $exception instanceof UserException) { $request = '';
$viewName = $this->errorView; foreach (array('GET', 'POST', 'SERVER', 'FILES', 'COOKIE', 'SESSION', 'ENV') as $name) {
} else { if (!empty($GLOBALS['_' . $name])) {
$viewName = $this->exceptionView; $request .= '$_' . $name . ' = ' . var_export($GLOBALS['_' . $name], true) . ";\n\n";
}
} }
echo $view->renderFile($viewName, array( $request = rtrim($request, "\n\n");
echo $view->renderFile($this->mainView, array(
'exception' => $exception, 'exception' => $exception,
'request' => $request,
), $this); ), $this);
} }
} else {
\Yii::$app->renderException($exception);
} }
} }
/** /**
* Returns server and Yii version information. * Converts special characters to HTML entities.
* @return string server version information. * @param string $text to encode.
* @return string encoded original text.
*/ */
public function getVersionInfo() public function htmlEncode($text)
{ {
$version = '<a href="http://www.yiiframework.com/">Yii Framework</a>/' . \Yii::getVersion(); return htmlspecialchars($text, ENT_QUOTES, Yii::$app->charset);
if (isset($_SERVER['SERVER_SOFTWARE'])) {
$version = $_SERVER['SERVER_SOFTWARE'] . ' ' . $version;
}
return $version;
} }
/** /**
* Converts arguments array to its string representation * Removes all output echoed before calling this method.
*
* @param array $args arguments array to be converted
* @return string string representation of the arguments array
*/ */
public function argumentsToString($args) public function clearOutput()
{ {
$isAssoc = $args !== array_values($args); // the following manual level counting is to deal with zlib.output_compression set to On
$count = 0; for ($level = ob_get_level(); $level > 0; --$level) {
foreach ($args as $key => $value) { @ob_end_clean();
$count++;
if ($count >= 5) {
if ($count > 5) {
unset($args[$key]);
} else {
$args[$key] = '...';
}
continue;
}
if (is_object($value)) {
$args[$key] = get_class($value);
} elseif (is_bool($value)) {
$args[$key] = $value ? 'true' : 'false';
} elseif (is_string($value)) {
if (strlen($value) > 64) {
$args[$key] = '"' . substr($value, 0, 64) . '..."';
} else {
$args[$key] = '"' . $value . '"';
} }
} elseif (is_array($value)) {
$args[$key] = 'array(' . $this->argumentsToString($value) . ')';
} elseif ($value === null) {
$args[$key] = 'null';
} elseif (is_resource($value)) {
$args[$key] = 'resource';
} }
if (is_string($key)) { /**
$args[$key] = '"' . $key . '" => ' . $args[$key]; * Adds informational links to the given PHP type/class.
} elseif ($isAssoc) { * @param string $code type/class name to be linkified.
$args[$key] = $key . ' => ' . $args[$key]; * @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, '\\');
} }
return implode(', ', $args); return $html;
} }
/** /**
* Returns a value indicating whether the call stack is from application code. * Creates HTML containing link to the page with the information on given HTTP status code.
* @param array $trace the trace data * @param integer $statusCode to be used to generate information link.
* @return boolean whether the call stack is from application code. * @return string generated HTML with HTTP status code information.
*/ */
public function isCoreCode($trace) public function createHttpStatusLink($statusCode)
{ {
if (isset($trace['file'])) { return '<a href="http://en.wikipedia.org/wiki/List_of_HTTP_status_codes#' . (int)$statusCode .'" target="_blank">' . (int)$statusCode . '</a>';
return $trace['file'] === 'unknown' || strpos(realpath($trace['file']), YII_PATH . DIRECTORY_SEPARATOR) === 0;
}
return false;
} }
/** /**
* Renders the source code around the error line. * Renders a single call stack element.
* @param string $file source file path * @param string $file name where call has happened.
* @param integer $errorLine the error line number * @param integer $line number on which call has happened.
* @param integer $maxLines maximum number of lines to display * @param integer $index number of the call stack element.
* @return string HTML content of the rendered call stack element.
*/ */
public function renderSourceCode($file, $errorLine, $maxLines) public function renderCallStackItem($file, $line, $index)
{ {
$errorLine--; // adjust line number to 0-based from 1-based $line--; // adjust line number from one-based to zero-based
if ($errorLine < 0 || ($lines = @file($file)) === false || ($lineCount = count($lines)) <= $errorLine) { $lines = @file($file);
return; if ($line < 0 || $lines === false || ($lineCount = count($lines)) < $line + 1) {
} return '';
}
$halfLines = (int)($maxLines / 2);
$beginLine = $errorLine - $halfLines > 0 ? $errorLine - $halfLines : 0; $half = (int)(($index == 0 ? $this->maxSourceLines : $this->maxTraceSourceLines) / 2);
$endLine = $errorLine + $halfLines < $lineCount ? $errorLine + $halfLines : $lineCount - 1; $begin = $line - $half > 0 ? $line - $half : 0;
$lineNumberWidth = strlen($endLine + 1); $end = $line + $half < $lineCount ? $line + $half : $lineCount - 1;
$output = ''; $view = new View();
for ($i = $beginLine; $i <= $endLine; ++$i) { return $view->renderFile($this->callStackItemView, array(
$isErrorLine = $i === $errorLine; 'file' => $file,
$code = sprintf("<span class=\"ln" . ($isErrorLine ? ' error-ln' : '') . "\">%0{$lineNumberWidth}d</span> %s", $i + 1, $this->htmlEncode(str_replace("\t", ' ', $lines[$i]))); 'line' => $line,
if (!$isErrorLine) { 'index' => $index,
$output .= $code; 'lines' => $lines,
} else { 'begin' => $begin,
$output .= '<span class="error">' . $code . '</span>'; 'end' => $end,
} ), $this);
}
echo '<div class="code"><pre>' . $output . '</pre></div>';
} }
/** /**
* Renders calls stack trace * Determines whether given name of the file belongs to the framework.
* @param array $trace * @param string $file name to be checked.
* @return boolean whether given name of the file belongs to the framework.
*/ */
public function renderTrace($trace) public function isCoreFile($file)
{ {
$count = 0; return $file === 'unknown' || strpos(realpath($file), YII_PATH . DIRECTORY_SEPARATOR) === 0;
echo "<table>\n";
foreach ($trace as $n => $t) {
if ($this->isCoreCode($t)) {
$cssClass = 'core collapsed';
} elseif (++$count > 3) {
$cssClass = 'app collapsed';
} else {
$cssClass = 'app expanded';
}
$hasCode = isset($t['file']) && $t['file'] !== 'unknown' && is_file($t['file']);
echo "<tr class=\"trace $cssClass\"><td class=\"number\">#$n</td><td class=\"content\">";
echo '<div class="trace-file">';
if ($hasCode) {
echo '<div class="plus">+</div><div class="minus">-</div>';
}
echo '&nbsp;';
if (isset($t['file'])) {
echo $this->htmlEncode($t['file']) . '(' . $t['line'] . '): ';
}
if (!empty($t['class'])) {
echo '<strong>' . $t['class'] . '</strong>' . $t['type'];
}
echo '<strong>' . $t['function'] . '</strong>';
echo '(' . (empty($t['args']) ? '' : $this->htmlEncode($this->argumentsToString($t['args']))) . ')';
echo '</div>';
if ($hasCode) {
$this->renderSourceCode($t['file'], $t['line'], $this->maxTraceSourceLines);
}
echo "</td></tr>\n";
}
echo '</table>';
} }
/** /**
* Converts special characters to HTML entities * Creates string containing HTML link which refers to the home page of determined web-server software
* @param string $text text to encode * and its full name.
* @return string * @return string server software information hyperlink.
*/ */
public function htmlEncode($text) public function createServerInformationLink()
{ {
return htmlspecialchars($text, ENT_QUOTES, \Yii::$app->charset); static $serverUrls = array(
'http://httpd.apache.org/' => array('apache'),
'http://nginx.org/' => array('nginx'),
'http://lighttpd.net/' => array('lighttpd'),
'http://gwan.com/' => array('g-wan', 'gwan'),
'http://iis.net/' => array('iis', 'services'),
'http://php.net/manual/en/features.commandline.webserver.php' => array('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>';
}
} }
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 '';
}
/** /**
* @param \Exception $exception * 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 renderAsHtml($exception) public function createFrameworkVersionLink()
{ {
$view = new View; return '<a href="http://github.com/yiisoft/yii2/" target="_blank">' . $this->htmlEncode(Yii::getVersion()) . '</a>';
$name = !YII_DEBUG || $exception instanceof HttpException ? $this->errorView : $this->exceptionView;
echo $view->renderFile($name, array(
'exception' => $exception,
), $this);
} }
} }

67
framework/yii/views/error.php

@ -1,67 +0,0 @@
<?php
/**
* @var \Exception $exception
* @var \yii\base\ErrorHandler $context
*/
$context = $this->context;
$title = $context->htmlEncode($exception instanceof \yii\base\Exception ? $exception->getName() : get_class($exception));
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title><?php echo $title?></title>
<style>
body {
font: normal 9pt "Verdana";
color: #000;
background: #fff;
}
h1 {
font: normal 18pt "Verdana";
color: #f00;
margin-bottom: .5em;
}
h2 {
font: normal 14pt "Verdana";
color: #800000;
margin-bottom: .5em;
}
h3 {
font: bold 11pt "Verdana";
}
p {
font: normal 9pt "Verdana";
color: #000;
}
.version {
color: gray;
font-size: 8pt;
border-top: 1px solid #aaa;
padding-top: 1em;
margin-bottom: 1em;
}
</style>
</head>
<body>
<h1><?php echo $title?></h1>
<h2><?php echo nl2br($context->htmlEncode($exception->getMessage()))?></h2>
<p>
The above error occurred while the Web server was processing your request.
</p>
<p>
Please contact us if you think this is a server error. Thank you.
</p>
<div class="version">
<?php echo date('Y-m-d H:i:s', time())?>
<?php echo YII_DEBUG ? $context->versionInfo : ''?>
</div>
</body>
</html>

34
framework/yii/views/errorHandler/callStackItem.php

@ -0,0 +1,34 @@
<?php
/**
* @var \yii\base\View $this
* @var string $file
* @var integer $line
* @var integer $index
* @var string[] $lines
* @var integer $begin
* @var integer $end
* @var \yii\base\ErrorHandler $context
*/
$context = $this->context;
?>
<li class="<?php if (!$context->isCoreFile($file)) echo 'application'; ?> call-stack-item">
<div class="element-wrap">
<div class="element">
<span class="number"><?php echo (int)$index; ?>.</span>
<span class="text">in <?php echo $context->htmlEncode($file); ?></span>
<span class="at">at line</span>
<span class="line"><?php echo (int)$line; ?></span>
</div>
</div>
<div class="code-wrap">
<div class="error-line" style="top: <?php echo 18 * (int)($line - $begin); ?>px;"></div>
<?php for ($i = $begin; $i <= $end; ++$i): ?>
<div class="hover-line" style="top: <?php echo 18 * (int)($i - $begin); ?>px;"></div>
<?php endfor; ?>
<div class="code">
<span class="lines"><?php for ($i = $begin; $i <= $end; ++$i) echo (int)$i . '<br/>'; ?></span>
<pre><?php for ($i = $begin; $i <= $end; ++$i) echo $context->htmlEncode($lines[$i]); ?></pre>
</div>
</div>
</li>

416
framework/yii/views/errorHandler/main.php

File diff suppressed because one or more lines are too long

210
framework/yii/views/exception.php

@ -1,210 +0,0 @@
<?php
/**
* @var \Exception $exception
* @var \yii\base\ErrorHandler $context
*/
$context = $this->context;
$title = $context->htmlEncode($exception instanceof \yii\base\Exception ? $exception->getName().' ('.get_class($exception).')' : get_class($exception));
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title><?php echo $title?></title>
<style>
html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,font,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td{border:0;outline:0;font-size:100%;vertical-align:baseline;background:transparent;margin:0;padding:0;}
body{line-height:1;}
ol,ul{list-style:none;}
blockquote,q{quotes:none;}
blockquote:before,blockquote:after,q:before,q:after{content:none;}
:focus{outline:0;}
ins{text-decoration:none;}
del{text-decoration:line-through;}
table{border-collapse:collapse;border-spacing:0;}
body {
font: normal 9pt "Verdana";
color: #000;
background: #fff;
}
h1 {
font: normal 18pt "Verdana";
color: #f00;
margin-bottom: .5em;
}
h2 {
font: normal 14pt "Verdana";
color: #800000;
margin-bottom: .5em;
}
h3 {
font: bold 11pt "Verdana";
}
pre {
font: normal 11pt Menlo, Consolas, "Lucida Console", Monospace;
}
pre span.error {
display: block;
background: #fce3e3;
}
pre span.ln {
color: #999;
padding-right: 0.5em;
border-right: 1px solid #ccc;
}
pre span.error-ln {
font-weight: bold;
}
.container {
margin: 1em 4em;
}
.version {
color: gray;
font-size: 8pt;
border-top: 1px solid #aaa;
padding-top: 1em;
margin-bottom: 1em;
}
.message {
color: #000;
padding: 1em;
font-size: 11pt;
background: #f3f3f3;
-webkit-border-radius: 10px;
-moz-border-radius: 10px;
border-radius: 10px;
margin-bottom: 1em;
line-height: 160%;
}
.source {
margin-bottom: 1em;
}
.code pre {
background-color: #ffe;
margin: 0.5em 0;
padding: 0.5em;
line-height: 125%;
border: 1px solid #eee;
}
.source .file {
margin-bottom: 1em;
font-weight: bold;
}
.traces {
margin: 2em 0;
}
.trace {
margin: 0.5em 0;
padding: 0.5em;
}
.trace.app {
border: 1px dashed #c00;
}
.trace .number {
text-align: right;
width: 2em;
padding: 0.5em;
}
.trace .content {
padding: 0.5em;
}
.trace .plus,
.trace .minus {
display: inline;
vertical-align: middle;
text-align: center;
border: 1px solid #000;
color: #000;
font-size: 10px;
line-height: 10px;
margin: 0;
padding: 0 1px;
width: 10px;
height: 10px;
}
.trace.collapsed .minus,
.trace.expanded .plus,
.trace.collapsed pre {
display: none;
}
.trace-file {
cursor: pointer;
padding: 0.2em;
}
.trace-file:hover {
background: #f0ffff;
}
</style>
</head>
<body>
<div class="container">
<h1><?php echo $title?></h1>
<p class="message">
<?php echo nl2br($context->htmlEncode($exception->getMessage()))?>
</p>
<div class="source">
<p class="file">
<?php echo $context->htmlEncode($exception->getFile()) . '(' . $exception->getLine() . ')'?>
</p>
<?php if (YII_DEBUG) $context->renderSourceCode($exception->getFile(), $exception->getLine(), $context->maxSourceLines)?>
</div>
<?php if (YII_DEBUG):?>
<div class="traces">
<h2>Stack Trace</h2>
<?php $context->renderTrace($exception->getTrace())?>
</div>
<?php endif?>
<div class="version">
<?php echo date('Y-m-d H:i:s', time())?>
<?php echo YII_DEBUG ? $context->getVersionInfo() : ''?>
</div>
</div>
<script>
var traceReg = new RegExp("(^|\\s)trace-file(\\s|$)");
var collapsedReg = new RegExp("(^|\\s)collapsed(\\s|$)");
var e = document.getElementsByTagName('div');
for (var j = 0, len = e.length; j < len; j++) {
if (traceReg.test(e[j].className)) {
e[j].onclick = function() {
var trace = this.parentNode.parentNode;
if (collapsedReg.test(trace.className)) {
trace.className = trace.className.replace('collapsed', 'expanded');
} else {
trace.className = trace.className.replace('expanded', 'collapsed');
}
}
}
}
</script>
</body>
</html>
Loading…
Cancel
Save