Issues (5)

src/Parser/FieldParser.php (1 issue)

Labels
Severity
1
<?php
2
3
/**
4
 * @copyright   (c) 2006-present brian ridley
5
 * @author      brian ridley <[email protected]>
6
 * @license     http://opensource.org/licenses/MIT MIT
7
 */
8
9
namespace ptlis\ConNeg\Parser;
10
11
use ptlis\ConNeg\Exception\InvalidVariantException;
12
use ptlis\ConNeg\Preference\Builder\PreferenceBuilderInterface;
13
use ptlis\ConNeg\Preference\PreferenceInterface;
14
15
/**
16
 * Parser that accepts a tokenized HTTP Accept or Accept-* field and returns an array of Preference value objects.
17
 */
18
class FieldParser
19
{
20
    /**
21
     * @var PreferenceBuilderInterface
22
     */
23
    private $prefBuilder;
24
25
    /**
26
     * @var PreferenceBuilderInterface
27
     */
28
    private $mimePrefBuilder;
29
30
31
    /**
32
     * Constructor.
33
     *
34
     * @param PreferenceBuilderInterface $prefBuilder
35
     * @param PreferenceBuilderInterface $mimePrefBuilder
36
     */
37 89
    public function __construct(
38
        PreferenceBuilderInterface $prefBuilder,
39
        PreferenceBuilderInterface $mimePrefBuilder
40
    ) {
41 89
        $this->prefBuilder = $prefBuilder;
42 89
        $this->mimePrefBuilder = $mimePrefBuilder;
43 89
    }
44
45
    /**
46
     * Accepts a tokenized Accept* HTTP field and returns an array of Preference value objects.
47
     *
48
     * @throws InvalidVariantException
49
     *
50
     * @param array<string> $tokenList Parsed tokens
51
     * @param bool $serverField If true the field came from the server & we error on malformed data, otherwise we
52
     *                          suppress errors for client preferences..
53
     * @param string $fromField Which field the tokens came from
54
     *
55
     * @return PreferenceInterface[]
56
     */
57 89
    public function parse(array $tokenList, $serverField, $fromField)
58
    {
59
        // Bundle tokens by variant.
60 89
        $bundleList = $this->bundleTokens($tokenList, Tokens::VARIANT_SEPARATOR);
61
62 89
        $prefList = array();
63 89
        foreach ($bundleList as $bundle) {
64 81
            $prefList[] = $this->parseBundle($bundle, $serverField, $fromField);
65
        }
66
67 85
        return $prefList;
68
    }
69
70
    /**
71
     * Accepts tokens for a single variant and returns the Preference value object encapsulating that data.
72
     *
73
     * @throws InvalidVariantException
74
     *
75
     * @param array<string> $tokenBundle
76
     * @param bool $serverField
77
     * @param string $fromField
78
     *
79
     * @return null|PreferenceInterface
80
     */
81 81
    private function parseBundle(array $tokenBundle, $serverField, $fromField)
82
    {
83 81
        $pref = null;
84
        try {
85 81
            list($variantTokenList, $paramTokenList) = $this->splitVariantAndParamTokens($tokenBundle, $fromField);
86
87 79
            $paramBundleList = $this->bundleTokens($paramTokenList, Tokens::PARAMS_SEPARATOR);
88 79
            $this->validateParamBundleList($paramBundleList, $serverField);
89
90 78
            $pref = $this->createPreference($variantTokenList, $paramBundleList, $serverField, $fromField);
91
92 4
        } catch (InvalidVariantException $e) {
93 4
            if ($serverField) {
94 4
                throw $e;
95
            }
96
        }
97
98 78
        return $pref;
99
    }
100
101
    /**
102
     * Splits the token list into variant & parameter arrays.
103
     *
104
     * @throws InvalidVariantException
105
     *
106
     * @param array<string> $tokenBundle
107
     * @param string $fromField
108
     *
109
     * @return string[][]
110
     */
111 81
    private function splitVariantAndParamTokens(array $tokenBundle, $fromField)
112
    {
113 81
        if (PreferenceInterface::MIME === $fromField) {
114 35
            $this->validateBundleMimeVariant($tokenBundle);
115 33
            $variantTokenList = array_slice($tokenBundle, 0, 3);
116 33
            $paramTokenList = array_slice($tokenBundle, 3);
117
        } else {
118 46
            $variantTokenList = array_slice($tokenBundle, 0, 1);
119 46
            $paramTokenList = array_slice($tokenBundle, 1);
120
        }
121
122 79
        return array($variantTokenList, $paramTokenList);
123
    }
124
125
    /**
126
     * Accepts the bundled tokens for variant & parameter data and builds the Preference value object.
127
     *
128
     * @param array<string> $variantTokenList
129
     * @param array<array<string>> $paramBundleList
130
     * @param bool $serverField
131
     * @param string $fromField
132
     *
133
     * @return PreferenceInterface
134
     */
135 78
    private function createPreference(array $variantTokenList, array $paramBundleList, $serverField, $fromField)
136
    {
137 78
        $builder = $this->getBuilder($fromField)
138 78
            ->setFromField($fromField)
139 78
            ->setFromServer($serverField)
140 78
            ->setVariant(implode('', $variantTokenList));
141
142
        // Look for quality factor, discarding accept-extens
143 78
        foreach ($paramBundleList as $paramBundle) {
144
            // Correct format for quality factor
145 76
            if ($this->isQualityFactor($paramBundle)) {
146 76
                $builder = $builder->setQualityFactor($paramBundle[2]);
0 ignored issues
show
$paramBundle[2] of type string is incompatible with the type double expected by parameter $qFactor of ptlis\ConNeg\Preference\...ace::setQualityFactor(). ( Ignorable by Annotation )

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

146
                $builder = $builder->setQualityFactor(/** @scrutinizer ignore-type */ $paramBundle[2]);
Loading history...
147 76
                break;
148
            }
149
        }
150
151 78
        return $builder->get();
152
    }
153
154
    /**
155
     * Returns true if the provided parameter bundle is a quality factor.
156
     *
157
     * @param array<string> $paramBundle
158
     *
159
     * @return bool
160
     */
161 76
    private function isQualityFactor(array $paramBundle)
162
    {
163 76
        return 3 == count($paramBundle)
164 76
            && 'q' === $paramBundle[0]
165 76
            && Tokens::PARAMS_KV_SEPARATOR === $paramBundle[1];
166
    }
167
168
    /**
169
     * Splits token list up into one bundle per variant for later processing.
170
     *
171
     * @param array<string> $tokenList
172
     * @param string $targetToken The token to split the list up by.
173
     *
174
     * @return array<array<string>> an array of arrays - the child array contains the tokens for a single variant.
175
     */
176 89
    private function bundleTokens(array $tokenList, $targetToken)
177
    {
178 89
        $bundleList = array();
179 89
        $bundle = array();
180
181 89
        foreach ($tokenList as $token) {
182
            // On token match add the bundle to list & re-initialize empty bundle
183 81
            if ($targetToken === $token) {
184 79
                $bundleList[] = $bundle;
185 79
                $bundle = array();
186
187
            // Otherwise collect tokens
188
            } else {
189 81
                $bundle[] = $token;
190
            }
191
        }
192
193
        // Handle trailing type
194 89
        $bundleList[] = $bundle;
195
196
        // Remove empty types
197 89
        $bundleList = array_filter($bundleList);
198
199 89
        return $bundleList;
200
    }
201
202
    /**
203
     * Checks to see if the bundle is valid for a mime type, if an anomaly is detected then an exception is thrown.
204
     *
205
     * @throws InvalidVariantException
206
     *
207
     * @param array<string> $bundle
208
     */
209 35
    private function validateBundleMimeVariant(array $bundle)
210
    {
211 35
        if (count($bundle) < 3                          // Too few items in bundle
212 34
            || Tokens::MIME_SEPARATOR !== $bundle[1]    // Invalid separator
213 34
            || Tokens::isSeparator($bundle[0], true)    // Invalid type
214 35
            || Tokens::isSeparator($bundle[2], true)    // Invalid subtype
215
        ) {
216 2
            throw new InvalidVariantException(
217 2
                '"' . implode('', $bundle) . '" is not a valid mime type'
218
            );
219
        }
220 33
    }
221
222
    /**
223
     * Checks to see if the parameters are correctly formed, if an anomaly is detected then an exception is thrown.
224
     *
225
     * @throws InvalidVariantException
226
     *
227
     * @param array<array<string>> $paramBundleList
228
     * @param bool $serverField     If true the field came from the server & we error on malformed data otherwise
229
     *                              we suppress errors for client preferences.
230
     */
231 79
    private function validateParamBundleList(array $paramBundleList, $serverField)
232
    {
233 79
        foreach ($paramBundleList as $paramBundle) {
234
            try {
235 77
                $this->validateParamBundle($paramBundle);
236 3
            } catch (InvalidVariantException $e) {
237
                // Rethrow exception only if the field was provided by the server
238 3
                if ($serverField) {
239 2
                    throw $e;
240
                }
241
            }
242
        }
243 78
    }
244
245
    /**
246
     * Validate a single parameter bundle.
247
     *
248
     * Due to the way we process tokens the only required should be for the correct number of tokens.
249
     *
250
     * @throws InvalidVariantException
251
     *
252
     * @param string[] $paramBundle
253
     */
254 77
    private function validateParamBundle(array $paramBundle)
255
    {
256
        // Wrong number of components
257 77
        if (1 !== count($paramBundle) && 3 !== count($paramBundle)) {
258 3
            throw new InvalidVariantException(
259 3
                'Invalid count for parameters; expecting 1 or 3, got "' . count($paramBundle) . '"'
260
            );
261
        }
262 76
    }
263
264
    /**
265
     * Get a preference builder for the specified HTTP field.
266
     *
267
     * @param string $fromField
268
     *
269
     * @return PreferenceBuilderInterface
270
     */
271 78
    private function getBuilder($fromField)
272
    {
273 78
        if (PreferenceInterface::MIME === $fromField) {
274 32
            return $this->mimePrefBuilder;
275
        } else {
276 46
            return $this->prefBuilder;
277
        }
278
    }
279
}
280