Passed
Push — master ( d017f3...f47d5f )
by Gabor
03:51
created

FinalMiddleware::getExceptionAsString()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 7
ccs 5
cts 5
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 5
nc 1
nop 1
crap 1
1
<?php
2
/**
3
 * WebHemi.
4
 *
5
 * PHP version 7.1
6
 *
7
 * @copyright 2012 - 2017 Gixx-web (http://www.gixx-web.com)
8
 * @license   https://opensource.org/licenses/MIT The MIT License (MIT)
9
 *
10
 * @link      http://www.gixx-web.com
11
 */
12
declare(strict_types = 1);
13
14
namespace WebHemi\Middleware\Common;
15
16
use RuntimeException;
17
use Throwable;
18
use WebHemi\Auth\ServiceInterface as AuthInterface;
19
use WebHemi\Data\Entity\User\UserEntity;
20
use WebHemi\Environment\ServiceInterface as EnvironmentInterface;
21
use WebHemi\Http\ResponseInterface;
22
use WebHemi\Http\ServerRequestInterface;
23
use WebHemi\Logger\ServiceInterface as LoggerInterface;
24
use WebHemi\Middleware\MiddlewareInterface;
25
use WebHemi\Renderer\ServiceInterface as RendererInterface;
26
27
/**
28
 * Class FinalMiddleware.
29
 */
