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

HttpProcessUtility::readQuotedParameterValue()   C

Complexity

Conditions 12
Paths 51

Size

Total Lines 61
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 12
eloc 33
nc 51
nop 3
dl 0
loc 61
rs 6.9666
c 2
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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