BaseErrorHandler.php 12 KB

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