Passed
Pull Request — master (#261)
by Alex
03:41
created

HttpProcessUtility::isHttpSeparator()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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