Passed
Push — master ( 437a9c...91e741 )
by Andreas
12:09
created

midcom_exception_handler::process_actions()   A

Complexity

Conditions 5
Paths 7

Size

Total Lines 22
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
cc 5
eloc 11
nc 7
nop 3
dl 0
loc 22
ccs 0
cts 12
cp 0
crap 30
rs 9.6111
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
use Monolog\Logger;
11
use Monolog\Handler\StreamHandler;
12
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
13
use Symfony\Component\HttpKernel\KernelEvents;
14
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
15
use Symfony\Component\HttpFoundation\ServerBag;
16
17
/**
18
 * Class for intercepting PHP errors and unhandled exceptions. Each fault is caught
19
 * and converted into Exception handled by midcom_exception_handler::show() with
20
 * code 500 thus can be customized and make user friendly.
21
 *
22
 * @package midcom
23
 */
24
class midcom_exception_handler implements EventSubscriberInterface
25
{
26
    private Throwable $error;
27
28
    public static function getSubscribedEvents()
29
    {
30
        return [
31
            KernelEvents::EXCEPTION => ['handle']
32
        ];
33
    }
34
35
    /**
36
     * Render an error response.
37
     *
38
     * This will display a simple HTML Page reporting the error described by $httpcode and $message.
39
     * The $httpcode is also used to send an appropriate HTTP Response.
40
     *
41
     * The error pages can be customized by creating style elements named midcom_error_$httpcode.
42
     *
43
     * For a list of the allowed HTTP codes see the Response::HTTP_... constants
44
     */
45
    public function handle(ExceptionEvent $event)
46
    {
47
        $this->error = $event->getThrowable();
48
49
        $httpcode = $this->error->getCode();
50
        $message = $this->error->getMessage();
51
        debug_print_r('Exception occurred: ' . $httpcode . ', Message: ' . $message . ', exception trace:', $this->error->getTraceAsString());
52
53
        if (!array_key_exists($httpcode, Response::$statusTexts)) {
54
            debug_add("Unknown Errorcode {$httpcode} encountered, assuming 500");
55
            $httpcode = Response::HTTP_INTERNAL_SERVER_ERROR;
56
        }
57
58
        // Send error to special log or recipient as per configuration.
59
        $this->process_actions($event->getRequest()->server, $httpcode, $message);
60
61
        if (PHP_SAPI === 'cli') {
62
            throw $this->error;
63
        }
64
65
        $event->allowCustomResponseCode();
66
        $event->setResponse($this->process($httpcode, $message));
67
    }
68
69
    private function process(int $httpcode, string $message) : Response
70
    {
71
72
        if ($httpcode == Response::HTTP_FORBIDDEN) {
73
            return new midcom_response_accessdenied($message);
74
        }
75
        if ($httpcode == Response::HTTP_UNAUTHORIZED) {
76
            if ($this->error instanceof midcom_error_forbidden) {
77
                return new midcom_response_login($this->error->get_method());
0 ignored issues
show
Bug introduced by
The method get_method() does not exist on Throwable. It seems like you code against a sub-type of Throwable such as midcom_error_forbidden. ( Ignorable by Annotation )

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

77
                return new midcom_response_login($this->error->/** @scrutinizer ignore-call */ get_method());
Loading history...
78
            }
79
80
            return new midcom_response_login;
81
        }
82
83
        $style = midcom::get()->style;
84
85
        $style->data['error_title'] = Response::$statusTexts[$httpcode];
86
        $style->data['error_message'] = $message;
87
        $style->data['error_code'] = $httpcode;
88
        $style->data['error_exception'] = $this->error;
89
        $style->data['error_handler'] = $this;
90
91
        ob_start();
92
        if (!$style->show_midcom('midcom_error_' . $httpcode)) {
93
            $style->show_midcom('midcom_error');
94
        }
95
        $content = ob_get_clean();
96
97
        return new Response($content, $httpcode);
98
    }
99
100
    /**
101
     * Send error for processing.
102
     *
103
     * If the given error code has an action configured for it, that action will be
104
     * performed. This means that system administrators can request email notifications
105
     * of 500 "Internal Errors" and a special log of 404 "Not Founds".
106
     */
107
    private function process_actions(ServerBag $server, int $httpcode, string $message)
108
    {
109
        $error_actions = midcom::get()->config->get_array('error_actions');
110
        if (!isset($error_actions[$httpcode]['action'])) {
111
            // No action specified for this error code, skip
112
            return;
113
        }
114
115
        // Prepare the message
116
        $msg = "{$server->getString('REQUEST_METHOD')} request to {$server->getString('REQUEST_URI')}: ";
117
        $msg .= "{$httpcode} {$message}\n";
118
        if ($server->has('HTTP_REFERER')) {
119
            $msg .= "(Referrer: {$server->getString('HTTP_REFERER')})\n";
120
        }
121
122
        // Send as email handler
123
        if ($error_actions[$httpcode]['action'] == 'email') {
124
            $this->_send_email($msg, $error_actions[$httpcode], $server->getString('SERVER_NAME'));
125
        }
126
        // Append to log file handler
127
        elseif ($error_actions[$httpcode]['action'] == 'log') {
128
            $this->_log($msg, $error_actions[$httpcode]);
129
        }
130
    }
131
132
    private function _log(string $msg, array $config)
133
    {
134
        if (empty($config['filename'])) {
135
            // No log file specified, skip
136
            return;
137
        }
138
139
        if (   !is_writable($config['filename'])
140
            && !is_writable(dirname($config['filename']))) {
141
            debug_add("Error logging file {$config['filename']} is not writable", MIDCOM_LOG_WARN);
142
            return;
143
        }
144
145
        // Add the line to the error-specific log
146
        $logger = new Logger(__CLASS__);
147
        $logger->pushHandler(new StreamHandler($config['filename']));
148
        $logger = new midcom_debug($logger);
149
        $logger->log($msg, MIDCOM_LOG_INFO);
150
    }
151
152
    private function _send_email(string $msg, array $config, string $servername)
153
    {
154
        if (empty($config['email'])) {
155
            // No recipient specified, skip
156
            return;
157
        }
158
159
        if (!midcom::get()->componentloader->is_installed('org.openpsa.mail')) {
160
            debug_add("Email sending library org.openpsa.mail, used for error notifications is not installed", MIDCOM_LOG_WARN);
161
            return;
162
        }
163
164
        $mail = new org_openpsa_mail();
165
        $mail->to = $config['email'];
166
        $mail->from = "\"MidCOM error notifier\" <webmaster@{$servername}>";
167
        $mail->subject = "[{$servername}] " . str_replace("\n", ' ', $msg);
168
        $mail->body = "{$servername}:\n{$msg}";
169
170
        $stacktrace = $this->get_function_stack();
171
172
        $mail->body .= "\n" . implode("\n", $stacktrace);
173
174
        if (!$mail->send()) {
175
            debug_add("failed to send error notification email to {$mail->to}, reason: " . $mail->get_error_message(), MIDCOM_LOG_WARN);
176
        }
177
    }
178
179
    public function get_function_stack()
180
    {
181
        $stack = $this->error->getTrace();
182
183
        $stacktrace = [];
184
        foreach ($stack as $number => $frame) {
185
            $line = $number + 1;
186
            if (array_key_exists('file', $frame)) {
187
                $file = str_replace(MIDCOM_ROOT, '[midcom_root]', $frame['file']);
188
                $line .= ": {$file}:{$frame['line']}  ";
189
            } else {
190
                $line .= ': [internal]  ';
191
            }
192
            if (array_key_exists('class', $frame)) {
193
                $line .= $frame['class'];
194
                if (array_key_exists('type', $frame)) {
195
                    $line .= $frame['type'];
196
                } else {
197
                    $line .= '::';
198
                }
199
            }
200
            if (array_key_exists('function', $frame)) {
201
                $line .= $frame['function'];
202
            } else {
203
                $line .= 'require, include or eval';
204
            }
205
            $stacktrace[] = $line;
206
        }
207
208
        return $stacktrace;
209
    }
210
}
211