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
![]() |
|||
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 |