Passed
Push — master ( 7ca617...9cb733 )
by Alexander
26:25 queued 24:16
created

HeaderValueHelper::getSortedValueAndParameters()   B

Complexity

Conditions 10
Paths 9

Size

Total Lines 47
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 24
CRAP Score 10

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 23
c 1
b 0
f 0
dl 0
loc 47
ccs 24
cts 24
cp 1
rs 7.6666
cc 10
nc 9
nop 4
crap 10

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Http;
6
7
use InvalidArgumentException;
8
9
use function array_shift;
10
use function asort;
11
use function count;
12
use function explode;
13
use function implode;
14
use function is_array;
15
use function is_string;
16
use function mb_strtolower;
17
use function mb_strpos;
18
use function mb_substr;
19
use function preg_match;
20
use function preg_split;
21
use function preg_replace;
22
use function preg_replace_callback;
23
use function reset;
24
use function rtrim;
25
use function strtolower;
26
use function strpos;
27
use function substr;
28
use function trim;
29
use function usort;
30
31
/**
32
 * HeaderValueHelper parses the header value parameters.
33
 */
34
final class HeaderValueHelper
35
{
36
    /**
37
     * @link https://www.rfc-editor.org/rfc/rfc2616.html#section-2.2
38
     * token = 1*<any CHAR except CTLs or separators>
39
     */
40
    private const PATTERN_TOKEN = '(?:(?:[^()<>@,;:\\"\/[\\]?={} \t\x7f]|[\x00-\x1f])+)';
41
42
    /**
43
     * @link https://www.rfc-editor.org/rfc/rfc2616.html#section-3.6
44
     * attribute = token
45
     */
46
    private const PATTERN_ATTRIBUTE = self::PATTERN_TOKEN;
47
48
    /**
49
     * @link https://www.rfc-editor.org/rfc/rfc2616.html#section-2.2
50
     * quoted-string  = ( <"> *(qdtext | quoted-pair ) <"> )
51
     * qdtext         = <any TEXT except <">>
52
     * quoted-pair    = "\" CHAR
53
     */
54
    private const PATTERN_QUOTED_STRING = '(?:"(?:(?:\\\\.)+|[^\\"]+)*")';
55
56
    /**
57
     * @link https://www.rfc-editor.org/rfc/rfc2616.html#section-3.6
58
     * value = token | quoted-string
59
     */
60
    private const PATTERN_VALUE = '(?:' . self::PATTERN_QUOTED_STRING . '|' . self::PATTERN_TOKEN . ')';
61
62
    /**
63
     * Explodes a header value to value and parameters (eg. text/html;q=2;version=6)
64
     *
65
     * @link https://www.rfc-editor.org/rfc/rfc2616.html#section-3.6
66
     * transfer-extension = token *( ";" parameter )
67
     *
68
     * @param string $headerValue Header value.
69
     * @param bool $lowerCaseValue Whether should cast header value to lowercase.
70
     * @param bool $lowerCaseParameter Whether should cast header parameter name to lowercase.
71
     * @param bool $lowerCaseParameterValue Whether should cast header parameter value to lowercase.
72
     *
73
     * @return array First element is the value, and key-value are the parameters.
74
     */
75 31
    public static function getValueAndParameters(
76
        string $headerValue,
77
        bool $lowerCaseValue = true,
78
        bool $lowerCaseParameter = true,
79
        bool $lowerCaseParameterValue = true
80
    ): array {
81 31
        $headerValue = trim($headerValue);
82
83 31
        if ($headerValue === '') {
84 1
            return [];
85
        }
86
87 30
        $parts = explode(';', $headerValue, 2);
88 30
        $output = [$lowerCaseValue ? strtolower($parts[0]) : $parts[0]];
89
90 30
        if (count($parts) === 1) {
91 12
            return $output;
92
        }
93
94 27
        return $output + self::getParameters($parts[1], $lowerCaseParameter, $lowerCaseParameterValue);
95
    }
96
97
    /**
98
     * Explodes a header value parameters (eg. q=2;version=6)
99
     *
100
     * @link https://tools.ietf.org/html/rfc7230#section-3.2.6
101
     *
102
     * @param string $headerValueParameters Header value parameters.
103
     * @param bool $lowerCaseParameter Whether should cast header parameter name to lowercase.
104
     * @param bool $lowerCaseParameterValue Whether should cast header parameter value to lowercase.
105
     *
106
     * @return array Key-value are the parameters.
107
     */
108 60
    public static function getParameters(
109
        string $headerValueParameters,
110
        bool $lowerCaseParameter = true,
111
        bool $lowerCaseParameterValue = true
112
    ): array {
113 60
        $headerValueParameters = trim($headerValueParameters);
114
115 60
        if ($headerValueParameters === '') {
116
            return [];
117
        }
118
119 60
        if (rtrim($headerValueParameters, ';') !== $headerValueParameters) {
120 1
            throw new InvalidArgumentException('Cannot end with a semicolon.');
121
        }
122
123 59
        $output = [];
124
125
        do {
126
            /** @psalm-suppress InvalidArgument */
127 59
            $headerValueParameters = preg_replace_callback(
128 59
                '/^[ \t]*(?<parameter>' . self::PATTERN_ATTRIBUTE . ')[ \t]*=[ \t]*(?<value>' . self::PATTERN_VALUE . ')[ \t]*(?:;|$)/u',
129 59
                static function (array $matches) use (&$output, $lowerCaseParameter, $lowerCaseParameterValue) {
130 50
                    $value = $matches['value'];
131
132 50
                    if (mb_strpos($matches['value'], '"') === 0) {
133
                        // unescape + remove first and last quote
134 13
                        $value = preg_replace('/\\\\(.)/u', '$1', mb_substr($value, 1, -1));
135
                    }
136
137 50
                    $key = $lowerCaseParameter ? mb_strtolower($matches['parameter']) : $matches['parameter'];
138
139 50
                    if (isset($output[$key])) {
140
                        // The first is the winner.
141 2
                        return;
142
                    }
143
144 50
                    $output[$key] = $lowerCaseParameterValue ? mb_strtolower($value) : $value;
145
                },
146
                $headerValueParameters,
147
                1,
148
                $count
149
            );
150
151 59
            if ($count !== 1) {
152 11
                throw new InvalidArgumentException('Invalid input: ' . $headerValueParameters);
153
            }
154 50
        } while ($headerValueParameters !== '');
155
156 48
        return $output;
157
    }
158
159
    /**
160
     * Returns a header value as "q" factor sorted list.
161
     *
162
     * @param mixed $values Header value as a comma-separated string or already exploded string array.
163
     * @param bool $lowerCaseValue Whether should cast header value to lowercase.
164
     * @param bool $lowerCaseParameter Whether should cast header parameter name to lowercase.
165
     * @param bool $lowerCaseParameterValue Whether should cast header parameter value to lowercase.
166
     *
167
     * @return array The q factor sorted list.
168
     *
169
     * @link https://developer.mozilla.org/en-US/docs/Glossary/Quality_values
170
     * @link https://www.ietf.org/rfc/rfc2045.html#section-2
171
     * @see getValueAndParameters
172
     */
173 27
    public static function getSortedValueAndParameters(
174
        $values,
175
        bool $lowerCaseValue = true,
176
        bool $lowerCaseParameter = true,
177
        bool $lowerCaseParameterValue = true
178
    ): array {
179 27
        if (!is_array($values) && !is_string($values)) {
180 2
            throw new InvalidArgumentException('Values are neither array nor string.');
181
        }
182
183 25
        $list = [];
184
185 25
        foreach ((array) $values as $headerValue) {
186 23
            $list = [...$list, ...preg_split('/\s*,\s*/', trim($headerValue), -1, PREG_SPLIT_NO_EMPTY)];
187
        }
188
189 25
        if (count($list) === 0) {
190 4
            return [];
191
        }
192
193 21
        $output = [];
194
195 21
        foreach ($list as $value) {
196 21
            $parse = self::getValueAndParameters($value, $lowerCaseValue, $lowerCaseParameter, $lowerCaseParameterValue);
197
            // case-insensitive "q" parameter
198 21
            $q = $parse['q'] ?? $parse['Q'] ?? 1.0;
199
200
            // min 0.000 max 1.000, max 3 digits, without digits allowed
201 21
            if (is_string($q) && preg_match('/^(?:0(?:\.\d{1,3})?|1(?:\.0{1,3})?)$/', $q) === 0) {
202 4
                throw new InvalidArgumentException('Invalid q factor.');
203
            }
204
205 17
            $parse['q'] = (float) $q;
206 17
            unset($parse['Q']);
207 17
            $output[] = $parse;
208
        }
209
210 17
        usort($output, static function (array $a, array $b) {
211 17
            $a = $a['q'];
212 17
            $b = $b['q'];
213 17
            if ($a === $b) {
214 9
                return 0;
215
            }
216 9
            return $a > $b ? -1 : 1;
217
        });
218
219 17
        return $output;
220
    }
221
222
    /**
223
     * Returns a list of sorted content types from the accept header values.
224
     *
225
     * @param string|string[] $values Header value as a comma-separated string or already exploded string array.
226
     *
227
     * @return string[] Sorted accept types. Note: According to RFC 7231, special parameters (except the q factor)
228
     * are added to the type, which are always appended by a semicolon and sorted by string.
229
     *
230
     * @link https://tools.ietf.org/html/rfc7231#section-5.3.2
231
     * @link https://www.ietf.org/rfc/rfc2045.html#section-2
232
     */
233 14
    public static function getSortedAcceptTypes($values): array
234
    {
235 14
        $output = self::getSortedValueAndParameters($values);
236
237 14
        usort($output, static function (array $a, array $b) {
238 12
            if ($a['q'] !== $b['q']) {
239
                // The higher q value wins
240 5
                return $a['q'] > $b['q'] ? -1 : 1;
241
            }
242
243 8
            $typeA = reset($a);
244 8
            $typeB = reset($b);
245
246 8
            if (strpos($typeA, '*') === false && strpos($typeB, '*') === false) {
247 8
                $countA = count($a);
248 8
                $countB = count($b);
249 8
                if ($countA === $countB) {
250
                    // They are equivalent for the same parameter number
251 6
                    return 0;
252
                }
253
                // No wildcard character, higher parameter number wins
254 2
                return $countA > $countB ? -1 : 1;
255
            }
256
257 1
            $endWildcardA = substr($typeA, -1, 1) === '*';
258 1
            $endWildcardB = substr($typeB, -1, 1) === '*';
259
260 1
            if (($endWildcardA && !$endWildcardB) || (!$endWildcardA && $endWildcardB)) {
261
                // The wildcard ends is the loser.
262 1
                return $endWildcardA ? 1 : -1;
263
            }
264
265
            // The wildcard starts is the loser.
266 1
            return strpos($typeA, '*') === 0 ? 1 : -1;
267
        });
268
269 14
        foreach ($output as $key => $value) {
270 12
            $type = array_shift($value);
271 12
            unset($value['q']);
272
273 12
            if (count($value) === 0) {
274 11
                $output[$key] = $type;
275 11
                continue;
276
            }
277
278 4
            foreach ($value as $k => $v) {
279 4
                $value[$k] = $k . '=' . $v;
280
            }
281
282
            // Parameters are sorted for easier use of parameter variations.
283 4
            asort($value, SORT_STRING);
284 4
            $output[$key] = $type . ';' . implode(';', $value);
285
        }
286
287 14
        return $output;
288
    }
289
}
290