Passed
Push — master ( a9a0cb...36269a )
by Alexander
02:21
created

HeaderHelper::getValueAndParameters()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 4

Importance

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