xref: /webtrees/app/Http/Middleware/EmitResponse.php (revision 1792ff1cf1956b41f3e3c853cfb279a803a71ed2)
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
18declare(strict_types=1);
19
20namespace Fisharebest\Webtrees\Http\Middleware;
21
22use Psr\Http\Message\ResponseInterface;
23use Psr\Http\Message\ServerRequestInterface;
24use Psr\Http\Server\MiddlewareInterface;
25use Psr\Http\Server\RequestHandlerInterface;
26use RuntimeException;
27
28use function connection_status;
29use function fastcgi_finish_request;
30use function function_exists;
31use function header;
32use function header_remove;
33use function headers_sent;
34use function http_response_code;
35use function ob_get_length;
36use function ob_get_level;
37use function sprintf;
38
39use const CONNECTION_NORMAL;
40
41/**
42 * Middleware to emit the response - send it back to the webserver.
43 */
44class 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