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