Issues (2502)

app/Http/Middleware/EmitResponse.php (1 issue)

Labels
Severity
1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2025 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 <https://www.gnu.org/licenses/>.
16
 */
17
18
declare(strict_types=1);
19
20
namespace Fisharebest\Webtrees\Http\Middleware;
21
22
use Fisharebest\Webtrees\Services\PhpService;
23
use Psr\Http\Message\ResponseInterface;
24
use Psr\Http\Message\ServerRequestInterface;
25
use Psr\Http\Server\MiddlewareInterface;
26
use Psr\Http\Server\RequestHandlerInterface;
27
use RuntimeException;
28
29
use function connection_status;
30
use function fastcgi_finish_request;
31
use function header;
32
use function header_remove;
33
use function headers_sent;
34
use function http_response_code;
35
use function ob_get_length;
36
use function ob_get_level;
37
use function sprintf;
38
39
use const CONNECTION_NORMAL;
40
41
/**
42
 * Middleware to emit the response - send it back to the webserver.
43
 */
44
class EmitResponse implements MiddlewareInterface
45
{
46
    // Stream the output in chunks.
47
    private const int CHUNK_SIZE = 65536;
0 ignored issues
show
A parse error occurred: Syntax error, unexpected T_STRING, expecting '=' on line 47 at column 22
Loading history...
48
49
    public function __construct(private PhpService $php_service)
50
    {
51
    }
52
53
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
54
    {
55
        $response = $handler->handle($request);
56
57
        $this->assertHeadersNotEmitted();
58
        $this->removeDefaultPhpHeaders();
59
60
        // Unless webtrees set a cache-control header, assume the page cannot be cached
61
        if (!$response->hasHeader('cache-control')) {
62
            $response = $response->withHeader('cache-control', 'no-store');
63
        }
64
65
        $this->assertBodyNotEmitted();
66
        $this->emitStatusLine($response);
67
        $this->emitHeaders($response);
68
        $this->emitBody($response);
69
        $this->closeConnection();
70
71
        return $response;
72
    }
73
74
    private function removeDefaultPhpHeaders(): void
75
    {
76
        header_remove('X-Powered-By');
77
        header_remove('cache-control');
78
        header_remove('Expires');
79
        header_remove('Pragma');
80
    }
81
82
    private function assertHeadersNotEmitted(): void
83
    {
84
        if (headers_sent($file, $line)) {
85
            $message = sprintf('Headers already sent at %s:%d', $file, $line);
86
87
            throw new RuntimeException($message);
88
        }
89
    }
90
91
    private function assertBodyNotEmitted(): void
92
    {
93
        if (ob_get_level() > 0 && ob_get_length() > 0) {
94
            // The output probably contains an error message.
95
            $output = ob_get_clean();
96
97
            throw new RuntimeException('Output already started: ' . $output);
98
        }
99
    }
100
101
    private function emitStatusLine(ResponseInterface $response): void
102
    {
103
        http_response_code($response->getStatusCode());
104
105
        header(sprintf(
106
            'HTTP/%s %d %s',
107
            $response->getProtocolVersion(),
108
            $response->getStatusCode(),
109
            $response->getReasonPhrase()
110
        ));
111
    }
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
    private function emitBody(ResponseInterface $response): void
127
    {
128
        $body = $response->getBody();
129
130
        if ($body->isSeekable()) {
131
            $body->rewind();
132
        }
133
134
        while (!$body->eof() && connection_status() === CONNECTION_NORMAL) {
135
            echo $body->read(self::CHUNK_SIZE);
136
        }
137
    }
138
139
    private function closeConnection(): void
140
    {
141
        if ($this->php_service->functionExists(function: 'fastcgi_finish_request')) {
142
            fastcgi_finish_request();
143
        }
144
    }
145
}
146