1 <?php
2 
3 namespace dokuwiki;
4 
5 use dokuwiki\Exception\FatalException;
6 
7 /**
8  * Manage the global handling of errors and exceptions
9  *
10  * Developer may use this to log and display exceptions themselves
11  */
12 class ErrorHandler
13 {
14     /**
15      * Standard error codes used in PHP errors
16      * @see https://www.php.net/manual/en/errorfunc.constants.php
17      */
18     protected const ERRORCODES = [
19         1 => 'E_ERROR',
20         2 => 'E_WARNING',
21         4 => 'E_PARSE',
22         8 => 'E_NOTICE',
23         16 => 'E_CORE_ERROR',
24         32 => 'E_CORE_WARNING',
25         64 => 'E_COMPILE_ERROR',
26         128 => 'E_COMPILE_WARNING',
27         256 => 'E_USER_ERROR',
28         512 => 'E_USER_WARNING',
29         1024 => 'E_USER_NOTICE',
30         2048 => 'E_STRICT',
31         4096 => 'E_RECOVERABLE_ERROR',
32         8192 => 'E_DEPRECATED',
33         16384 => 'E_USER_DEPRECATED',
34     ];
35 
36     /**
37      * Register the default error handling
38      */
39     public static function register()
40     {
41         if (!defined('DOKU_UNITTEST')) {
42             set_exception_handler([ErrorHandler::class, 'fatalException']);
43             register_shutdown_function([ErrorHandler::class, 'fatalShutdown']);
44             set_error_handler(
45                 [ErrorHandler::class, 'errorHandler'],
46                 E_WARNING | E_USER_ERROR | E_USER_WARNING | E_RECOVERABLE_ERROR
47             );
48         }
49     }
50 
51     /**
52      * Default Exception handler to show a nice user message before dieing
53      *
54      * The exception is logged to the error log
55      *
56      * @param \Throwable $e
57      */
58     public static function fatalException($e)
59     {
60         $plugin = self::guessPlugin($e);
61         $title = hsc(get_class($e) . ': ' . $e->getMessage());
62         $msg = 'An unforeseen error has occured. This is most likely a bug somewhere.';
63         if ($plugin) $msg .= ' It might be a problem in the ' . $plugin . ' plugin.';
64         $logged = self::logException($e)
65             ? 'More info has been written to the DokuWiki error log.'
66             : $e->getFile() . ':' . $e->getLine();
67 
68         echo <<<EOT
69 <!DOCTYPE html>
70 <html>
71 <head><title>$title</title></head>
72 <body style="font-family: Arial, sans-serif">
73     <div style="width:60%; margin: auto; background-color: #fcc;
74                 border: 1px solid #faa; padding: 0.5em 1em;">
75         <h1 style="font-size: 120%">$title</h1>
76         <p>$msg</p>
77         <p>$logged</p>
78     </div>
79 </body>
80 </html>
81 EOT;
82     }
83 
84     /**
85      * Convenience method to display an error message for the given Exception
86      *
87      * @param \Throwable $e
88      * @param string $intro
89      */
90     public static function showExceptionMsg($e, $intro = 'Error!')
91     {
92         $msg = hsc($intro) . '<br />' . hsc(get_class($e) . ': ' . $e->getMessage());
93         if (self::logException($e)) $msg .= '<br />More info is available in the error log.';
94         msg($msg, -1);
95     }
96 
97     /**
98      * Last resort to handle fatal errors that still can't be caught
99      */
100     public static function fatalShutdown()
101     {
102         $error = error_get_last();
103         // Check if it's a core/fatal error, otherwise it's a normal shutdown
104         if (
105             $error !== null &&
106             in_array(
107                 $error['type'],
108                 [
109                     E_ERROR,
110                     E_CORE_ERROR,
111                     E_COMPILE_ERROR,
112                 ]
113             )
114         ) {
115             self::fatalException(
116                 new FatalException($error['message'], 0, $error['type'], $error['file'], $error['line'])
117             );
118         }
119     }
120 
121     /**
122      * Log the given exception to the error log
123      *
124      * @param \Throwable $e
125      * @return bool false if the logging failed
126      */
127     public static function logException($e)
128     {
129         if ($e instanceof \ErrorException) {
130             $prefix = self::ERRORCODES[$e->getSeverity()];
131         } else {
132             $prefix = get_class($e);
133         }
134 
135         return Logger::getInstance()->log(
136             $prefix . ': ' . $e->getMessage(),
137             $e->getTraceAsString(),
138             $e->getFile(),
139             $e->getLine()
140         );
141     }
142 
143     /**
144      * Error handler to log non-exception errors
145      *
146      * @param int $errno
147      * @param string $errstr
148      * @param string $errfile
149      * @param int $errline
150      * @return bool
151      */
152     public static function errorHandler($errno, $errstr, $errfile, $errline)
153     {
154         global $conf;
155 
156         // ignore supressed warnings
157         if (!(error_reporting() & $errno)) return false;
158 
159         $ex = new \ErrorException(
160             $errstr,
161             0,
162             $errno,
163             $errfile,
164             $errline
165         );
166         self::logException($ex);
167 
168         if ($ex->getSeverity() === E_WARNING && $conf['hidewarnings']) {
169             return true;
170         }
171 
172         return false;
173     }
174 
175     /**
176      * Checks the the stacktrace for plugin files
177      *
178      * @param \Throwable $e
179      * @return false|string
180      */
181     protected static function guessPlugin($e)
182     {
183         if (preg_match('/lib\/plugins\/(\w+)\//', str_replace('\\', '/', $e->getFile()), $match)) {
184             return $match[1];
185         }
186 
187         foreach ($e->getTrace() as $line) {
188             if (
189                 isset($line['class']) &&
190                 preg_match('/\w+?_plugin_(\w+)/', $line['class'], $match)
191             ) {
192                 return $match[1];
193             }
194 
195             if (
196                 isset($line['file']) &&
197                 preg_match('/lib\/plugins\/(\w+)\//', str_replace('\\', '/', $line['file']), $match)
198             ) {
199                 return $match[1];
200             }
201         }
202 
203         return false;
204     }
205 }
206