Passed
Pull Request — master (#261)
by Christopher
03:00
created

HttpProcessUtility::selectRequiredMimeType()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 34
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 15
Bugs 0 Features 0
Metric Value
cc 5
eloc 16
c 15
b 0
f 0
nc 5
nop 3
dl 0
loc 34
rs 9.4222
1
<?php
2
3
declare(strict_types=1);
4
5
namespace POData;
6
7
use POData\Common\HttpHeaderFailure;
8
use POData\Common\Messages;
9
use POData\Providers\Metadata\Type\Char;
0 ignored issues
show
Bug introduced by
The type POData\Providers\Metadata\Type\Char was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
10
11
/**
12
 * Class HttpProcessUtility.
13
 */
14
class HttpProcessUtility
15
{
16
    /**
17
     * Gets the appropriate MIME type for the request, throwing if there is none.
18
     *
19
     * @param string|null   $acceptTypesText    Text as it appears in an HTTP Accepts header
20
     * @param string[]      $exactContentTypes  Preferred content type to match if an exact media type is given - in
21
     *                                          descending order of preference
22
     * @param string|null   $inexactContentType Preferred fallback content type for inexact matches
23
     *
24
     * @throws HttpHeaderFailure
25
     * @return string|null       One of exactContentType or inexactContentType
26
     */
27
    public static function selectRequiredMimeType(
28
        ?string $acceptTypesText,
29
        array $exactContentTypes,
30
        ?string $inexactContentType
31
    ): ?string {
32
        if (null === $acceptTypesText) {
33
            throw new HttpHeaderFailure(Messages::unsupportedMediaType(), 415);
34
        }
35
        // fetch types client will accept
36
        $acceptTypes = self::mimeTypesFromAcceptHeaders($acceptTypesText);
37
        if (empty($acceptTypes)) {
38
            return $inexactContentType;
39
        }
40
        // filter out transmitted types which have a zero quality
41
        $acceptTypes = array_filter($acceptTypes, function(MediaType $accpetType){
42
            return 0 !== $accpetType->getQualityValue();
43
        });
44
        // reduce acceptable types down to any that matches are exact set.
45
        $exactMatch = array_uintersect($acceptTypes, $exactContentTypes, function (MediaType $acceptType, $exactType) {
46
            return strcasecmp($acceptType->getMimeType(), $exactType);
47
        });
48
        // check if an exact match was found
49
        if (0 !== count($exactMatch)) {
50
            return $exactMatch[0]->getMimeType();
51
        }
52
        // filter down all remaining accept types to any that are a partial match for inexact
53
        $acceptTypes = array_filter($acceptTypes, function(MediaType $acceptType) use ($inexactContentType){
54
            return 0 < $acceptType->getMatchingRating($inexactContentType);
55
        });
56
        // if no inexact type go boom.
57
        if (count($acceptTypes) === 0) {
58
            throw new HttpHeaderFailure(Messages::unsupportedMediaType(), 415);
59
        }
60
        return $inexactContentType;
61
    }
62
63
    /**
64
     * Returns all MIME types from the $text.
65
     *
66
     * @param string $text Text as it appears on an HTTP Accepts header
67
     *
68
     * @throws HttpHeaderFailure If found any syntax error in the given text
69
     * @return MediaType[]       Array of media (MIME) type description
70
     */
71
    public static function mimeTypesFromAcceptHeaders(string $text): array
72
    {
73
        $mediaTypes = [];
74
        $textIndex  = 0;
75
        while (!self::skipWhiteSpace($text, $textIndex)) {
76
            $type    = null;
77
            $subType = null;
78
            self::readMediaTypeAndSubtype($text, $textIndex, $type, $subType);
79
80
            $parameters = [];
81
            while (!self::skipWhitespace($text, $textIndex)) {
82
                if (',' == $text[$textIndex]) {
83
                    ++$textIndex;
84
                    break;
85
                }
86
87
                if (';' != $text[$textIndex]) {
88
                    throw new HttpHeaderFailure(
89
                        Messages::httpProcessUtilityMediaTypeRequiresSemicolonBeforeParameter(),
90
                        400
91
                    );
92
                }
93
94
                ++$textIndex;
95
                if (self::skipWhiteSpace($text, $textIndex)) {
96
                    break;
97
                }
98
99
                self::readMediaTypeParameter($text, $textIndex, $parameters);
100
            }
101
102
            $mediaTypes[] = new MediaType($type, $subType, $parameters);
103
        }
104
105
        return $mediaTypes;
106
    }
107
108
    /**
109
     * Skips whitespace in the specified text by advancing an index to
110
     * the next non-whitespace character.
111
     *
112
     * @param string $text       Text to scan
113
     * @param int    &$textIndex Index to begin scanning from
114
     *
115
     * @return bool true if the end of the string was reached, false otherwise
116
     */
117
    public static function skipWhiteSpace(string $text, int &$textIndex): bool
118
    {
119
        $textLen = strlen(strval($text));
120
        while (($textIndex < $textLen) && Char::isWhiteSpace($text[$textIndex])) {
121
            ++$textIndex;
122
        }
123
124
        return $textLen == $textIndex;
125
    }
126
127
    /**
128
     * Reads the type and subtype specifications for a MIME type.
129
     *
130
     * @param string $text       Text in which specification exists
131
     * @param int    &$textIndex Pointer into text
132
     * @param string &$type      Type of media found
133
     * @param string &$subType   Subtype of media found
134
     *
135
     * @throws HttpHeaderFailure If failed to read type and sub-type
136
     */
137
    public static function readMediaTypeAndSubtype(
138
        string $text,
139
        int &$textIndex,
140
        &$type,
141
        &$subType
142
    ): void {
143
        $textStart = $textIndex;
144
        if (self::readToken($text, $textIndex)) {
145
            throw new HttpHeaderFailure(
146
                Messages::httpProcessUtilityMediaTypeUnspecified(),
147
                400
148
            );
149
        }
150
151
        if ('/' != $text[$textIndex]) {
152
            throw new HttpHeaderFailure(
153
                Messages::httpProcessUtilityMediaTypeRequiresSlash(),
154
                400
155
            );
156
        }
157
158
        $type = substr($text, $textStart, $textIndex - $textStart);
159
        ++$textIndex;
160
161
        $subTypeStart = $textIndex;
162
        self::readToken($text, $textIndex);
163
        if ($textIndex == $subTypeStart) {
164
            throw new HttpHeaderFailure(
165
                Messages::httpProcessUtilityMediaTypeRequiresSubType(),
166
                400
167
            );
168
        }
169
170
        $subType = substr($text, $subTypeStart, $textIndex - $subTypeStart);
171
    }
172
173
    /**
174
     * Reads a token on the specified text by advancing an index on it.
175
     *
176
     * @param string $text       Text to read token from
177
     * @param int    &$textIndex Index for the position being scanned on text
178
     *
179
     * @return bool true if the end of the text was reached; false otherwise
180
     */
181
    public static function readToken(string $text, int &$textIndex): bool
182
    {
183
        $textLen = strlen($text);
184
        while (($textIndex < $textLen) && self::isHttpTokenChar($text[$textIndex])) {
185
            ++$textIndex;
186
        }
187
188
        return $textLen == $textIndex;
189
    }
190
191
    /**
192
     * To check whether the given character is a HTTP token character
193
     * or not.
194
     *
195
     * @param string $char The character to inspect
196
     *
197
     * @return bool True if the given character is a valid HTTP token
198
     *              character, False otherwise
199
     */
200
    public static function isHttpTokenChar(string $char): bool
201
    {
202
        return 126 > ord($char) && 31 < ord($char) && !self::isHttpSeparator($char);
203
    }
204
205
    /**
206
     * To check whether the given character is a HTTP separator character.
207
     *
208
     * @param string $char The character to inspect
209
     *
210
     * @return bool True if the given character is a valid HTTP separator
211
     *              character, False otherwise
212
     */
213
    public static function isHttpSeparator(string $char): bool
214
    {
215
        $httpSeperators = ['(', ')', '<', '>', '@', ',', ';', ':', '\\', '"', '/', '[', ']', '?', '=', '{', '}', ' '];
216
        return in_array($char, $httpSeperators) || ord($char) == Char::TAB;
217
    }
218
219
    /**
220
     * Read a parameter for a media type/range.
221
     *
222
     * @param string $text        Text to read from
223
     * @param int    &$textIndex  Pointer in text
224
     * @param array  &$parameters Array with parameters
225
     *
226
     * @throws HttpHeaderFailure If found parameter value missing
227
     */
228
    public static function readMediaTypeParameter(string $text, int &$textIndex, array &$parameters)
229
    {
230
        $textStart = $textIndex;
231
        if (self::readToken($text, $textIndex)) {
232
            throw new HttpHeaderFailure(
233
                Messages::httpProcessUtilityMediaTypeMissingValue(),
234
                400
235
            );
236
        }
237
238
        $parameterName = substr($text, $textStart, $textIndex - $textStart);
239
        if ('=' != $text[$textIndex]) {
240
            throw new HttpHeaderFailure(
241
                Messages::httpProcessUtilityMediaTypeMissingValue(),
242
                400
243
            );
244
        }
245
246
        ++$textIndex;
247
        $parameterValue = self::readQuotedParameterValue($parameterName, $text, $textIndex);
248
        $parameters[]   = [$parameterName => $parameterValue];
249
    }
250
251
    /**
252
     * Reads Mime type parameter value for a particular parameter in the
253
     * Content-Type/Accept headers.
254
     *
255
     * @param string $parameterName Name of parameter
256
     * @param string $text          Header text
257
     * @param int    &$textIndex    Parsing index in $text
258
     *
259
     * @throws HttpHeaderFailure
260
     * @return string            String representing the value of the $parameterName parameter
261
     */
262
    public static function readQuotedParameterValue(
263
        string $parameterName,
264
        string $text,
265
        int &$textIndex
266
    ): ?string {
267
        $parameterValue = [];
268
        $textLen        = strlen($text);
269
        $valueIsQuoted  = false;
270
        if ($textIndex < $textLen) {
271
            if ('"' == $text[$textIndex]) {
272
                ++$textIndex;
273
                $valueIsQuoted = true;
274
            }
275
        }
276
277
        while ($textIndex < $textLen) {
278
            $currentChar = $text[$textIndex];
279
280
            if ('\\' == $currentChar || '"' == $currentChar) {
281
                if (!$valueIsQuoted) {
282
                    throw new HttpHeaderFailure(
283
                        Messages::httpProcessUtilityEscapeCharWithoutQuotes(
284
                            $parameterName
285
                        ),
286
                        400
287
                    );
288
                }
289
290
                ++$textIndex;
291
292
                // End of quoted parameter value.
293
                if ('"' == $currentChar) {
294
                    $valueIsQuoted = false;
295
                    break;
296
                }
297
298
                if ($textIndex >= $textLen) {
299
                    throw new HttpHeaderFailure(
300
                        Messages::httpProcessUtilityEscapeCharAtEnd($parameterName),
301
                        400
302
                    );
303
                }
304
305
                $currentChar = $text[$textIndex];
306
            } elseif (!self::isHttpTokenChar($currentChar)) {
307
                // If the given character is special, we stop processing.
308
                break;
309
            }
310
311
            $parameterValue[] = $currentChar;
312
            ++$textIndex;
313
        }
314
315
        if ($valueIsQuoted) {
316
            throw new HttpHeaderFailure(
317
                Messages::httpProcessUtilityClosingQuoteNotFound($parameterName),
318
                400
319
            );
320
        }
321
322
        return empty($parameterValue) ? null : implode('', $parameterValue);
323
    }
324
325
    /**
326
     * Selects an acceptable MIME type that satisfies the Accepts header.
327
     *
328
     * @param string   $acceptTypesText Text for Accepts header
329
     * @param string[] $availableTypes  Types that the server is willing to return, in descending order of preference
330
     *
331
     * @throws HttpHeaderFailure
332
     * @return string|null       The best MIME type for the client
333
     */
334
    public static function selectMimeType(string $acceptTypesText, array $availableTypes): ?string
335
    {
336
        $selectedContentType     = null;
337
        $selectedMatchingParts   = -1;
338
        $selectedQualityValue    = 0;
339
        $selectedPreferenceIndex = PHP_INT_MAX;
340
        $acceptable              = false;
341
        $acceptTypesEmpty        = true;
342
343
        $acceptTypes  = self::mimeTypesFromAcceptHeaders($acceptTypesText);
344
        $numAvailable = count($availableTypes);
345
        foreach ($acceptTypes as $acceptType) {
346
            $acceptTypesEmpty = false;
347
            for ($i = 0; $i < $numAvailable; ++$i) {
348
                $availableType = $availableTypes[$i];
349
                $matchRating   = $acceptType->getMatchingRating($availableType);
350
                if (0 > $matchRating) {
351
                    continue;
352
                }
353
354
                $candidateQualityValue = $acceptType->getQualityValue();
355
                if ($matchRating > $selectedMatchingParts) {
356
                    // A more specific type wins.
357
                    $selectedContentType     = $availableType;
358
                    $selectedMatchingParts   = $matchRating;
359
                    $selectedQualityValue    = $candidateQualityValue;
360
                    $selectedPreferenceIndex = $i;
361
                    $acceptable              = 0 != $selectedQualityValue;
362
                } elseif ($matchRating == $selectedMatchingParts) {
363
                    // A type with a higher q-value wins.
364
                    if ($candidateQualityValue > $selectedQualityValue) {
365
                        $selectedContentType     = $availableType;
366
                        $selectedQualityValue    = $candidateQualityValue;
367
                        $selectedPreferenceIndex = $i;
368
                        $acceptable              = 0 != $selectedQualityValue;
369
                    } elseif ($candidateQualityValue == $selectedQualityValue) {
370
                        // A type that is earlier in the availableTypes array wins.
371
                        if ($i < $selectedPreferenceIndex) {
372
                            $selectedContentType     = $availableType;
373
                            $selectedPreferenceIndex = $i;
374
                        }
375
                    }
376
                }
377
            }
378
        }
379
380
        if ($acceptTypesEmpty) {
381
            $selectedContentType = $availableTypes[0];
382
        } elseif (!$acceptable) {
0 ignored issues
show
introduced by
The condition $acceptable is always false.
Loading history...
383
            $selectedContentType = null;
384
        }
385
386
        return $selectedContentType;
387
    }
388
389
    /**
390
     * Reads the numeric part of a quality value substring, normalizing it to 0-1000.
391
     * rather than the standard 0.000-1.000 ranges.
392
     *
393
     * @param string $text       Text to read qvalue from
394
     * @param int    &$textIndex Index into text where the qvalue starts
395
     *
396
     * @throws HttpHeaderFailure If any error occurred while reading and processing the quality factor
397
     * @return int               The normalised qvalue
398
     */
399
    public static function readQualityValue(string $text, int &$textIndex): int
400
    {
401
        $digit = $text[$textIndex++];
402
        if ('0' == $digit) {
403
            $qualityValue = 0;
404
        } elseif ('1' == $digit) {
405
            $qualityValue = 1;
406
        } else {
407
            throw new HttpHeaderFailure(
408
                Messages::httpProcessUtilityMalformedHeaderValue(),
409
                400
410
            );
411
        }
412
413
        $textLen = strlen($text);
414
        if ($textIndex >= $textLen || '.' != $text[$textIndex]) {
415
            return $qualityValue * 1000;
416
        }
417
418
        ++$textIndex;
419
420
        $adjustFactor = 1000;
421
        while (1 < $adjustFactor && $textIndex < $textLen) {
422
            $c = $text[$textIndex];
423
            $charValue = self::digitToInt32($c);
424
            if (0 <= $charValue) {
425
                ++$textIndex;
426
                $adjustFactor /= 10;
427
                $qualityValue *= 10;
428
                $qualityValue += $charValue;
429
            } else {
430
                break;
431
            }
432
        }
433
434
        $qualityValue *= $adjustFactor;
435
        if ($qualityValue > 1000) {
436
            // Too high of a value in qvalue.
437
            throw new HttpHeaderFailure(
438
                Messages::httpProcessUtilityMalformedHeaderValue(),
439
                400
440
            );
441
        }
442
443
        return $qualityValue;
444
    }
445
446
    /**
447
     * Converts the specified character from the ASCII range to a digit.
448
     *
449
     * @param string $c Character to convert
450
     *
451
     * @throws HttpHeaderFailure If $c is not ASCII value for digit or element separator
452
     * @return int               The Int32 value for $c, or -1 if it is an element separator
453
     */
454
    public static function digitToInt32(string $c): int
455
    {
456
        if ('0' <= $c && '9' >= $c) {
457
            return intval($c);
458
        }
459
        if (self::isHttpElementSeparator($c)) {
460
            return -1;
461
        }
462
        throw new HttpHeaderFailure(
463
            Messages::httpProcessUtilityMalformedHeaderValue(),
464
            400
465
        );
466
    }
467
468
    /**
469
     * Verifies whether the specified character is a valid separator in.
470
     * an HTTP header list of element.
471
     *
472
     * @param string $c Character to verify
473
     *
474
     * @return bool true if c is a valid character for separating elements; false otherwise
475
     */
476
    public static function isHttpElementSeparator(string $c): bool
477
    {
478
        return ',' == $c || ' ' == $c || '\t' == $c;
479
    }
480
481
    /**
482
     * Get server key by header.
483
     *
484
     * @param  string $headerName Name of header
485
     * @return string
486
     */
487
    public static function headerToServerKey(string $headerName): string
488
    {
489
        $name = strtoupper(str_replace('-', '_', $headerName));
490
        $prefixableKeys = ['HOST', 'CONNECTION', 'CACHE_CONTROL', 'ORIGIN', 'USER_AGENT', 'POSTMAN_TOKEN', 'ACCEPT',
491
            'ACCEPT_ENCODING', 'ACCEPT_LANGUAGE', 'DATASERVICEVERSION', 'MAXDATASERVICEVERSION'];
492
        return in_array($name, $prefixableKeys) ? 'HTTP_' . $name : $name;
493
    }
494
}
495