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