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