1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14:
15: namespace Cake\Error;
16:
17: use Cake\Core\Configure;
18: use Cake\Log\Log;
19: use Cake\Routing\Router;
20: use Error;
21: use Exception;
22:
23: 24: 25: 26: 27: 28: 29:
30: abstract class BaseErrorHandler
31: {
32: 33: 34: 35: 36:
37: protected $_options = [];
38:
39: 40: 41:
42: protected $_handled = false;
43:
44: 45: 46: 47: 48: 49: 50: 51: 52: 53:
54: abstract protected function _displayError($error, $debug);
55:
56: 57: 58: 59: 60: 61: 62: 63: 64:
65: abstract protected function _displayException($exception);
66:
67: 68: 69: 70: 71:
72: public function register()
73: {
74: $level = -1;
75: if (isset($this->_options['errorLevel'])) {
76: $level = $this->_options['errorLevel'];
77: }
78: error_reporting($level);
79: set_error_handler([$this, 'handleError'], $level);
80: set_exception_handler([$this, 'wrapAndHandleException']);
81: register_shutdown_function(function () {
82: if ((PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') && $this->_handled) {
83: return;
84: }
85: $megabytes = Configure::read('Error.extraFatalErrorMemory');
86: if ($megabytes === null) {
87: $megabytes = 4;
88: }
89: if ($megabytes > 0) {
90: $this->increaseMemoryLimit($megabytes * 1024);
91: }
92: $error = error_get_last();
93: if (!is_array($error)) {
94: return;
95: }
96: $fatals = [
97: E_USER_ERROR,
98: E_ERROR,
99: E_PARSE,
100: ];
101: if (!in_array($error['type'], $fatals, true)) {
102: return;
103: }
104: $this->handleFatalError(
105: $error['type'],
106: $error['message'],
107: $error['file'],
108: $error['line']
109: );
110: });
111: }
112:
113: 114: 115: 116: 117: 118: 119: 120: 121: 122: 123: 124: 125: 126: 127: 128: 129:
130: public function handleError($code, $description, $file = null, $line = null, $context = null)
131: {
132: if (error_reporting() === 0) {
133: return false;
134: }
135: $this->_handled = true;
136: list($error, $log) = static::mapErrorCode($code);
137: if ($log === LOG_ERR) {
138: return $this->handleFatalError($code, $description, $file, $line);
139: }
140: $data = [
141: 'level' => $log,
142: 'code' => $code,
143: 'error' => $error,
144: 'description' => $description,
145: 'file' => $file,
146: 'line' => $line,
147: ];
148:
149: $debug = Configure::read('debug');
150: if ($debug) {
151: $data += [
152: 'context' => $context,
153: 'start' => 3,
154: 'path' => Debugger::trimPath($file)
155: ];
156: }
157: $this->_displayError($data, $debug);
158: $this->_logError($log, $data);
159:
160: return true;
161: }
162:
163: 164: 165: 166: 167: 168: 169: 170:
171: public function wrapAndHandleException($exception)
172: {
173: if ($exception instanceof Error) {
174: $exception = new PHP7ErrorException($exception);
175: }
176: $this->handleException($exception);
177: }
178:
179: 180: 181: 182: 183: 184: 185: 186: 187: 188: 189:
190: public function handleException(Exception $exception)
191: {
192: $this->_displayException($exception);
193: $this->_logException($exception);
194: $this->_stop($exception->getCode() ?: 1);
195: }
196:
197: 198: 199: 200: 201: 202: 203: 204:
205: protected function _stop($code)
206: {
207:
208: }
209:
210: 211: 212: 213: 214: 215: 216: 217: 218:
219: public function handleFatalError($code, $description, $file, $line)
220: {
221: $data = [
222: 'code' => $code,
223: 'description' => $description,
224: 'file' => $file,
225: 'line' => $line,
226: 'error' => 'Fatal Error',
227: ];
228: $this->_logError(LOG_ERR, $data);
229:
230: $this->handleException(new FatalErrorException($description, 500, $file, $line));
231:
232: return true;
233: }
234:
235: 236: 237: 238: 239: 240: 241:
242: public function increaseMemoryLimit($additionalKb)
243: {
244: $limit = ini_get('memory_limit');
245: if (!strlen($limit) || $limit === '-1') {
246: return;
247: }
248: $limit = trim($limit);
249: $units = strtoupper(substr($limit, -1));
250: $current = (int)substr($limit, 0, strlen($limit) - 1);
251: if ($units === 'M') {
252: $current *= 1024;
253: $units = 'K';
254: }
255: if ($units === 'G') {
256: $current = $current * 1024 * 1024;
257: $units = 'K';
258: }
259:
260: if ($units === 'K') {
261: ini_set('memory_limit', ceil($current + $additionalKb) . 'K');
262: }
263: }
264:
265: 266: 267: 268: 269: 270: 271:
272: protected function _logError($level, $data)
273: {
274: $message = sprintf(
275: '%s (%s): %s in [%s, line %s]',
276: $data['error'],
277: $data['code'],
278: $data['description'],
279: $data['file'],
280: $data['line']
281: );
282: if (!empty($this->_options['trace'])) {
283: $trace = Debugger::trace([
284: 'start' => 1,
285: 'format' => 'log'
286: ]);
287:
288: $request = Router::getRequest();
289: if ($request) {
290: $message .= $this->_requestContext($request);
291: }
292: $message .= "\nTrace:\n" . $trace . "\n";
293: }
294: $message .= "\n\n";
295:
296: return Log::write($level, $message);
297: }
298:
299: 300: 301: 302: 303: 304:
305: protected function _logException(Exception $exception)
306: {
307: $config = $this->_options;
308: $unwrapped = $exception instanceof PHP7ErrorException ?
309: $exception->getError() :
310: $exception;
311:
312: if (empty($config['log'])) {
313: return false;
314: }
315:
316: if (!empty($config['skipLog'])) {
317: foreach ((array)$config['skipLog'] as $class) {
318: if ($unwrapped instanceof $class) {
319: return false;
320: }
321: }
322: }
323:
324: return Log::error($this->_getMessage($exception));
325: }
326:
327: 328: 329: 330: 331: 332:
333: protected function _requestContext($request)
334: {
335: $message = "\nRequest URL: " . $request->getRequestTarget();
336:
337: $referer = $request->getEnv('HTTP_REFERER');
338: if ($referer) {
339: $message .= "\nReferer URL: " . $referer;
340: }
341: $clientIp = $request->clientIp();
342: if ($clientIp && $clientIp !== '::1') {
343: $message .= "\nClient IP: " . $clientIp;
344: }
345:
346: return $message;
347: }
348:
349: 350: 351: 352: 353: 354:
355: protected function _getMessage(Exception $exception)
356: {
357: $message = $this->getMessageForException($exception);
358:
359: $request = Router::getRequest();
360: if ($request) {
361: $message .= $this->_requestContext($request);
362: }
363:
364: return $message;
365: }
366:
367: 368: 369: 370: 371: 372: 373:
374: protected function getMessageForException($exception, $isPrevious = false)
375: {
376: $exception = $exception instanceof PHP7ErrorException ?
377: $exception->getError() :
378: $exception;
379: $config = $this->_options;
380:
381: $message = sprintf(
382: '%s[%s] %s in %s on line %s',
383: $isPrevious ? "\nCaused by: " : '',
384: get_class($exception),
385: $exception->getMessage(),
386: $exception->getFile(),
387: $exception->getLine()
388: );
389: $debug = Configure::read('debug');
390:
391: if ($debug && method_exists($exception, 'getAttributes')) {
392: $attributes = $exception->getAttributes();
393: if ($attributes) {
394: $message .= "\nException Attributes: " . var_export($exception->getAttributes(), true);
395: }
396: }
397:
398: if (!empty($config['trace'])) {
399: $message .= "\nStack Trace:\n" . $exception->getTraceAsString() . "\n\n";
400: }
401:
402: $previous = $exception->getPrevious();
403: if ($previous) {
404: $message .= $this->getMessageForException($previous, true);
405: }
406:
407: return $message;
408: }
409:
410: 411: 412: 413: 414: 415:
416: public static function mapErrorCode($code)
417: {
418: $levelMap = [
419: E_PARSE => 'error',
420: E_ERROR => 'error',
421: E_CORE_ERROR => 'error',
422: E_COMPILE_ERROR => 'error',
423: E_USER_ERROR => 'error',
424: E_WARNING => 'warning',
425: E_USER_WARNING => 'warning',
426: E_COMPILE_WARNING => 'warning',
427: E_RECOVERABLE_ERROR => 'warning',
428: E_NOTICE => 'notice',
429: E_USER_NOTICE => 'notice',
430: E_STRICT => 'strict',
431: E_DEPRECATED => 'deprecated',
432: E_USER_DEPRECATED => 'deprecated',
433: ];
434: $logMap = [
435: 'error' => LOG_ERR,
436: 'warning' => LOG_WARNING,
437: 'notice' => LOG_NOTICE,
438: 'strict' => LOG_NOTICE,
439: 'deprecated' => LOG_NOTICE,
440: ];
441:
442: $error = $levelMap[$code];
443: $log = $logMap[$error];
444:
445: return [ucfirst($error), $log];
446: }
447: }
448: