Completed
Push — master ( f5d71d...bfd9cb )
by Sam
12:52
created

HTTPRequest::getBody()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 3
rs 10
1
<?php
2
3
namespace SilverStripe\Control;
4
5
use SilverStripe\Core\ClassInfo;
6
use SilverStripe\ORM\ArrayLib;
7
use ArrayAccess;
8
9
/**
10
 * Represents a HTTP-request, including a URL that is tokenised for parsing, and a request method
11
 * (GET/POST/PUT/DELETE). This is used by {@link RequestHandler} objects to decide what to do.
12
 *
13
 * Caution: objects of this class are immutable, e.g. echo $request['a']; works as expected,
14
 * but $request['a'] = '1'; has no effect.
15
 *
16
 * The intention is that a single HTTPRequest object can be passed from one object to another, each object calling
17
 * match() to get the information that they need out of the URL.  This is generally handled by
18
 * {@link RequestHandler::handleRequest()}.
19
 *
20
 * @todo Accept X_HTTP_METHOD_OVERRIDE http header and $_REQUEST['_method'] to override request types (useful for
21
 *       webclients not supporting PUT and DELETE)
22
 */
23
class HTTPRequest implements ArrayAccess {
24
25
	/**
26
	 * @var string
27
	 */
28
	protected $url;
29
30
	/**
31
	 * The non-extension parts of the passed URL as an array, originally exploded by the "/" separator.
32
	 * All elements of the URL are loaded in here,
33
	 * and subsequently popped out of the array by {@link shift()}.
34
	 * Only use this structure for internal request handling purposes.
35
	 *
36
	 * @var array
37
	 */
38
	protected $dirParts;
39
40
	/**
41
	 * The URL extension (if present)
42
	 *
43
	 * @var string
44
	 */
45
	protected $extension;
46
47
	/**
48
	 * The HTTP method in all uppercase: GET/PUT/POST/DELETE/HEAD
49
	 *
50
	 * @var string
51
	 */
52
	protected $httpMethod;
53
54
	/**
55
	 * Contains alls HTTP GET parameters passed into this request.
56
	 *
57
	 * @var array
58
	 */
59
	protected $getVars = array();
60
61
	/**
62
	 * Contains alls HTTP POST parameters passed into this request.
63
	 *
64
	 * @var array
65
	 */
66
	protected $postVars = array();
67
68
	/**
69
	 * HTTP Headers like "Content-Type: text/xml"
70
	 *
71
	 * @see http://en.wikipedia.org/wiki/List_of_HTTP_headers
72
	 * @var array
73
	 */
74
	protected $headers = array();
75
76
	/**
77
	 * Raw HTTP body, used by PUT and POST requests.
78
	 *
79
	 * @var string
80
	 */
81
	protected $body;
82
83
	/**
84
	 * Contains an associative array of all
85
	 * arguments matched in all calls to {@link RequestHandler->handleRequest()}.
86
	 * It's a "historical record" that's specific to the current call of
87
	 * {@link handleRequest()}, and is only complete once the "last call" to that method is made.
88
	 *
89
	 * @var array
90
	 */
91
	protected $allParams = array();
92
93
	/**
94
	 * Contains an associative array of all
95
	 * arguments matched in the current call from {@link RequestHandler->handleRequest()},
96
	 * as denoted with a "$"-prefix in the $url_handlers definitions.
97
	 * Contains different states throughout its lifespan, so just useful
98
	 * while processed in {@link RequestHandler} and to get the last
99
	 * processes arguments.
100
	 *
101
	 * @var array
102
	 */
103
	protected $latestParams = array();
104
105
	/**
106
	 * Contains an associative array of all arguments
107
	 * explicitly set in the route table for the current request.
108
	 * Useful for passing generic arguments via custom routes.
109
	 *
110
	 * E.g. The "Locale" parameter would be assigned "en_NZ" below
111
	 *
112
	 * Director:
113
	 *   rules:
114
	 *     'en_NZ/$URLSegment!//$Action/$ID/$OtherID':
115
	 *       Controller: 'ModelAsController'
116
	 *       Locale: 'en_NZ'
117
	 *
118
	 * @var array
119
	 */
120
	protected $routeParams = array();
121
122
	/**
123
	 * @var int
124
	 */
125
	protected $unshiftedButParsedParts = 0;
126
127
	/**
128
	 * Construct a HTTPRequest from a URL relative to the site root.
129
	 *
130
	 * @param string $httpMethod
131
	 * @param string $url
132
	 * @param array $getVars
133
	 * @param array $postVars
134
	 * @param string $body
135
	 */
136
	public function __construct($httpMethod, $url, $getVars = array(), $postVars = array(), $body = null) {
137
		$this->httpMethod = strtoupper(self::detect_method($httpMethod, $postVars));
138
		$this->setUrl($url);
139
140
		$this->getVars = (array) $getVars;
141
		$this->postVars = (array) $postVars;
142
		$this->body = $body;
143
	}
144
145
	/**
146
	 * Allow the setting of a URL
147
	 *
148
	 * This is here so that RootURLController can change the URL of the request
149
	 * without us loosing all the other info attached (like headers)
150
	 *
151
	 * @param string $url The new URL
152
	 * @return HTTPRequest The updated request
153
	 */
154
	public function setUrl($url) {
155
		$this->url = $url;
156
157
		// Normalize URL if its relative (strictly speaking), or has leading slashes
158
		if(Director::is_relative_url($url) || preg_match('/^\//', $url)) {
159
			$this->url = preg_replace(array('/\/+/','/^\//', '/\/$/'),array('/','',''), $this->url);
160
		}
161
		if(preg_match('/^(.*)\.([A-Za-z][A-Za-z0-9]*)$/', $this->url, $matches)) {
162
			$this->url = $matches[1];
163
			$this->extension = $matches[2];
164
		}
165
		if($this->url) {
166
			$this->dirParts = preg_split('|/+|', $this->url);
167
		} else {
168
			$this->dirParts = array();
169
		}
170
171
		return $this;
172
	}
173
174
	/**
175
	 * @return bool
176
	 */
177
	public function isGET() {
178
		return $this->httpMethod == 'GET';
179
	}
180
181
	/**
182
	 * @return bool
183
	 */
184
	public function isPOST() {
185
		return $this->httpMethod == 'POST';
186
	}
187
188
	/**
189
	 * @return bool
190
	 */
191
	public function isPUT() {
192
		return $this->httpMethod == 'PUT';
193
	}
194
195
	/**
196
	 * @return bool
197
	 */
198
	public function isDELETE() {
199
		return $this->httpMethod == 'DELETE';
200
	}
201
202
	/**
203
	 * @return bool
204
	 */
205
	public function isHEAD() {
206
		return $this->httpMethod == 'HEAD';
207
	}
208
209
	/**
210
	 * @param string $body
211
	 * @return HTTPRequest $this
212
	 */
213
	public function setBody($body) {
214
		$this->body = $body;
215
		return $this;
216
	}
217
218
	/**
219
	 * @return null|string
220
	 */
221
	public function getBody() {
222
		return $this->body;
223
	}
224
225
	/**
226
	 * @return array
227
	 */
228
	public function getVars() {
229
		return $this->getVars;
230
	}
231
232
	/**
233
	 * @return array
234
	 */
235
	public function postVars() {
236
		return $this->postVars;
237
	}
238
239
	/**
240
	 * Returns all combined HTTP GET and POST parameters
241
	 * passed into this request. If a parameter with the same
242
	 * name exists in both arrays, the POST value is returned.
243
	 *
244
	 * @return array
245
	 */
246
	public function requestVars() {
247
		return ArrayLib::array_merge_recursive($this->getVars, $this->postVars);
248
	}
249
250
	/**
251
	 * @param string $name
252
	 * @return mixed
253
	 */
254
	public function getVar($name) {
255
		if(isset($this->getVars[$name])) {
256
			return $this->getVars[$name];
257
		}
258
		return null;
259
	}
260
261
	/**
262
	 * @param string $name
263
	 * @return mixed
264
	 */
265
	public function postVar($name) {
266
		if(isset($this->postVars[$name])) {
267
			return $this->postVars[$name];
268
		}
269
		return null;
270
	}
271
272
	/**
273
	 * @param string $name
274
	 * @return mixed
275
	 */
276
	public function requestVar($name) {
277
		if(isset($this->postVars[$name])) {
278
			return $this->postVars[$name];
279
		}
280
		if(isset($this->getVars[$name])) {
281
			return $this->getVars[$name];
282
		}
283
		return null;
284
	}
285
286
	/**
287
	 * Returns a possible file extension found in parsing the URL
288
	 * as denoted by a "."-character near the end of the URL.
289
	 * Doesn't necessarily have to belong to an existing file,
290
	 * as extensions can be also used for content-type-switching.
291
	 *
292
	 * @return string
293
	 */
294
	public function getExtension() {
295
		return $this->extension;
296
	}
297
298
	/**
299
	 * Checks if the {@link HTTPRequest->getExtension()} on this request matches one of the more common media types
300
	 * embedded into a webpage - e.g. css, png.
301
	 *
302
	 * This is useful for things like determining wether to display a fully rendered error page or not. Note that the
303
	 * media file types is not at all comprehensive.
304
	 *
305
	 * @return bool
306
	 */
307
	public function isMedia() {
308
		return in_array($this->getExtension(), array('css', 'js', 'jpg', 'jpeg', 'gif', 'png', 'bmp', 'ico'));
309
	}
310
311
	/**
312
	 * Add a HTTP header to the response, replacing any header of the same name.
313
	 *
314
	 * @param string $header Example: "Content-Type"
315
	 * @param string $value Example: "text/xml"
316
	 */
317
	public function addHeader($header, $value) {
318
		$this->headers[$header] = $value;
319
	}
320
321
	/**
322
	 * @return array
323
	 */
324
	public function getHeaders() {
325
		return $this->headers;
326
	}
327
328
	/**
329
	 * Remove an existing HTTP header
330
	 *
331
	 * @param string $header
332
	 * @return mixed
333
	 */
334
	public function getHeader($header) {
335
		return (isset($this->headers[$header])) ? $this->headers[$header] : null;
336
	}
337
338
	/**
339
	 * Remove an existing HTTP header by its name,
340
	 * e.g. "Content-Type".
341
	 *
342
	 * @param string $header
343
	 * @return HTTPRequest $this
344
	 */
345
	public function removeHeader($header) {
346
		if(isset($this->headers[$header])) unset($this->headers[$header]);
347
		return $this;
348
	}
349
350
	/**
351
	 * Returns the URL used to generate the page
352
	 *
353
	 * @param bool $includeGetVars whether or not to include the get parameters\
354
	 * @return string
355
	 */
356
	public function getURL($includeGetVars = false) {
357
		$url = ($this->getExtension()) ? $this->url . '.' . $this->getExtension() : $this->url;
358
359
		if ($includeGetVars) {
360
			// if we don't unset $vars['url'] we end up with /my/url?url=my/url&foo=bar etc
361
362
			$vars = $this->getVars();
363
			unset($vars['url']);
364
365
			if (count($vars)) {
366
				$url .= '?' . http_build_query($vars);
367
			}
368
		}
369
		else if(strpos($url, "?") !== false) {
370
			$url = substr($url, 0, strpos($url, "?"));
371
		}
372
373
		return $url;
374
	}
375
376
	/**
377
	 * Returns true if this request an ajax request,
378
	 * based on custom HTTP ajax added by common JavaScript libraries,
379
	 * or based on an explicit "ajax" request parameter.
380
	 *
381
	 * @return boolean
382
	 */
383
	public function isAjax() {
384
		return (
385
			$this->requestVar('ajax') ||
386
			$this->getHeader('X-Requested-With') && $this->getHeader('X-Requested-With') == "XMLHttpRequest"
387
		);
388
	}
389
390
	/**
391
	 * Enables the existence of a key-value pair in the request to be checked using
392
	 * array syntax, so isset($request['title']) will check for $_POST['title'] and $_GET['title']
393
	 *
394
	 * @param string $offset
395
	 * @return bool
396
	 */
397
	public function offsetExists($offset) {
398
		return isset($this->postVars[$offset]) || isset($this->getVars[$offset]);
399
	}
400
401
	/**
402
	 * Access a request variable using array syntax. eg: $request['title'] instead of $request->postVar('title')
403
	 *
404
	 * @param string $offset
405
	 * @return mixed
406
	 */
407
	public function offsetGet($offset) {
408
		return $this->requestVar($offset);
409
	}
410
411
	/**
412
	 * @ignore
413
	 * @param string $offset
414
	 * @param mixed $value
415
	 */
416
	public function offsetSet($offset, $value) {}
417
418
	/**
419
	 * @ignore
420
	 * @param mixed $offset
421
	 */
422
	public function offsetUnset($offset) {}
423
424
	/**
425
	 * Construct an HTTPResponse that will deliver a file to the client.
426
	 * Caution: Since it requires $fileData to be passed as binary data (no stream support),
427
	 * it's only advisable to send small files through this method.
428
	 *
429
	 * @static
430
	 * @param $fileData
431
	 * @param $fileName
432
	 * @param null $mimeType
433
	 * @return HTTPResponse
434
	 */
435
	public static function send_file($fileData, $fileName, $mimeType = null) {
436
		if(!$mimeType) {
437
			$mimeType = HTTP::get_mime_type($fileName);
438
		}
439
		$response = new HTTPResponse($fileData);
440
		$response->addHeader("Content-Type", "$mimeType; name=\"" . addslashes($fileName) . "\"");
441
		// Note a IE-only fix that inspects this header in HTTP::add_cache_headers().
442
		$response->addHeader("Content-Disposition", "attachment; filename=" . addslashes($fileName));
443
		$response->addHeader("Content-Length", strlen($fileData));
444
445
		return $response;
446
	}
447
448
	/**
449
	 * Matches a URL pattern
450
	 * The pattern can contain a number of segments, separated by / (and an extension indicated by a .)
451
	 *
452
	 * The parts can be either literals, or, if they start with a $ they are interpreted as variables.
453
	 *  - Literals must be provided in order to match
454
	 *  - $Variables are optional
455
	 *  - However, if you put ! at the end of a variable, then it becomes mandatory.
456
	 *
457
	 * For example:
458
	 *  - admin/crm/list will match admin/crm/$Action/$ID/$OtherID, but it won't match admin/crm/$Action!/$ClassName!
459
	 *
460
	 * The pattern can optionally start with an HTTP method and a space.  For example, "POST $Controller/$Action".
461
	 * This is used to define a rule that only matches on a specific HTTP method.
462
	 *
463
	 * @param $pattern
464
	 * @param bool $shiftOnSuccess
465
	 * @return array|bool
466
	 */
467
	public function match($pattern, $shiftOnSuccess = false) {
468
		// Check if a specific method is required
469
		if(preg_match('/^([A-Za-z]+) +(.*)$/', $pattern, $matches)) {
470
			$requiredMethod = $matches[1];
471
			if($requiredMethod != $this->httpMethod) return false;
472
473
			// If we get this far, we can match the URL pattern as usual.
474
			$pattern = $matches[2];
475
		}
476
477
		// Special case for the root URL controller
478
		if(!$pattern) {
479
			return ($this->dirParts == array()) ? array('Matched' => true) : false;
480
		}
481
482
		// Check for the '//' marker that represents the "shifting point"
483
		$doubleSlashPoint = strpos($pattern, '//');
484
		if($doubleSlashPoint !== false) {
485
			$shiftCount = substr_count(substr($pattern,0,$doubleSlashPoint), '/') + 1;
486
			$pattern = str_replace('//', '/', $pattern);
487
			$patternParts = explode('/', $pattern);
488
489
490
		} else {
491
			$patternParts = explode('/', $pattern);
492
			$shiftCount = sizeof($patternParts);
493
		}
494
495
		// Filter out any "empty" matching parts - either from an initial / or a trailing /
496
		$patternParts = array_values(array_filter($patternParts));
497
498
		$arguments = array();
499
		foreach($patternParts as $i => $part) {
500
			$part = trim($part);
501
502
			// Match a variable
503
			if(isset($part[0]) && $part[0] == '$') {
504
				// A variable ending in ! is required
505
				if(substr($part,-1) == '!') {
506
					$varRequired = true;
507
					$varName = substr($part,1,-1);
508
				} else {
509
					$varRequired = false;
510
					$varName = substr($part,1);
511
				}
512
513
				// Fail if a required variable isn't populated
514
				if($varRequired && !isset($this->dirParts[$i])) return false;
515
516
				/** @skipUpgrade */
517
				$key = "Controller";
518
				$arguments[$varName] = isset($this->dirParts[$i]) ? $this->dirParts[$i] : null;
519
				if( $part == '$Controller'
520
					&& (
521
						!ClassInfo::exists($arguments[$key])
522
						|| !is_subclass_of($arguments[$key], 'SilverStripe\\Control\\Controller')
523
					)
524
				) {
525
526
					return false;
527
				}
528
529
			// Literal parts with extension
530
			} else if(isset($this->dirParts[$i]) && $this->dirParts[$i] . '.' . $this->extension == $part) {
531
				continue;
532
533
			// Literal parts must always be there
534
			} else if(!isset($this->dirParts[$i]) || $this->dirParts[$i] != $part) {
535
				return false;
536
			}
537
538
		}
539
540
		if($shiftOnSuccess) {
541
			$this->shift($shiftCount);
542
			// We keep track of pattern parts that we looked at but didn't shift off.
543
			// This lets us say that we have *parsed* the whole URL even when we haven't *shifted* it all
544
			$this->unshiftedButParsedParts = sizeof($patternParts) - $shiftCount;
0 ignored issues
show
Documentation Bug introduced by
It seems like sizeof($patternParts) - $shiftCount can also be of type double. However, the property $unshiftedButParsedParts is declared as type integer. 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...
545
		}
546
547
		$this->latestParams = $arguments;
548
549
		// Load the arguments that actually have a value into $this->allParams
550
		// This ensures that previous values aren't overridden with blanks
551
		foreach($arguments as $k => $v) {
552
			if($v || !isset($this->allParams[$k])) $this->allParams[$k] = $v;
553
		}
554
555
		if($arguments === array()) $arguments['_matched'] = true;
556
		return $arguments;
557
	}
558
559
	/**
560
	 * @return array
561
	 */
562
	public function allParams() {
563
		return $this->allParams;
564
	}
565
566
	/**
567
	 * Shift all the parameter values down a key space, and return the shifted value.
568
	 *
569
	 * @return string
570
	 */
571
	public function shiftAllParams() {
572
		$keys    = array_keys($this->allParams);
573
		$values  = array_values($this->allParams);
574
		$value   = array_shift($values);
575
576
		// push additional unparsed URL parts onto the parameter stack
577
		if(array_key_exists($this->unshiftedButParsedParts, $this->dirParts)) {
578
			$values[] = $this->dirParts[$this->unshiftedButParsedParts];
579
		}
580
581
		foreach($keys as $position => $key) {
582
			$this->allParams[$key] = isset($values[$position]) ? $values[$position] : null;
583
		}
584
585
		return $value;
586
	}
587
588
	/**
589
	 * @return array
590
	 */
591
	public function latestParams() {
592
		return $this->latestParams;
593
	}
594
595
	/**
596
	 * @param string $name
597
	 * @return string|null
598
	 */
599
	public function latestParam($name) {
600
		if(isset($this->latestParams[$name])) return $this->latestParams[$name];
601
		else return null;
602
	}
603
604
	/**
605
	 * @return array
606
	 */
607
	public function routeParams() {
608
		return $this->routeParams;
609
	}
610
611
	/**
612
	 * @param $params
613
	 * @return HTTPRequest $this
614
	 */
615
	public function setRouteParams($params) {
616
		$this->routeParams = $params;
617
		return $this;
618
	}
619
620
	/**
621
	 * @return array
622
	 */
623
	public function params() {
624
		return array_merge($this->allParams, $this->routeParams);
625
	}
626
627
	/**
628
	 * Finds a named URL parameter (denoted by "$"-prefix in $url_handlers)
629
	 * from the full URL, or a parameter specified in the route table
630
	 *
631
	 * @param string $name
632
	 * @return string Value of the URL parameter (if found)
633
	 */
634
	public function param($name) {
635
		$params = $this->params();
636
		if(isset($params[$name])) return $params[$name];
637
		else return null;
638
	}
639
640
	/**
641
	 * Returns the unparsed part of the original URL
642
	 * separated by commas. This is used by {@link RequestHandler->handleRequest()}
643
	 * to determine if further URL processing is necessary.
644
	 *
645
	 * @return string Partial URL
646
	 */
647
	public function remaining() {
648
		return implode("/", $this->dirParts);
649
	}
650
651
	/**
652
	 * Returns true if this is a URL that will match without shifting off any of the URL.
653
	 * This is used by the request handler to prevent infinite parsing loops.
654
	 *
655
	 * @param string $pattern
656
	 * @return bool
657
	 */
658
	public function isEmptyPattern($pattern) {
659
		if(preg_match('/^([A-Za-z]+) +(.*)$/', $pattern, $matches)) {
660
			$pattern = $matches[2];
661
		}
662
663
		if(trim($pattern) == "") {
664
			return true;
665
		}
666
		return false;
667
	}
668
669
	/**
670
	 * Shift one or more parts off the beginning of the URL.
671
	 * If you specify shifting more than 1 item off, then the items will be returned as an array
672
	 *
673
	 * @param int $count Shift Count
674
	 * @return string|array
675
	 */
676
	public function shift($count = 1) {
677
		$return = array();
678
679
		if($count == 1) return array_shift($this->dirParts);
680
681
		for($i=0;$i<$count;$i++) {
682
			$value = array_shift($this->dirParts);
683
684
			if($value === null) break;
685
686
			$return[] = $value;
687
		}
688
689
		return $return;
690
	}
691
692
	/**
693
	 * Returns true if the URL has been completely parsed.
694
	 * This will respect parsed but unshifted directory parts.
695
	 *
696
	 * @return bool
697
	 */
698
	public function allParsed() {
699
		return sizeof($this->dirParts) <= $this->unshiftedButParsedParts;
700
	}
701
702
	/**
703
	 * Returns the client IP address which
704
	 * originated this request.
705
	 *
706
	 * @return string
707
	 */
708
	public function getIP() {
0 ignored issues
show
Coding Style introduced by
getIP uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

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

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
709
		$headerOverrideIP = null;
710
		if(TRUSTED_PROXY) {
711
			$headers = (defined('SS_TRUSTED_PROXY_IP_HEADER')) ? array(SS_TRUSTED_PROXY_IP_HEADER) : null;
712
			if(!$headers) {
713
				// Backwards compatible defaults
714
				$headers = array('HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR');
715
			}
716
			foreach($headers as $header) {
717
				if(!empty($_SERVER[$header])) {
718
					$headerOverrideIP = $_SERVER[$header];
719
					break;
720
				}
721
			}
722
		}
723
724
		if ($headerOverrideIP && filter_var($headerOverrideIP, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
725
			return $this->getIPFromHeaderValue($headerOverrideIP);
726
		} elseif(isset($_SERVER['REMOTE_ADDR'])) {
727
			return $_SERVER['REMOTE_ADDR'];
728
		} else {
729
			return null;
730
		}
731
	}
732
733
	/**
734
	 * Extract an IP address from a header value that has been obtained. Accepts single IP or comma separated string of
735
	 * IPs
736
	 *
737
	 * @param string $headerValue The value from a trusted header
738
	 * @return string The IP address
739
	 */
740
	protected function getIPFromHeaderValue($headerValue) {
741
		if (strpos($headerValue, ',') !== false) {
742
			//sometimes the IP from a load balancer could be "x.x.x.x, y.y.y.y, z.z.z.z" so we need to find the most
743
			// likely candidate
744
			$ips = explode(',', $headerValue);
745
			foreach ($ips as $ip) {
746
				$ip = trim($ip);
747
				if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE)) {
748
					return $ip;
749
				}
750
			}
751
		}
752
		return $headerValue;
753
	}
