Passed
Pull Request — master (#25)
by Evgeniy
02:12
created

HeaderHelper   A

Complexity

Total Complexity 39

Size/Duplication

Total Lines 203
Duplicated Lines 0 %

Test Coverage

Coverage 98.88%

Importance

Changes 1
Bugs 1 Features 0
Metric Value
wmc 39
eloc 86
dl 0
loc 203
ccs 88
cts 89
cp 0.9888
rs 9.28
c 1
b 1
f 0

4 Methods

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