Completed
Push — master ( 8849ee...a1b70f )
by Andreas
24:35
created

midcom_exception_handler   A

Complexity

Total Complexity 39

Size/Duplication

Total Lines 253
Duplicated Lines 0 %

Test Coverage

Coverage 13.11%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 116
dl 0
loc 253
ccs 16
cts 122
cp 0.1311
rs 9.28
c 2
b 0
f 0
wmc 39

9 Methods

Rating   Name   Duplication   Size   Complexity  
A _log() 0 17 4
A _send_email() 0 24 4
B get_function_stack() 0 36 8
A handle_error() 0 9 2
A handle_exception() 0 7 2
B render() 0 59 10
A send() 0 23 6
A register() 0 7 2
A set_kernel() 0 3 1
1
<?php
2
/**
3
 * @package midcom
4
 * @author The Midgard Project, http://www.midgard-project.org
5
 * @copyright The Midgard Project, http://www.midgard-project.org
6
 * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
7
 */
8
9
use Symfony\Component\HttpKernel\HttpKernel;
10
use Symfony\Component\Debug\Exception\FatalErrorException;
11
use Symfony\Component\HttpFoundation\Response;
12
13
/**
14
 * Class for intercepting PHP errors and unhandled exceptions. Each fault is caught
15
 * and converted into Exception handled by midcom_exception_handler::show() with
16
 * code 500 thus can be customized and make user friendly.
17
 *
18
 * @package midcom
19
 */
