1cfbf56adSGreg Roach<?php 23976b470SGreg Roach 3cfbf56adSGreg Roach/** 4cfbf56adSGreg Roach * webtrees: online genealogy 5*f1d11eefSGreg Roach * Copyright (C) 2020 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 15cfbf56adSGreg Roach * along with this program. If not, see <http://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(); 60*f1d11eefSGreg Roach $this->removeDefaultPhpHeaders(); 61*f1d11eefSGreg Roach 62*f1d11eefSGreg Roach // Unless webtrees set a cache-control header, assume the page cannot be cached 63*f1d11eefSGreg Roach if (!$response->hasHeader('Cache-Control')) { 64*f1d11eefSGreg Roach $response = $response->withHeader('Cache-Control', 'no-store'); 65*f1d11eefSGreg Roach } 66*f1d11eefSGreg 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'); 84*f1d11eefSGreg Roach header_remove('Cache-control'); 85*f1d11eefSGreg Roach header_remove('Expires'); 86*f1d11eefSGreg 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