Passed
Push — master ( 9b63e5...2b9e2e )
by Gabor
03:08
created

FinalMiddleware::sendHttpHeader()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

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