xref: /webtrees/app/Http/Middleware/EmitResponse.php (revision d11be7027e34e3121be11cc025421873364403f9)
1cfbf56adSGreg Roach<?php
23976b470SGreg Roach
3cfbf56adSGreg Roach/**
4cfbf56adSGreg Roach * webtrees: online genealogy
5*d11be702SGreg Roach * Copyright (C) 2023 webtrees development team
6cfbf56adSGreg Roach * This program is free software: you can redistribute it and/or modify
7cfbf56adSGreg Roach * it under the terms of the GNU General Public License as published by
8cfbf56adSGreg Roach * the Free Software Foundation, either version 3 of the License, or
9cfbf56adSGreg Roach * (at your option) any later version.
10cfbf56adSGreg Roach * This program is distributed in the hope that it will be useful,
11cfbf56adSGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of
12cfbf56adSGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13cfbf56adSGreg Roach * GNU General Public License for more details.
14cfbf56adSGreg Roach * You should have received a copy of the GNU General Public License
1589f7189bSGreg Roach * along with this program. If not, see <https://www.gnu.org/licenses/>.
16cfbf56adSGreg Roach */
17fcfa147eSGreg Roach
18cfbf56adSGreg Roachdeclare(strict_types=1);
19cfbf56adSGreg Roach
20cfbf56adSGreg Roachnamespace Fisharebest\Webtrees\Http\Middleware;
21cfbf56adSGreg Roach
22cfbf56adSGreg Roachuse Psr\Http\Message\ResponseInterface;
23cfbf56adSGreg Roachuse Psr\Http\Message\ServerRequestInterface;
24cfbf56adSGreg Roachuse Psr\Http\Server\MiddlewareInterface;
25cfbf56adSGreg Roachuse Psr\Http\Server\RequestHandlerInterface;
26cfbf56adSGreg Roachuse RuntimeException;
273976b470SGreg Roach
28cfbf56adSGreg Roachuse function connection_status;
29cfbf56adSGreg Roachuse function fastcgi_finish_request;
30cfbf56adSGreg Roachuse function function_exists;
31cfbf56adSGreg Roachuse function header;
32e4d64f76SGreg Roachuse function header_remove;
33cfbf56adSGreg Roachuse function headers_sent;
34cfbf56adSGreg Roachuse function http_response_code;
35cfbf56adSGreg Roachuse function ob_get_length;
36cfbf56adSGreg Roachuse function ob_get_level;
37cfbf56adSGreg Roachuse function sprintf;
383976b470SGreg Roach
39cfbf56adSGreg Roachuse const CONNECTION_NORMAL;
40cfbf56adSGreg Roach
41cfbf56adSGreg Roach/**
42cfbf56adSGreg Roach * Middleware to emit the response - send it back to the webserver.
43cfbf56adSGreg Roach */
44cfbf56adSGreg Roachclass EmitResponse implements MiddlewareInterface
45cfbf56adSGreg Roach{
46cfbf56adSGreg Roach    // Stream the output in chunks.
47cfbf56adSGreg Roach    private const CHUNK_SIZE = 65536;
48cfbf56adSGreg Roach
49cfbf56adSGreg Roach    /**
50cfbf56adSGreg Roach     * @param ServerRequestInterface  $request
51cfbf56adSGreg Roach     * @param RequestHandlerInterface $handler
52cfbf56adSGreg Roach     *
53cfbf56adSGreg Roach     * @return ResponseInterface
54cfbf56adSGreg Roach     */
55cfbf56adSGreg Roach    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
56cfbf56adSGreg Roach    {
57cfbf56adSGreg Roach        $response = $handler->handle($request);
58cfbf56adSGreg Roach
59cfbf56adSGreg Roach        $this->assertHeadersNotEmitted();
60f1d11eefSGreg Roach        $this->removeDefaultPhpHeaders();
61f1d11eefSGreg Roach
62f1d11eefSGreg Roach        // Unless webtrees set a cache-control header, assume the page cannot be cached
636172e7f6SGreg Roach        if (!$response->hasHeader('cache-control')) {
646172e7f6SGreg Roach            $response = $response->withHeader('cache-control', 'no-store');
65f1d11eefSGreg Roach        }
66f1d11eefSGreg Roach
67cfbf56adSGreg Roach        $this->assertBodyNotEmitted();
68cfbf56adSGreg Roach        $this->emitStatusLine($response);
69cfbf56adSGreg Roach        $this->emitHeaders($response);
70cfbf56adSGreg Roach        $this->emitBody($response);
71cfbf56adSGreg Roach        $this->closeConnection();
72cfbf56adSGreg Roach
73cfbf56adSGreg Roach        return $response;
74cfbf56adSGreg Roach    }
75cfbf56adSGreg Roach
76cfbf56adSGreg Roach    /**
77e4d64f76SGreg Roach     * Remove the default PHP header.
78e4d64f76SGreg Roach     *
79e4d64f76SGreg Roach     * @return void
80e4d64f76SGreg Roach     */
81e4d64f76SGreg Roach    private function removeDefaultPhpHeaders(): void
82e4d64f76SGreg Roach    {
83e4d64f76SGreg Roach        header_remove('X-Powered-By');
846172e7f6SGreg Roach        header_remove('cache-control');
85f1d11eefSGreg Roach        header_remove('Expires');
86f1d11eefSGreg Roach        header_remove('Pragma');
87e4d64f76SGreg Roach    }
88e4d64f76SGreg Roach
89e4d64f76SGreg Roach    /**
90cfbf56adSGreg Roach     * @return void
91cfbf56adSGreg Roach     * @throws RuntimeException
92cfbf56adSGreg Roach     */
93cfbf56adSGreg Roach    private function assertHeadersNotEmitted(): void
94cfbf56adSGreg Roach    {
95cfbf56adSGreg Roach        if (headers_sent($file, $line)) {
96cfbf56adSGreg Roach            $message = sprintf('Headers already sent at %s:%d', $file, $line);
97cfbf56adSGreg Roach
98cfbf56adSGreg Roach            throw new RuntimeException($message);
99cfbf56adSGreg Roach        }
100cfbf56adSGreg Roach    }
101cfbf56adSGreg Roach
102cfbf56adSGreg Roach    /**
103cfbf56adSGreg Roach     * @return void
104cfbf56adSGreg Roach     * @throws RuntimeException
105cfbf56adSGreg Roach     */
106cfbf56adSGreg Roach    private function assertBodyNotEmitted(): void
107cfbf56adSGreg Roach    {
108cfbf56adSGreg Roach        if (ob_get_level() > 0 && ob_get_length() > 0) {
1097bd2dae1SGreg Roach            // The output probably contains an error message.
1107bd2dae1SGreg Roach            $output = ob_get_clean();
1117bd2dae1SGreg Roach
1127bd2dae1SGreg Roach            throw new RuntimeException('Output already started: ' . $output);
113cfbf56adSGreg Roach        }
114cfbf56adSGreg Roach    }
115cfbf56adSGreg Roach
116cfbf56adSGreg Roach    /**
117cfbf56adSGreg Roach     * @param ResponseInterface $response
118cfbf56adSGreg Roach     */
119cfbf56adSGreg Roach    private function emitStatusLine(ResponseInterface $response): void
120cfbf56adSGreg Roach    {
121cfbf56adSGreg Roach        http_response_code($response->getStatusCode());
122cfbf56adSGreg Roach
12374d6dc0eSGreg Roach        header(sprintf(
12474d6dc0eSGreg Roach            'HTTP/%s %d %s',
125cfbf56adSGreg Roach            $response->getProtocolVersion(),
126cfbf56adSGreg Roach            $response->getStatusCode(),
127cfbf56adSGreg Roach            $response->getReasonPhrase()
128cfbf56adSGreg Roach        ));
129cfbf56adSGreg Roach    }
130cfbf56adSGreg Roach
131cfbf56adSGreg Roach    /**
132cfbf56adSGreg Roach     * @param ResponseInterface $response
133cfbf56adSGreg Roach     */
134cfbf56adSGreg Roach    private function emitHeaders(ResponseInterface $response): void
135cfbf56adSGreg Roach    {
136cfbf56adSGreg Roach        foreach ($response->getHeaders() as $name => $values) {
137cfbf56adSGreg Roach            foreach ($values as $value) {
138cfbf56adSGreg Roach                header(
139cfbf56adSGreg Roach                    sprintf('%s: %s', $name, $value),
140cfbf56adSGreg Roach                    false,
141cfbf56adSGreg Roach                    $response->getStatusCode()
142cfbf56adSGreg Roach                );
143cfbf56adSGreg Roach            }
144cfbf56adSGreg Roach        }
145cfbf56adSGreg Roach    }
146cfbf56adSGreg Roach
147cfbf56adSGreg Roach    /**
148cfbf56adSGreg Roach     * @param ResponseInterface $response
149cfbf56adSGreg Roach     *
150cfbf56adSGreg Roach     * @return void
151cfbf56adSGreg Roach     */
152cfbf56adSGreg Roach    private function emitBody(ResponseInterface $response): void
153cfbf56adSGreg Roach    {
154cfbf56adSGreg Roach        $body = $response->getBody();
155cfbf56adSGreg Roach
156cfbf56adSGreg Roach        if ($body->isSeekable()) {
157cfbf56adSGreg Roach            $body->rewind();
158cfbf56adSGreg Roach        }
159cfbf56adSGreg Roach
160cfbf56adSGreg Roach        while (!$body->eof() && connection_status() === CONNECTION_NORMAL) {
161cfbf56adSGreg Roach            echo $body->read(self::CHUNK_SIZE);
162cfbf56adSGreg Roach        }
163cfbf56adSGreg Roach    }
164cfbf56adSGreg Roach
165cfbf56adSGreg Roach    /**
166cfbf56adSGreg Roach     * @return void
167cfbf56adSGreg Roach     */
168cfbf56adSGreg Roach    private function closeConnection(): void
169cfbf56adSGreg Roach    {
170cfbf56adSGreg Roach        if (function_exists('fastcgi_finish_request')) {
171cfbf56adSGreg Roach            fastcgi_finish_request();
172cfbf56adSGreg Roach        }
173cfbf56adSGreg Roach    }
174cfbf56adSGreg Roach}
175