xref: /webtrees/app/Http/Middleware/CompressResponse.php (revision 8d018ab2fe3b6881cab8124d97e1a69e0be79d32)
1<?php
2
3/**
4 * webtrees: online genealogy
5 * Copyright (C) 2023 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