HTTPRequest   F
last analyzed

Complexity

Total Complexity 113

Size/Duplication

Total Lines 915
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 223
c 1
b 0
f 0
dl 0
loc 915
rs 2
wmc 113

52 Methods

Rating   Name   Duplication   Size   Complexity  
A getExtension() 0 3 1
A isPOST() 0 3 1
A requestVars() 0 3 1
A isMedia() 0 3 1
A postVars() 0 3 1
A offsetExists() 0 3 2
A isPUT() 0 3 1
A getVars() 0 3 1
A isGET() 0 3 1
A offsetSet() 0 3 1
A setBody() 0 4 1
A offsetGet() 0 3 1
A getHeaders() 0 3 1
A getBody() 0 3 1
A isHEAD() 0 3 1
A offsetUnset() 0 4 1
A isDELETE() 0 3 1
A getHeader() 0 4 2
A requestVar() 0 9 3
A addHeader() 0 5 1
A setUrl() 0 19 5
A getURL() 0 14 5
A __construct() 0 8 1
A getVar() 0 6 2
A isAjax() 0 5 2
A postVar() 0 6 2
A removeHeader() 0 5 1
A send_file() 0 12 2
A allParsed() 0 3 1
A detect_method() 0 9 3
A setRouteParams() 0 4 1
F match() 0 119 30
A hasSession() 0 3 1
A setIP() 0 7 2
A param() 0 7 2
A getHost() 0 3 1
A routeParams() 0 3 1
A getSession() 0 6 2
A getIP() 0 3 1
A params() 0 3 1
A getAcceptMimetypes() 0 8 3
A remaining() 0 3 1
A latestParam() 0 6 2
A getScheme() 0 3 1
A shift() 0 19 4
A setSession() 0 4 1
A httpMethod() 0 3 1
A shiftAllParams() 0 16 4
A latestParams() 0 3 1
A allParams() 0 3 1
A setScheme() 0 4 1
A isEmptyPattern() 0 10 3

How to fix   Complexity   

Complex Class

Complex classes like HTTPRequest 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 HTTPRequest, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace SilverStripe\Control;
4
5
use ArrayAccess;
6
use BadMethodCallException;
7
use InvalidArgumentException;
8
use SilverStripe\Core\ClassInfo;
9
use SilverStripe\ORM\ArrayLib;
10
11
/**
12
 * Represents a HTTP-request, including a URL that is tokenised for parsing, and a request method
13
 * (GET/POST/PUT/DELETE). This is used by {@link RequestHandler} objects to decide what to do.
14
 *
15
 * Caution: objects of this class are immutable, e.g. echo $request['a']; works as expected,
16
 * but $request['a'] = '1'; has no effect.
17
 *
18
 * The intention is that a single HTTPRequest object can be passed from one object to another, each object calling
19
 * match() to get the information that they need out of the URL.  This is generally handled by
20
 * {@link RequestHandler::handleRequest()}.
21
 *
22
 * @todo Accept X_HTTP_METHOD_OVERRIDE http header and $_REQUEST['_method'] to override request types (useful for
23
 *       webclients not supporting PUT and DELETE)
24
 */
