Completed
Push — master ( 71dea2...179fae )
by Greg
09:14
created

CompressResponse::process()   B

Complexity

Conditions 6
Paths 7

Size

Total Lines 33
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 20
nc 7
nop 2
dl 0
loc 33
rs 8.9777
c 1
b 0
f 0
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
18
declare(strict_types=1);
19
20
namespace Fisharebest\Webtrees\Http\Middleware;
21
22
use Psr\Http\Message\RequestInterface;
23
use Psr\Http\Message\ResponseInterface;
24
use Psr\Http\Message\ServerRequestInterface;
25
use Psr\Http\Message\StreamFactoryInterface;
26
use Psr\Http\Server\MiddlewareInterface;
27
use Psr\Http\Server\RequestHandlerInterface;
28
29
use function extension_loaded;
30
use function gzdeflate;
31
use function gzencode;
32
use function in_array;
33
use function str_contains;
34
use function strlen;
35
use function strstr;
36
use function strtolower;
37
use function strtr;
38
39
/**
40
 * Middleware to compress (gzip or deflate) a response.
41
 */
42
class 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) {
1 ignored issue
show
introduced by
The condition $content === false is always false.
Loading history...
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