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 /** @var StreamFactoryInterface */ 55 protected $stream_factory; 56 57 /** 58 * CompressResponse constructor. 59 * 60 * @param StreamFactoryInterface $stream_factory 61 */ 62 public function __construct(StreamFactoryInterface $stream_factory) 63 { 64 $this->stream_factory = $stream_factory; 65 } 66 67 /** 68 * @param ServerRequestInterface $request 69 * @param RequestHandlerInterface $handler 70 * 71 * @return ResponseInterface 72 */ 73 public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface 74 { 75 $response = $handler->handle($request); 76 77 $method = $this->compressionMethod($request); 78 79 if ($method !== null && $this->isCompressible($response)) { 80 $content = (string) $response->getBody(); 81 82 switch ($method) { 83 case 'deflate': 84 $content = gzdeflate($content); 85 break; 86 87 case 'gzip': 88 $content = gzencode($content); 89 break; 90 } 91 92 if ($content === false) { 93 return $response; 94 } 95 96 $stream = $this->stream_factory->createStream($content); 97 98 return $response 99 ->withBody($stream) 100 ->withHeader('content-encoding', $method) 101 ->withHeader('content-length', (string) strlen($content)) 102 ->withHeader('vary', 'accept-encoding'); 103 } 104 105 return $response; 106 } 107 108 /** 109 * @param RequestInterface $request 110 * 111 * @return string|null 112 */ 113 protected function compressionMethod(RequestInterface $request): ?string 114 { 115 $accept_encoding = strtolower($request->getHeaderLine('accept-encoding')); 116 $zlib_available = extension_loaded('zlib'); 117 118 if ($zlib_available) { 119 if (str_contains($accept_encoding, 'gzip')) { 120 return 'gzip'; 121 } 122 123 if (str_contains($accept_encoding, 'deflate')) { 124 return 'deflate'; 125 } 126 } 127 128 return null; 129 } 130 131 /** 132 * @param ResponseInterface $response 133 * 134 * @return bool 135 */ 136 protected function isCompressible(ResponseInterface $response): bool 137 { 138 // Already encoded? 139 if ($response->hasHeader('content-encoding')) { 140 return false; 141 } 142 143 $content_type = $response->getHeaderLine('content-type'); 144 $content_type = strtr($content_type, [' ' => '']); 145 $content_type = strstr($content_type, ';', true) ?: $content_type; 146 $content_type = strtolower($content_type); 147 148 if (str_starts_with($content_type, 'text/')) { 149 return true; 150 } 151 152 return in_array($content_type, static::MIME_TYPES, true); 153 } 154} 155