ErrorHandler.php 8.78 KB
Newer Older
Qiang Xue committed
1 2
<?php
/**
Qiang Xue committed
3
 * ErrorHandler class file.
Qiang Xue committed
4 5
 *
 * @link http://www.yiiframework.com/
Qiang Xue committed
6
 * @copyright Copyright &copy; 2008-2012 Yii Software LLC
Qiang Xue committed
7 8 9
 * @license http://www.yiiframework.com/license/
 */

Qiang Xue committed
10
namespace yii\base;
Qiang Xue committed
11 12

/**
Qiang Xue committed
13
 * ErrorHandler handles uncaught PHP errors and exceptions.
Qiang Xue committed
14
 *
Qiang Xue committed
15 16 17
 * ErrorHandler displays these errors using appropriate views based on the
 * nature of the errors and the mode the application runs at.
 *
Qiang Xue committed
18
 * @author Qiang Xue <qiang.xue@gmail.com>
Qiang Xue committed
19
 * @since 2.0
Qiang Xue committed
20
 */
Qiang Xue committed
21
class ErrorHandler extends ApplicationComponent
Qiang Xue committed
22 23 24 25
{
	/**
	 * @var integer maximum number of source code lines to be displayed. Defaults to 25.
	 */
Qiang Xue committed
26
	public $maxSourceLines = 25;
Qiang Xue committed
27 28 29 30 31 32 33
	/**
	 * @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.
	 */
Qiang Xue committed
34
	public $discardExistingOutput = true;
Qiang Xue committed
35 36 37
	/**
	 * @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.
Qiang Xue committed
38
	 * This property defaults to null, meaning ErrorHandler will handle the error display.
Qiang Xue committed
39 40
	 */
	public $errorAction;
Qiang Xue committed
41 42 43
	/**
	 * @var string the path of the view file for rendering exceptions
	 */
Qiang Xue committed
44
	public $exceptionView = '@yii/views/exception.php';
Qiang Xue committed
45 46 47
	/**
	 * @var string the path of the view file for rendering errors
	 */
Qiang Xue committed
48 49 50 51
	public $errorView = '@yii/views/error.php';
	/**
	 * @var \Exception the exception that is being handled currently
	 */
Qiang Xue committed
52 53 54 55 56 57 58 59
	public $exception;

	public function init()
	{
		set_exception_handler(array($this, 'handleException'));
		set_error_handler(array($this, 'handleError'), error_reporting());
	}

Qiang Xue committed
60 61 62
	/**
	 * Handles PHP execution errors such as warnings, notices.
	 *
Qiang Xue committed
63
	 * This method is used as a PHP error handler. It will simply raise an `ErrorException`.
Qiang Xue committed
64 65 66 67 68
	 *
	 * @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
Qiang Xue committed
69
	 * @throws \ErrorException the error exception
Qiang Xue committed
70 71
	 */
	public function handleError($code, $message, $file, $line)
Qiang Xue committed
72
	{
Qiang Xue committed
73
		throw new \ErrorException($message, 0, $code, $file, $line);
Qiang Xue committed
74
	}
Qiang Xue committed
75 76

	/**
Qiang Xue committed
77
	 * @param \Exception $exception
Qiang Xue committed
78
	 */
Qiang Xue committed
79
	public function handleException($exception)
Qiang Xue committed
80
	{
Qiang Xue committed
81
		// disable error capturing to avoid recursive errors while handling exceptions
Qiang Xue committed
82 83 84
		restore_error_handler();
		restore_exception_handler();

Qiang Xue committed
85
		$this->exception = $exception;
Qiang Xue committed
86
		$this->logException($exception);
Qiang Xue committed
87

Qiang Xue committed
88 89 90 91
		if ($this->discardExistingOutput) {
			$this->clearOutput();
		}

Qiang Xue committed
92 93 94 95 96 97
		try {
			$this->render($exception);
		} catch (\Exception $e) {
			// use the most primitive way to display exception thrown in the error view
			$this->renderAsText($e);
		}
Qiang Xue committed
98 99
	}

Qiang Xue committed
100
	protected function render($exception)
Qiang Xue committed
101
	{
Qiang Xue committed
102 103 104 105 106 107 108 109 110
		if ($this->errorAction !== null) {
			\Yii::$application->runController($this->errorAction);
		} elseif (\Yii::$application instanceof \yii\web\Application) {
			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);
Qiang Xue committed
111
			} else {
Qiang Xue committed
112
				$this->renderAsHtml($exception);
Qiang Xue committed
113
			}
Qiang Xue committed
114 115
		} else {
			$this->renderAsText($exception);
Qiang Xue committed
116 117 118 119
		}
	}

	/**
Qiang Xue committed
120 121
	 * Returns server and Yii version information.
	 * @return string server version information.
Qiang Xue committed
122
	 */
Qiang Xue committed
123
	public function getVersionInfo()
Qiang Xue committed
124
	{
Qiang Xue committed
125 126 127
		$version = '<a href="http://www.yiiframework.com/">Yii Framework</a>/' . \Yii::getVersion();
		if (isset($_SERVER['SERVER_SOFTWARE'])) {
			$version = $_SERVER['SERVER_SOFTWARE'] . ' ' . $version;
Qiang Xue committed
128 129 130 131 132 133 134 135 136 137
		}
		return $version;
	}

	/**
	 * Converts arguments array to its string representation
	 *
	 * @param array $args arguments array to be converted
	 * @return string string representation of the arguments array
	 */
Qiang Xue committed
138
	public function argumentsToString($args)
