Completed
Pull Request — master (#183)
by
unknown
02:46 queued 49s
created

HeaderHelper::getSortedValueAndParameters()   B

Complexity

Conditions 9
Paths 10

Size

Total Lines 32
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 9

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 9
eloc 21
c 1
b 0
f 0
nc 10
nop 1
dl 0
loc 32
rs 8.0555
ccs 12
cts 12
cp 1
crap 9
1
<?php
2
3
namespace Yiisoft\Yii\Web\Helper;
4
5
use Psr\Http\Message\RequestInterface;
6
7
final class HeaderHelper
8
{
9
    /**
10
     * Explode header value to value and parameters (eg. text/html;q=2;version=6)
11
     * @param string $headerValue
12
     * @return array first element is the value, and key-value are the parameters
13
     */
14 29
    public static function getValueAndParameters(string $headerValue): array
15
    {
16 29
        $headerValue = trim($headerValue);
17 29
        if ($headerValue === '') {
18 1
            return [];
19
        }
20 28
        $parts = preg_split('/\s*;\s*/', $headerValue, 2, PREG_SPLIT_NO_EMPTY);
21 28
        $output = [$parts[0]];
22 28
        if (count($parts) === 1) {
1 ignored issue
show
Bug introduced by
It seems like $parts can also be of type false; however, parameter $var of count() does only seem to accept Countable|array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

22
        if (count(/** @scrutinizer ignore-type */ $parts) === 1) {
Loading history...
23 21
            return $output;
24 21
        }
25 1
        return array_merge($output, self::getParameters($parts[1]));
26
    }
27 21
28
    /**
29 28
     * Explode header value to parameters (eg. q=2;version=6)
30
     *
31
     * @link https://tools.ietf.org/html/rfc7230#section-3.2.6
32
     */
33
    public static function getParameters(string $headerValue): array
34
    {
35
        $headerValue = trim($headerValue);
36
        if ($headerValue === '') {
37
            return [];
38 32
        }
39
        $output = new \stdClass();
40 32
        $output->data = [];
41 21
        do {
42
            $headerValue = preg_replace_callback(
43 32
                '/^\s*(?<parameter>\w+)\s*=\s*(?:(?<qoute>")(?<valueQuoted>[^"]+)\k<qoute>|(?<value>[!#$%&\'*+.^`|~\w\d-]+))\s*(?:;|$)/',
44 3
                function ($matches) use ($output) {
45
                    $output->data[$matches['parameter']] = $matches['value'] ?? $matches['valueQuoted'];
46 29
                }, $headerValue, 1, $count);
47 4
            if ($count !== 1) {
48
                throw new \InvalidArgumentException('Invalid input: ' . $headerValue);
49 25
            }
50 25
        } while ($headerValue !== '');
51 25
        return $output->data;
52 25
    }
53
54
    /**
55 25
     * Getting header value as q factor sorted list
56 4
     * @param string|string[] $values Header value as a comma-separated string or already exploded string array.
57
     * @see getValueAndParameters
58 21
     * @link https://developer.mozilla.org/en-US/docs/Glossary/Quality_values
59 21
     */
60
    public static function getSortedValueAndParameters($values): array
61
    {
62 17
        if (is_string($values)) {
63 17
            $values = preg_split('/\s*,\s*/', trim($values), -1, PREG_SPLIT_NO_EMPTY);
64 17
        }
65 9
        if (!is_array($values)) {
66
            throw new \InvalidArgumentException('Values ​​are neither array nor string');
67 8
        }
68 21
        if (count($values) === 0) {
69 21
            return [];
70
        }
71
        $output = [];
72
        foreach ($values as $value) {
73
            $parse = self::getValueAndParameters($value);
74
            $q = $parse['q'] ?? 1.0;
75 6
76
            // min 0.000 max 1.000, max 3 digits, without digits allowed
77 6
            if (is_string($q) && preg_match('/^(?:0(?:\.\d{1,3})?|1(?:\.0{1,3})?)$/', $q) === 0) {
78
                throw new \InvalidArgumentException('Invalid q factor');
79
            }
80
            $parse['q'] = floatval($q);
81
            $output[] = $parse;
82
        }
83
        usort($output, static function ($a, $b) {
84
            $a = $a['q'];
85
            $b = $b['q'];
86 18
            if ($a === $b) {
87
                return 0;
88 18
            }
89
            return $a > $b ? -1 : 1;
90 12
        });
91
        return $output;
92 4
    }
93
94 8
    /**
95 8
     * @see getSortedAcceptTypes
96 8
     */
97 8
    public static function getSortedAcceptTypesFromRequest(RequestInterface $request): array
98 8
    {
99 8
        return static::getSortedAcceptTypes($request->getHeader('accept'));
100
    }
101 5
102
    /**
103
     * @param $values string|string[] $values Header value as a comma-separated string or already exploded string array
104 3
     * @return string[] sorted accept types. Note: According to RFC 7231, special parameters (except the q factor) are
105
     *                  added to the type, which are always appended by a semicolon and sorted by string.
106 1
     * @link https://tools.ietf.org/html/rfc7231#section-5.3.2
107 1
     */
108 1
    public static function getSortedAcceptTypes($values): array
109
    {
110 1
        $output = self::getSortedValueAndParameters($values);
111
        usort($output, static function ($a, $b) {
112
            if ($a['q'] !== $b['q']) {
113 1
                // The higher q value wins
114 18
                return $a['q'] > $b['q'] ? -1 : 1;
115 18
            }
116 16
            $typeA = reset($a);
117 16
            $typeB = reset($b);
118 16
            if (strpos($typeA, '*') === false && strpos($typeB, '*') === false) {
119 15
                $countA = count($a);
120 15
                $countB = count($b);
121
                if ($countA === $countB) {
122 5
                    // They are equivalent for the same parameter number
123 5
                    return 0;
124
                }
125
                // No wildcard character, higher parameter number wins
126 5
                return $countA > $countB ? -1 : 1;
127 5
            }
128
            $endWildcardA = substr($typeA, -1, 1) === '*';
129 18
            $endWildcardB = substr($typeB, -1, 1) === '*';
130
            if (($endWildcardA && !$endWildcardB) || (!$endWildcardA && $endWildcardB)) {
131
                // The wildcard ends is the loser.
132
                return $endWildcardA ? 1 : -1;
133
            }
134
            // The wildcard starts is the loser.
135
            return strpos($typeA, '*') === 0 ? 1 : -1;
136
        });
137
        foreach ($output as $key => $value) {
138
            $type = array_shift($value);
139
            unset($value['q']);
140
            if (count($value) === 0) {
141
                $output[$key] = $type;
142
                continue;
143
            }
144
            foreach ($value as $k => $v) {
145
                $value[$k] = $k . '=' . $v;
146
            }
147
            // Parameters are sorted for easier use of parameter variations.
148
            asort($value, SORT_STRING);
149
            $output[$key] = $type . ';' . join(';', $value);
150
        }
151
        return $output;
152
    }
153
}
154