BaseErrorHandler.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  1. <?php
  2. /**
  3. * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
  4. * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  5. *
  6. * Licensed under The MIT License
  7. * For full copyright and license information, please see the LICENSE.txt
  8. * Redistributions of files must retain the above copyright notice.
  9. *
  10. * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  11. * @link https://cakephp.org CakePHP(tm) Project
  12. * @since 2.0.0
  13. * @license https://opensource.org/licenses/mit-license.php MIT License
  14. */
  15. namespace Cake\Error;
  16. use Cake\Core\Configure;
  17. use Cake\Log\Log;
  18. use Cake\Routing\Router;
  19. use Error;
  20. use Exception;
  21. /**
  22. * Base error handler that provides logic common to the CLI + web
  23. * error/exception handlers.
  24. *
  25. * Subclasses are required to implement the template methods to handle displaying
  26. * the errors in their environment.
  27. */
  28. abstract class BaseErrorHandler
  29. {
  30. /**
  31. * Options to use for the Error handling.
  32. *
  33. * @var array
  34. */
  35. protected $_options = [];
  36. /**
  37. * @var bool
  38. */
  39. protected $_handled = false;
  40. /**
  41. * Display an error message in an environment specific way.
  42. *
  43. * Subclasses should implement this method to display the error as
  44. * desired for the runtime they operate in.
  45. *
  46. * @param array $error An array of error data.
  47. * @param bool $debug Whether or not the app is in debug mode.
  48. * @return void
  49. */
  50. abstract protected function _displayError($error, $debug);
  51. /**
  52. * Display an exception in an environment specific way.
  53. *
  54. * Subclasses should implement this method to display an uncaught exception as
  55. * desired for the runtime they operate in.
  56. *
  57. * @param \Exception $exception The uncaught exception.
  58. * @return void
  59. */
  60. abstract protected function _displayException($exception);
  61. /**
  62. * Register the error and exception handlers.
  63. *
  64. * @return void
  65. */
  66. public function register()
  67. {
  68. $level = -1;
  69. if (isset($this->_options['errorLevel'])) {
  70. $level = $this->_options['errorLevel'];
  71. }
  72. error_reporting($level);
  73. set_error_handler([$this, 'handleError'], $level);
  74. set_exception_handler([$this, 'wrapAndHandleException']);
  75. register_shutdown_function(function () {
  76. if ((PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') && $this->_handled) {
  77. return;
  78. }
  79. $megabytes = Configure::read('Error.extraFatalErrorMemory');
  80. if ($megabytes === null) {
  81. $megabytes = 4;
  82. }
  83. if ($megabytes > 0) {
  84. $this->increaseMemoryLimit($megabytes * 1024);
  85. }
  86. $error = error_get_last();
  87. if (!is_array($error)) {
  88. return;
  89. }
  90. $fatals = [
  91. E_USER_ERROR,
  92. E_ERROR,
  93. E_PARSE,
  94. ];
  95. if (!in_array($error['type'], $fatals, true)) {
  96. return;
  97. }
  98. $this->handleFatalError(
  99. $error['type'],
  100. $error['message'],
  101. $error['file'],
  102. $error['line']
  103. );
  104. });
  105. }
  106. /**
  107. * Set as the default error handler by CakePHP.
  108. *
  109. * Use config/error.php to customize or replace this error handler.
  110. * This function will use Debugger to display errors when debug > 0. And
  111. * will log errors to Log, when debug == 0.
  112. *
  113. * You can use the 'errorLevel' option to set what type of errors will be handled.
  114. * Stack traces for errors can be enabled with the 'trace' option.
  115. *
  116. * @param int $code Code of error
  117. * @param string $description Error description
  118. * @param string|null $file File on which error occurred
  119. * @param int|null $line Line that triggered the error
  120. * @param array|null $context Context
  121. * @return bool True if error was handled
  122. */
  123. public function handleError($code, $description, $file = null, $line = null, $context = null)
  124. {
  125. if (error_reporting() === 0) {
  126. return false;
  127. }
  128. $this->_handled = true;
  129. list($error, $log) = static::mapErrorCode($code);
  130. if ($log === LOG_ERR) {
  131. return $this->handleFatalError($code, $description, $file, $line);
  132. }
  133. $data = [
  134. 'level' => $log,
  135. 'code' => $code,
  136. 'error' => $error,
  137. 'description' => $description,
  138. 'file' => $file,
  139. 'line' => $line,
  140. ];
  141. $debug = Configure::read('debug');
  142. if ($debug) {
  143. $data += [
  144. 'context' => $context,
  145. 'start' => 3,
  146. 'path' => Debugger::trimPath($file)
  147. ];
  148. }
  149. $this->_displayError($data, $debug);
  150. $this->_logError($log, $data);
  151. return true;
  152. }
  153. /**
  154. * Checks the passed exception type. If it is an instance of `Error`
  155. * then, it wraps the passed object inside another Exception object
  156. * for backwards compatibility purposes.
  157. *
  158. * @param \Exception|\Error $exception The exception to handle
  159. * @return void
  160. */
  161. public function wrapAndHandleException($exception)
  162. {
  163. if ($exception instanceof Error) {
  164. $exception = new PHP7ErrorException($exception);
  165. }
  166. $this->handleException($exception);
  167. }
  168. /**
  169. * Handle uncaught exceptions.
  170. *
  171. * Uses a template method provided by subclasses to display errors in an
  172. * environment appropriate way.
  173. *
  174. * @param \Exception $exception Exception instance.
  175. * @return void
  176. * @throws \Exception When renderer class not found
  177. * @see https://secure.php.net/manual/en/function.set-exception-handler.php
  178. */
  179. public function handleException(Exception $exception)
  180. {
  181. $this->_displayException($exception);
  182. $this->_logException($exception);
  183. $this->_stop($exception->getCode() ?: 1);
  184. }
  185. /**
  186. * Stop the process.
  187. *
  188. * Implemented in subclasses that need it.
  189. *
  190. * @param int $code Exit code.
  191. * @return void
  192. */
  193. protected function _stop($code)
  194. {
  195. // Do nothing.
  196. }
  197. /**
  198. * Display/Log a fatal error.
  199. *
  200. * @param int $code Code of error
  201. * @param string $description Error description
  202. * @param string $file File on which error occurred
  203. * @param int $line Line that triggered the error
  204. * @return bool
  205. */
  206. public function handleFatalError($code, $description, $file, $line)
  207. {
  208. $data = [
  209. 'code' => $code,
  210. 'description' => $description,
  211. 'file' => $file,
  212. 'line' => $line,
  213. 'error' => 'Fatal Error',
  214. ];
  215. $this->_logError(LOG_ERR, $data);
  216. $this->handleException(new FatalErrorException($description, 500, $file, $line));
  217. return true;
  218. }
  219. /**
  220. * Increases the PHP "memory_limit" ini setting by the specified amount
  221. * in kilobytes
  222. *
  223. * @param int $additionalKb Number in kilobytes
  224. * @return void
  225. */
  226. public function increaseMemoryLimit($additionalKb)
  227. {
  228. $limit = ini_get('memory_limit');
  229. if (!strlen($limit) || $limit === '-1') {
  230. return;
  231. }
  232. $limit = trim($limit);
  233. $units = strtoupper(substr($limit, -1));
  234. $current = (int)substr($limit, 0, strlen($limit) - 1);
  235. if ($units === 'M') {
  236. $current *= 1024;
  237. $units = 'K';
  238. }
  239. if ($units === 'G') {
  240. $current = $current * 1024 * 1024;
  241. $units = 'K';
  242. }
  243. if ($units === 'K') {
  244. ini_set('memory_limit', ceil($current + $additionalKb) . 'K');
  245. }
  246. }
  247. /**
  248. * Log an error.
  249. *
  250. * @param string $level The level name of the log.
  251. * @param array $data Array of error data.
  252. * @return bool
  253. */
  254. protected function _logError($level, $data)
  255. {
  256. $message = sprintf(
  257. '%s (%s): %s in [%s, line %s]',
  258. $data['error'],
  259. $data['code'],
  260. $data['description'],
  261. $data['file'],
  262. $data['line']
  263. );
  264. if (!empty($this->_options['trace'])) {
  265. $trace = Debugger::trace([
  266. 'start' => 1,
  267. 'format' => 'log'
  268. ]);
  269. $request = Router::getRequest();
  270. if ($request) {
  271. $message .= $this->_requestContext($request);
  272. }
  273. $message .= "\nTrace:\n" . $trace . "\n";
  274. }
  275. $message .= "\n\n";
  276. return Log::write($level, $message);
  277. }
  278. /**
  279. * Handles exception logging
  280. *
  281. * @param \Exception $exception Exception instance.
  282. * @return bool
  283. */
  284. protected function _logException(Exception $exception)
  285. {
  286. $config = $this->_options;
  287. $unwrapped = $exception instanceof PHP7ErrorException ?
  288. $exception->getError() :
  289. $exception;
  290. if (empty($config['log'])) {
  291. return false;
  292. }
  293. if (!empty($config['skipLog'])) {
  294. foreach ((array)$config['skipLog'] as $class) {
  295. if ($unwrapped instanceof $class) {
  296. return false;
  297. }
  298. }
  299. }
  300. return Log::error($this->_getMessage($exception));
  301. }
  302. /**
  303. * Get the request context for an error/exception trace.
  304. *
  305. * @param \Cake\Http\ServerRequest $request The request to read from.
  306. * @return string
  307. */
  308. protected function _requestContext($request)
  309. {
  310. $message = "\nRequest URL: " . $request->getRequestTarget();
  311. $referer = $request->getEnv('HTTP_REFERER');
  312. if ($referer) {
  313. $message .= "\nReferer URL: " . $referer;
  314. }
  315. $clientIp = $request->clientIp();
  316. if ($clientIp && $clientIp !== '::1') {
  317. $message .= "\nClient IP: " . $clientIp;
  318. }
  319. return $message;
  320. }
  321. /**
  322. * Generates a formatted error message
  323. *
  324. * @param \Exception $exception Exception instance
  325. * @return string Formatted message
  326. */
  327. protected function _getMessage(Exception $exception)
  328. {
  329. $message = $this->getMessageForException($exception);
  330. $request = Router::getRequest();
  331. if ($request) {
  332. $message .= $this->_requestContext($request);
  333. }
  334. return $message;
  335. }
  336. /**
  337. * Generate the message for the exception
  338. *
  339. * @param \Exception $exception The exception to log a message for.
  340. * @param bool $isPrevious False for original exception, true for previous
  341. * @return string Error message
  342. */
  343. protected function getMessageForException($exception, $isPrevious = false)
  344. {
  345. $exception = $exception instanceof PHP7ErrorException ?
  346. $exception->getError() :
  347. $exception;
  348. $config = $this->_options;
  349. $message = sprintf(
  350. '%s[%s] %s in %s on line %s',
  351. $isPrevious ? "\nCaused by: " : '',
  352. get_class($exception),
  353. $exception->getMessage(),
  354. $exception->getFile(),
  355. $exception->getLine()
  356. );
  357. $debug = Configure::read('debug');
  358. if ($debug && method_exists($exception, 'getAttributes')) {
  359. $attributes = $exception->getAttributes();
  360. if ($attributes) {
  361. $message .= "\nException Attributes: " . var_export($exception->getAttributes(), true);
  362. }
  363. }
  364. if (!empty($config['trace'])) {
  365. $message .= "\nStack Trace:\n" . $exception->getTraceAsString() . "\n\n";
  366. }
  367. $previous = $exception->getPrevious();
  368. if ($previous) {
  369. $message .= $this->getMessageForException($previous, true);
  370. }
  371. return $message;
  372. }
  373. /**
  374. * Map an error code into an Error word, and log location.
  375. *
  376. * @param int $code Error code to map
  377. * @return array Array of error word, and log location.
  378. */
  379. public static function mapErrorCode($code)
  380. {
  381. $levelMap = [
  382. E_PARSE => 'error',
  383. E_ERROR => 'error',
  384. E_CORE_ERROR => 'error',
  385. E_COMPILE_ERROR => 'error',
  386. E_USER_ERROR => 'error',
  387. E_WARNING => 'warning',
  388. E_USER_WARNING => 'warning',
  389. E_COMPILE_WARNING => 'warning',
  390. E_RECOVERABLE_ERROR => 'warning',
  391. E_NOTICE => 'notice',
  392. E_USER_NOTICE => 'notice',
  393. E_STRICT => 'strict',
  394. E_DEPRECATED => 'deprecated',
  395. E_USER_DEPRECATED => 'deprecated',
  396. ];
  397. $logMap = [
  398. 'error' => LOG_ERR,
  399. 'warning' => LOG_WARNING,
  400. 'notice' => LOG_NOTICE,
  401. 'strict' => LOG_NOTICE,
  402. 'deprecated' => LOG_NOTICE,
  403. ];
  404. $error = $levelMap[$code];
  405. $log = $logMap[$error];
  406. return [ucfirst($error), $log];
  407. }
  408. }