25
class HTTPRequest implements ArrayAccess
26
{
27
    /**
28
     * @var string
29
     */
30
    protected $url;
31
32
    /**
33
     * The non-extension parts of the passed URL as an array, originally exploded by the "/" separator.
34
     * All elements of the URL are loaded in here,
35
     * and subsequently popped out of the array by {@link shift()}.
36
     * Only use this structure for internal request handling purposes.
37
     *
38
     * @var array
39
     */
40
    protected $dirParts;
41
42
    /**
43
     * The URL extension (if present)
44
     *
45
     * @var string
46
     */
47
    protected $extension;
48
49
    /**
50
     * The HTTP method in all uppercase: GET/PUT/POST/DELETE/HEAD
51
     *
52
     * @var string
53
     */
54
    protected $httpMethod;
55
56
    /**
57
     * The URL scheme in lowercase: http or https
58
     *
59
     * @var string
60
     */
61
    protected $scheme;
62
63
    /**
64
     * The client IP address
65
     *
66
     * @var string
67
     */
68
    protected $ip;
69
70
    /**
71
     * Contains alls HTTP GET parameters passed into this request.
72
     *
73
     * @var array
74
     */
75
    protected $getVars = array();
76
77
    /**
78
     * Contains alls HTTP POST parameters passed into this request.
79
     *
80
     * @var array
81
     */
82
    protected $postVars = array();
83
84
    /**
85
     * HTTP Headers like "Content-Type: text/xml"
86
     *
87
     * @see http://en.wikipedia.org/wiki/List_of_HTTP_headers
88
     * @var array
89
     */
90
    protected $headers = array();
91
92
    /**
93
     * Raw HTTP body, used by PUT and POST requests.
94
     *
95
     * @var string
96
     */
97
    protected $body;
98
99
    /**
100
     * Contains an associative array of all
101
     * arguments matched in all calls to {@link RequestHandler->handleRequest()}.
102
     * It's a "historical record" that's specific to the current call of
103
     * {@link handleRequest()}, and is only complete once the "last call" to that method is made.
104
     *
105
     * @var array
106
     */
107
    protected $allParams = array();
108
109
    /**
110
     * Contains an associative array of all
111
     * arguments matched in the current call from {@link RequestHandler->handleRequest()},
112
     * as denoted with a "$"-prefix in the $url_handlers definitions.
113
     * Contains different states throughout its lifespan, so just useful
114
     * while processed in {@link RequestHandler} and to get the last
115
     * processes arguments.
116
     *
117
     * @var array
118
     */
119
    protected $latestParams = array();
120
121
    /**
122
     * Contains an associative array of all arguments
123
     * explicitly set in the route table for the current request.
124
     * Useful for passing generic arguments via custom routes.
125
     *
126
     * E.g. The "Locale" parameter would be assigned "en_NZ" below
127
     *
128
     * Director:
129
     *   rules:
130
     *     'en_NZ/$URLSegment!//$Action/$ID/$OtherID':
131
     *       Controller: 'ModelAsController'
132
     *       Locale: 'en_NZ'
133
     *
134
     * @var array
135
     */
136
    protected $routeParams = array();
137
138
    /**
139
     * @var int
140
     */
141
    protected $unshiftedButParsedParts = 0;
142
143
    /**
144
     * @var Session
145
     */
146
    protected $session;
147
148
    /**
149
     * Construct a HTTPRequest from a URL relative to the site root.
150
     *
151
     * @param string $httpMethod
152
     * @param string $url
153
     * @param array $getVars
154
     * @param array $postVars
155
     * @param string $body
156
     */
157
    public function __construct($httpMethod, $url, $getVars = array(), $postVars = array(), $body = null)
158
    {
159
        $this->httpMethod = strtoupper(self::detect_method($httpMethod, $postVars));
160
        $this->setUrl($url);
161
        $this->getVars = (array) $getVars;
162
        $this->postVars = (array) $postVars;
163
        $this->body = $body;
164
        $this->scheme = "http";
165
    }
166
167
    /**
168
     * Allow the setting of a URL
169
     *
170
     * This is here so that RootURLController can change the URL of the request
171
     * without us loosing all the other info attached (like headers)
172
     *
173
     * @param string $url The new URL
174
     * @return HTTPRequest The updated request
175
     */
176
    public function setUrl($url)
177
    {
178
        $this->url = $url;
179
180
        // Normalize URL if its relative (strictly speaking), or has leading slashes
181
        if (Director::is_relative_url($url) || preg_match('/^\//', $url)) {
182
            $this->url = preg_replace(array('/\/+/','/^\//', '/\/$/'), array('/','',''), $this->url);
183
        }
184
        if (preg_match('/^(.*)\.([A-Za-z][A-Za-z0-9]*)$/', $this->url, $matches)) {
185
            $this->url = $matches[1];
186
            $this->extension = $matches[2];
187
        }
188
        if ($this->url) {
189
            $this->dirParts = preg_split('|/+|', $this->url);
0 ignored issues
show
Documentation Bug introduced by
It seems like preg_split('|/+|', $this->url) can also be of type false. However, the property $dirParts is declared as type array. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
190
        } else {
191
            $this->dirParts = array();
192
        }
193
194
        return $this;
195
    }
196
197
    /**
198
     * @return bool
199
     */
200
    public function isGET()
201
    {
202
        return $this->httpMethod == 'GET';
203
    }
204
205
    /**
206
     * @return bool
207
     */
208
    public function isPOST()
209
    {
210
        return $this->httpMethod == 'POST';
211
    }
212
213
    /**
214
     * @return bool
215
     */
216
    public function isPUT()
217
    {
218
        return $this->httpMethod == 'PUT';
219
    }
220
221
    /**
222
     * @return bool
223
     */
224
    public function isDELETE()
225
    {
226
        return $this->httpMethod == 'DELETE';
227
    }
228
229
    /**
230
     * @return bool
231
     */
232
    public function isHEAD()
233
    {
234
        return $this->httpMethod == 'HEAD';
235
    }
236
237
    /**
238
     * @param string $body
239
     * @return HTTPRequest $this
240
     */
241
    public function setBody($body)
242
    {
243
        $this->body = $body;
244
        return $this;
245
    }
246
247
    /**
248
     * @return null|string
249
     */
250
    public function getBody()
251
    {
252
        return $this->body;
253
    }
254
255
    /**
256
     * @return array
257
     */
258
    public function getVars()
259
    {
260
        return $this->getVars;
261
    }
262
263
    /**
264
     * @return array
265
     */
266
    public function postVars()
267
    {
268
        return $this->postVars;
269
    }
270
271
    /**
272
     * Returns all combined HTTP GET and POST parameters
273
     * passed into this request. If a parameter with the same
274
     * name exists in both arrays, the POST value is returned.
275
     *
276
     * @return array
277
     */
278
    public function requestVars()
279
    {
280
        return ArrayLib::array_merge_recursive($this->getVars, $this->postVars);
281
    }
282
283
    /**
284
     * @param string $name
285
     * @return mixed
286
     */
287
    public function getVar($name)
288
    {
289
        if (isset($this->getVars[$name])) {
290
            return $this->getVars[$name];
291
        }
292
        return null;
293
    }
294
295
    /**
296
     * @param string $name
297
     * @return mixed
298
     */
299
    public function postVar($name)
300
    {
301
        if (isset($this->postVars[$name])) {
302
            return $this->postVars[$name];
303
        }
304
        return null;
305
    }
306
307
    /**
308
     * @param string $name
309
     * @return mixed
310
     */
311
    public function requestVar($name)
312
    {
313
        if (isset($this->postVars[$name])) {
314
            return $this->postVars[$name];
315
        }
316
        if (isset($this->getVars[$name])) {
317
            return $this->getVars[$name];
318
        }
319
        return null;
320
    }
321
322
    /**
323
     * Returns a possible file extension found in parsing the URL
324
     * as denoted by a "."-character near the end of the URL.
325
     * Doesn't necessarily have to belong to an existing file,
326
     * as extensions can be also used for content-type-switching.
327
     *
328
     * @return string
329
     */
330
    public function getExtension()
331
    {
332
        return $this->extension;
333
    }
334
335
    /**
336
     * Checks if the {@link HTTPRequest->getExtension()} on this request matches one of the more common media types
337
     * embedded into a webpage - e.g. css, png.
338
     *
339
     * This is useful for things like determining whether to display a fully rendered error page or not. Note that the
340
     * media file types is not at all comprehensive.
341
     *
342
     * @return bool
343
     */
344
    public function isMedia()
345
    {
346
        return in_array($this->getExtension(), array('css', 'js', 'jpg', 'jpeg', 'gif', 'png', 'bmp', 'ico'));
347
    }
348
349
    /**
350
     * Add a HTTP header to the response, replacing any header of the same name.
351
     *
352
     * @param string $header Example: "content-type"
353
     * @param string $value Example: "text/xml"
354
     */
355
    public function addHeader($header, $value)
356
    {
357
        $header = strtolower($header);
358
        $this->headers[$header] = $value;
359
        return $this;
360
    }
361
362
    /**
363
     * @return array
364
     */
365
    public function getHeaders()
366
    {
367
        return $this->headers;
368
    }
369
370
    /**
371
     * Remove an existing HTTP header
372
     *
373
     * @param string $header
374
     * @return mixed
375
     */
376
    public function getHeader($header)
377
    {
378
        $header = strtolower($header);
379
        return (isset($this->headers[$header])) ? $this->headers[$header] : null;
380
    }
381
382
    /**
383
     * Remove an existing HTTP header by its name,
384
     * e.g. "Content-Type".
385
     *
386
     * @param string $header
387
     * @return HTTPRequest $this
388
     */
389
    public function removeHeader($header)
390
    {
391
        $header = strtolower($header);
392
        unset($this->headers[$header]);
393
        return $this;
394
    }
395
396
    /**
397
     * Returns the URL used to generate the page
398
     *
399
     * @param bool $includeGetVars whether or not to include the get parameters\
400
     * @return string
401
     */
402
    public function getURL($includeGetVars = false)
403
    {
404
        $url = ($this->getExtension()) ? $this->url . '.' . $this->getExtension() : $this->url;
405
406
        if ($includeGetVars) {
407
            $vars = $this->getVars();
408
            if (count($vars)) {
409
                $url .= '?' . http_build_query($vars);
410
            }
411
        } elseif (strpos($url, "?") !== false) {
412
            $url = substr($url, 0, strpos($url, "?"));
413
        }
414
415
        return $url;
416
    }
417
418
    /**
419
     * Returns true if this request an ajax request,
420
     * based on custom HTTP ajax added by common JavaScript libraries,
421
     * or based on an explicit "ajax" request parameter.
422
     *
423
     * @return boolean
424
     */
425
    public function isAjax()
426
    {
427
        return (
428
            $this->requestVar('ajax') ||
429
            $this->getHeader('x-requested-with') === "XMLHttpRequest"
430
        );
431
    }
432
433
    /**
434
     * Enables the existence of a key-value pair in the request to be checked using
435
     * array syntax, so isset($request['title']) will check for $_POST['title'] and $_GET['title']
436
     *
437
     * @param string $offset
438
     * @return bool
439
     */
440
    public function offsetExists($offset)
441
    {
442
        return isset($this->postVars[$offset]) || isset($this->getVars[$offset]);
443
    }
444
445
    /**
446
     * Access a request variable using array syntax. eg: $request['title'] instead of $request->postVar('title')
447
     *
448
     * @param string $offset
449
     * @return mixed
450
     */
451
    public function offsetGet($offset)
452
    {
453
        return $this->requestVar($offset);
454
    }
455
456
    public function offsetSet($offset, $value)
457
    {
458
        $this->getVars[$offset] = $value;
459
    }
460
461
    public function offsetUnset($offset)
462
    {
463
        unset($this->getVars[$offset]);
464
        unset($this->postVars[$offset]);
465
    }
466
467
    /**
468
     * Construct an HTTPResponse that will deliver a file to the client.
469
     * Caution: Since it requires $fileData to be passed as binary data (no stream support),
470
     * it's only advisable to send small files through this method.
471
     * This function needs to be called inside the controller’s response, e.g.:
472
     * <code>$this->setResponse(HTTPRequest::send_file('the content', 'filename.txt'));</code>
473
     *
474
     * @static
475
     * @param $fileData
476
     * @param $fileName
477
     * @param null $mimeType
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $mimeType is correct as it would always require null to be passed?
Loading history...
478
     * @return HTTPResponse
479
     */
480
    public static function send_file($fileData, $fileName, $mimeType = null)
481
    {
482
        if (!$mimeType) {
0 ignored issues
show
introduced by
$mimeType is of type null, thus it always evaluated to false.
Loading history...
483
            $mimeType = HTTP::get_mime_type($fileName);
484
        }
485
        $response = new HTTPResponse($fileData);
486
        $response->addHeader("content-type", "$mimeType; name=\"" . addslashes($fileName) . "\"");
487
        // Note a IE-only fix that inspects this header in HTTP::add_cache_headers().
488
        $response->addHeader("content-disposition", "attachment; filename=\"" . addslashes($fileName) . "\"");
489
        $response->addHeader("content-length", strlen($fileData));
490
491
        return $response;
492
    }
493
494
    /**
495
     * Matches a URL pattern
496
     * The pattern can contain a number of segments, separated by / (and an extension indicated by a .)
497
     *
498
     * The parts can be either literals, or, if they start with a $ they are interpreted as variables.
499
     *  - Literals must be provided in order to match
500
     *  - $Variables are optional
501
     *  - However, if you put ! at the end of a variable, then it becomes mandatory.
502
     *
503
     * For example:
504
     *  - admin/crm/list will match admin/crm/$Action/$ID/$OtherID, but it won't match admin/crm/$Action!/$ClassName!
505
     *
506
     * The pattern can optionally start with an HTTP method and a space.  For example, "POST $Controller/$Action".
507
     * This is used to define a rule that only matches on a specific HTTP method.
508
     *
509
     * @param $pattern
510
     * @param bool $shiftOnSuccess
511
     * @return array|bool
512
     */
513
    public function match($pattern, $shiftOnSuccess = false)
514
    {
515
        // Check if a specific method is required
516
        if (preg_match('/^([A-Za-z]+) +(.*)$/', $pattern, $matches)) {
517
            $requiredMethod = $matches[1];
518
            if ($requiredMethod != $this->httpMethod) {
519
                return false;
520
            }
521
522
            // If we get this far, we can match the URL pattern as usual.
523
            $pattern = $matches[2];
524
        }
525
526
        // Special case for the root URL controller (designated as an empty string, or a slash)
527
        if (!$pattern || $pattern === '/') {
528
            return ($this->dirParts == array()) ? array('Matched' => true) : false;
529
        }
530
531
        // Check for the '//' marker that represents the "shifting point"
532
        $doubleSlashPoint = strpos($pattern, '//');
533
        if ($doubleSlashPoint !== false) {
534
            $shiftCount = substr_count(substr($pattern, 0, $doubleSlashPoint), '/') + 1;
535
            $pattern = str_replace('//', '/', $pattern);
536
            $patternParts = explode('/', $pattern);
537
        } else {
538
            $patternParts = explode('/', $pattern);
539
            $shiftCount = sizeof($patternParts);
540
        }
541
542
        // Filter out any "empty" matching parts - either from an initial / or a trailing /
543
        $patternParts = array_values(array_filter($patternParts));
544
545
        $arguments = array();
546
        foreach ($patternParts as $i => $part) {
547
            $part = trim($part);
548
549
            // Match a variable
550
            if (isset($part[0]) && $part[0] == '$') {
551
                // A variable ending in ! is required
552
                if (substr($part, -1) == '!') {
553
                    $varRequired = true;
554
                    $varName = substr($part, 1, -1);
555
                } else {
556
                    $varRequired = false;
557
                    $varName = substr($part, 1);
558
                }
559
560
                // Fail if a required variable isn't populated
561
                if ($varRequired && !isset($this->dirParts[$i])) {
562
                    return false;
563
                }
564
565
                /** @skipUpgrade */
566
                $key = "Controller";
567
                if ($varName === '*' || $varName === '@') {
568
                    if (isset($patternParts[$i + 1])) {
569
                        user_error(
570
                            sprintf('All URL params after wildcard parameter $%s will be ignored', $varName),
571
                            E_USER_WARNING
572
                        );
573
                    }
574
                    if ($varName === '*') {
575
                        array_pop($patternParts);
576
                        $shiftCount = sizeof($patternParts);
577
                        $patternParts = array_merge($patternParts, array_slice($this->dirParts, $i));
578
                        break;
579
                    } else {
580
                        array_pop($patternParts);
581
                        $shiftCount = sizeof($patternParts);
582
                        $remaining = count($this->dirParts) - $i;
583
                        for ($j = 1; $j <= $remaining; $j++) {
584
                            $arguments["$${j}"] = $this->dirParts[$j + $i - 1];
585
                        }
586
                        $patternParts = array_merge($patternParts, array_keys($arguments));
587
                        break;
588
                    }
589
                } else {
590
                    $arguments[$varName] = $this->dirParts[$i] ?? null;
591
                }
592
                if ($part == '$Controller'
593
                    && (
594
                        !ClassInfo::exists($arguments[$key])
595
                        || !is_subclass_of($arguments[$key], 'SilverStripe\\Control\\Controller')
596
                    )
597
                ) {
598
                    return false;
599
                }
600
601
            // Literal parts with extension
602
            } elseif (isset($this->dirParts[$i]) && $this->dirParts[$i] . '.' . $this->extension == $part) {
603
                continue;
604
605
            // Literal parts must always be there
606
            } elseif (!isset($this->dirParts[$i]) || $this->dirParts[$i] != $part) {
607
                return false;
608
            }
609
        }
610
611
        if ($shiftOnSuccess) {
612
            $this->shift($shiftCount);
613
            // We keep track of pattern parts that we looked at but didn't shift off.
614
            // This lets us say that we have *parsed* the whole URL even when we haven't *shifted* it all
615
            $this->unshiftedButParsedParts = sizeof($patternParts) - $shiftCount;
616
        }
617
618
        $this->latestParams = $arguments;
619
620
        // Load the arguments that actually have a value into $this->allParams
621
        // This ensures that previous values aren't overridden with blanks
622
        foreach ($arguments as $k => $v) {
623
            if ($v || !isset($this->allParams[$k])) {
624
                $this->allParams[$k] = $v;
625
            }
626
        }
627
628
        if ($arguments === array()) {
629
            $arguments['_matched'] = true;
630
        }
631
        return $arguments;
632
    }
633
634
    /**
635
     * @return array
636
     */
637
    public function allParams()
638
    {
639
        return $this->allParams;
640
    }
641
642
    /**
643
     * Shift all the parameter values down a key space, and return the shifted value.
644
     *
645
     * @return string
646
     */
647
    public function shiftAllParams()
648
    {
649
        $keys    = array_keys($this->allParams);
650
        $values  = array_values($this->allParams);
651
        $value   = array_shift($values);
652
653
        // push additional unparsed URL parts onto the parameter stack
654
        if (array_key_exists($this->unshiftedButParsedParts, $this->dirParts)) {
655
            $values[] = $this->dirParts[$this->unshiftedButParsedParts];
656
        }
657
658
        foreach ($keys as $position => $key) {
659
            $this->allParams[$key] = isset($values[$position]) ? $values[$position] : null;
660
        }
661
662
        return $value;
663
    }
664
665
    /**
666
     * @return array
667
     */
668
    public function latestParams()
669
    {
670
        return $this->latestParams;
671
    }
672
673
    /**
674
     * @param string $name
675
     * @return string|null
676
     */
677
    public function latestParam($name)
678
    {
679
        if (isset($this->latestParams[$name])) {
680
            return $this->latestParams[$name];
681
        } else {
682
            return null;
683
        }
684
    }
685
686
    /**
687
     * @return array
688
     */
689
    public function routeParams()
690
    {
691
        return $this->routeParams;
692
    }
693
694
    /**
695
     * @param $params
696
     * @return HTTPRequest $this
697
     */
698
    public function setRouteParams($params)
699
    {
700
        $this->routeParams = $params;
701
        return $this;
702
    }
703
704
    /**
705
     * @return array
706
     */
707
    public function params()
708
    {
709
        return array_merge($this->allParams, $this->routeParams);
710
    }
711
712
    /**
713
     * Finds a named URL parameter (denoted by "$"-prefix in $url_handlers)
714
     * from the full URL, or a parameter specified in the route table
715
     *
716
     * @param string $name
717
     * @return string Value of the URL parameter (if found)
718
     */
719
    public function param($name)
720
    {
721
        $params = $this->params();
722
        if (isset($params[$name])) {
723
            return $params[$name];
724
        } else {
725
            return null;
726
        }
727
    }
728
729
    /**
730
     * Returns the unparsed part of the original URL
731
     * separated by commas. This is used by {@link RequestHandler->handleRequest()}
732
     * to determine if further URL processing is necessary.
733
     *
734
     * @return string Partial URL
735
     */
736
    public function remaining()
737
    {
738
        return implode("/", $this->dirParts);
739
    }
740
741
    /**
742
     * Returns true if this is a URL that will match without shifting off any of the URL.
743
     * This is used by the request handler to prevent infinite parsing loops.
744
     *
745
     * @param string $pattern
746
     * @return bool
747
     */
748
    public function isEmptyPattern($pattern)
749
    {
750
        if (preg_match('/^([A-Za-z]+) +(.*)$/', $pattern, $matches)) {
751
            $pattern = $matches[2];
752
        }
753
754
        if (trim($pattern) == "") {
755
            return true;
756
        }
757
        return false;
758
    }
759
760
    /**
761
     * Shift one or more parts off the beginning of the URL.
762
     * If you specify shifting more than 1 item off, then the items will be returned as an array
763
     *
764
     * @param int $count Shift Count
765
     * @return string|array
766
     */
767
    public function shift($count = 1)
768
    {
769
        $return = array();
770
771
        if ($count == 1) {
772
            return array_shift($this->dirParts);
773
        }
774
775
        for ($i=0; $i<$count; $i++) {
776
            $value = array_shift($this->dirParts);
777
778
            if ($value === null) {
779
                break;
780
            }
781
782
            $return[] = $value;
783
        }
784
785
        return $return;
786
    }
787
788
    /**
789
     * Returns true if the URL has been completely parsed.
790
     * This will respect parsed but unshifted directory parts.
791
     *
792
     * @return bool
793
     */
794
    public function allParsed()
795
    {
796
        return sizeof($this->dirParts) <= $this->unshiftedButParsedParts;
797
    }
798
799
    /**
800
     * @return string Return the host from the request
801
     */
802
    public function getHost()
803
    {
804
        return $this->getHeader('host');
805
    }
806
807
    /**
808
     * Returns the client IP address which originated this request.
809
     *
810
     * @return string
811
     */
812
    public function getIP()
813
    {
814
        return $this->ip;
815
    }
816
817
    /**
818
     * Sets the client IP address which originated this request.
819
     * Use setIPFromHeaderValue if assigning from header value.
820
     *
821
     * @param $ip string
822
     * @return $this
823
     */
824
    public function setIP($ip)
825
    {
826
        if (!filter_var($ip, FILTER_VALIDATE_IP)) {
827
            throw new InvalidArgumentException("Invalid ip $ip");
828
        }
829
        $this->ip = $ip;
830
        return $this;
831
    }
832
833
    /**
834
     * Returns all mimetypes from the HTTP "Accept" header
835
     * as an array.
836
     *
837
     * @param boolean $includeQuality Don't strip away optional "quality indicators", e.g. "application/xml;q=0.9"
838
     *                                (Default: false)
839
     * @return array
840
     */
841
    public function getAcceptMimetypes($includeQuality = false)
842
    {
843
        $mimetypes = array();
844
        $mimetypesWithQuality = preg_split('#\s*,\s*#', $this->getHeader('accept'));
845
        foreach ($mimetypesWithQuality as $mimetypeWithQuality) {
846
            $mimetypes[] = ($includeQuality) ? $mimetypeWithQuality : preg_replace('/;.*/', '', $mimetypeWithQuality);
847
        }
848
        return $mimetypes;
849
    }
850
851
    /**
852
     * @return string HTTP method (all uppercase)
853
     */
854
    public function httpMethod()
855
    {
856
        return $this->httpMethod;
857
    }
858
859
    /**
860
     * Return the URL scheme (e.g. "http" or "https").
861
     * Equivalent to PSR-7 getUri()->getScheme()
862
     *
863
     * @return string
864
     */
865
    public function getScheme()
866
    {
867
        return $this->scheme;
868
    }
869
870
    /**
871
     * Set the URL scheme (e.g. "http" or "https").
872
     * Equivalent to PSR-7 getUri()->getScheme(),
873
     *
874
     * @param string $scheme
875
     * @return $this
876
     */
877
    public function setScheme($scheme)
878
    {
879
        $this->scheme = $scheme;
880
        return $this;
881
    }
882
883
    /**
884
     * Gets the "real" HTTP method for a request.
885
     *
886
     * Used to work around browser limitations of form
887
     * submissions to GET and POST, by overriding the HTTP method
888
     * with a POST parameter called "_method" for PUT, DELETE, HEAD.
889
     * Using GET for the "_method" override is not supported,
890
     * as GET should never carry out state changes.
891
     * Alternatively you can use a custom HTTP header 'X-HTTP-Method-Override'
892
     * to override the original method.
893
     * The '_method' POST parameter overrules the custom HTTP header.
894
     *
895
     * @param string $origMethod Original HTTP method from the browser request
896
     * @param array $postVars
897
     * @return string HTTP method (all uppercase)
898
     */
899
    public static function detect_method($origMethod, $postVars)
900
    {
901
        if (isset($postVars['_method'])) {
902
            if (!in_array(strtoupper($postVars['_method']), array('GET','POST','PUT','DELETE','HEAD'))) {
903
                user_error('HTTPRequest::detect_method(): Invalid "_method" parameter', E_USER_ERROR);
904
            }
905
            return strtoupper($postVars['_method']);
906
        } else {
907
            return $origMethod;
908
        }
909
    }
910
911
    /**
912
     * Determines whether the request has a session
913
     *
914
     * @return bool
915
     */
916
    public function hasSession(): bool
917
    {
918
        return !empty($this->session);
919
    }
920
921
    /**
922
     * @return Session
923
     */
924
    public function getSession()
925
    {
926
        if (!$this->hasSession()) {
927
            throw new BadMethodCallException("No session available for this HTTPRequest");
928
        }
929
        return $this->session;
930
    }
931
932
    /**
933
     * @param Session $session
934
     * @return $this
935
     */
936
    public function setSession(Session $session)
937
    {
938
        $this->session = $session;
939
        return $this;
940
    }
941
}
942