Passed
Push — master ( e748a6...3f6736 )
by Alex
03:06 queued 11s
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|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 $acceptType) {
42
            return 0 !== $acceptType->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
            '"' == $text[$textIndex]) {
272
            ++$textIndex;
273
            $valueIsQuoted = true;
274
        }
275
276
        while ($textIndex < $textLen) {
277
            $currentChar = $text[$textIndex];
278
279
            if ('\\' == $currentChar || '"' == $currentChar) {
280
                if (!$valueIsQuoted) {
281
                    throw new HttpHeaderFailure(
282
                        Messages::httpProcessUtilityEscapeCharWithoutQuotes(
283
                            $parameterName
284
                        ),
285
                        400
286
                    );
287
                }
288
289
                ++$textIndex;
290
291
                // End of quoted parameter value.
292
                if ('"' == $currentChar) {
293
                    $valueIsQuoted = false;
294
                    break;
295
                }
296
297
                if ($textIndex >= $textLen) {
298
                    throw new HttpHeaderFailure(
299
                        Messages::httpProcessUtilityEscapeCharAtEnd($parameterName),
300
                        400
301
                    );
302
                }
303
304
                $currentChar = $text[$textIndex];
305
            } elseif (!self::isHttpTokenChar($currentChar)) {
306
                // If the given character is special, we stop processing.
307
                break;
308
            }
309
310
            $parameterValue[] = $currentChar;
311
            ++$textIndex;
312
        }
313
314
        if ($valueIsQuoted) {
315
            throw new HttpHeaderFailure(
316
                Messages::httpProcessUtilityClosingQuoteNotFound($parameterName),
317
                400
318
            );
319
        }
320
321
        return empty($parameterValue) ? null : implode('', $parameterValue);
322
    }
323
324
    /**
325
     * Selects an acceptable MIME type that satisfies the Accepts header.
326
     *
327
     * @param string   $acceptTypesText Text for Accepts header
328
     * @param string[] $availableTypes  Types that the server is willing to return, in descending order of preference
329
     *
330
     * @throws HttpHeaderFailure
331
     * @return string|null       The best MIME type for the client
332
     */
333
    public static function selectMimeType(string $acceptTypesText, array $availableTypes): ?string
334
    {
335
        $selectedContentType     = null;
336
        $selectedMatchingParts   = -1;
337
        $selectedQualityValue    = 0;
338
        $selectedPreferenceIndex = PHP_INT_MAX;
339
        $acceptable              = false;
340
        $acceptTypesEmpty        = true;
341
342
        $acceptTypes  = self::mimeTypesFromAcceptHeaders($acceptTypesText);
343
        $numAvailable = count($availableTypes);
344
        foreach ($acceptTypes as $acceptType) {
345
            $acceptTypesEmpty = false;
346
            for ($i = 0; $i < $numAvailable; ++$i) {
347
                $availableType = $availableTypes[$i];
348
                $matchRating   = $acceptType->getMatchingRating($availableType);
349
                if (0 > $matchRating) {
350
                    continue;
351
                }
352
353
                $candidateQualityValue = $acceptType->getQualityValue();
354
                if ($matchRating > $selectedMatchingParts) {
355
                    // A more specific type wins.
356
                    $selectedContentType     = $availableType;
357
                    $selectedMatchingParts   = $matchRating;
358
                    $selectedQualityValue    = $candidateQualityValue;
359
                    $selectedPreferenceIndex = $i;
360
                    $acceptable              = 0 != $selectedQualityValue;
361
                } elseif ($matchRating == $selectedMatchingParts) {
362
                    // A type with a higher q-value wins.
363
                    if ($candidateQualityValue > $selectedQualityValue) {
364
                        $selectedContentType     = $availableType;
365
                        $selectedQualityValue    = $candidateQualityValue;
366
                        $selectedPreferenceIndex = $i;
367
                        $acceptable              = 0 != $selectedQualityValue;
368
                    } elseif ($candidateQualityValue == $selectedQualityValue) {
369
                        // A type that is earlier in the availableTypes array wins.
370
                        if ($i < $selectedPreferenceIndex) {
371
                            $selectedContentType     = $availableType;
372
                            $selectedPreferenceIndex = $i;
373
                        }
374
                    }
375
                }
376
            }
377
        }
378
379
        if ($acceptTypesEmpty) {
380
            $selectedContentType = $availableTypes[0];
381
        } elseif (!$acceptable) {
0 ignored issues
show
introduced by
The condition $acceptable is always false.
Loading history...
382
            $selectedContentType = null;
383
        }
384
385
        return $selectedContentType;
386
    }
