Lines
44.66%
67 / 150
Methods
14.28%
1 / 7
Classes
0.00%
0 / 1
| Name | Lines | Methods | CRAP | ||||
|---|---|---|---|---|---|---|---|
| __invoke | 0.00% | 0 / 3 | 0.00% | 0 / 1 | 2 | ||
| withHtml | 0.00% | 0 / 48 | 0.00% | 0 / 1 | 56 | ||
| getExceptionTemplate | 0.00% | 0 / 17 | 0.00% | 0 / 1 | 90 | ||
| getRequestData | 84.61% | 11 / 13 | 0.00% | 0 / 1 | 4.06 | ||
| getExtendedExceptionInfo | 84.37% | 54 / 64 | 0.00% | 0 / 1 | 12.55 | ||
| replaceLogText | 0.00% | 0 / 3 | 0.00% | 0 / 1 | 6 | ||
| getExceptionField | 100.00% | 2 / 2 | 100.00% | 1 / 1 | 2 | ||
| 15 | class TwigExceptionHandler implements ErrorHandlerInterface | |
| 16 | { | |
| 17 | const string DEFAULT_TEMPLATE = "exception/default.twig"; | |
| 18 | ||
| 19 | /** | |
| 20 | * @SuppressWarnings("PMD.UnusedFormalParameter") | |
| 21 | * @param ServerRequestInterface $request | |
| 22 | * @param \Throwable $exception | |
| 23 | * @param bool $displayErrorDetails | |
| 24 | * @param bool $logErrors | |
| 25 | * @param bool $logErrorDetails | |
| 26 | * @return ResponseInterface | |
| 27 | */ | |
| 28 | #[\Override] | |
| 29 | public function __invoke( | |
| 30 | ServerRequestInterface $request, | |
| 31 | \Throwable $exception, | |
| 32 | bool $displayErrorDetails, | |
| 33 | bool $logErrors, | |
| 34 | bool $logErrorDetails | |
| 35 | ): ResponseInterface { | |
| 36 | ||
| 37 | $decoratedRequest = \BO\Slim\Middleware\ZmsSlimRequest::getDecoratedRequest($request); | |
| 38 | $response = \App::$slim->getResponseFactory()->createResponse(); | |
| 39 | return static::withHtml($decoratedRequest, $response, $exception); | |
| 40 | } | |
| 41 | ||
| 42 | public static function withHtml( | |
| 43 | RequestInterface $request, | |
| 44 | ResponseInterface $response, | |
| 45 | \Throwable $exception, | |
| 46 | int $status = 500 | |
| 47 | ): ResponseInterface { | |
| 48 | try { | |
| 49 | $request = Controller::prepareRequest($request); | |
| 50 | $code = (int) $exception->getCode(); | |
| 51 | if ($code >= 200) { | |
| 52 | $status = $code; | |
| 53 | } | |
| 54 | $template = self::getExceptionTemplate($exception); | |
| 55 | $extendedInfo = self::getExtendedExceptionInfo($exception, $request); | |
| 56 | if ($status >= 500 || $status < 200 || $template == static::DEFAULT_TEMPLATE) { | |
| 57 | $logInfo = $extendedInfo; | |
| 58 | unset($logInfo['responsedata']); | |
| 59 | unset($logInfo['exception']); | |
| 60 | unset($logInfo['workstation']); | |
| 61 | //ksort($logInfo); | |
| 62 | // Some error-reporting is limited to a defined amount of chars | |
| 63 | // Remove unnecessary chars, for ordering see self::getExtendedExceptionInfo() | |
| 64 | $logText = json_encode($logInfo); | |
| 65 | $logText = is_string($logText) ? $logText : ''; | |
| 66 | $logText = self::replaceLogText('#' . preg_quote('\\/') . '#', "/", $logText); | |
| 67 | $logText = self::replaceLogText('#' . preg_quote('\\"') . '#', "", $logText); | |
| 68 | $logText = self::replaceLogText('#' . preg_quote('\\n') . '#', " ", $logText); | |
| 69 | $logText = self::replaceLogText('#' . preg_quote('\\') . '#', ".", $logText); | |
| 70 | $logText = self::replaceLogText('#"#', "", $logText); | |
| 71 | $logText = self::replaceLogText('#\s+#', ' ', $logText); | |
| 72 | $logText = self::replaceLogText('#\.\.#', ".", $logText); | |
| 73 | $logText = self::replaceLogText('#(/[^/\s]+)+/([^/\s]+/[^/\s]+)\.php#', "$2.php", $logText); | |
| 74 | $logText = self::replaceLogText('#' . preg_quote(\App::APP_PATH) . '/?#', "", $logText); | |
| 75 | \App::$log->critical("PHP-Exception #{$extendedInfo['uniqueid']}: " . $logText); | |
| 76 | /* | |
| 77 | \App::$log->critical( | |
| 78 | "PHP Fatal Exception #{$extendedInfo['uniqueid']}" | |
| 79 | . " in {$extendedInfo['file']} +{$extendedInfo['line']} : " . | |
| 80 | $exception->getMessage() | |
| 81 | . " || Trace: " . str_replace("\n", " || ", substr($exception->getTraceAsString(), 0, 1024)) | |
| 82 | ); | |
| 83 | */ | |
| 84 | } | |
| 85 | $response = Render::withLastModified($response, time(), '0'); | |
| 86 | ||
| 87 | return Render::withHtml( | |
| 88 | $response, | |
| 89 | $template, | |
| 90 | array_merge($extendedInfo, array( | |
| 91 | "title" => "Bitte entschuldigen Sie den Fehler", | |
| 92 | )), | |
| 93 | $status | |
| 94 | ); | |
| 95 | } catch (\Throwable $subexception) { | |
| 96 | \App::$log->critical('Not catchable exception while rendering error page', [ | |
| 97 | 'exception' => get_class($exception), | |
| 98 | 'message' => $exception->getMessage(), | |
| 99 | 'file' => $exception->getFile(), | |
| 100 | 'line' => $exception->getLine(), | |
| 101 | 'trace' => $exception->getTraceAsString(), | |
| 102 | 'cause' => get_class($subexception), | |
| 103 | 'causeMessage' => $subexception->getMessage(), | |
| 104 | 'causeTrace' => $subexception->getTraceAsString(), | |
| 105 | ]); | |
| 106 | ||
| 107 | $fallback = $response | |
| 108 | ->withStatus(500) | |
| 109 | ->withHeader('Content-Type', 'text/plain; charset=UTF-8'); | |
| 110 | $fallback->getBody()->write('Internal Server Error'); | |
| 111 | ||
| 112 | return $fallback; | |
| 113 | } | |
| 114 | } | |
| 115 | ||
| 116 | public static function getExceptionTemplate(\Throwable $exception): string | |
| 117 | { | |
| 118 | $container = \App::$slim->getContainer(); | |
| 119 | if ($container === null) { | |
| 120 | return static::DEFAULT_TEMPLATE; | |
| 121 | } | |
| 122 | $twig = $container->get('view'); | |
| 123 | if (!is_object($twig) || !method_exists($twig, 'getLoader')) { | |
| 124 | return static::DEFAULT_TEMPLATE; | |
| 125 | } | |
| 126 | $loader = $twig->getLoader(); | |
| 127 | $templateField = self::getExceptionField($exception, 'template'); | |
| 128 | if (is_string($templateField) && $templateField !== '') { | |
| 129 | $classname = $templateField; | |
| 130 | } else { | |
| 131 | $classname = get_class($exception); | |
| 132 | } | |
| 133 | $classname = strtolower($classname); | |
| 134 | $classname = preg_replace('#[\\\]+#', '/', $classname) ?? $classname; | |
| 135 | $template = "exception/$classname.twig"; | |
| 136 | ||
| 137 | if (!is_object($loader) || !method_exists($loader, 'exists') || !$loader->exists($template)) { | |
| 138 | $template = static::DEFAULT_TEMPLATE; | |
| 139 | } | |
| 140 | return $template; | |
| 141 | } | |
| 142 | ||
| 143 | /** | |
| 144 | * @return false|string | |
| 145 | */ | |
| 146 | protected static function getRequestData(RequestInterface $request): string|false | |
| 147 | { | |
| 148 | $queryParams = []; | |
| 149 | $parsedBody = []; | |
| 150 | if ($request instanceof ServerRequestInterface) { | |
| 151 | $queryParams = $request->getQueryParams(); | |
| 152 | $parsedBody = $request->getParsedBody(); | |
| 153 | $parsedBody = is_array($parsedBody) ? $parsedBody : []; | |
| 154 | } else { | |
| 155 | parse_str($request->getUri()->getQuery(), $queryParams); | |
| 156 | } | |
| 157 | $requestdata = array_merge($queryParams, $parsedBody); | |
| 158 | $json_opt = JSON_HEX_TAG | JSON_PRETTY_PRINT | JSON_HEX_AMP; | |
| 159 | if (json_decode((string)$request->getBody())) { | |
| 160 | $requestdata = json_encode(json_decode((string)$request->getBody()), $json_opt); | |
| 161 | } else { | |
| 162 | $requestdata = json_encode($requestdata, $json_opt); | |
| 163 | } | |
| 164 | return $requestdata; | |
| 165 | } | |
| 166 | ||
| 167 | /** | |
| 168 | * @SuppressWarnings(PHPMD.NPathComplexity) | |
| 169 | */ | |
| 170 | public static function getExtendedExceptionInfo(\Throwable $exception, RequestInterface $request): array | |
| 171 | { | |
| 172 | $servertime = Helper::getFormatedDates((new \DateTimeImmutable())->getTimestamp(), 'yyyy-MM-dd HH:mm:ss'); | |
| 173 | $servertime = $servertime !== false ? $servertime : ''; | |
| 174 | $templateField = self::getExceptionField($exception, 'template'); | |
| 175 | $exceptionclass = is_string($templateField) && $templateField !== '' | |
| 176 | ? $templateField | |
| 177 | : get_class($exception); | |
| 178 | $response = null; | |
| 179 | $responsedata = ''; | |
| 180 | $apirequest = null; | |
| 181 | $apirequestdata = ''; | |
| 182 | $apirequesturi = ''; | |
| 183 | $apirequestmethod = ''; | |
| 184 | $apiRequestField = self::getExceptionField($exception, 'request'); | |
| 185 | if ($apiRequestField instanceof RequestInterface) { | |
| 186 | $apirequest = $apiRequestField; | |
| 187 | $apirequest = $apirequest->withUri($apirequest->getUri()->withUserInfo('')); | |
| 188 | $apirequestdata = static::getRequestData($apirequest); | |
| 189 | $apirequesturi = $apirequest->getUri(); | |
| 190 | $apirequestmethod = $apirequest->getMethod(); | |
| 191 | } | |
| 192 | $routename = ''; | |
| 193 | if ($request instanceof ServerRequestInterface) { | |
| 194 | $route = $request->getAttribute('route'); | |
| 195 | if ($route instanceof \Slim\Routing\Route) { | |
| 196 | $routename = $route->getName() ?? ''; | |
| 197 | } | |
| 198 | } | |
| 199 | ||
| 200 | // Do not log username or password | |
| 201 | $request = $request->withUri($request->getUri()->withUserInfo('')); | |
| 202 | $apiResponseField = self::getExceptionField($exception, 'response'); | |
| 203 | if ($apiResponseField instanceof ResponseInterface) { | |
| 204 | $response = $apiResponseField; | |
| 205 | $responsedata = (string)$response->getBody(); | |
| 206 | } | |
| 207 | $trace = substr($exception->getTraceAsString(), 0, 2048); | |
| 208 | ||
| 209 | $traceField = self::getExceptionField($exception, 'trace'); | |
| 210 | if (is_string($traceField) && $traceField !== '') { | |
| 211 | $trace = $traceField; | |
| 212 | } | |
| 213 | $requestdata = static::getRequestData($request); | |
| 214 | $uniqueId = substr(sha1($servertime . rand(1, 60)), 0, 6); | |
| 215 | $dataField = self::getExceptionField($exception, 'data'); | |
| 216 | $data = is_array($dataField) ? $dataField : []; | |
| 217 | $templateDataField = self::getExceptionField($exception, 'templatedata'); | |
| 218 | $templatedata = is_array($templateDataField) ? $templateDataField : []; | |
| 219 | ||
| 220 | // Due to shortened error logs in some reportings, important informations first! | |
| 221 | return array_merge(array( | |
| 222 | "exceptionclass" => $exceptionclass, | |
| 223 | "requesturi" => $request->getUri()->getPath(), | |
| 224 | "apirequesturi" => (string)$apirequesturi, | |
| 225 | "route" => $routename, | |
| 226 | "_file" => $exception->getFile(), | |
| 227 | "_line" => $exception->getLine(), | |
| 228 | "failed" => $exception->getMessage(), | |
| 229 | "requestmethod" => $request->getMethod(), | |
| 230 | "data" => $data, | |
| 231 | "x-requestdata" => $requestdata, | |
| 232 | "servertime" => $servertime, | |
| 233 | "exception" => $exception, | |
| 234 | "exceptioncode" => $exception->getCode(), | |
| 235 | "basefile" => basename($exception->getFile(), '.php'), | |
| 236 | "x-requestdata_api" => $apirequestdata, | |
| 237 | "_trace" => $trace, | |
| 238 | "debug" => \App::DEBUG, | |
| 239 | "uniqueid" => $uniqueId, | |
| 240 | "request" => $request, | |
| 241 | "apirequest" => $apirequest, | |
| 242 | "apirequestmethod" => $apirequestmethod, | |
| 243 | "response" => $response, | |
| 244 | "x-responsedata" => $responsedata, | |
| 245 | ), $templatedata); | |
| 246 | } | |
| 247 | ||
| 248 | private static function replaceLogText(string $pattern, string $replacement, string $subject): string | |
| 249 | { | |
| 250 | if ($pattern === '') { | |
| 251 | return $subject; | |
| 252 | } | |
| 253 | return preg_replace($pattern, $replacement, $subject) ?? $subject; | |
| 254 | } | |
| 255 | ||
| 256 | private static function getExceptionField(\Throwable $exception, string $field): mixed | |
| 257 | { | |
| 258 | $vars = get_object_vars($exception); | |
| 259 | return array_key_exists($field, $vars) ? $vars[$field] : null; | |
| 260 | } | |
| 261 | } |