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