midcom_exception_handler::process()   A
last analyzed

Complexity

Conditions 5
Paths 5

Size

Total Lines 26
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
cc 5
eloc 16
nc 5
nop 2
dl 0
loc 26
ccs 0
cts 17
cp 0
crap 30
rs 9.4222
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 function __construct(private array $error_actions, private midcom_helper_style $style)
29
    {}
30
31
    public static function getSubscribedEvents()
32
    {
33
        return [
34
            KernelEvents::EXCEPTION => ['handle']
35
        ];
36
    }
37
38
    /**
39
     * Render an error response.
40
     *
41
     * This will display a simple HTML Page reporting the error described by $httpcode and $message.
42
     * The $httpcode is also used to send an appropriate HTTP Response.
43
     *
44
     * The error pages can be customized by creating style elements named midcom_error_$httpcode.
45
     *
46
     * For a list of the allowed HTTP codes see the Response::HTTP_... constants
47
     */
48
    public function handle(ExceptionEvent $event)
49
    {
50
        $this->error = $event->getThrowable();
51
52
        $httpcode = $this->error->getCode();
53
        $message = $this->error->getMessage();
54
        debug_print_r('Exception occurred: ' . $httpcode . ', Message: ' . $message . ', exception trace:', $this->error->getTraceAsString());
55
56
        if (!array_key_exists($httpcode, Response::$statusTexts)) {
57
            debug_add("Unknown Errorcode {$httpcode} encountered, assuming 500");
58
            $httpcode = Response::HTTP_INTERNAL_SERVER_ERROR;
59
        }
60
61
        // Send error to special log or recipient as per configuration.
62
        $this->process_actions($event->getRequest()->server, $httpcode, $message);
63
64
        if (PHP_SAPI === 'cli') {
65
            throw $this->error;
66
        }
67
68
        $event->allowCustomResponseCode();
69
        $event->setResponse($this->process($httpcode, $message));
70
    }
71
72
    private function process(int $httpcode, string $message) : Response
73
    {
74
        if ($httpcode == Response::HTTP_FORBIDDEN) {
75
            return new midcom_response_accessdenied($message);
76
        }
77
        if ($httpcode == Response::HTTP_UNAUTHORIZED) {
78
            if ($this->error instanceof midcom_error_forbidden) {
79
                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

79
                return new midcom_response_login($this->error->/** @scrutinizer ignore-call */ get_method());
Loading history...
80
            }
81
82
            return new midcom_response_login;
83
        }
84
85
        $this->style->data['error_title'] = Response::$statusTexts[$httpcode];
86
        $this->style->data['error_message'] = $message;
87
        $this->style->data['error_code'] = $httpcode;
88
        $this->style->data['error_exception'] = $this->error;
89
        $this->style->data['error_handler'] = $this;
90
91
        ob_start();
92
        if (!$this->style->show_midcom('midcom_error_' . $httpcode)) {
93
            $this->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
        if (!isset($this->error_actions[$httpcode]['action'])) {
110
            // No action specified for this error code, skip
111
            return;
112
        }
113
114
        // Prepare the message
115
        $msg = "{$server->getString('REQUEST_METHOD')} request to {$server->getString('REQUEST_URI')}: ";
116
        $msg .= "{$httpcode} {$message}\n";
117
        if ($server->has('HTTP_REFERER')) {
118
            $msg .= "(Referrer: {$server->getString('HTTP_REFERER')})\n";
119
        }
120
121
        // Send as email handler
122
        if ($this->error_actions[$httpcode]['action'] == 'email') {
123
            $this->_send_email($msg, $this->error_actions[$httpcode], $server->getString('SERVER_NAME'));
124
        }
125
        // Append to log file handler
126
        elseif ($this->error_actions[$httpcode]['action'] == 'log') {
127
            $this->_log($msg, $this->error_actions[$httpcode]);
128
        }
129
    }
130
131
    private function _log(string $msg, array $config)
132
    {
133
        if (empty($config['filename'])) {
134
            // No log file specified, skip
135
            return;
136
        }
137
138
        if (   !is_writable($config['filename'])
139
            && !is_writable(dirname($config['filename']))) {
140
            debug_add("Error logging file {$config['filename']} is not writable", MIDCOM_LOG_WARN);
141
            return;
142
        }
143
144
        // Add the line to the error-specific log
145
        $logger = new Logger(__CLASS__);
146
        $logger->pushHandler(new StreamHandler($config['filename']));
147
        $logger = new midcom_debug($logger);
148
        $logger->log($msg, MIDCOM_LOG_INFO);
149
    }
150
151
    private function _send_email(string $msg, array $config, string $servername)
152
    {
153
        if (empty($config['email'])) {
154
            // No recipient specified, skip
155
            return;
156
        }
157
158
        $mail = new org_openpsa_mail();
159
        $mail->to = $config['email'];
160
        $mail->from = "\"MidCOM error notifier\" <webmaster@{$servername}>";
161
        $mail->subject = "[{$servername}] " . str_replace("\n", ' ', $msg);
162
        $mail->body = "{$servername}:\n{$msg}";
163
164
        $stacktrace = $this->get_function_stack();
165
166
        $mail->body .= "\n" . implode("\n", $stacktrace);
167
168
        if (!$mail->send()) {
169
            debug_add("failed to send error notification email to {$mail->to}, reason: " . $mail->get_error_message(), MIDCOM_LOG_WARN);
170
        }
171
    }
172
173
    public function get_function_stack(?Throwable $error = null)
174
    {
175
        $error = $error ?? $this->error;
176
        $stack = $error->getTrace();
177
178
        $stacktrace = [];
179
        foreach ($stack as $number => $frame) {
180
            $line = $number + 1;
181
            if (array_key_exists('file', $frame)) {
182
                $file = str_replace(MIDCOM_ROOT, '[midcom_root]', $frame['file']);
183
                $line .= ": {$file}:{$frame['line']}  ";
184
            } else {
185
                $line .= ': [internal]  ';
186
            }
187
            if (array_key_exists('class', $frame)) {
188
                $line .= $frame['class'];
189
                if (array_key_exists('type', $frame)) {
190
                    $line .= $frame['type'];
191
                } else {
192
                    $line .= '::';
193
                }
194
            }
195
            if (array_key_exists('function', $frame)) {
196
                $line .= $frame['function'];
197
            } else {
198
                $line .= 'require, include or eval';
199
            }
200
            $stacktrace[] = $line;
201
        }
202
203
        return $stacktrace;
204
    }
205
}
206