Passed
Pull Request — master (#261)
by Christopher
02:58
created

HttpProcessUtility::mimeTypesFromAcceptHeaders()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 35
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

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