Passed
Push — master ( 27f23d...ae3d19 )
by Alexander
07:54
created

HeaderHelper::getSortedAcceptTypes()   C

Complexity

Conditions 16
Paths 4

Size

Total Lines 44
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 30
CRAP Score 16

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 16
eloc 28
c 2
b 0
f 0
nc 4
nop 1
dl 0
loc 44
ccs 30
cts 30
cp 1
crap 16
rs 5.5666

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