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