xref: /webtrees/app/Http/Middleware/EmitResponse.php (revision 3976b4703df669696105ed6b024b96d433c8fbdb)
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 */
17declare(strict_types=1);
18
19namespace Fisharebest\Webtrees\Http\Middleware;
20
21use Psr\Http\Message\ResponseInterface;
22use Psr\Http\Message\ServerRequestInterface;
23use Psr\Http\Server\MiddlewareInterface;
24use Psr\Http\Server\RequestHandlerInterface;
25use RuntimeException;
26
27use function connection_status;
28use function fastcgi_finish_request;
29use function function_exists;
30use function header;
31use function headers_sent;
32use function http_response_code;
33use function ob_get_length;
34use function ob_get_level;
35use function sprintf;
36
37use const CONNECTION_NORMAL;
38
39/**
40 * Middleware to emit the response - send it back to the webserver.
41 */
42class EmitResponse implements MiddlewareInterface
43{
44    // Stream the output in chunks.
45    private const CHUNK_SIZE = 65536;
46
47    /**
48     * @param ServerRequestInterface  $request
49     * @param RequestHandlerInterface $handler
50     *
51     * @return ResponseInterface
52     */
53    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
54    {
55        $response = $handler->handle($request);
56
57        $this->assertHeadersNotEmitted();
58        $this->assertBodyNotEmitted();
59        $this->emitStatusLine($response);
60        $this->emitHeaders($response);
61        $this->emitBody($response);
62        $this->closeConnection();
63
64        return $response;
65    }
66
67    /**
68     * @return void
69     * @throws RuntimeException
70     */
71    private function assertHeadersNotEmitted(): void
72    {
73        if (headers_sent($file, $line)) {
74            $message = sprintf('Headers already sent at %s:%d', $file, $line);
75
76            throw new RuntimeException($message);
77        }
78    }
79
80    /**
81     * @return void
82     * @throws RuntimeException
83     */
84    private function assertBodyNotEmitted(): void
85    {
86        if (ob_get_level() > 0 && ob_get_length() > 0) {
87            // The output probably contains an error message.
88            $output = ob_get_clean();
89
90            throw new RuntimeException('Output already started: ' . $output);
91        }
92    }
93
94    /**
95     * @param ResponseInterface $response
96     */
97    private function emitStatusLine(ResponseInterface $response): void
98    {
99        http_response_code($response->getStatusCode());
100
101        header(sprintf(
102            'HTTP/%s %d %s',
103            $response->getProtocolVersion(),
104            $response->getStatusCode(),
105            $response->getReasonPhrase()
106        ));
107    }
108
109    /**
110     * @param ResponseInterface $response
111     */
112    private function emitHeaders(ResponseInterface $response): void
113    {
114        foreach ($response->getHeaders() as $name => $values) {
115            foreach ($values as $value) {
116                header(
117                    sprintf('%s: %s', $name, $value),
118                    false,
119                    $response->getStatusCode()
120                );
121            }
122        }
123    }
124
125    /**
126     * @param ResponseInterface $response
127     *
128     * @return void
129     */
130    private function emitBody(ResponseInterface $response): void
131    {
132        $body = $response->getBody();
133
134        if ($body->isSeekable()) {
135            $body->rewind();
136        }
137
138        while (!$body->eof() && connection_status() === CONNECTION_NORMAL) {
139            echo $body->read(self::CHUNK_SIZE);
140        }
141    }
142
143    /**
144     * @return void
145     */
146    private function closeConnection(): void
147    {
148        if (function_exists('fastcgi_finish_request')) {
149            fastcgi_finish_request();
150        }
151    }
152}
153