Passed
Push — master ( 6bc8e4...08f33e )
by Andreas
30:22
created

midcom_exception_handler   A

Complexity

Total Complexity 31

Size/Duplication

Total Lines 196
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 4
Bugs 0 Features 0
Metric Value
eloc 95
c 4
b 0
f 0
dl 0
loc 196
ccs 0
cts 131
cp 0
rs 9.92
wmc 31

6 Methods

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

180
            debug_add("failed to send error notification email to {$mail->to}, reason: " . /** @scrutinizer ignore-type */ $mail->get_error_message(), MIDCOM_LOG_WARN);
Loading history...
181
        }
182
    }
183
184
    public function get_function_stack()
185
    {
186
        $stack = $this->error->getTrace();
187
188
        $stacktrace = [];
189
        foreach ($stack as $number => $frame) {
190
            $line = $number + 1;
191
            if (array_key_exists('file', $frame)) {
192
                $file = str_replace(MIDCOM_ROOT, '[midcom_root]', $frame['file']);
193
                $line .= ": {$file}:{$frame['line']}  ";
194
            } else {
195
                $line .= ': [internal]  ';
196
            }
197
            if (array_key_exists('class', $frame)) {
198
                $line .= $frame['class'];
199
                if (array_key_exists('type', $frame)) {
200
                    $line .= $frame['type'];
201
                } else {
202
                    $line .= '::';
203
                }
204
            }
205
            if (array_key_exists('function', $frame)) {
206
                $line .= $frame['function'];
207
            } else {
208
                $line .= 'require, include or eval';
209
            }
210
            $stacktrace[] = $line;
211
        }
212
213
        return $stacktrace;
214
    }
215
}
216