flack /
openpsa
| 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() : array |
||
| 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
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 |