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