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()); |
|
|
|
|
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
|
|
|
|