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