Passed
Pull Request — master (#261)
by Christopher
03:34
created

HttpProcessUtility::skipWhiteSpace()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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