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

HttpProcessUtility   F

Complexity

Total Complexity 78

Size/Duplication

Total Lines 515
Duplicated Lines 0 %

Importance

Changes 7
Bugs 0 Features 0
Metric Value
eloc 219
c 7
b 0
f 0
dl 0
loc 515
rs 2.16
wmc 78

14 Methods

Rating   Name   Duplication   Size   Complexity  
C selectRequiredMimeType() 0 64 13
A readToken() 0 8 3
A skipWhiteSpace() 0 8 3
B mimeTypesFromAcceptHeaders() 0 35 6
A isHttpTokenChar() 0 3 3
A readMediaTypeAndSubtype() 0 34 4
B selectMimeType() 0 53 11
A headerToServerKey() 0 6 2
A readMediaTypeParameter() 0 22 3
B readQualityValue() 0 45 9
A digitToInt32() 0 11 4
A isHttpElementSeparator() 0 3 3
C readQuotedParameterValue() 0 61 12
A isHttpSeparator() 0 4 2

How to fix   Complexity   

Complex Class

Complex classes like HttpProcessUtility often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use HttpProcessUtility, and based on these observations, apply Extract Interface, too.

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