xref: /webtrees/app/Http/Middleware/EmitResponse.php (revision 74d6dc0ec259c643834b111577684e38e74234c8)
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            throw new RuntimeException('Output already started');
85        }
86    }
87
88    /**
89     * @param ResponseInterface $response
90     */
91    private function emitStatusLine(ResponseInterface $response): void
92    {
93        http_response_code($response->getStatusCode());
94
95        header(sprintf(
96            'HTTP/%s %d %s',
97            $response->getProtocolVersion(),
98            $response->getStatusCode(),
99            $response->getReasonPhrase()
100        ));
101    }
102
103    /**
104     * @param ResponseInterface $response
105     */
106    private function emitHeaders(ResponseInterface $response): void
107    {
108        foreach ($response->getHeaders() as $name => $values) {
109            foreach ($values as $value) {
110                header(
111                    sprintf('%s: %s', $name, $value),
112                    false,
113                    $response->getStatusCode()
114                );
115            }
116        }
117    }
118
119    /**
120     * @param ResponseInterface $response
121     *
122     * @return void
123     */
124    private function emitBody(ResponseInterface $response): void
125    {
126        $body = $response->getBody();
127
128        if ($body->isSeekable()) {
129            $body->rewind();
130        }
131
132        while (!$body->eof() && connection_status() === CONNECTION_NORMAL) {
133            echo $body->read(self::CHUNK_SIZE);
134        }
135    }
136
137    /**
138     * @return void
139     */
140    private function closeConnection(): void
141    {
142        if (function_exists('fastcgi_finish_request')) {
143            fastcgi_finish_request();
144        }
145    }
146}
147