Qiang Xue committed
139
	{
Qiang Xue committed
140
		$isAssoc = $args !== array_values($args);
Qiang Xue committed
141 142
		$count = 0;
		foreach ($args as $key => $value) {
Qiang Xue committed
143
			$count++;
Qiang Xue committed
144
			if ($count >= 5) {
Qiang Xue committed
145
				if ($count > 5) {
Qiang Xue committed
146
					unset($args[$key]);
Qiang Xue committed
147
				} else {
Qiang Xue committed
148
					$args[$key] = '...';
Qiang Xue committed
149
				}
Qiang Xue committed
150 151 152
				continue;
			}

Qiang Xue committed
153
			if (is_object($value)) {
Qiang Xue committed
154
				$args[$key] = get_class($value);
Qiang Xue committed
155
			} elseif (is_bool($value)) {
Qiang Xue committed
156
				$args[$key] = $value ? 'true' : 'false';
Qiang Xue committed
157 158
			} elseif (is_string($value)) {
				if (strlen($value) > 64) {
Qiang Xue committed
159
					$args[$key] = '"' . substr($value, 0, 64) . '..."';
Qiang Xue committed
160
				} else {
Qiang Xue committed
161
					$args[$key] = '"' . $value . '"';
Qiang Xue committed
162 163
				}
			} elseif (is_array($value)) {
Qiang Xue committed
164
				$args[$key] = 'array(' . $this->argumentsToString($value) . ')';
Qiang Xue committed
165
			} elseif ($value === null) {
Qiang Xue committed
166
				$args[$key] = 'null';
Qiang Xue committed
167
			} elseif (is_resource($value)) {
Qiang Xue committed
168
				$args[$key] = 'resource';
Qiang Xue committed
169
			}
Qiang Xue committed
170

Qiang Xue committed
171 172
			if (is_string($key)) {
				$args[$key] = '"' . $key . '" => ' . $args[$key];
Qiang Xue committed
173
			} elseif ($isAssoc) {
Qiang Xue committed
174
				$args[$key] = $key . ' => ' . $args[$key];
Qiang Xue committed
175 176
			}
		}
Qiang Xue committed
177
		return implode(', ', $args);
Qiang Xue committed
178 179 180 181 182 183 184
	}

	/**
	 * 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.
	 */
Qiang Xue committed
185
	public function isCoreCode($trace)
Qiang Xue committed
186
	{
Qiang Xue committed
187
		if (isset($trace['file'])) {
Qiang Xue committed
188
			return $trace['file'] === 'unknown' || strpos(realpath($trace['file']), YII_PATH . DIRECTORY_SEPARATOR) === 0;
Qiang Xue committed
189 190 191 192 193 194 195 196 197 198
		}
		return false;
	}

	/**
	 * 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
	 */
Qiang Xue committed
199
	public function renderSourceCode($file, $errorLine, $maxLines)
Qiang Xue committed
200
	{
Qiang Xue committed
201
		$errorLine--; // adjust line number to 0-based from 1-based
Qiang Xue committed
202 203 204
		if ($errorLine < 0 || ($lines = @file($file)) === false || ($lineCount = count($lines)) <= $errorLine) {
			return;
		}
Qiang Xue committed
205

Qiang Xue committed
206 207 208 209
		$halfLines = (int)($maxLines / 2);
		$beginLine = $errorLine - $halfLines > 0 ? $errorLine - $halfLines : 0;
		$endLine = $errorLine + $halfLines < $lineCount ? $errorLine + $halfLines : $lineCount - 1;
		$lineNumberWidth = strlen($endLine + 1);
Qiang Xue committed
210

Qiang Xue committed
211
		$output = '';
Qiang Xue committed
212
		for ($i = $beginLine; $i <= $endLine; ++$i) {
Qiang Xue committed
213
			$isErrorLine = $i === $errorLine;
Qiang Xue committed
214 215
			$code = sprintf("<span class=\"ln" . ($isErrorLine ? ' error-ln' : '') . "\">%0{$lineNumberWidth}d</span> %s", $i + 1, $this->htmlEncode(str_replace("\t", '    ', $lines[$i])));
			if (!$isErrorLine) {
Qiang Xue committed
216
				$output .= $code;
Qiang Xue committed
217
			} else {
Qiang Xue committed
218
				$output .= '<span class="error">' . $code . '</span>';
Qiang Xue committed
219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278
			}
		}
		echo '<div class="code"><pre>' . $output . '</pre></div>';
	}

	public function renderTrace($trace)
	{
		$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 = $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;';
			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>';
	}

	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();
Qiang Xue committed
279
		}
Qiang Xue committed
280 281 282
	}

	/**
Qiang Xue committed
283
	 * @param \Exception $exception
Qiang Xue committed
284
	 */
Qiang Xue committed
285
	public function renderAsText($exception)
Qiang Xue committed
286
	{
Qiang Xue committed
287
		if (YII_DEBUG) {
Qiang Xue committed
288
			echo $exception;
Qiang Xue committed
289
		} else {
Qiang Xue committed
290
			echo get_class($exception) . ': ' . $exception->getMessage();
Qiang Xue committed
291 292 293 294 295 296 297 298 299 300 301
		}
	}

	/**
	 * @param \Exception $exception
	 */
	public function renderAsHtml($exception)
	{
		$view = new View;
		$view->owner = $this;
		$name = !YII_DEBUG || $exception instanceof HttpException ? $this->errorView : $this->exceptionView;
302
		echo $view->render($name, array(
Qiang Xue committed
303 304
			'exception' => $exception,
		));
Qiang Xue committed
305 306
	}
}