387
388
    /**
389
     * Reads the numeric part of a quality value substring, normalizing it to 0-1000.
390
     * rather than the standard 0.000-1.000 ranges.
391
     *
392
     * @param string $text       Text to read qvalue from
393
     * @param int    &$textIndex Index into text where the qvalue starts
394
     *
395
     * @throws HttpHeaderFailure If any error occurred while reading and processing the quality factor
396
     * @return int               The normalised qvalue
397
     */
398
    public static function readQualityValue(string $text, int &$textIndex): int
399
    {
400
        $digit = $text[$textIndex++];
401
        if ('0' == $digit) {
402
            $qualityValue = 0;
403
        } elseif ('1' == $digit) {
404
            $qualityValue = 1;
405
        } else {
406
            throw new HttpHeaderFailure(
407
                Messages::httpProcessUtilityMalformedHeaderValue(),
408
                400
409
            );
410
        }
411
412
        $textLen = strlen($text);
413
        if ($textIndex >= $textLen || '.' != $text[$textIndex]) {
414
            return $qualityValue * 1000;
415
        }
416
417
        ++$textIndex;
418
419
        $adjustFactor = 1000;
420
        while (1 < $adjustFactor && $textIndex < $textLen) {
421
            $c = $text[$textIndex];
422
            $charValue = self::digitToInt32($c);
423
            if (0 <= $charValue) {
424
                ++$textIndex;
425
                $adjustFactor /= 10;
426
                $qualityValue *= 10;
427
                $qualityValue += $charValue;
428
            } else {
429
                break;
430
            }
431
        }
432
433
        $qualityValue *= $adjustFactor;
434
        if ($qualityValue > 1000) {
435
            // Too high of a value in qvalue.
436
            throw new HttpHeaderFailure(
437
                Messages::httpProcessUtilityMalformedHeaderValue(),
438
                400
439
            );
440
        }
441
442
        return $qualityValue;
443
    }
444
445
    /**
446
     * Converts the specified character from the ASCII range to a digit.
447
     *
448
     * @param string $c Character to convert
449
     *
450
     * @throws HttpHeaderFailure If $c is not ASCII value for digit or element separator
451
     * @return int               The Int32 value for $c, or -1 if it is an element separator
452
     */
453
    public static function digitToInt32(string $c): int
454
    {
455
        if ('0' <= $c && '9' >= $c) {
456
            return intval($c);
457
        }
458
        if (self::isHttpElementSeparator($c)) {
459
            return -1;
460
        }
461
        throw new HttpHeaderFailure(
462
            Messages::httpProcessUtilityMalformedHeaderValue(),
463
            400
464
        );
465
    }
466
467
    /**
468
     * Verifies whether the specified character is a valid separator in.
469
     * an HTTP header list of element.
470
     *
471
     * @param string $c Character to verify
472
     *
473
     * @return bool true if c is a valid character for separating elements; false otherwise
474
     */
475
    public static function isHttpElementSeparator(string $c): bool
476
    {
477
        return ',' == $c || ' ' == $c || '\t' == $c;
478
    }
479
480
    /**
481
     * Get server key by header.
482
     *
483
     * @param  string $headerName Name of header
484
     * @return string
485
     */
486
    public static function headerToServerKey(string $headerName): string
487
    {
488
        $name = strtoupper(str_replace('-', '_', $headerName));
489
        $prefixableKeys = ['HOST', 'CONNECTION', 'CACHE_CONTROL', 'ORIGIN', 'USER_AGENT', 'POSTMAN_TOKEN', 'ACCEPT',
490
            'ACCEPT_ENCODING', 'ACCEPT_LANGUAGE', 'DATASERVICEVERSION', 'MAXDATASERVICEVERSION'];
491
        return in_array($name, $prefixableKeys) ? 'HTTP_' . $name : $name;
492
    }
493
}
494