Passed
Pull Request — master (#185)
by
unknown
02:40
created

HeaderHelper   A

Complexity

Total Complexity 39

Size/Duplication

Total Lines 190
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 86
c 3
b 0
f 0
dl 0
loc 190
rs 9.28
ccs 65
cts 65
cp 1
wmc 39

5 Methods

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