1<?php 2 3/** 4 * webtrees: online genealogy 5 * Copyright (C) 2021 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 18declare(strict_types=1); 19 20namespace Fisharebest\Webtrees\Http\Middleware; 21 22use Psr\Http\Message\RequestInterface; 23use Psr\Http\Message\ResponseInterface; 24use Psr\Http\Message\ServerRequestInterface; 25use Psr\Http\Message\StreamFactoryInterface; 26use Psr\Http\Server\MiddlewareInterface; 27use Psr\Http\Server\RequestHandlerInterface; 28 29use function extension_loaded; 30use function gzdeflate; 31use function gzencode; 32use function in_array; 33use function str_contains; 34use function strstr; 35use function strtolower; 36use function strtr; 37 38/** 39 * Middleware to compress (gzip or deflate) a response. 40 */ 41class CompressResponse implements MiddlewareInterface 42{ 43 // Non-text responses that will benefit from compression. 44 protected const MIME_TYPES = [ 45 'application/javascript', 46 'application/json', 47 'application/pdf', 48 'application/vnd.geo+json', 49 'application/xml', 50 'image/svg+xml', 51 ]; 52 53 protected StreamFactoryInterface $stream_factory; 54 55 /** 56 * CompressResponse constructor. 57 * 58 * @param StreamFactoryInterface $stream_factory 59 */ 60 public function __construct(StreamFactoryInterface $stream_factory) 61 { 62 $this->stream_factory = $stream_factory; 63 } 64 65 /** 66 * @param ServerRequestInterface $request 67 * @param RequestHandlerInterface $handler 68 * 69 * @return ResponseInterface 70 */ 71 public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface 72 { 73 $response = $handler->handle($request); 74 75 $method = $this->compressionMethod($request); 76 77 if ($method !== null && $this->isCompressible($response)) { 78 $content = (string) $response->getBody(); 79 80 switch ($method) { 81 case 'deflate': 82 $content = gzdeflate($content); 83 break; 84 85 case 'gzip': 86 $content = gzencode($content); 87 break; 88 } 89 90 if ($content === false) { 91 return $response; 92 } 93 94 $stream = $this->stream_factory->createStream($content); 95 96 return $response 97 ->withBody($stream) 98 ->withHeader('content-encoding', $method) 99 ->withHeader('vary', 'accept-encoding'); 100 } 101 102 return $response; 103 } 104 105 /** 106 * @param RequestInterface $request 107 * 108 * @return string|null 109 */ 110 protected function compressionMethod(RequestInterface $request): ?string 111 { 112 $accept_encoding = strtolower($request->getHeaderLine('accept-encoding')); 113 $zlib_available = extension_loaded('zlib'); 114 115 if ($zlib_available) { 116 if (str_contains($accept_encoding, 'gzip')) { 117 return 'gzip'; 118 } 119 120 if (str_contains($accept_encoding, 'deflate')) { 121 return 'deflate'; 122 } 123 } 124 125 return null; 126 } 127 128 /** 129 * @param ResponseInterface $response 130 * 131 * @return bool 132 */ 133 protected function isCompressible(ResponseInterface $response): bool 134 { 135 // Already encoded? 136 if ($response->hasHeader('content-encoding')) { 137 return false; 138 } 139 140 $content_type = $response->getHeaderLine('content-type'); 141 $content_type = strtr($content_type, [' ' => '']); 142 $content_type = strstr($content_type, ';', true) ?: $content_type; 143 $content_type = strtolower($content_type); 144 145 if (str_starts_with($content_type, 'text/')) { 146 return true; 147 } 148 149 return in_array($content_type, static::MIME_TYPES, true); 150 } 151} 152