20
class midcom_exception_handler
21
{
22
    /**
23
     * Holds the current exception
24
     *
25
     * @var Exception
26
     */
27
    private $_exception;
28
29
    /**
30
     * @var HttpKernel
31
     */
32
    private $kernel;
33
34
    /**
35
     * Register the error and Exception handlers
36
     */
37
    public static function register(HttpKernel $kernel)
38
    {
39
        if (!defined('OPENPSA2_UNITTEST_RUN')) {
40
            $handler = new self;
41
            $handler->set_kernel($kernel);
42
            set_error_handler([$handler, 'handle_error'], E_ALL ^ (E_NOTICE | E_WARNING));
43
            set_exception_handler([$handler, 'handle_exception']);
44
        }
45
    }
46
47
    public function set_kernel(HttpKernel $kernel)
48
    {
49
        $this->kernel = $kernel;
50
    }
51
52
    /**
53
     * This is mostly there because symfony doesn't catch Errors for some reason
54
     *
55
     * @param Throwable $error
56
     * @throws Exception
57
     */
58
    public function handle_exception($error)
59
    {
60
        if ($error instanceof Error) {
61
            $exception = new FatalErrorException($error->getMessage(), $error->getCode(), 0, $error->getFile(), $error->getLine(), null, true, $error->getTrace());
62
            $this->kernel->terminateWithException($exception);
63
        } else {
64
            throw $error;
65
        }
66
    }
67
68
    /**
69
     * Catch a PHP error and turn it into an Exception to unify error handling
70
     */
71
    public function handle_error($errno, $errstr, $errfile, $errline, $errcontext)
72
    {
73
        $msg = "PHP Error: {$errstr} \n in {$errfile} line {$errline}";
74
        if (!empty($errcontext)) {
75
            debug_print_r('Error context', $errcontext);
76
        }
77
78
        // PONDER: use throw new ErrorException($errstr, 0, $errno, $errfile, $errline); instead?
79
        throw new midcom_error($msg, $errno);
80
    }
81
82
    /**
83
     * Render an error response.
84
     *
85
     * This will display a simple HTML Page reporting the error described by $httpcode and $message.
86
     * The $httpcode is also used to send an appropriate HTTP Response.
87
     *
88
     * The error pages can be customized by creating style elements named midcom_error_$httpcode.
89
     *
90
     * For a list of the allowed HTTP codes see the MIDCOM_ERR... constants
91
     *
92
     * @param Exception $e The exception we're working with
93
     */
94 1
    public function render($e)
95
    {
96 1
        $this->_exception = $e;
97 1
        $httpcode = $e->getCode();
98 1
        $message = $e->getMessage();
99 1
        debug_print_r('Exception occurred: ' . $httpcode . ', Message: ' . $message . ', exception trace:', $e->getTraceAsString());
100
101 1
        if (!in_array($httpcode, [MIDCOM_ERROK, MIDCOM_ERRNOTFOUND, MIDCOM_ERRFORBIDDEN, MIDCOM_ERRAUTH, MIDCOM_ERRCRIT])) {
102 1
            debug_add("Unknown Errorcode {$httpcode} encountered, assuming 500");
103 1
            $httpcode = MIDCOM_ERRCRIT;
104
        }
105
106
        // Send error to special log or recipient as per configuration.
107 1
        $this->send($httpcode, $message);
108
109 1
        if (PHP_SAPI === 'cli') {
110 1
            throw $e;
111
        }
112
113
        if ($httpcode == MIDCOM_ERRFORBIDDEN) {
114
            return new midcom_response_accessdenied($message);
115
        }
116
        if ($httpcode == MIDCOM_ERRAUTH) {
117
            if ($e instanceof midcom_error_forbidden) {
118
                return new midcom_response_login($e->get_method());
119
            }
120
121
            return new midcom_response_login;
122
        }
123
124
        switch ($httpcode) {
125
            case MIDCOM_ERROK:
126
                $title = "OK";
127
                break;
128
129
            case MIDCOM_ERRNOTFOUND:
130
                $title = "Not Found";
131
                break;
132
133
            case MIDCOM_ERRCRIT:
134
                $title = "Server Error";
135
                break;
136
        }
137
138
        $style = midcom::get()->style;
139
140
        $style->data['error_title'] = $title;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $title does not seem to be defined for all execution paths leading up to this point.
Loading history...
141
        $style->data['error_message'] = $message;
142
        $style->data['error_code'] = $httpcode;
143
        $style->data['error_exception'] = $e;
144
        $style->data['error_handler'] = $this;
145
146
        ob_start();
147
        if (!$style->show_midcom('midcom_error_' . $httpcode)) {
148
            $style->show_midcom('midcom_error');
149
        }
150
        $content = ob_get_clean();
151
152
        return new Response($content, $httpcode);
153
    }
154
155
    /**
156
     * Send error for processing.
157
     *
158
     * If the given error code has an action configured for it, that action will be
159
     * performed. This means that system administrators can request email notifications
160
     * of 500 "Internal Errors" and a special log of 404 "Not Founds".
161
     *
162
     * @param int $httpcode        The error code to send.
163
     * @param string $message    The message to print.
164
     */
165 1
    private function send($httpcode, $message)
166
    {
167 1
        $error_actions = midcom::get()->config->get('error_actions');
168 1
        if (   !isset($error_actions[$httpcode])
169 1
            || !isset($error_actions[$httpcode]['action'])) {
170
            // No action specified for this error code, skip
171 1
            return;
172
        }
173
174
        // Prepare the message
175
        $msg = "{$_SERVER['REQUEST_METHOD']} request to {$_SERVER['REQUEST_URI']}: ";
176
        $msg .= "{$httpcode} {$message}\n";
177
        if (isset($_SERVER['HTTP_REFERER'])) {
178
            $msg .= "(Referrer: {$_SERVER['HTTP_REFERER']})\n";
179
        }
180
181
        // Send as email handler
182
        if ($error_actions[$httpcode]['action'] == 'email') {
183
            $this->_send_email($msg, $error_actions[$httpcode]);
184
        }
185
        // Append to log file handler
186
        elseif ($error_actions[$httpcode]['action'] == 'log') {
187
            $this->_log($msg, $error_actions[$httpcode]);
188
        }
189
    }
190
191
    private function _log($msg, array $config)
192
    {
193
        if (empty($config['filename'])) {
194
            // No log file specified, skip
195
            return;
196
        }
197
198
        if (   !is_writable($config['filename'])
199
            && !is_writable(dirname($config['filename']))) {
200
            debug_add("Error logging file {$config['filename']} is not writable", MIDCOM_LOG_WARN);
201
            return;
202
        }
203
204
        // Add the line to the error-specific log
205
        $logger = new midcom_debug($config['filename']);
206
        $logger->set_loglevel(MIDCOM_LOG_INFO);
207
        $logger->log($msg, MIDCOM_LOG_INFO);
208
    }
209
210
    private function _send_email($msg, array $config)
211
    {
212
        if (empty($config['email'])) {
213
            // No recipient specified, skip
214
            return;
215
        }
216
217
        if (!midcom::get()->componentloader->is_installed('org.openpsa.mail')) {
218
            debug_add("Email sending library org.openpsa.mail, used for error notifications is not installed", MIDCOM_LOG_WARN);
219
            return;
220
        }
221
222
        $mail = new org_openpsa_mail();
223
        $mail->to = $config['email'];
224
        $mail->from = "\"MidCOM error notifier\" <webmaster@{$_SERVER['SERVER_NAME']}>";
225
        $mail->subject = "[{$_SERVER['SERVER_NAME']}] {$msg}";
226
        $mail->body = "{$_SERVER['SERVER_NAME']}:\n{$msg}";
227
228
        $stacktrace = $this->get_function_stack();
229
230
        $mail->body .= "\n" . implode("\n", $stacktrace);
231
232
        if (!$mail->send()) {
233
            debug_add("failed to send error notification email to {$mail->to}, reason: " . $mail->get_error_message(), MIDCOM_LOG_WARN);
0 ignored issues
show
Bug introduced by
Are you sure $mail->get_error_message() of type false|mixed|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

233
            debug_add("failed to send error notification email to {$mail->to}, reason: " . /** @scrutinizer ignore-type */ $mail->get_error_message(), MIDCOM_LOG_WARN);
Loading history...
234
        }
235
    }
236
237
    public function get_function_stack()
238
    {
239
        if ($this->_exception) {
240
            $stack = $this->_exception->getTrace();
241
        } elseif (function_exists('xdebug_get_function_stack')) {
242
            $stack = xdebug_get_function_stack();
243
        } else {
244
            $stack = array_reverse(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS));
245
        }
246
247
        $stacktrace = [];
248
        foreach ($stack as $number => $frame) {
249
            $line = $number + 1;
250
            if (array_key_exists('file', $frame)) {
251
                $file = str_replace(MIDCOM_ROOT, '[midcom_root]', $frame['file']);
252
                $line .= ": {$file}:{$frame['line']}  ";
253
            } else {
254
                $line .= ': [internal]  ';
255
            }
256
            if (array_key_exists('class', $frame)) {
257
                $line .= $frame['class'];
258
                if (array_key_exists('type', $frame)) {
259
                    $line .= $frame['type'];
260
                } else {
261
                    $line .= '::';
262
                }
263
            }
264
            if (array_key_exists('function', $frame)) {
265
                $line .= $frame['function'];
266
            } else {
267
                $line .= 'require, include or eval';
268
            }
269
            $stacktrace[] = $line;
270
        }
271
272
        return $stacktrace;
273
    }
274
}
275