30
class FinalMiddleware implements MiddlewareInterface
31
{
32
    /** @var RendererInterface */
33
    private $templateRenderer;
34
    /** @var AuthInterface */
35
    private $authAdapter;
36
    /** @var EnvironmentInterface */
37
    private $environmentManager;
38
    /** @var LoggerInterface */
39
    private $logAdapter;
40
41
    /**
42
     * FinalMiddleware constructor.
43
     *
44
     * @param RendererInterface    $templateRenderer
45
     * @param AuthInterface        $authAdapter
46
     * @param EnvironmentInterface $environmentManager
47
     * @param LoggerInterface      $logAdapter
48
     */
49 8
    public function __construct(
50
        RendererInterface $templateRenderer,
51
        AuthInterface $authAdapter,
52
        EnvironmentInterface $environmentManager,
53
        LoggerInterface $logAdapter
54
    ) {
55 8
        $this->templateRenderer = $templateRenderer;
56 8
        $this->authAdapter = $authAdapter;
57 8
        $this->environmentManager = $environmentManager;
58 8
        $this->logAdapter = $logAdapter;
59 8
    }
60
61
    /**
62
     * Sends out the headers and prints the response body to the output.
63
     *
64
     * @param ServerRequestInterface $request
65
     * @param ResponseInterface      $response
66
     * @return void
67
     */
68 3
    public function __invoke(ServerRequestInterface&$request, ResponseInterface&$response) : void
69
    {
70
        // @codeCoverageIgnoreStart
71
        if (!defined('PHPUNIT_WEBHEMI_TESTSUITE') && headers_sent()) {
72
            throw new RuntimeException('Unable to emit response; headers already sent', 1000);
73
        }
74
        // @codeCoverageIgnoreEnd
75
76
        // Handle errors here.
77 3
        if (!in_array($response->getStatusCode(), [ResponseInterface::STATUS_OK, ResponseInterface::STATUS_REDIRECT])) {
78 2
            $exception = $request->getAttribute(ServerRequestInterface::REQUEST_ATTR_MIDDLEWARE_EXCEPTION)
79 2
                ?? new RuntimeException($response->getReasonPhrase(), $response->getStatusCode());
80 2
            $this->prepareErrorResponse($exception, $request, $response);
81 2
            $this->logErrorResponse($exception, $request, $response);
82
        }
83
84
        // Skip sending output when PHP Unit is running.
85
        // @codeCoverageIgnoreStart
86
        if (!defined('PHPUNIT_WEBHEMI_TESTSUITE')) {
87
            $this->sendOutput($request, $response);
88
        }
89
        // @codeCoverageIgnoreEnd
90 3
    }
91
92
    /**
93
     * Prepares error response: Body and Data
94
     *
95
     * @param Throwable              $exception
96
     * @param ServerRequestInterface $request
97
     * @param ResponseInterface      $response
98
     */
99 2
    private function prepareErrorResponse(
100
        Throwable $exception,
101
        ServerRequestInterface&$request,
102
        ResponseInterface&$response
103
    ) : void {
104 2
        $errorTemplate = 'error-'.$response->getStatusCode();
105
106
        /** @var array $data */
107 2
        $templateData = $request->getAttribute(ServerRequestInterface::REQUEST_ATTR_DISPATCH_DATA);
108 2
        $templateData['exception'] = $this->getExceptionAsString($exception);
109
110 2
        if ($request->isXmlHttpRequest()) {
111 1
            $request = $request->withAttribute(ServerRequestInterface::REQUEST_ATTR_DISPATCH_DATA, $templateData);
112
        } else {
113 1
            $body = $this->templateRenderer->render($errorTemplate, $templateData);
114 1
            $response = $response->withBody($body);
115
        }
116 2
    }
117
118
    /**
119
     * Logs the error.
120
     *
121
     * @param Throwable              $exception
122
     * @param ServerRequestInterface $request
123
     * @param ResponseInterface      $response
124
     */
125 2
    private function logErrorResponse(
126
        Throwable $exception,
127
        ServerRequestInterface&$request,
128
        ResponseInterface&$response
129
    ) : void {
130 2
        $identity = 'Unauthenticated user';
131
132 2
        if ($this->authAdapter->hasIdentity()) {
133
            /** @var UserEntity $userEntity */
134 2
            $userEntity = $this->authAdapter->getIdentity();
135 2
            $identity = $userEntity->getEmail();
136
        }
137
138
        $logData = [
139 2
            'User' => $identity,
140 2
            'IP' => $this->environmentManager->getClientIp(),
141 2
            'RequestUri' => $request->getUri()->getPath().'?'.$request->getUri()->getQuery(),
142 2
            'RequestMethod' => $request->getMethod(),
143 2
            'Error' => $response->getStatusCode().' '.$response->getReasonPhrase(),
144 2
            'Exception' => $this->getExceptionAsString($exception),
145 2
            'Parameters' => $request->getParsedBody()
146
        ];
147 2
        $this->logAdapter->log('error', json_encode($logData));
148 2
    }
149
150
    /**
151
     * Inject the Content-Length header if is not already present.
152
     *
153
     * NOTE: if there will be chunk content displayed, check if the response getSize counts the real size correctly
154
     *
155
     * @param ResponseInterface $response
156
     * @return void
157
     *
158
     * @codeCoverageIgnore - no putput for tests.
159
     */
160
    private function injectContentLength(ResponseInterface&$response) : void
161
    {
162
        if (!$response->hasHeader('Content-Length') && !is_null($response->getBody()->getSize())) {
163
            $response = $response->withHeader('Content-Length', (string) $response->getBody()->getSize());
164
        }
165
    }
166
167
    /**
168
     * Filter a header name to word case.
169
     *
170
     * @param string $headerName
171
     * @return string
172
     */
173 5
    private function filterHeaderName(string $headerName) : string
174
    {
175 5
        $filtered = str_replace('-', ' ', $headerName);
176 5
        $filtered = ucwords($filtered);
177 5
        return str_replace(' ', '-', $filtered);
178
    }
179
180
    /**
181
     * Sends the HTTP header.
182
     *
183
     * @param ResponseInterface $response
184
     * @return void
185
     *
186
     * @codeCoverageIgnore - vendor and core function calls
187
     */
188
    private function sendHttpHeader(ResponseInterface $response) : void
189
    {
190
        $reasonPhrase = $response->getReasonPhrase();
191
        header(sprintf(
192
            'HTTP/%s %d%s',
193
            $response->getProtocolVersion(),
194
            $response->getStatusCode(),
195
            ($reasonPhrase ? ' '.$reasonPhrase : '')
196
        ));
197
    }
198
199
    /**
200
     * Sends out output headers.
201
     *
202
     * @param array $headers
203
     * @return void
204
     *
205
     * @codeCoverageIgnore - vendor and core function calls in loop
206
     */
207
    private function sendOutputHeaders(array $headers) : void
208
    {
209
        foreach ($headers as $headerName => $values) {
210
            $name  = $this->filterHeaderName($headerName);
211
            $first = true;
212
            foreach ($values as $value) {
213
                header(sprintf('%s: %s', $name, $value), $first);
214
                $first = false;
215
            }
216
        }
217
    }
218
219
    /**
220
     * Sends output according to the request.
221
     *
222
     * @param ServerRequestInterface $request
223
     * @param ResponseInterface      $response
224
     * @return void
225
     *
226
     * @codeCoverageIgnore - no output for tests
227
     */
228
    private function sendOutput(ServerRequestInterface $request, ResponseInterface $response) : void
229
    {
230
        if ($request->isXmlHttpRequest()) {
231
            $templateData = $request->getAttribute(ServerRequestInterface::REQUEST_ATTR_DISPATCH_DATA);
232
233
            $output = json_encode($templateData);
234
        } else {
235
            $this->injectContentLength($response);
236
            $output = $response->getBody();
237
        }
238
239
        $this->sendHttpHeader($response);
240
        $this->sendOutputHeaders($response->getHeaders());
241
242
        echo $output;
243
    }
244
245
    /**
246
     * Convert the exception into plain text instead of the fancy HTML output of the xdebug...
247
     *
248
     * @param Throwable $exception
249
     * @return string
250
     */
251 2
    private function getExceptionAsString(Throwable $exception)
252
    {
253 2
        return 'Exception ('.$exception->getCode().'): "'.$exception->getMessage().'" '
254 2
            .'in '.$exception->getFile().' on line '.$exception->getLine().PHP_EOL
255 2
            .'Call stack'.PHP_EOL
256 2
            .$exception->getTraceAsString();
257
    }
258
}
259