Passed
Push — master ( 52d6d0...ade603 )
by Andreas
33:03
created

midcom_exception_handler::set_kernel()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
ccs 0
cts 2
cp 0
crap 2
rs 10
c 0
b 0
f 0
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\HttpFoundation\Response;
10
11
/**
12
 * Class for intercepting PHP errors and unhandled exceptions. Each fault is caught
13
 * and converted into Exception handled by midcom_exception_handler::show() with
14
 * code 500 thus can be customized and make user friendly.
15
 *
16
 * @package midcom
17
 */
18
class midcom_exception_handler
19
{
20
    /**
21
     * Holds the current exception
22
     *
23
     * @var Exception
24
     */
25
    private $_exception;
26
27
    /**
28
     * Register the error and Exception handlers
29
     */
30 1
    public static function register()
31
    {
32 1
        if (!defined('OPENPSA2_UNITTEST_RUN')) {
33
            set_exception_handler([new self, 'handle_exception']);
34
        }
35 1
    }
36
37
    /**
38
     * This is mostly there because symfony doesn't catch Errors for some reason
39
     *
40
     * @param Throwable $error
41
     * @throws Exception
42
     */
43
    public function handle_exception($error)
44
    {
45
        if ($error instanceof Error) {
46
            midcom::get()->getHttpkernel()->terminateWithException($error);
0 ignored issues
show
Bug introduced by
The method terminateWithException() does not exist on Symfony\Component\HttpKernel\HttpKernelInterface. It seems like you code against a sub-type of Symfony\Component\HttpKernel\HttpKernelInterface such as Symfony\Component\HttpKernel\HttpKernel. ( Ignorable by Annotation )

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

46
            midcom::get()->getHttpkernel()->/** @scrutinizer ignore-call */ terminateWithException($error);
Loading history...
47
        } else {
48
            throw $error;
49
        }
50
    }
51
52
    /**
53
     * Render an error response.
54
     *
55
     * This will display a simple HTML Page reporting the error described by $httpcode and $message.
56
     * The $httpcode is also used to send an appropriate HTTP Response.
57
     *
58
     * The error pages can be customized by creating style elements named midcom_error_$httpcode.
59
     *
60
     * For a list of the allowed HTTP codes see the MIDCOM_ERR... constants
61
     *
62
     * @param Exception $e The exception we're working with
63
     */
64
    public function render($e)
65
    {
66
        $this->_exception = $e;
67
        $httpcode = $e->getCode();
68
        $message = $e->getMessage();
69
        debug_print_r('Exception occurred: ' . $httpcode . ', Message: ' . $message . ', exception trace:', $e->getTraceAsString());
70
71
        if (!in_array($httpcode, [MIDCOM_ERROK, MIDCOM_ERRNOTFOUND, MIDCOM_ERRFORBIDDEN, MIDCOM_ERRAUTH, MIDCOM_ERRCRIT])) {
72
            debug_add("Unknown Errorcode {$httpcode} encountered, assuming 500");
73
            $httpcode = MIDCOM_ERRCRIT;
74
        }
75
76
        // Send error to special log or recipient as per configuration.
77
        $this->send($httpcode, $message);
78
79
        if (PHP_SAPI === 'cli') {
80
            throw $e;
81
        }
82
83
        if ($httpcode == MIDCOM_ERRFORBIDDEN) {
84
            return new midcom_response_accessdenied($message);
85
        }
86
        if ($httpcode == MIDCOM_ERRAUTH) {
87
            if ($e instanceof midcom_error_forbidden) {
88
                return new midcom_response_login($e->get_method());
89
            }
90
91
            return new midcom_response_login;
92
        }
93
94
        switch ($httpcode) {
95
            case MIDCOM_ERROK:
96
                $title = "OK";
97
                break;
98
99
            case MIDCOM_ERRNOTFOUND:
100
                $title = "Not Found";
101
                break;
102
103
            case MIDCOM_ERRCRIT:
104
                $title = "Server Error";
105
                break;
106
        }
107
108
        $style = midcom::get()->style;
109
110
        $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...
111
        $style->data['error_message'] = $message;
112
        $style->data['error_code'] = $httpcode;
113
        $style->data['error_exception'] = $e;
114
        $style->data['error_handler'] = $this;
115
116
        ob_start();
117
        if (!$style->show_midcom('midcom_error_' . $httpcode)) {
118
            $style->show_midcom('midcom_error');
119
        }
120
        $content = ob_get_clean();
121
122
        return new Response($content, $httpcode);
123
    }
124
125
    /**
126
     * Send error for processing.
127
     *
128
     * If the given error code has an action configured for it, that action will be
129
     * performed. This means that system administrators can request email notifications
130
     * of 500 "Internal Errors" and a special log of 404 "Not Founds".
131
     *
132
     * @param int $httpcode        The error code to send.
133
     * @param string $message    The message to print.
134
     */
135
    private function send($httpcode, $message)
136
    {
137
        $error_actions = midcom::get()->config->get('error_actions');
138
        if (   !isset($error_actions[$httpcode])
139
            || !isset($error_actions[$httpcode]['action'])) {
140
            // No action specified for this error code, skip
141
            return;
142
        }
143
144
        // Prepare the message
145
        $msg = "{$_SERVER['REQUEST_METHOD']} request to {$_SERVER['REQUEST_URI']}: ";
146
        $msg .= "{$httpcode} {$message}\n";
147
        if (isset($_SERVER['HTTP_REFERER'])) {
148
            $msg .= "(Referrer: {$_SERVER['HTTP_REFERER']})\n";
149
        }
150
151
        // Send as email handler
152
        if ($error_actions[$httpcode]['action'] == 'email') {
153
            $this->_send_email($msg, $error_actions[$httpcode]);
154
        }
155
        // Append to log file handler
156
        elseif ($error_actions[$httpcode]['action'] == 'log') {
157
            $this->_log($msg, $error_actions[$httpcode]);
158
        }
159
    }
160
161
    private function _log(string $msg, array $config)
162
    {
163
        if (empty($config['filename'])) {
164
            // No log file specified, skip
165
            return;
166
        }
167
168
        if (   !is_writable($config['filename'])
169
            && !is_writable(dirname($config['filename']))) {
170
            debug_add("Error logging file {$config['filename']} is not writable", MIDCOM_LOG_WARN);
171
            return;
172
        }
173
174
        // Add the line to the error-specific log
175
        $logger = new midcom_debug($config['filename']);
176
        $logger->set_loglevel(MIDCOM_LOG_INFO);
177
        $logger->log($msg, MIDCOM_LOG_INFO);
178
    }
179
180
    private function _send_email(string $msg, array $config)
181
    {
182
        if (empty($config['email'])) {
183
            // No recipient specified, skip
184
            return;
185
        }
186
187
        if (!midcom::get()->componentloader->is_installed('org.openpsa.mail')) {
188
            debug_add("Email sending library org.openpsa.mail, used for error notifications is not installed", MIDCOM_LOG_WARN);
189
            return;
190
        }
191
192
        $mail = new org_openpsa_mail();
193
        $mail->to = $config['email'];
194
        $mail->from = "\"MidCOM error notifier\" <webmaster@{$_SERVER['SERVER_NAME']}>";
195
        $mail->subject = "[{$_SERVER['SERVER_NAME']}] {$msg}";
196
        $mail->body = "{$_SERVER['SERVER_NAME']}:\n{$msg}";
197
198
        $stacktrace = $this->get_function_stack();
199
200
        $mail->body .= "\n" . implode("\n", $stacktrace);
201
202
        if (!$mail->send()) {
203
            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

203
            debug_add("failed to send error notification email to {$mail->to}, reason: " . /** @scrutinizer ignore-type */ $mail->get_error_message(), MIDCOM_LOG_WARN);
Loading history...
204
        }
205
    }
206
207
    public function get_function_stack()
208
    {
209
        if ($this->_exception) {
210
            $stack = $this->_exception->getTrace();
211
        } elseif (function_exists('xdebug_get_function_stack')) {
212
            $stack = xdebug_get_function_stack();
213
        } else {
214
            $stack = array_reverse(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS));
215
        }
216
217
        $stacktrace = [];
218
        foreach ($stack as $number => $frame) {
219
            $line = $number + 1;
220
            if (array_key_exists('file', $frame)) {
221
                $file = str_replace(MIDCOM_ROOT, '[midcom_root]', $frame['file']);
222
                $line .= ": {$file}:{$frame['line']}  ";
223
            } else {
224
                $line .= ': [internal]  ';
225
            }
226
            if (array_key_exists('class', $frame)) {
227
                $line .= $frame['class'];
228
                if (array_key_exists('type', $frame)) {
229
                    $line .= $frame['type'];
230
                } else {
231
                    $line .= '::';
232
                }
233
            }
234
            if (array_key_exists('function', $frame)) {
235
                $line .= $frame['function'];
236
            } else {
237
                $line .= 'require, include or eval';
238
            }
239
            $stacktrace[] = $line;
240
        }
241
242
        return $stacktrace;
243
    }
244
}
245