*
  • development views, named as exception.php; *
  • production views, named as error<StatusCode>.php; * * where <StatusCode> stands for the HTTP error code (e.g. error500.php). * Localized views are named similarly but located under a subdirectory * whose name is the language code (e.g. zh_cn/error500.php). * * Development views are displayed when the application is in debug mode * (i.e. YII_DEBUG is defined as true). Detailed error information with source code * are displayed in these views. Production views are meant to be shown * to end-users and are used when the application is in production mode. * For security reasons, they only display the error message without any * sensitive information. * * ErrorHandler looks for the view templates from the following locations in order: *
      *
    1. themes/ThemeName/views/system: when a theme is active.
    2. *
    3. protected/views/system
    4. *
    5. framework/views
    6. *
    * If the view is not found in a directory, it will be looked for in the next directory. * * The property {@link maxSourceLines} can be changed to specify the number * of source code lines to be displayed in development views. * * ErrorHandler is a core application component that can be accessed via * {@link CApplication::getErrorHandler()}. * * @property array $error The error details. Null if there is no error. * * @author Qiang Xue * @since 2.0 */ class ErrorHandler extends ApplicationComponent { /** * @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 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. */ 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. */ public $errorAction; private $_error; public $exception; public $compactOutput; public function init() { set_exception_handler(array($this, 'handleException')); 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() { 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() . '

    '; } } /** * @param \Exception $exception */ public function handleException($exception) { $this->exception = $exception; // disable error capturing to avoid recursive errors restore_error_handler(); restore_exception_handler(); $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'])); } } /** * 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) { $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; } } } /** * 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. */ protected function getVersionInfo() { if (YII_DEBUG) { $version = 'Yii Framework/' . \Yii::getVersion(); if (isset($_SERVER['SERVER_SOFTWARE'])) { $version = $_SERVER['SERVER_SOFTWARE'] . ' ' . $version; } } else { $version = ''; } 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 */ protected function argumentsToString($args) { $count = 0; $isAssoc = $args !== array_values($args); 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); else if (is_bool($value)) $args[$key] = $value ? 'true' : 'false'; else if (is_string($value)) { if (strlen($value) > 64) $args[$key] = '"' . substr($value, 0, 64) . '..."'; else $args[$key] = '"' . $value . '"'; } else if (is_array($value)) $args[$key] = 'array(' . $this->argumentsToString($value) . ')'; else if ($value === null) $args[$key] = 'null'; else if (is_resource($value)) $args[$key] = 'resource'; if (is_string($key)) { $args[$key] = '"' . $key . '" => ' . $args[$key]; } else if ($isAssoc) { $args[$key] = $key . ' => ' . $args[$key]; } } $out = implode(", ", $args); return $out; } /** * 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. */ protected function isCoreCode($trace) { if (isset($trace['file'])) { $systemPath = realpath(dirname(__FILE__) . '/..'); return $trace['file'] === 'unknown' || strpos(realpath($trace['file']), $systemPath . DIRECTORY_SEPARATOR) === 0; } 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 * @return string the rendering result */ protected 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 ''; $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("%0{$lineNumberWidth}d %s", $i + 1, CHtml::encode(str_replace("\t", ' ', $lines[$i]))); if (!$isErrorLine) $output .= $code; else $output .= '' . $code . ''; } 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 */ public function handleError($code, $message, $file, $line) { throw new \ErrorException($message, 0, $code, $file, $line); } }