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

HttpProcessUtility::isHttpElementSeparator()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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