754
755
	/**
756
	 * Returns all mimetypes from the HTTP "Accept" header
757
	 * as an array.
758
	 *
759
	 * @param boolean $includeQuality Don't strip away optional "quality indicators", e.g. "application/xml;q=0.9"
760
	 *                                (Default: false)
761
	 * @return array
762
	 */
763
	public function getAcceptMimetypes($includeQuality = false) {
764
		$mimetypes = array();
765
		$mimetypesWithQuality = explode(',',$this->getHeader('Accept'));
766
		foreach($mimetypesWithQuality as $mimetypeWithQuality) {
767
			$mimetypes[] = ($includeQuality) ? $mimetypeWithQuality : preg_replace('/;.*/', '', $mimetypeWithQuality);
768
		}
769
		return $mimetypes;
770
	}
771
772
	/**
773
	 * @return string HTTP method (all uppercase)
774
	 */
775
	public function httpMethod() {
776
		return $this->httpMethod;
777
	}
778
779
	/**
780
	 * Gets the "real" HTTP method for a request.
781
	 *
782
	 * Used to work around browser limitations of form
783
	 * submissions to GET and POST, by overriding the HTTP method
784
	 * with a POST parameter called "_method" for PUT, DELETE, HEAD.
785
	 * Using GET for the "_method" override is not supported,
786
	 * as GET should never carry out state changes.
787
	 * Alternatively you can use a custom HTTP header 'X-HTTP-Method-Override'
788
	 * to override the original method in {@link Director::direct()}.
789
	 * The '_method' POST parameter overrules the custom HTTP header.
790
	 *
791
	 * @param string $origMethod Original HTTP method from the browser request
792
	 * @param array $postVars
793
	 * @return string HTTP method (all uppercase)
794
	 */
795
	public static function detect_method($origMethod, $postVars) {
796
		if(isset($postVars['_method'])) {
797
			if(!in_array(strtoupper($postVars['_method']), array('GET','POST','PUT','DELETE','HEAD'))) {
798
				user_error('Director::direct(): Invalid "_method" parameter', E_USER_ERROR);
799
			}
800
			return strtoupper($postVars['_method']);
801
		} else {
802
			return $origMethod;
803
		}
804
	}
805
}
806