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

HttpProcessUtility::headerToServerKey()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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