Passed
Push — master ( cd51db...9c88b4 )
by Greg
06:32
created

EmitResponse   A

Complexity

Total Complexity 17

Size/Duplication

Total Lines 119
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 36
c 1
b 0
f 0
dl 0
loc 119
rs 10
wmc 17

8 Methods

Rating   Name   Duplication   Size   Complexity  
A closeConnection() 0 4 2
A emitBody() 0 10 4
A process() 0 13 1
A assertHeadersNotEmitted() 0 6 2
A removeDefaultPhpHeaders() 0 3 1
A emitStatusLine() 0 9 1
A assertBodyNotEmitted() 0 7 3
A emitHeaders() 0 8 3
1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2019 webtrees development team
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation, either version 3 of the License, or
9
 * (at your option) any later version.
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 * You should have received a copy of the GNU General Public License
15
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16
 */
17
18
declare(strict_types=1);
19
20
namespace Fisharebest\Webtrees\Http\Middleware;
21
22
use Psr\Http\Message\ResponseInterface;
23
use Psr\Http\Message\ServerRequestInterface;
24
use Psr\Http\Server\MiddlewareInterface;
25
use Psr\Http\Server\RequestHandlerInterface;
26
use RuntimeException;
27
28
use function connection_status;
29
use function fastcgi_finish_request;
30
use function function_exists;
31
use function header;
32
use function header_remove;
33
use function headers_sent;
34
use function http_response_code;
35
use function ob_get_length;
36
use function ob_get_level;
37
use function sprintf;
38
39
use const CONNECTION_NORMAL;
40
41
/**
42
 * Middleware to emit the response - send it back to the webserver.
43
 */
44
class EmitResponse implements MiddlewareInterface
45
{
46
    // Stream the output in chunks.
47
    private const CHUNK_SIZE = 65536;
48
49
    /**
50
     * @param ServerRequestInterface  $request
51
     * @param RequestHandlerInterface $handler
52
     *
53
     * @return ResponseInterface
54
     */
55
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
56
    {
57
        $response = $handler->handle($request);
58
59
        $this->removeDefaultPhpHeaders();
60
        $this->assertHeadersNotEmitted();
61
        $this->assertBodyNotEmitted();
62
        $this->emitStatusLine($response);
63
        $this->emitHeaders($response);
64
        $this->emitBody($response);
65
        $this->closeConnection();
66
67
        return $response;
68
    }
69
70
    /**
71
     * Remove the default PHP header.
72
     *
73
     * @return void
74
     */
75
    private function removeDefaultPhpHeaders(): void
76
    {
77
        header_remove('X-Powered-By');
78
    }
79
80
    /**
81
     * @return void
82
     * @throws RuntimeException
83
     */
84
    private function assertHeadersNotEmitted(): void
85
    {
86
        if (headers_sent($file, $line)) {
87
            $message = sprintf('Headers already sent at %s:%d', $file, $line);
88
89
            throw new RuntimeException($message);
90
        }
91
    }
92
93
    /**
94
     * @return void
95
     * @throws RuntimeException
96
     */
97
    private function assertBodyNotEmitted(): void
98
    {
99
        if (ob_get_level() > 0 && ob_get_length() > 0) {
100
            // The output probably contains an error message.
101
            $output = ob_get_clean();
102
103
            throw new RuntimeException('Output already started: ' . $output);
104
        }
105
    }
106
107
    /**
108
     * @param ResponseInterface $response
109
     */
110
    private function emitStatusLine(ResponseInterface $response): void
111
    {
112
        http_response_code($response->getStatusCode());
113
114
        header(sprintf(
115
            'HTTP/%s %d %s',
116
            $response->getProtocolVersion(),
117
            $response->getStatusCode(),
118
            $response->getReasonPhrase()
119
        ));
120
    }
121
122
    /**
123
     * @param ResponseInterface $response
124
     */
125
    private function emitHeaders(ResponseInterface $response): void
126
    {
127
        foreach ($response->getHeaders() as $name => $values) {
128
            foreach ($values as $value) {
129
                header(
130
                    sprintf('%s: %s', $name, $value),
131
                    false,
132
                    $response->getStatusCode()
133
                );
134
            }
135
        }
136
    }
137
138
    /**
139
     * @param ResponseInterface $response
140
     *
141
     * @return void
142
     */
143
    private function emitBody(ResponseInterface $response): void
144
    {
145
        $body = $response->getBody();
146
147
        if ($body->isSeekable()) {
148
            $body->rewind();
149
        }
150
151
        while (!$body->eof() && connection_status() === CONNECTION_NORMAL) {
152
            echo $body->read(self::CHUNK_SIZE);
153
        }
154
    }
155
156
    /**
157
     * @return void
158
     */
159
    private function closeConnection(): void
160
    {
161
        if (function_exists('fastcgi_finish_request')) {
162
            fastcgi_finish_request();
163
        }
164
    }
165
}
166