ErrorHandler.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  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\base;
  8. use common\helpers\LoggerTool;
  9. use Yii;
  10. use yii\helpers\VarDumper;
  11. use yii\web\HttpException;
  12. /**
  13. * ErrorHandler handles uncaught PHP errors and exceptions.
  14. *
  15. * ErrorHandler is configured as an application component in [[\yii\base\Application]] by default.
  16. * You can access that instance via `Yii::$app->errorHandler`.
  17. *
  18. * For more details and usage information on ErrorHandler, see the [guide article on handling errors](guide:runtime-handling-errors).
  19. *
  20. * @author Qiang Xue <qiang.xue@gmail.com>
  21. * @author Alexander Makarov <sam@rmcreative.ru>
  22. * @author Carsten Brandt <mail@cebe.cc>
  23. * @since 2.0
  24. */
  25. abstract class ErrorHandler extends Component
  26. {
  27. /**
  28. * @var bool whether to discard any existing page output before error display. Defaults to true.
  29. */
  30. public $discardExistingOutput = true;
  31. /**
  32. * @var int the size of the reserved memory. A portion of memory is pre-allocated so that
  33. * when an out-of-memory issue occurs, the error handler is able to handle the error with
  34. * the help of this reserved memory. If you set this value to be 0, no memory will be reserved.
  35. * Defaults to 256KB.
  36. */
  37. public $memoryReserveSize = 262144;
  38. /**
  39. * @var \Exception|null the exception that is being handled currently.
  40. */
  41. public $exception;
  42. /**
  43. * @var bool if true - `handleException()` will finish script with `ExitCode::OK`.
  44. * false - `ExitCode::UNSPECIFIED_ERROR`.
  45. * @since 2.0.36
  46. */
  47. public $silentExitOnException;
  48. /**
  49. * @var string Used to reserve memory for fatal error handler.
  50. */
  51. private $_memoryReserve;
  52. /**
  53. * @var \Exception from HHVM error that stores backtrace
  54. */
  55. private $_hhvmException;
  56. /**
  57. * @var bool whether this instance has been registered using `register()`
  58. */
  59. private $_registered = false;
  60. public function init()
  61. {
  62. $this->silentExitOnException = $this->silentExitOnException !== null ? $this->silentExitOnException : YII_ENV_TEST;
  63. parent::init();
  64. }
  65. /**
  66. * Register this error handler.
  67. * @since 2.0.32 this will not do anything if the error handler was already registered
  68. */
  69. public function register()
  70. {
  71. if (!$this->_registered) {
  72. ini_set('display_errors', false);
  73. set_exception_handler([$this, 'handleException']);
  74. if (defined('HHVM_VERSION')) {
  75. set_error_handler([$this, 'handleHhvmError']);
  76. } else {
  77. set_error_handler([$this, 'handleError']);
  78. }
  79. if ($this->memoryReserveSize > 0) {
  80. $this->_memoryReserve = str_repeat('x', $this->memoryReserveSize);
  81. }
  82. register_shutdown_function([$this, 'handleFatalError']);
  83. $this->_registered = true;
  84. }
  85. }
  86. /**
  87. * Unregisters this error handler by restoring the PHP error and exception handlers.
  88. * @since 2.0.32 this will not do anything if the error handler was not registered
  89. */
  90. public function unregister()
  91. {
  92. if ($this->_registered) {
  93. restore_error_handler();
  94. restore_exception_handler();
  95. $this->_registered = false;
  96. }
  97. }
  98. /**
  99. * Handles uncaught PHP exceptions.
  100. *
  101. * This method is implemented as a PHP exception handler.
  102. *
  103. * @param \Exception $exception the exception that is not caught
  104. */
  105. public function handleException($exception)
  106. {
  107. if ($exception instanceof ExitException) {
  108. return;
  109. }
  110. $this->exception = $exception;
  111. // disable error capturing to avoid recursive errors while handling exceptions
  112. $this->unregister();
  113. // set preventive HTTP status code to 500 in case error handling somehow fails and headers are sent
  114. // HTTP exceptions will override this value in renderException()
  115. if (PHP_SAPI !== 'cli') {
  116. http_response_code(500);
  117. }
  118. try {
  119. $this->logException($exception);
  120. if ($this->discardExistingOutput) {
  121. $this->clearOutput();
  122. }
  123. $this->renderException($exception);
  124. if (!$this->silentExitOnException) {
  125. \Yii::getLogger()->flush(true);
  126. if (defined('HHVM_VERSION')) {
  127. flush();
  128. }
  129. exit(1);
  130. }
  131. } catch (\Exception $e) {
  132. // an other exception could be thrown while displaying the exception
  133. $this->handleFallbackExceptionMessage($e, $exception);
  134. } catch (\Throwable $e) {
  135. // additional check for \Throwable introduced in PHP 7
  136. $this->handleFallbackExceptionMessage($e, $exception);
  137. }
  138. $this->exception = null;
  139. }
  140. /**
  141. * Handles exception thrown during exception processing in [[handleException()]].
  142. * @param \Exception|\Throwable $exception Exception that was thrown during main exception processing.
  143. * @param \Exception $previousException Main exception processed in [[handleException()]].
  144. * @since 2.0.11
  145. */
  146. protected function handleFallbackExceptionMessage($exception, $previousException)
  147. {
  148. $msg = "An Error occurred while handling another error:\n";
  149. $msg .= (string) $exception;
  150. $msg .= "\nPrevious exception:\n";
  151. $msg .= (string) $previousException;
  152. if (YII_DEBUG) {
  153. if (PHP_SAPI === 'cli') {
  154. echo $msg . "\n";
  155. } else {
  156. echo '<pre>' . htmlspecialchars($msg, ENT_QUOTES, Yii::$app->charset) . '</pre>';
  157. }
  158. $msg .= "\n\$_SERVER = " . VarDumper::export($_SERVER);
  159. } else {
  160. echo 'An internal server error occurred.';
  161. }
  162. error_log($msg);
  163. if (defined('HHVM_VERSION')) {
  164. flush();
  165. }
  166. exit(1);
  167. }
  168. /**
  169. * Handles HHVM execution errors such as warnings and notices.
  170. *
  171. * This method is used as a HHVM error handler. It will store exception that will
  172. * be used in fatal error handler
  173. *
  174. * @param int $code the level of the error raised.
  175. * @param string $message the error message.
  176. * @param string $file the filename that the error was raised in.
  177. * @param int $line the line number the error was raised at.
  178. * @param mixed $context
  179. * @param mixed $backtrace trace of error
  180. * @return bool whether the normal error handler continues.
  181. *
  182. * @throws ErrorException
  183. * @since 2.0.6
  184. */
  185. public function handleHhvmError($code, $message, $file, $line, $context, $backtrace)
  186. {
  187. if ($this->handleError($code, $message, $file, $line)) {
  188. return true;
  189. }
  190. if (E_ERROR & $code) {
  191. $exception = new ErrorException($message, $code, $code, $file, $line);
  192. $ref = new \ReflectionProperty('\Exception', 'trace');
  193. $ref->setAccessible(true);
  194. $ref->setValue($exception, $backtrace);
  195. $this->_hhvmException = $exception;
  196. }
  197. return false;
  198. }
  199. /**
  200. * Handles PHP execution errors such as warnings and notices.
  201. *
  202. * This method is used as a PHP error handler. It will simply raise an [[ErrorException]].
  203. *
  204. * @param int $code the level of the error raised.
  205. * @param string $message the error message.
  206. * @param string $file the filename that the error was raised in.
  207. * @param int $line the line number the error was raised at.
  208. * @return bool whether the normal error handler continues.
  209. *
  210. * @throws ErrorException
  211. */
  212. public function handleError($code, $message, $file, $line)
  213. {
  214. if (error_reporting() & $code) {
  215. // load ErrorException manually here because autoloading them will not work
  216. // when error occurs while autoloading a class
  217. if (!class_exists('yii\\base\\ErrorException', false)) {
  218. require_once __DIR__ . '/ErrorException.php';
  219. }
  220. $exception = new ErrorException($message, $code, $code, $file, $line);
  221. if (PHP_VERSION_ID < 70400) {
  222. // prior to PHP 7.4 we can't throw exceptions inside of __toString() - it will result a fatal error
  223. $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
  224. array_shift($trace);
  225. foreach ($trace as $frame) {
  226. if ($frame['function'] === '__toString') {
  227. $this->handleException($exception);
  228. if (defined('HHVM_VERSION')) {
  229. flush();
  230. }
  231. exit(1);
  232. }
  233. }
  234. }
  235. throw $exception;
  236. }
  237. return false;
  238. }
  239. /**
  240. * Handles fatal PHP errors.
  241. */
  242. public function handleFatalError()
  243. {
  244. unset($this->_memoryReserve);
  245. // load ErrorException manually here because autoloading them will not work
  246. // when error occurs while autoloading a class
  247. if (!class_exists('yii\\base\\ErrorException', false)) {
  248. require_once __DIR__ . '/ErrorException.php';
  249. }
  250. $error = error_get_last();
  251. if (ErrorException::isFatalError($error)) {
  252. if (!empty($this->_hhvmException)) {
  253. $exception = $this->_hhvmException;
  254. } else {
  255. $exception = new ErrorException($error['message'], $error['type'], $error['type'], $error['file'], $error['line']);
  256. }
  257. $this->exception = $exception;
  258. $this->logException($exception);
  259. if ($this->discardExistingOutput) {
  260. $this->clearOutput();
  261. }
  262. $this->renderException($exception);
  263. // need to explicitly flush logs because exit() next will terminate the app immediately
  264. Yii::getLogger()->flush(true);
  265. if (defined('HHVM_VERSION')) {
  266. flush();
  267. }
  268. exit(1);
  269. }
  270. }
  271. /**
  272. * Renders the exception.
  273. * @param \Exception|\Error|\Throwable $exception the exception to be rendered.
  274. */
  275. abstract protected function renderException($exception);
  276. /**
  277. * Logs the given exception.
  278. * @param \Exception $exception the exception to be logged
  279. * @since 2.0.3 this method is now public.
  280. */
  281. public function logException($exception)
  282. {
  283. $category = get_class($exception);
  284. if ($exception instanceof HttpException) {
  285. $category = 'yii\\web\\HttpException:' . $exception->statusCode;
  286. } elseif ($exception instanceof \ErrorException) {
  287. $category .= ':' . $exception->getSeverity();
  288. }
  289. Yii::error($exception, $category);
  290. }
  291. /**
  292. * Removes all output echoed before calling this method.
  293. */
  294. public function clearOutput()
  295. {
  296. // the following manual level counting is to deal with zlib.output_compression set to On
  297. for ($level = ob_get_level(); $level > 0; --$level) {
  298. if (!@ob_end_clean()) {
  299. ob_clean();
  300. }
  301. }
  302. }
  303. /**
  304. * Converts an exception into a PHP error.
  305. *
  306. * This method can be used to convert exceptions inside of methods like `__toString()`
  307. * to PHP errors because exceptions cannot be thrown inside of them.
  308. * @param \Exception|\Throwable $exception the exception to convert to a PHP error.
  309. */
  310. public static function convertExceptionToError($exception)
  311. {
  312. trigger_error(static::convertExceptionToString($exception), E_USER_ERROR);
  313. }
  314. /**
  315. * Converts an exception into a simple string.
  316. * @param \Exception|\Error|\Throwable $exception the exception being converted
  317. * @return string the string representation of the exception.
  318. */
  319. public static function convertExceptionToString($exception)
  320. {
  321. if ($exception instanceof UserException) {
  322. return "{$exception->getName()}: {$exception->getMessage()}";
  323. }
  324. if (YII_DEBUG) {
  325. return static::convertExceptionToVerboseString($exception);
  326. }
  327. return 'An internal server error occurred.';
  328. }
  329. /**
  330. * Converts an exception into a string that has verbose information about the exception and its trace.
  331. * @param \Exception|\Error|\Throwable $exception the exception being converted
  332. * @return string the string representation of the exception.
  333. *
  334. * @since 2.0.14
  335. */
  336. public static function convertExceptionToVerboseString($exception)
  337. {
  338. if ($exception instanceof Exception) {
  339. $message = "Exception ({$exception->getName()})";
  340. } elseif ($exception instanceof ErrorException) {
  341. $message = (string)$exception->getName();
  342. } else {
  343. $message = 'Exception';
  344. }
  345. $message .= " '" . get_class($exception) . "' with message '{$exception->getMessage()}' \n\nin "
  346. . $exception->getFile() . ':' . $exception->getLine() . "\n\n"
  347. . "Stack trace:\n" . $exception->getTraceAsString();
  348. return $message;
  349. }
  350. }