Passed
Push — 4.6 ( fae61c...fbe0f5 )
by Garion
18:28 queued 10:28
created

HTTPRequest::allParams()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
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
class HTTPRequest implements ArrayAccess
23
{
24
    /**
25
     * @var string
26
     */
27
    protected $url;
28
29
    /**
30
     * The non-extension parts of the passed URL as an array, originally exploded by the "/" separator.
31
     * All elements of the URL are loaded in here,
32
     * and subsequently popped out of the array by {@link shift()}.
33
     * Only use this structure for internal request handling purposes.
34
     *
35
     * @var array
36
     */
37
    protected $dirParts;
38
39
    /**
40
     * The URL extension (if present)
41
     *
42
     * @var string
43
     */
44
    protected $extension;
45
46
    /**
47
     * The HTTP method in all uppercase: GET/PUT/POST/DELETE/HEAD
48
     *
49
     * @var string
50
     */
51
    protected $httpMethod;
52
53
    /**
54
     * The URL scheme in lowercase: http or https
55
     *
56
     * @var string
57
     */
58
    protected $scheme;
59
60
    /**
61
     * The client IP address
62
     *
63
     * @var string
64
     */
65
    protected $ip;
66
67
    /**
68
     * Contains alls HTTP GET parameters passed into this request.
69
     *
70
     * @var array
71
     */
72
    protected $getVars = [];
73
74
    /**
75
     * Contains alls HTTP POST parameters passed into this request.
76
     *
77
     * @var array
78
     */
79
    protected $postVars = [];
80
81
    /**
82
     * HTTP Headers like "Content-Type: text/xml"
83
     *
84
     * @see http://en.wikipedia.org/wiki/List_of_HTTP_headers
85
     * @var array
86
     */
87
    protected $headers = [];
88
89
    /**
90
     * Raw HTTP body, used by PUT and POST requests.
91
     *
92
     * @var string
93
     */
94
    protected $body;
95
96
    /**
97
     * Contains an associative array of all
98
     * arguments matched in all calls to {@link RequestHandler->handleRequest()}.
99
     * It's a "historical record" that's specific to the current call of
100
     * {@link handleRequest()}, and is only complete once the "last call" to that method is made.
101
     *
102
     * @var array
103
     */
104
    protected $allParams = [];
105
106
    /**
107
     * Contains an associative array of all
108
     * arguments matched in the current call from {@link RequestHandler->handleRequest()},
109
     * as denoted with a "$"-prefix in the $url_handlers definitions.
110
     * Contains different states throughout its lifespan, so just useful
111
     * while processed in {@link RequestHandler} and to get the last
112
     * processes arguments.
113
     *
114
     * @var array
115
     */
116
    protected $latestParams = [];
117
118
    /**
119
     * Contains an associative array of all arguments
120
     * explicitly set in the route table for the current request.
121
     * Useful for passing generic arguments via custom routes.
122
     *
123
     * E.g. The "Locale" parameter would be assigned "en_NZ" below
124
     *
125
     * Director:
126
     *   rules:
127
     *     'en_NZ/$URLSegment!//$Action/$ID/$OtherID':
128
     *       Controller: 'ModelAsController'
129
     *       Locale: 'en_NZ'
130
     *
131
     * @var array
132
     */
133
    protected $routeParams = [];
134
135
    /**
136
     * @var int
137
     */
138
    protected $unshiftedButParsedParts = 0;
139
140
    /**
141
     * @var Session
142
     */
143
    protected $session;
144
145
    /**
146
     * Construct a HTTPRequest from a URL relative to the site root.
147
     *
148
     * @param string $httpMethod
149
     * @param string $url
150
     * @param array $getVars
151
     * @param array $postVars
152
     * @param string $body
153
     */
154
    public function __construct($httpMethod, $url, $getVars = [], $postVars = [], $body = null)
155
    {
156
        $this->httpMethod = strtoupper($httpMethod);
157
        $this->setUrl($url);
158
        $this->getVars = (array) $getVars;
159
        $this->postVars = (array) $postVars;
160
        $this->body = $body;
161
        $this->scheme = "http";
162
    }
163
164
    /**
165
     * Allow the setting of a URL
166
     *
167
     * This is here so that RootURLController can change the URL of the request
168
     * without us loosing all the other info attached (like headers)
169
     *
170
     * @param string $url The new URL
171
     * @return HTTPRequest The updated request
172
     */
173
    public function setUrl($url)
174
    {
175
        $this->url = $url;
176
177
        // Normalize URL if its relative (strictly speaking), or has leading slashes
178
        if (Director::is_relative_url($url) || preg_match('/^\//', $url)) {
179
            $this->url = preg_replace(['/\/+/','/^\//', '/\/$/'], ['/','',''], $this->url);
180
        }
181
        if (preg_match('/^(.*)\.([A-Za-z][A-Za-z0-9]*)$/', $this->url, $matches)) {
182
            $this->url = $matches[1];
183
            $this->extension = $matches[2];
184
        }
185
        if ($this->url) {
186
            $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...
187
        } else {
188
            $this->dirParts = [];
189
        }
190
191
        return $this;
192
    }
193
194
    /**
195
     * @return bool
196
     */
197
    public function isGET()
198
    {
199
        return $this->httpMethod == 'GET';
200
    }
201
202
    /**
203
     * @return bool
204
     */
205
    public function isPOST()
206
    {
207
        return $this->httpMethod == 'POST';
208
    }
209
210
    /**
211
     * @return bool
212
     */
213
    public function isPUT()
214
    {
215
        return $this->httpMethod == 'PUT';
216
    }
217
218
    /**
219
     * @return bool
220
     */
221
    public function isDELETE()
222
    {
223
        return $this->httpMethod == 'DELETE';
224
    }
225
226
    /**
227
     * @return bool
228
     */
229
    public function isHEAD()
230
    {
231
        return $this->httpMethod == 'HEAD';
232
    }
233
234
    /**
235
     * @param string $body
236
     * @return HTTPRequest $this
237
     */
238
    public function setBody($body)
239
    {
240
        $this->body = $body;
241
        return $this;
242
    }
243
244
    /**
245
     * @return null|string
246
     */
247
    public function getBody()
248
    {
249
        return $this->body;
250
    }
251
252
    /**
253
     * @return array
254
     */
255
    public function getVars()
256
    {
257
        return $this->getVars;
258
    }
259
260
    /**
261
     * @return array
262
     */
263
    public function postVars()
264
    {
265
        return $this->postVars;
266
    }
267
268
    /**
269
     * Returns all combined HTTP GET and POST parameters
270
     * passed into this request. If a parameter with the same
271
     * name exists in both arrays, the POST value is returned.
272
     *
273
     * @return array
274
     */
275
    public function requestVars()
276
    {
277
        return ArrayLib::array_merge_recursive($this->getVars, $this->postVars);
278
    }
279
280
    /**
281
     * @param string $name
282
     * @return mixed
283
     */
284
    public function getVar($name)
285
    {
286
        if (isset($this->getVars[$name])) {
287
            return $this->getVars[$name];
288
        }
289
        return null;
290
    }
291
292
    /**
293
     * @param string $name
294
     * @return mixed
295
     */
296
    public function postVar($name)
297
    {
298
        if (isset($this->postVars[$name])) {
299
            return $this->postVars[$name];
300
        }
301
        return null;
302
    }
303
304
    /**
305
     * @param string $name
306
     * @return mixed
307
     */
308
    public function requestVar($name)
309
    {
310
        if (isset($this->postVars[$name])) {
311
            return $this->postVars[$name];
312
        }
313
        if (isset($this->getVars[$name])) {
314
            return $this->getVars[$name];
315
        }
316
        return null;
317
    }
318
319
    /**
320
     * Returns a possible file extension found in parsing the URL
321
     * as denoted by a "."-character near the end of the URL.
322
     * Doesn't necessarily have to belong to an existing file,
323
     * as extensions can be also used for content-type-switching.
324
     *
325
     * @return string
326
     */
327
    public function getExtension()
328
    {
329
        return $this->extension;
330
    }
331
332
    /**
333
     * Checks if the {@link HTTPRequest->getExtension()} on this request matches one of the more common media types
334
     * embedded into a webpage - e.g. css, png.
335
     *
336
     * This is useful for things like determining whether to display a fully rendered error page or not. Note that the
337
     * media file types is not at all comprehensive.
338
     *
339
     * @return bool
340
     */
341
    public function isMedia()
342
    {
343
        return in_array($this->getExtension(), ['css', 'js', 'jpg', 'jpeg', 'gif', 'png', 'bmp', 'ico']);
344
    }
345
346
    /**
347
     * Add a HTTP header to the response, replacing any header of the same name.
348
     *
349
     * @param string $header Example: "content-type"
350
     * @param string $value Example: "text/xml"
351
     */
352
    public function addHeader($header, $value)
353
    {
354
        $header = strtolower($header);
355
        $this->headers[$header] = $value;
356
        return $this;
357
    }
358
359
    /**
360
     * @return array
361
     */
362
    public function getHeaders()
363
    {
364
        return $this->headers;
365
    }
366
367
    /**
368
     * Remove an existing HTTP header
369
     *
370
     * @param string $header
371
     * @return mixed
372
     */
373
    public function getHeader($header)
374
    {
375
        $header = strtolower($header);
376
        return (isset($this->headers[$header])) ? $this->headers[$header] : null;
377
    }
378
379
    /**
380
     * Remove an existing HTTP header by its name,
381
     * e.g. "Content-Type".
382
     *
383
     * @param string $header
384
     * @return HTTPRequest $this
385
     */
386
    public function removeHeader($header)
387
    {
388
        $header = strtolower($header);
389
        unset($this->headers[$header]);
390
        return $this;
391
    }
392
393
    /**
394
     * Returns the URL used to generate the page
395
     *
396
     * @param bool $includeGetVars whether or not to include the get parameters\
397
     * @return string
398
     */
399
    public function getURL($includeGetVars = false)
400
    {
401
        $url = ($this->getExtension()) ? $this->url . '.' . $this->getExtension() : $this->url;
402
403
        if ($includeGetVars) {
404
            $vars = $this->getVars();
405
            if (count($vars)) {
406
                $url .= '?' . http_build_query($vars);
407
            }
408
        } elseif (strpos($url, "?") !== false) {
409
            $url = substr($url, 0, strpos($url, "?"));
410
        }
411
412
        return $url;
413
    }
414
415
    /**
416
     * Returns true if this request an ajax request,
417
     * based on custom HTTP ajax added by common JavaScript libraries,
418
     * or based on an explicit "ajax" request parameter.
419
     *
420
     * @return boolean
421
     */
422
    public function isAjax()
423
    {
424
        return (
425
            $this->requestVar('ajax') ||
426
            $this->getHeader('x-requested-with') === "XMLHttpRequest"
427
        );
428
    }
429
430
    /**
431
     * Enables the existence of a key-value pair in the request to be checked using
432
     * array syntax, so isset($request['title']) will check for $_POST['title'] and $_GET['title']
433
     *
434
     * @param string $offset
435
     * @return bool
436
     */
437
    public function offsetExists($offset)
438
    {
439
        return isset($this->postVars[$offset]) || isset($this->getVars[$offset]);
440
    }
441
442
    /**
443
     * Access a request variable using array syntax. eg: $request['title'] instead of $request->postVar('title')
444
     *
445
     * @param string $offset
446
     * @return mixed
447
     */
448
    public function offsetGet($offset)
449
    {
450
        return $this->requestVar($offset);
451
    }
452
453
    public function offsetSet($offset, $value)
454
    {
455
        $this->getVars[$offset] = $value;
456
    }
457
458
    public function offsetUnset($offset)
459
    {
460
        unset($this->getVars[$offset]);
461
        unset($this->postVars[$offset]);
462
    }
463
464
    /**
465
     * Construct an HTTPResponse that will deliver a file to the client.
466
     * Caution: Since it requires $fileData to be passed as binary data (no stream support),
467
     * it's only advisable to send small files through this method.
468
     * This function needs to be called inside the controller’s response, e.g.:
469
     * <code>$this->setResponse(HTTPRequest::send_file('the content', 'filename.txt'));</code>
470
     *
471
     * @static
472
     * @param $fileData
473
     * @param $fileName
474
     * @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...
475
     * @return HTTPResponse
476
     */
477
    public static function send_file($fileData, $fileName, $mimeType = null)
478
    {
479
        if (!$mimeType) {
0 ignored issues
show
introduced by
$mimeType is of type null, thus it always evaluated to false.
Loading history...
480
            $mimeType = HTTP::get_mime_type($fileName);
481
        }
482
        $response = new HTTPResponse($fileData);
483
        $response->addHeader("content-type", "$mimeType; name=\"" . addslashes($fileName) . "\"");
484
        // Note a IE-only fix that inspects this header in HTTP::add_cache_headers().
485
        $response->addHeader("content-disposition", "attachment; filename=\"" . addslashes($fileName) . "\"");
486
        $response->addHeader("content-length", strlen($fileData));
487
488
        return $response;
489
    }
490
491
    /**
492
     * Matches a URL pattern
493
     * The pattern can contain a number of segments, separated by / (and an extension indicated by a .)
494
     *
495
     * The parts can be either literals, or, if they start with a $ they are interpreted as variables.
496
     *  - Literals must be provided in order to match
497
     *  - $Variables are optional
498
     *  - However, if you put ! at the end of a variable, then it becomes mandatory.
499
     *
500
     * For example:
501
     *  - admin/crm/list will match admin/crm/$Action/$ID/$OtherID, but it won't match admin/crm/$Action!/$ClassName!
502
     *
503
     * The pattern can optionally start with an HTTP method and a space.  For example, "POST $Controller/$Action".
504
     * This is used to define a rule that only matches on a specific HTTP method.
505
     *
506
     * @param $pattern
507
     * @param bool $shiftOnSuccess
508
     * @return array|bool
509
     */
510
    public function match($pattern, $shiftOnSuccess = false)
511
    {
512
        // Check if a specific method is required
513
        if (preg_match('/^([A-Za-z]+) +(.*)$/', $pattern, $matches)) {
514
            $requiredMethod = $matches[1];
515
            if ($requiredMethod != $this->httpMethod) {
516
                return false;
517
            }
518
519
            // If we get this far, we can match the URL pattern as usual.
520
            $pattern = $matches[2];
521
        }
522
523
        // Special case for the root URL controller (designated as an empty string, or a slash)
524
        if (!$pattern || $pattern === '/') {
525
            return ($this->dirParts == []) ? ['Matched' => true] : false;
526
        }
527
528
        // Check for the '//' marker that represents the "shifting point"
529
        $doubleSlashPoint = strpos($pattern, '//');
530
        if ($doubleSlashPoint !== false) {
531
            $shiftCount = substr_count(substr($pattern, 0, $doubleSlashPoint), '/') + 1;
532
            $pattern = str_replace('//', '/', $pattern);
533
            $patternParts = explode('/', $pattern);
534
        } else {
535
            $patternParts = explode('/', $pattern);
536
            $shiftCount = sizeof($patternParts);
537
        }
538
539
        // Filter out any "empty" matching parts - either from an initial / or a trailing /
540
        $patternParts = array_values(array_filter($patternParts));
541
542
        $arguments = [];
543
        foreach ($patternParts as $i => $part) {
544
            $part = trim($part);
545
546
            // Match a variable
547
            if (isset($part[0]) && $part[0] == '$') {
548
                // A variable ending in ! is required
549
                if (substr($part, -1) == '!') {
550
                    $varRequired = true;
551
                    $varName = substr($part, 1, -1);
552
                } else {
553
                    $varRequired = false;
554
                    $varName = substr($part, 1);
555
                }
556
557
                // Fail if a required variable isn't populated
558
                if ($varRequired && !isset($this->dirParts[$i])) {
559
                    return false;
560
                }
561
562
                /** @skipUpgrade */
563
                $key = "Controller";
564
                if ($varName === '*' || $varName === '@') {
565
                    if (isset($patternParts[$i + 1])) {
566
                        user_error(sprintf('All URL params after wildcard parameter $%s will be ignored', $varName), E_USER_WARNING);
567
                    }
568
                    if ($varName === '*') {
569
                        array_pop($patternParts);
570
                        $shiftCount = sizeof($patternParts);
571
                        $patternParts = array_merge($patternParts, array_slice($this->dirParts, $i));
572
                        break;
573
                    } else {
574
                        array_pop($patternParts);
575
                        $shiftCount = sizeof($patternParts);
576
                        $remaining = count($this->dirParts) - $i;
577
                        for ($j = 1; $j <= $remaining; $j++) {
578
                            $arguments["$${j}"] = $this->dirParts[$j + $i - 1];
579
                        }
580
                        $patternParts = array_merge($patternParts, array_keys($arguments));
581
                        break;
582
                    }
583
                } else {
584
                    $arguments[$varName] = $this->dirParts[$i] ?? null;
585
                }
586
                if ($part == '$Controller'
587
                    && (
588
                        !ClassInfo::exists($arguments[$key])
589
                        || !is_subclass_of($arguments[$key], 'SilverStripe\\Control\\Controller')
590
                    )
591
                ) {
592
                    return false;
593
                }
594
595
            // Literal parts with extension
596
            } elseif (isset($this->dirParts[$i]) && $this->dirParts[$i] . '.' . $this->extension == $part) {
597
                continue;
598
599
            // Literal parts must always be there
600
            } elseif (!isset($this->dirParts[$i]) || $this->dirParts[$i] != $part) {
601
                return false;
602
            }
603
        }
604
605
        if ($shiftOnSuccess) {
606
            $this->shift($shiftCount);
607
            // We keep track of pattern parts that we looked at but didn't shift off.
608
            // This lets us say that we have *parsed* the whole URL even when we haven't *shifted* it all
609
            $this->unshiftedButParsedParts = sizeof($patternParts) - $shiftCount;
610
        }
611
612
        $this->latestParams = $arguments;
613
614
        // Load the arguments that actually have a value into $this->allParams
615
        // This ensures that previous values aren't overridden with blanks
616
        foreach ($arguments as $k => $v) {
617
            if ($v || !isset($this->allParams[$k])) {
618
                $this->allParams[$k] = $v;
619
            }
620
        }
621
622
        if ($arguments === []) {
623
            $arguments['_matched'] = true;
624
        }
625
        return $arguments;
626
    }
627
628
    /**
629
     * @return array
630
     */
631
    public function allParams()
632
    {
633
        return $this->allParams;
634
    }
635
636
    /**
637
     * Shift all the parameter values down a key space, and return the shifted value.
638
     *
639
     * @return string
640
     */
641
    public function shiftAllParams()
642
    {
643
        $keys    = array_keys($this->allParams);
644
        $values  = array_values($this->allParams);
645
        $value   = array_shift($values);
646
647
        // push additional unparsed URL parts onto the parameter stack
648
        if (array_key_exists($this->unshiftedButParsedParts, $this->dirParts)) {
649
            $values[] = $this->dirParts[$this->unshiftedButParsedParts];
650
        }
651
652
        foreach ($keys as $position => $key) {
653
            $this->allParams[$key] = isset($values[$position]) ? $values[$position] : null;
654
        }
655
656
        return $value;
657
    }
658
659
    /**
660
     * @return array
661
     */
662
    public function latestParams()
663
    {
664
        return $this->latestParams;
665
    }
666
667
    /**
668
     * @param string $name
669
     * @return string|null
670
     */
671
    public function latestParam($name)
672
    {
673
        if (isset($this->latestParams[$name])) {
674
            return $this->latestParams[$name];
675
        } else {
676
            return null;
677
        }
678
    }
679
680
    /**
681
     * @return array
682
     */
683
    public function routeParams()
684
    {
685
        return $this->routeParams;
686
    }
687
688
    /**
689
     * @param $params
690
     * @return HTTPRequest $this
691
     */
692
    public function setRouteParams($params)
693
    {
694
        $this->routeParams = $params;
695
        return $this;
696
    }
697
698
    /**
699
     * @return array
700
     */
701
    public function params()
702
    {
703
        return array_merge($this->allParams, $this->routeParams);
704
    }
705
706
    /**
707
     * Finds a named URL parameter (denoted by "$"-prefix in $url_handlers)
708
     * from the full URL, or a parameter specified in the route table
709
     *
710
     * @param string $name
711
     * @return string Value of the URL parameter (if found)
712
     */
713
    public function param($name)
714
    {
715
        $params = $this->params();
716
        if (isset($params[$name])) {
717
            return $params[$name];
718
        } else {
719
            return null;
720
        }
721
    }
722
723
    /**
724
     * Returns the unparsed part of the original URL
725
     * separated by commas. This is used by {@link RequestHandler->handleRequest()}
726
     * to determine if further URL processing is necessary.
727
     *
728
     * @return string Partial URL
729
     */
730
    public function remaining()
731
    {
732
        return implode("/", $this->dirParts);
733
    }
734
735
    /**
736
     * Returns true if this is a URL that will match without shifting off any of the URL.
737
     * This is used by the request handler to prevent infinite parsing loops.
738
     *
739
     * @param string $pattern
740
     * @return bool
741
     */
742
    public function isEmptyPattern($pattern)
743
    {
744
        if (preg_match('/^([A-Za-z]+) +(.*)$/', $pattern, $matches)) {
745
            $pattern = $matches[2];
746
        }
747
748
        if (trim($pattern) == "") {
749
            return true;
750
        }
751
        return false;
752
    }
753
754
    /**
755
     * Shift one or more parts off the beginning of the URL.
756
     * If you specify shifting more than 1 item off, then the items will be returned as an array
757
     *
758
     * @param int $count Shift Count
759
     * @return string|array
760
     */
761
    public function shift($count = 1)
762
    {
763
        $return = [];
764
765
        if ($count == 1) {
766
            return array_shift($this->dirParts);
767
        }
768
769
        for ($i=0; $i<$count; $i++) {
770
            $value = array_shift($this->dirParts);
771
772
            if ($value === null) {
773
                break;
774
            }
775
776
            $return[] = $value;
777
        }
778
779
        return $return;
780
    }
781
782
    /**
783
     * Returns true if the URL has been completely parsed.
784
     * This will respect parsed but unshifted directory parts.
785
     *
786
     * @return bool
787
     */
788
    public function allParsed()
789
    {
790
        return sizeof($this->dirParts) <= $this->unshiftedButParsedParts;
791
    }
792
793
    /**
794
     * @return string Return the host from the request
795
     */
796
    public function getHost()
797
    {
798
        return $this->getHeader('host');
799
    }
800
801
    /**
802
     * Returns the client IP address which originated this request.
803
     *
804
     * @return string
805
     */
806
    public function getIP()
807
    {
808
        return $this->ip;
809
    }
810
811
    /**
812
     * Sets the client IP address which originated this request.
813
     * Use setIPFromHeaderValue if assigning from header value.
814
     *
815
     * @param $ip string
816
     * @return $this
817
     */
818
    public function setIP($ip)
819
    {
820
        if (!filter_var($ip, FILTER_VALIDATE_IP)) {
821
            throw new InvalidArgumentException("Invalid ip $ip");
822
        }
823
        $this->ip = $ip;
824
        return $this;
825
    }
826
827
    /**
828
     * Returns all mimetypes from the HTTP "Accept" header
829
     * as an array.
830
     *
831
     * @param boolean $includeQuality Don't strip away optional "quality indicators", e.g. "application/xml;q=0.9"
832
     *                                (Default: false)
833
     * @return array
834
     */
835
    public function getAcceptMimetypes($includeQuality = false)
836
    {
837
        $mimetypes = [];
838
        $mimetypesWithQuality = preg_split('#\s*,\s*#', $this->getHeader('accept'));
839
        foreach ($mimetypesWithQuality as $mimetypeWithQuality) {
840
            $mimetypes[] = ($includeQuality) ? $mimetypeWithQuality : preg_replace('/;.*/', '', $mimetypeWithQuality);
841
        }
842
        return $mimetypes;
843
    }
844
845
    /**
846
     * @return string HTTP method (all uppercase)
847
     */
848
    public function httpMethod()
849
    {
850
        return $this->httpMethod;
851
    }
852
853
    /**
854
     * Explicitly set the HTTP method for this request.
855
     * @param string $method
856
     * @return $this
857
     */
858
    public function setHttpMethod($method)
859
    {
860
        if (!self::isValidHttpMethod($method)) {
861
            user_error('HTTPRequest::setHttpMethod: Invalid HTTP method', E_USER_ERROR);
862
        }
863
864
        $this->httpMethod = strtoupper($method);
865
        return $this;
866
    }
867
868
    /**
869
     * Return the URL scheme (e.g. "http" or "https").
870
     * Equivalent to PSR-7 getUri()->getScheme()
871
     *
872
     * @return string
873
     */
874
    public function getScheme()
875
    {
876
        return $this->scheme;
877
    }
878
879
    /**
880
     * Set the URL scheme (e.g. "http" or "https").
881
     * Equivalent to PSR-7 getUri()->getScheme(),
882
     *
883
     * @param string $scheme
884
     * @return $this
885
     */
886
    public function setScheme($scheme)
887
    {
888
        $this->scheme = $scheme;
889
        return $this;
890
    }
891
892
    /**
893
     * @param string $method
894
     * @return bool
895
     */
896
    private static function isValidHttpMethod($method)
897
    {
898
        return in_array(strtoupper($method), ['GET','POST','PUT','DELETE','HEAD']);
899
    }
900
901
    /**
902
     * Gets the "real" HTTP method for a request. This method is no longer used to mitigate the risk of web cache
903
     * poisoning.
904
     *
905
     * @see https://www.silverstripe.org/download/security-releases/CVE-2019-19326
906
     * @param string $origMethod Original HTTP method from the browser request
907
     * @param array $postVars
908
     * @return string HTTP method (all uppercase)
909
     * @deprecated 4.4.7
910
     */
911
    public static function detect_method($origMethod, $postVars)
912
    {
913
        if (isset($postVars['_method'])) {
914
            if (!self::isValidHttpMethod($postVars['_method'])) {
915
                user_error('HTTPRequest::detect_method(): Invalid "_method" parameter', E_USER_ERROR);
916
            }
917
            return strtoupper($postVars['_method']);
918
        } else {
919
            return $origMethod;
920
        }
921
    }
922
923
    /**
924
     * Determines whether the request has a session
925
     *
926
     * @return bool
927
     */
928
    public function hasSession(): bool
929
    {
930
        return !empty($this->session);
931
    }
932
933
    /**
934
     * @return Session
935
     */
936
    public function getSession()
937
    {
938
        if (!$this->hasSession()) {
939
            throw new BadMethodCallException("No session available for this HTTPRequest");
940
        }
941
        return $this->session;
942
    }
943
944
    /**
945
     * @param Session $session
946
     * @return $this
947
     */
948
    public function setSession(Session $session)
949
    {
950
        $this->session = $session;
951
        return $this;
952
    }
953
}
954