You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

429 lines
16KB

  1. <?php
  2. /**
  3. * @link http://www.yiiframework.com/
  4. * @copyright Copyright (c) 2008 Yii Software LLC
  5. * @license http://www.yiiframework.com/license/
  6. */
  7. namespace yii\web;
  8. use Yii;
  9. use yii\base\Exception;
  10. use yii\base\ErrorException;
  11. use yii\base\UserException;
  12. use yii\helpers\VarDumper;
  13. /**
  14. * ErrorHandler handles uncaught PHP errors and exceptions.
  15. *
  16. * ErrorHandler displays these errors using appropriate views based on the
  17. * nature of the errors and the mode the application runs at.
  18. *
  19. * ErrorHandler is configured as an application component in [[\yii\base\Application]] by default.
  20. * You can access that instance via `Yii::$app->errorHandler`.
  21. *
  22. * @author Qiang Xue <qiang.xue@gmail.com>
  23. * @author Timur Ruziev <resurtm@gmail.com>
  24. * @since 2.0
  25. */
  26. class ErrorHandler extends \yii\base\ErrorHandler
  27. {
  28. /**
  29. * @var integer maximum number of source code lines to be displayed. Defaults to 19.
  30. */
  31. public $maxSourceLines = 19;
  32. /**
  33. * @var integer maximum number of trace source code lines to be displayed. Defaults to 13.
  34. */
  35. public $maxTraceSourceLines = 13;
  36. /**
  37. * @var string the route (e.g. 'site/error') to the controller action that will be used
  38. * to display external errors. Inside the action, it can retrieve the error information
  39. * using `Yii::$app->errorHandler->exception. This property defaults to null, meaning ErrorHandler
  40. * will handle the error display.
  41. */
  42. public $errorAction;
  43. /**
  44. * @var string the path of the view file for rendering exceptions without call stack information.
  45. */
  46. public $errorView = '@yii/views/errorHandler/error.php';
  47. /**
  48. * @var string the path of the view file for rendering exceptions.
  49. */
  50. public $exceptionView = '@yii/views/errorHandler/exception.php';
  51. /**
  52. * @var string the path of the view file for rendering exceptions and errors call stack element.
  53. */
  54. public $callStackItemView = '@yii/views/errorHandler/callStackItem.php';
  55. /**
  56. * @var string the path of the view file for rendering previous exceptions.
  57. */
  58. public $previousExceptionView = '@yii/views/errorHandler/previousException.php';
  59. /**
  60. * Renders the exception.
  61. * @param \Exception $exception the exception to be rendered.
  62. */
  63. protected function renderException($exception)
  64. {
  65. if (Yii::$app->has('response')) {
  66. $response = Yii::$app->getResponse();
  67. $response->isSent = false;
  68. } else {
  69. $response = new Response();
  70. }
  71. $useErrorView = $response->format === Response::FORMAT_HTML && (!YII_DEBUG || $exception instanceof UserException);
  72. if ($useErrorView && $this->errorAction !== null) {
  73. $result = Yii::$app->runAction($this->errorAction);
  74. if ($result instanceof Response) {
  75. $response = $result;
  76. } else {
  77. $response->data = $result;
  78. }
  79. } elseif ($response->format === Response::FORMAT_HTML) {
  80. if (YII_ENV_TEST || isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest') {
  81. // AJAX request
  82. $response->data = '<pre>' . $this->htmlEncode($this->convertExceptionToString($exception)) . '</pre>';
  83. } else {
  84. // if there is an error during error rendering it's useful to
  85. // display PHP error in debug mode instead of a blank screen
  86. if (YII_DEBUG) {
  87. ini_set('display_errors', 1);
  88. }
  89. $file = $useErrorView ? $this->errorView : $this->exceptionView;
  90. $response->data = $this->renderFile($file, [
  91. 'exception' => $exception,
  92. ]);
  93. }
  94. } elseif ($response->format === Response::FORMAT_RAW) {
  95. $response->data = $exception;
  96. } else {
  97. $response->data = $this->convertExceptionToArray($exception);
  98. }
  99. if ($exception instanceof HttpException) {
  100. $response->setStatusCode($exception->statusCode);
  101. } else {
  102. $response->setStatusCode(500);
  103. }
  104. $response->send();
  105. }
  106. /**
  107. * Converts an exception into an array.
  108. * @param \Exception $exception the exception being converted
  109. * @return array the array representation of the exception.
  110. */
  111. protected function convertExceptionToArray($exception)
  112. {
  113. if (!YII_DEBUG && !$exception instanceof UserException && !$exception instanceof HttpException) {
  114. $exception = new HttpException(500, 'There was an error at the server.');
  115. }
  116. $array = [
  117. 'name' => ($exception instanceof Exception || $exception instanceof ErrorException) ? $exception->getName() : 'Exception',
  118. 'message' => $exception->getMessage(),
  119. 'code' => $exception->getCode(),
  120. ];
  121. if ($exception instanceof HttpException) {
  122. $array['status'] = $exception->statusCode;
  123. }
  124. if (YII_DEBUG) {
  125. $array['type'] = get_class($exception);
  126. if (!$exception instanceof UserException) {
  127. $array['file'] = $exception->getFile();
  128. $array['line'] = $exception->getLine();
  129. $array['stack-trace'] = explode("\n", $exception->getTraceAsString());
  130. if ($exception instanceof \yii\db\Exception) {
  131. $array['error-info'] = $exception->errorInfo;
  132. }
  133. }
  134. }
  135. if (($prev = $exception->getPrevious()) !== null) {
  136. $array['previous'] = $this->convertExceptionToArray($prev);
  137. }
  138. return $array;
  139. }
  140. /**
  141. * Converts special characters to HTML entities.
  142. * @param string $text to encode.
  143. * @return string encoded original text.
  144. */
  145. public function htmlEncode($text)
  146. {
  147. return htmlspecialchars($text, ENT_QUOTES, Yii::$app->charset);
  148. }
  149. /**
  150. * Adds informational links to the given PHP type/class.
  151. * @param string $code type/class name to be linkified.
  152. * @return string linkified with HTML type/class name.
  153. */
  154. public function addTypeLinks($code)
  155. {
  156. if (preg_match('/(.*?)::([^(]+)/', $code, $matches)) {
  157. $class = $matches[1];
  158. $method = $matches[2];
  159. $text = $this->htmlEncode($class) . '::' . $this->htmlEncode($method);
  160. } else {
  161. $class = $code;
  162. $method = null;
  163. $text = $this->htmlEncode($class);
  164. }
  165. $url = $this->getTypeUrl($class, $method);
  166. if (!$url) {
  167. return $text;
  168. }
  169. return '<a href="' . $url . '" target="_blank">' . $text . '</a>';
  170. }
  171. /**
  172. * Returns the informational link URL for a given PHP type/class.
  173. * @param string $class the type or class name.
  174. * @param string|null $method the method name.
  175. * @return string|null the informational link URL.
  176. * @see addTypeLinks()
  177. */
  178. protected function getTypeUrl($class, $method)
  179. {
  180. if (strpos($class, 'yii\\') !== 0) {
  181. return null;
  182. }
  183. $page = $this->htmlEncode(strtolower(str_replace('\\', '-', $class)));
  184. $url = "http://www.yiiframework.com/doc-2.0/$page.html";
  185. if ($method) {
  186. $url .= "#$method()-detail";
  187. }
  188. return $url;
  189. }
  190. /**
  191. * Renders a view file as a PHP script.
  192. * @param string $_file_ the view file.
  193. * @param array $_params_ the parameters (name-value pairs) that will be extracted and made available in the view file.
  194. * @return string the rendering result
  195. */
  196. public function renderFile($_file_, $_params_)
  197. {
  198. $_params_['handler'] = $this;
  199. if ($this->exception instanceof ErrorException || !Yii::$app->has('view')) {
  200. ob_start();
  201. ob_implicit_flush(false);
  202. extract($_params_, EXTR_OVERWRITE);
  203. require(Yii::getAlias($_file_));
  204. return ob_get_clean();
  205. } else {
  206. return Yii::$app->getView()->renderFile($_file_, $_params_, $this);
  207. }
  208. }
  209. /**
  210. * Renders the previous exception stack for a given Exception.
  211. * @param \Exception $exception the exception whose precursors should be rendered.
  212. * @return string HTML content of the rendered previous exceptions.
  213. * Empty string if there are none.
  214. */
  215. public function renderPreviousExceptions($exception)
  216. {
  217. if (($previous = $exception->getPrevious()) !== null) {
  218. return $this->renderFile($this->previousExceptionView, ['exception' => $previous]);
  219. } else {
  220. return '';
  221. }
  222. }
  223. /**
  224. * Renders a single call stack element.
  225. * @param string|null $file name where call has happened.
  226. * @param integer|null $line number on which call has happened.
  227. * @param string|null $class called class name.
  228. * @param string|null $method called function/method name.
  229. * @param integer $index number of the call stack element.
  230. * @param array $args array of method arguments.
  231. * @return string HTML content of the rendered call stack element.
  232. */
  233. public function renderCallStackItem($file, $line, $class, $method, $args, $index)
  234. {
  235. $lines = [];
  236. $begin = $end = 0;
  237. if ($file !== null && $line !== null) {
  238. $line--; // adjust line number from one-based to zero-based
  239. $lines = @file($file);
  240. if ($line < 0 || $lines === false || ($lineCount = count($lines)) < $line + 1) {
  241. return '';
  242. }
  243. $half = (int) (($index == 1 ? $this->maxSourceLines : $this->maxTraceSourceLines) / 2);
  244. $begin = $line - $half > 0 ? $line - $half : 0;
  245. $end = $line + $half < $lineCount ? $line + $half : $lineCount - 1;
  246. }
  247. return $this->renderFile($this->callStackItemView, [
  248. 'file' => $file,
  249. 'line' => $line,
  250. 'class' => $class,
  251. 'method' => $method,
  252. 'index' => $index,
  253. 'lines' => $lines,
  254. 'begin' => $begin,
  255. 'end' => $end,
  256. 'args' => $args,
  257. ]);
  258. }
  259. /**
  260. * Renders the request information.
  261. * @return string the rendering result
  262. */
  263. public function renderRequest()
  264. {
  265. $request = '';
  266. foreach (['_GET', '_POST', '_SERVER', '_FILES', '_COOKIE', '_SESSION', '_ENV'] as $name) {
  267. if (!empty($GLOBALS[$name])) {
  268. $request .= '$' . $name . ' = ' . VarDumper::export($GLOBALS[$name]) . ";\n\n";
  269. }
  270. }
  271. return '<pre>' . rtrim($request, "\n") . '</pre>';
  272. }
  273. /**
  274. * Determines whether given name of the file belongs to the framework.
  275. * @param string $file name to be checked.
  276. * @return boolean whether given name of the file belongs to the framework.
  277. */
  278. public function isCoreFile($file)
  279. {
  280. return $file === null || strpos(realpath($file), YII2_PATH . DIRECTORY_SEPARATOR) === 0;
  281. }
  282. /**
  283. * Creates HTML containing link to the page with the information on given HTTP status code.
  284. * @param integer $statusCode to be used to generate information link.
  285. * @param string $statusDescription Description to display after the the status code.
  286. * @return string generated HTML with HTTP status code information.
  287. */
  288. public function createHttpStatusLink($statusCode, $statusDescription)
  289. {
  290. return '<a href="http://en.wikipedia.org/wiki/List_of_HTTP_status_codes#' . (int) $statusCode . '" target="_blank">HTTP ' . (int) $statusCode . ' &ndash; ' . $statusDescription . '</a>';
  291. }
  292. /**
  293. * Creates string containing HTML link which refers to the home page of determined web-server software
  294. * and its full name.
  295. * @return string server software information hyperlink.
  296. */
  297. public function createServerInformationLink()
  298. {
  299. $serverUrls = [
  300. 'http://httpd.apache.org/' => ['apache'],
  301. 'http://nginx.org/' => ['nginx'],
  302. 'http://lighttpd.net/' => ['lighttpd'],
  303. 'http://gwan.com/' => ['g-wan', 'gwan'],
  304. 'http://iis.net/' => ['iis', 'services'],
  305. 'http://php.net/manual/en/features.commandline.webserver.php' => ['development'],
  306. ];
  307. if (isset($_SERVER['SERVER_SOFTWARE'])) {
  308. foreach ($serverUrls as $url => $keywords) {
  309. foreach ($keywords as $keyword) {
  310. if (stripos($_SERVER['SERVER_SOFTWARE'], $keyword) !== false) {
  311. return '<a href="' . $url . '" target="_blank">' . $this->htmlEncode($_SERVER['SERVER_SOFTWARE']) . '</a>';
  312. }
  313. }
  314. }
  315. }
  316. return '';
  317. }
  318. /**
  319. * Creates string containing HTML link which refers to the page with the current version
  320. * of the framework and version number text.
  321. * @return string framework version information hyperlink.
  322. */
  323. public function createFrameworkVersionLink()
  324. {
  325. return '<a href="http://github.com/yiisoft/yii2/" target="_blank">' . $this->htmlEncode(Yii::getVersion()) . '</a>';
  326. }
  327. /**
  328. * Converts arguments array to its string representation
  329. *
  330. * @param array $args arguments array to be converted
  331. * @return string string representation of the arguments array
  332. */
  333. public function argumentsToString($args)
  334. {
  335. $count = 0;
  336. $isAssoc = $args !== array_values($args);
  337. foreach ($args as $key => $value) {
  338. $count++;
  339. if($count>=5) {
  340. if($count>5) {
  341. unset($args[$key]);
  342. } else {
  343. $args[$key] = '...';
  344. }
  345. continue;
  346. }
  347. if (is_object($value)) {
  348. $args[$key] = '<span class="title">' . $this->htmlEncode(get_class($value)) . '</span>';
  349. } elseif (is_bool($value)) {
  350. $args[$key] = '<span class="keyword">' . ($value ? 'true' : 'false') . '</span>';
  351. } elseif (is_string($value)) {
  352. $fullValue = $this->htmlEncode($value);
  353. if (mb_strlen($value, 'utf8') > 32) {
  354. $displayValue = $this->htmlEncode(mb_substr($value, 0, 32, 'utf8')) . '...';
  355. $args[$key] = "<span class=\"string\" title=\"$fullValue\">'$displayValue'</span>";
  356. } else {
  357. $args[$key] = "<span class=\"string\">'$fullValue'</span>";
  358. }
  359. } elseif (is_array($value)) {
  360. $args[$key] = '[' . $this->argumentsToString($value) . ']';
  361. } elseif ($value === null) {
  362. $args[$key] = '<span class="keyword">null</span>';
  363. } elseif(is_resource($value)) {
  364. $args[$key] = '<span class="keyword">resource</span>';
  365. } else {
  366. $args[$key] = '<span class="number">' . $value . '</span>';
  367. }
  368. if (is_string($key)) {
  369. $args[$key] = '<span class="string">\'' . $this->htmlEncode($key) . "'</span> => $args[$key]";
  370. } elseif ($isAssoc) {
  371. $args[$key] = "<span class=\"number\">$key</span> => $args[$key]";
  372. }
  373. }
  374. $out = implode(", ", $args);
  375. return $out;
  376. }
  377. /**
  378. * Returns human-readable exception name
  379. * @param \Exception $exception
  380. * @return string human-readable exception name or null if it cannot be determined
  381. */
  382. public function getExceptionName($exception)
  383. {
  384. if ($exception instanceof \yii\base\Exception || $exception instanceof \yii\base\InvalidCallException || $exception instanceof \yii\base\InvalidParamException || $exception instanceof \yii\base\UnknownMethodException) {
  385. return $exception->getName();
  386. }
  387. return null;
  388. }
  389. }