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