Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
44.30% covered (danger)
44.30%
66 / 149
14.29% covered (danger)
14.29%
1 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
TwigExceptionHandler
44.30% covered (danger)
44.30%
66 / 149
14.29% covered (danger)
14.29%
1 / 7
260.02
0.00% covered (danger)
0.00%
0 / 1
 __invoke
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 withHtml
0.00% covered (danger)
0.00%
0 / 48
0.00% covered (danger)
0.00%
0 / 1
56
 getExceptionTemplate
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
90
 getRequestData
83.33% covered (warning)
83.33%
10 / 12
0.00% covered (danger)
0.00%
0 / 1
3.04
 getExtendedExceptionInfo
84.38% covered (warning)
84.38%
54 / 64
0.00% covered (danger)
0.00%
0 / 1
12.55
 replaceLogText
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getExceptionField
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3/**
4 * @package   BO Slim
5 * @copyright BerlinOnline Stadtportal GmbH & Co. KG
6 **/
7
8namespace BO\Slim;
9
10use Psr\Http\Message\RequestInterface;
11use Psr\Http\Message\ResponseInterface;
12use Psr\Http\Message\ServerRequestInterface;
13use Slim\Interfaces\ErrorHandlerInterface;
14
15class 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 = (array) $request->getQueryParams();
152            $parsedBody = (array) $request->getParsedBody();
153        } else {
154            parse_str($request->getUri()->getQuery(), $queryParams);
155        }
156        $requestdata = array_merge($queryParams, $parsedBody);
157        $json_opt = JSON_HEX_TAG | JSON_PRETTY_PRINT | JSON_HEX_AMP;
158        if (json_decode((string)$request->getBody())) {
159            $requestdata = json_encode(json_decode((string)$request->getBody()), $json_opt);
160        } else {
161            $requestdata = json_encode($requestdata, $json_opt);
162        }
163        return $requestdata;
164    }
165
166    /**
167     * @SuppressWarnings(PHPMD.NPathComplexity)
168     */
169    public static function getExtendedExceptionInfo(\Throwable $exception, RequestInterface $request): array
170    {
171        $servertime = Helper::getFormatedDates((new \DateTimeImmutable())->getTimestamp(), 'yyyy-MM-dd HH:mm:ss');
172        $servertime = $servertime !== false ? $servertime : '';
173        $templateField = self::getExceptionField($exception, 'template');
174        $exceptionclass = is_string($templateField) && $templateField !== ''
175            ? $templateField
176            : get_class($exception);
177        $response = null;
178        $responsedata = '';
179        $apirequest = null;
180        $apirequestdata = '';
181        $apirequesturi = '';
182        $apirequestmethod = '';
183        $apiRequestField = self::getExceptionField($exception, 'request');
184        if ($apiRequestField instanceof RequestInterface) {
185            $apirequest = $apiRequestField;
186            $apirequest = $apirequest->withUri($apirequest->getUri()->withUserInfo(''));
187            $apirequestdata = static::getRequestData($apirequest);
188            $apirequesturi = $apirequest->getUri();
189            $apirequestmethod = $apirequest->getMethod();
190        }
191        $routename = '';
192        if ($request instanceof ServerRequestInterface) {
193            $route = $request->getAttribute('route');
194            if ($route instanceof \Slim\Routing\Route) {
195                $routename = $route->getName() ?? '';
196            }
197        }
198
199        // Do not log username or password
200        $request = $request->withUri($request->getUri()->withUserInfo(''));
201        $apiResponseField = self::getExceptionField($exception, 'response');
202        if ($apiResponseField instanceof ResponseInterface) {
203            $response = $apiResponseField;
204            $responsedata = (string)$response->getBody();
205        }
206        $trace = substr($exception->getTraceAsString(), 0, 2048);
207
208        $traceField = self::getExceptionField($exception, 'trace');
209        if (is_string($traceField) && $traceField !== '') {
210            $trace = $traceField;
211        }
212        $requestdata = static::getRequestData($request);
213        $uniqueId = substr(sha1($servertime . rand(1, 60)), 0, 6);
214        $dataField = self::getExceptionField($exception, 'data');
215        $data = is_array($dataField) ? $dataField : [];
216        $templateDataField = self::getExceptionField($exception, 'templatedata');
217        $templatedata = is_array($templateDataField) ? $templateDataField : [];
218
219        // Due to shortened error logs in some reportings, important informations first!
220        return array_merge(array(
221            "exceptionclass" => $exceptionclass,
222            "requesturi" => $request->getUri()->getPath(),
223            "apirequesturi" => (string)$apirequesturi,
224            "route" => $routename,
225            "_file" => $exception->getFile(),
226            "_line" => $exception->getLine(),
227            "failed" => $exception->getMessage(),
228            "requestmethod" => $request->getMethod(),
229            "data" => $data,
230            "x-requestdata" => $requestdata,
231            "servertime" => $servertime,
232            "exception" => $exception,
233            "exceptioncode" => $exception->getCode(),
234            "basefile" => basename($exception->getFile(), '.php'),
235            "x-requestdata_api" => $apirequestdata,
236            "_trace" => $trace,
237            "debug" => \App::DEBUG,
238            "uniqueid" => $uniqueId,
239            "request" => $request,
240            "apirequest" => $apirequest,
241            "apirequestmethod" => $apirequestmethod,
242            "response" => $response,
243            "x-responsedata" => $responsedata,
244        ), $templatedata);
245    }
246
247    private static function replaceLogText(string $pattern, string $replacement, string $subject): string
248    {
249        if ($pattern === '') {
250            return $subject;
251        }
252        return preg_replace($pattern, $replacement, $subject) ?? $subject;
253    }
254
255    private static function getExceptionField(\Throwable $exception, string $field): mixed
256    {
257        $vars = get_object_vars($exception);
258        return array_key_exists($field, $vars) ? $vars[$field] : null;
259    }
260}