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;
use Yii;
/**
* 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.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @author Timur Ruziev <resurtm@gmail.com>
* @since 2.0
*/
class ErrorHandler extends Component
@ -31,49 +34,50 @@ class ErrorHandler extends Component
*/
public $discardExistingOutput = true;
/**
* @var string the route (eg '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->errorHandler->error.
* This property defaults to null, meaning ErrorHandler will handle the error display.
* @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->errorHandler->error. 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
* @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;
/**
* Handles exception
* @param \Exception $exception
* Handles exception.
* @param \Exception $exception to be handled.
*/
public function handle($exception)
{
$this->exception = $exception;
if ($this->discardExistingOutput) {
$this->clearOutput();
}
$this->renderException($exception);
}
/**
* Renders exception
* @param \Exception $exception
* Renders exception.
* @param \Exception $exception to be handled.
*/
protected function renderException($exception)
{
if ($this->errorAction !== null) {
\Yii::$app->runAction($this->errorAction);
} elseif (\Yii::$app instanceof \yii\web\Application) {
Yii::$app->runAction($this->errorAction);
} elseif (!(Yii::$app instanceof \yii\web\Application)) {
Yii::$app->renderException($exception);
} else {
if (!headers_sent()) {
if ($exception instanceof HttpException) {
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') {
\Yii::$app->renderException($exception);
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
@ -90,194 +94,145 @@ class ErrorHandler extends Component
ini_set('display_errors', 1);
}
$view = new View;
if (!YII_DEBUG || $exception instanceof UserException) {
$viewName = $this->errorView;
} else {
$viewName = $this->exceptionView;
$view = new View();
$request = '';
foreach (array('GET', 'POST', 'SERVER', 'FILES', 'COOKIE', 'SESSION', 'ENV') as $name) {
if (!empty($GLOBALS['_' . $name])) {
$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,
'request' => $request,
), $this);
}
} else {
\Yii::$app->renderException($exception);
}
}
/**
* Returns server and Yii version information.
* @return string server version information.
* Converts special characters to HTML entities.
* @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();
if (isset($_SERVER['SERVER_SOFTWARE'])) {
$version = $_SERVER['SERVER_SOFTWARE'] . ' ' . $version;
}
return $version;
return htmlspecialchars($text, ENT_QUOTES, Yii::$app->charset);
}
/**
* Converts arguments array to its string representation
*
* @param array $args arguments array to be converted
* @return string string representation of the arguments array
* Removes all output echoed before calling this method.
*/
public function argumentsToString($args)
public function clearOutput()
{
$isAssoc = $args !== array_values($args);
$count = 0;
foreach ($args as $key => $value) {
$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 . '"';
// 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();
}
} 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];
} elseif ($isAssoc) {
$args[$key] = $key . ' => ' . $args[$key];
/**
* 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, '\\');
}
return implode(', ', $args);
return $html;
}
/**
* Returns a value indicating whether the call stack is from application code.
* @param array $trace the trace data
* @return boolean whether the call stack is from application code.
* 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.
* @return string generated HTML with HTTP status code information.
*/
public function isCoreCode($trace)
public function createHttpStatusLink($statusCode)
{
if (isset($trace['file'])) {
return $trace['file'] === 'unknown' || strpos(realpath($trace['file']), YII_PATH . DIRECTORY_SEPARATOR) === 0;
}
return false;
return '<a href="http://en.wikipedia.org/wiki/List_of_HTTP_status_codes#' . (int)$statusCode .'" target="_blank">' . (int)$statusCode . '</a>';
}
/**
* Renders the source code around the error line.
* @param string $file source file path
* @param integer $errorLine the error line number
* @param integer $maxLines maximum number of lines to display
* Renders a single call stack element.
* @param string $file name where call has happened.
* @param integer $line number on which call has happened.
* @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
if ($errorLine < 0 || ($lines = @file($file)) === false || ($lineCount = count($lines)) <= $errorLine) {
return;
}
$halfLines = (int)($maxLines / 2);
$beginLine = $errorLine - $halfLines > 0 ? $errorLine - $halfLines : 0;
$endLine = $errorLine + $halfLines < $lineCount ? $errorLine + $halfLines : $lineCount - 1;
$lineNumberWidth = strlen($endLine + 1);
$output = '';
for ($i = $beginLine; $i <= $endLine; ++$i) {
$isErrorLine = $i === $errorLine;
$code = sprintf("<span class=\"ln" . ($isErrorLine ? ' error-ln' : '') . "\">%0{$lineNumberWidth}d</span> %s", $i + 1, $this->htmlEncode(str_replace("\t", ' ', $lines[$i])));
if (!$isErrorLine) {
$output .= $code;
} else {
$output .= '<span class="error">' . $code . '</span>';
}
}
echo '<div class="code"><pre>' . $output . '</pre></div>';
$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;
$view = new View();
return $view->renderFile($this->callStackItemView, array(
'file' => $file,
'line' => $line,
'index' => $index,
'lines' => $lines,
'begin' => $begin,
'end' => $end,
), $this);
}
/**
* Renders calls stack trace
* @param array $trace
* 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 renderTrace($trace)
public function isCoreFile($file)
{
$count = 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>';
return $file === 'unknown' || strpos(realpath($file), YII_PATH . DIRECTORY_SEPARATOR) === 0;
}
/**
* Converts special characters to HTML entities
* @param string $text text to encode
* @return string
* 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 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;
$name = !YII_DEBUG || $exception instanceof HttpException ? $this->errorView : $this->exceptionView;
echo $view->renderFile($name, array(
'exception' => $exception,
), $this);
return '<a href="http://github.com/yiisoft/yii2/" target="_blank">' . $this->htmlEncode(Yii::getVersion()) . '</a>';
}
}

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