Issues (4122)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

includes/WebRequest.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * Deal with importing all those nasty globals and things
4
 *
5
 * Copyright © 2003 Brion Vibber <[email protected]>
6
 * https://www.mediawiki.org/
7
 *
8
 * This program is free software; you can redistribute it and/or modify
9
 * it under the terms of the GNU General Public License as published by
10
 * the Free Software Foundation; either version 2 of the License, or
11
 * (at your option) any later version.
12
 *
13
 * This program is distributed in the hope that it will be useful,
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
 * GNU General Public License for more details.
17
 *
18
 * You should have received a copy of the GNU General Public License along
19
 * with this program; if not, write to the Free Software Foundation, Inc.,
20
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
21
 * http://www.gnu.org/copyleft/gpl.html
22
 *
23
 * @file
24
 */
25
26
use MediaWiki\MediaWikiServices;
27
use MediaWiki\Session\Session;
28
use MediaWiki\Session\SessionId;
29
use MediaWiki\Session\SessionManager;
30
31
/**
32
 * The WebRequest class encapsulates getting at data passed in the
33
 * URL or via a POSTed form stripping illegal input characters and
34
 * normalizing Unicode sequences.
35
 *
36
 * @ingroup HTTP
37
 */
38
class WebRequest {
39
	protected $data, $headers = [];
40
41
	/**
42
	 * Flag to make WebRequest::getHeader return an array of values.
43
	 * @since 1.26
44
	 */
45
	const GETHEADER_LIST = 1;
46
47
	/**
48
	 * The unique request ID.
49
	 * @var string
50
	 */
51
	private static $reqId;
52
53
	/**
54
	 * Lazy-init response object
55
	 * @var WebResponse
56
	 */
57
	private $response;
58
59
	/**
60
	 * Cached client IP address
61
	 * @var string
62
	 */
63
	private $ip;
64
65
	/**
66
	 * The timestamp of the start of the request, with microsecond precision.
67
	 * @var float
68
	 */
69
	protected $requestTime;
70
71
	/**
72
	 * Cached URL protocol
73
	 * @var string
74
	 */
75
	protected $protocol;
76
77
	/**
78
	 * @var SessionId|null Session ID to use for this
79
	 *  request. We can't save the session directly due to reference cycles not
80
	 *  working too well (slow GC in Zend and never collected in HHVM).
81
	 */
82
	protected $sessionId = null;
83
84
	/** @var bool Whether this HTTP request is "safe" (even if it is an HTTP post) */
85
	protected $markedAsSafe = false;
86
87
	/**
88
	 * @codeCoverageIgnore
89
	 */
90
	public function __construct() {
91
		$this->requestTime = isset( $_SERVER['REQUEST_TIME_FLOAT'] )
92
			? $_SERVER['REQUEST_TIME_FLOAT'] : microtime( true );
93
94
		// POST overrides GET data
95
		// We don't use $_REQUEST here to avoid interference from cookies...
96
		$this->data = $_POST + $_GET;
97
	}
98
99
	/**
100
	 * Extract relevant query arguments from the http request uri's path
101
	 * to be merged with the normal php provided query arguments.
102
	 * Tries to use the REQUEST_URI data if available and parses it
103
	 * according to the wiki's configuration looking for any known pattern.
104
	 *
105
	 * If the REQUEST_URI is not provided we'll fall back on the PATH_INFO
106
	 * provided by the server if any and use that to set a 'title' parameter.
107
	 *
108
	 * @param string $want If this is not 'all', then the function
109
	 * will return an empty array if it determines that the URL is
110
	 * inside a rewrite path.
111
	 *
112
	 * @return array Any query arguments found in path matches.
113
	 */
114
	public static function getPathInfo( $want = 'all' ) {
115
		global $wgUsePathInfo;
116
		// PATH_INFO is mangled due to https://bugs.php.net/bug.php?id=31892
117
		// And also by Apache 2.x, double slashes are converted to single slashes.
118
		// So we will use REQUEST_URI if possible.
119
		$matches = [];
120
		if ( !empty( $_SERVER['REQUEST_URI'] ) ) {
121
			// Slurp out the path portion to examine...
122
			$url = $_SERVER['REQUEST_URI'];
123
			if ( !preg_match( '!^https?://!', $url ) ) {
124
				$url = 'http://unused' . $url;
125
			}
126
			MediaWiki\suppressWarnings();
127
			$a = parse_url( $url );
128
			MediaWiki\restoreWarnings();
129
			if ( $a ) {
130
				$path = isset( $a['path'] ) ? $a['path'] : '';
131
132
				global $wgScript;
133
				if ( $path == $wgScript && $want !== 'all' ) {
134
					// Script inside a rewrite path?
135
					// Abort to keep from breaking...
136
					return $matches;
137
				}
138
139
				$router = new PathRouter;
140
141
				// Raw PATH_INFO style
142
				$router->add( "$wgScript/$1" );
143
144
				if ( isset( $_SERVER['SCRIPT_NAME'] )
145
					&& preg_match( '/\.php5?/', $_SERVER['SCRIPT_NAME'] )
146
				) {
147
					# Check for SCRIPT_NAME, we handle index.php explicitly
148
					# But we do have some other .php files such as img_auth.php
149
					# Don't let root article paths clober the parsing for them
150
					$router->add( $_SERVER['SCRIPT_NAME'] . "/$1" );
151
				}
152
153
				global $wgArticlePath;
154
				if ( $wgArticlePath ) {
155
					$router->add( $wgArticlePath );
156
				}
157
158
				global $wgActionPaths;
159
				if ( $wgActionPaths ) {
160
					$router->add( $wgActionPaths, [ 'action' => '$key' ] );
161
				}
162
163
				global $wgVariantArticlePath, $wgContLang;
164
				if ( $wgVariantArticlePath ) {
165
					$router->add( $wgVariantArticlePath,
166
						[ 'variant' => '$2' ],
167
						[ '$2' => $wgContLang->getVariants() ]
168
					);
169
				}
170
171
				Hooks::run( 'WebRequestPathInfoRouter', [ $router ] );
172
173
				$matches = $router->parse( $path );
174
			}
175
		} elseif ( $wgUsePathInfo ) {
176
			if ( isset( $_SERVER['ORIG_PATH_INFO'] ) && $_SERVER['ORIG_PATH_INFO'] != '' ) {
177
				// Mangled PATH_INFO
178
				// https://bugs.php.net/bug.php?id=31892
179
				// Also reported when ini_get('cgi.fix_pathinfo')==false
180
				$matches['title'] = substr( $_SERVER['ORIG_PATH_INFO'], 1 );
181
182
			} elseif ( isset( $_SERVER['PATH_INFO'] ) && $_SERVER['PATH_INFO'] != '' ) {
183
				// Regular old PATH_INFO yay
184
				$matches['title'] = substr( $_SERVER['PATH_INFO'], 1 );
185
			}
186
		}
187
188
		return $matches;
189
	}
190
191
	/**
192
	 * Work out an appropriate URL prefix containing scheme and host, based on
193
	 * information detected from $_SERVER
194
	 *
195
	 * @return string
196
	 */
197
	public static function detectServer() {
198
		global $wgAssumeProxiesUseDefaultProtocolPorts;
199
200
		$proto = self::detectProtocol();
201
		$stdPort = $proto === 'https' ? 443 : 80;
202
203
		$varNames = [ 'HTTP_HOST', 'SERVER_NAME', 'HOSTNAME', 'SERVER_ADDR' ];
204
		$host = 'localhost';
205
		$port = $stdPort;
206
		foreach ( $varNames as $varName ) {
207
			if ( !isset( $_SERVER[$varName] ) ) {
208
				continue;
209
			}
210
211
			$parts = IP::splitHostAndPort( $_SERVER[$varName] );
212
			if ( !$parts ) {
213
				// Invalid, do not use
214
				continue;
215
			}
216
217
			$host = $parts[0];
218
			if ( $wgAssumeProxiesUseDefaultProtocolPorts && isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) ) {
219
				// Bug 70021: Assume that upstream proxy is running on the default
220
				// port based on the protocol. We have no reliable way to determine
221
				// the actual port in use upstream.
222
				$port = $stdPort;
223
			} elseif ( $parts[1] === false ) {
224
				if ( isset( $_SERVER['SERVER_PORT'] ) ) {
225
					$port = $_SERVER['SERVER_PORT'];
226
				} // else leave it as $stdPort
227
			} else {
228
				$port = $parts[1];
229
			}
230
			break;
231
		}
232
233
		return $proto . '://' . IP::combineHostAndPort( $host, $port, $stdPort );
234
	}
235
236
	/**
237
	 * Detect the protocol from $_SERVER.
238
	 * This is for use prior to Setup.php, when no WebRequest object is available.
239
	 * At other times, use the non-static function getProtocol().
240
	 *
241
	 * @return array
242
	 */
243
	public static function detectProtocol() {
244
		if ( ( !empty( $_SERVER['HTTPS'] ) && $_SERVER['HTTPS'] !== 'off' ) ||
245
			( isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) &&
246
			$_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https' ) ) {
247
			return 'https';
248
		} else {
249
			return 'http';
250
		}
251
	}
252
253
	/**
254
	 * Get the number of seconds to have elapsed since request start,
255
	 * in fractional seconds, with microsecond resolution.
256
	 *
257
	 * @return float
258
	 * @since 1.25
259
	 */
260
	public function getElapsedTime() {
261
		return microtime( true ) - $this->requestTime;
262
	}
263
264
	/**
265
	 * Get the unique request ID.
266
	 * This is either the value of the UNIQUE_ID envvar (if present) or a
267
	 * randomly-generated 24-character string.
268
	 *
269
	 * @return string
270
	 * @since 1.27
271
	 */
272
	public static function getRequestId() {
273
		if ( !self::$reqId ) {
274
			self::$reqId = isset( $_SERVER['UNIQUE_ID'] )
275
				? $_SERVER['UNIQUE_ID'] : wfRandomString( 24 );
276
		}
277
278
		return self::$reqId;
279
	}
280
281
	/**
282
	 * Override the unique request ID. This is for sub-requests, such as jobs,
283
	 * that wish to use the same id but are not part of the same execution context.
284
	 *
285
	 * @param string $id
286
	 * @since 1.27
287
	 */
288
	public static function overrideRequestId( $id ) {
289
		self::$reqId = $id;
290
	}
291
292
	/**
293
	 * Get the current URL protocol (http or https)
294
	 * @return string
295
	 */
296
	public function getProtocol() {
297
		if ( $this->protocol === null ) {
298
			$this->protocol = self::detectProtocol();
299
		}
300
		return $this->protocol;
301
	}
302
303
	/**
304
	 * Check for title, action, and/or variant data in the URL
305
	 * and interpolate it into the GET variables.
306
	 * This should only be run after $wgContLang is available,
307
	 * as we may need the list of language variants to determine
308
	 * available variant URLs.
309
	 */
310
	public function interpolateTitle() {
311
		// bug 16019: title interpolation on API queries is useless and sometimes harmful
312
		if ( defined( 'MW_API' ) ) {
313
			return;
314
		}
315
316
		$matches = self::getPathInfo( 'title' );
317
		foreach ( $matches as $key => $val ) {
318
			$this->data[$key] = $_GET[$key] = $_REQUEST[$key] = $val;
319
		}
320
	}
321
322
	/**
323
	 * URL rewriting function; tries to extract page title and,
324
	 * optionally, one other fixed parameter value from a URL path.
325
	 *
326
	 * @param string $path The URL path given from the client
327
	 * @param array $bases One or more URLs, optionally with $1 at the end
328
	 * @param string|bool $key If provided, the matching key in $bases will be
329
	 *    passed on as the value of this URL parameter
330
	 * @return array Array of URL variables to interpolate; empty if no match
331
	 */
332
	static function extractTitle( $path, $bases, $key = false ) {
333
		foreach ( (array)$bases as $keyValue => $base ) {
334
			// Find the part after $wgArticlePath
335
			$base = str_replace( '$1', '', $base );
336
			$baseLen = strlen( $base );
337
			if ( substr( $path, 0, $baseLen ) == $base ) {
338
				$raw = substr( $path, $baseLen );
339
				if ( $raw !== '' ) {
340
					$matches = [ 'title' => rawurldecode( $raw ) ];
341
					if ( $key ) {
342
						$matches[$key] = $keyValue;
343
					}
344
					return $matches;
345
				}
346
			}
347
		}
348
		return [];
349
	}
350
351
	/**
352
	 * Recursively normalizes UTF-8 strings in the given array.
353
	 *
354
	 * @param string|array $data
355
	 * @return array|string Cleaned-up version of the given
356
	 * @private
357
	 */
358
	public function normalizeUnicode( $data ) {
359
		if ( is_array( $data ) ) {
360
			foreach ( $data as $key => $val ) {
361
				$data[$key] = $this->normalizeUnicode( $val );
362
			}
363
		} else {
364
			global $wgContLang;
365
			$data = isset( $wgContLang ) ?
366
				$wgContLang->normalize( $data ) :
367
				UtfNormal\Validator::cleanUp( $data );
368
		}
369
		return $data;
370
	}
371
372
	/**
373
	 * Fetch a value from the given array or return $default if it's not set.
374
	 *
375
	 * @param array $arr
376
	 * @param string $name
377
	 * @param mixed $default
378
	 * @return mixed
379
	 */
380
	private function getGPCVal( $arr, $name, $default ) {
381
		# PHP is so nice to not touch input data, except sometimes:
382
		# https://secure.php.net/variables.external#language.variables.external.dot-in-names
383
		# Work around PHP *feature* to avoid *bugs* elsewhere.
384
		$name = strtr( $name, '.', '_' );
385
		if ( isset( $arr[$name] ) ) {
386
			global $wgContLang;
387
			$data = $arr[$name];
388
			if ( isset( $_GET[$name] ) && !is_array( $data ) ) {
389
				# Check for alternate/legacy character encoding.
390
				if ( isset( $wgContLang ) ) {
391
					$data = $wgContLang->checkTitleEncoding( $data );
392
				}
393
			}
394
			$data = $this->normalizeUnicode( $data );
395
			return $data;
396
		} else {
397
			return $default;
398
		}
399
	}
400
401
	/**
402
	 * Fetch a scalar from the input without normalization, or return $default
403
	 * if it's not set.
404
	 *
405
	 * Unlike self::getVal(), this does not perform any normalization on the
406
	 * input value.
407
	 *
408
	 * @since 1.28
409
	 * @param string $name
410
	 * @param string|null $default Optional default
411
	 * @return string
412
	 */
413
	public function getRawVal( $name, $default = null ) {
414
		$name = strtr( $name, '.', '_' ); // See comment in self::getGPCVal()
415
		if ( isset( $this->data[$name] ) && !is_array( $this->data[$name] ) ) {
416
			$val = $this->data[$name];
417
		} else {
418
			$val = $default;
419
		}
420
		if ( is_null( $val ) ) {
421
			return $val;
422
		} else {
423
			return (string)$val;
424
		}
425
	}
426
427
	/**
428
	 * Fetch a scalar from the input or return $default if it's not set.
429
	 * Returns a string. Arrays are discarded. Useful for
430
	 * non-freeform text inputs (e.g. predefined internal text keys
431
	 * selected by a drop-down menu). For freeform input, see getText().
432
	 *
433
	 * @param string $name
434
	 * @param string $default Optional default (or null)
435
	 * @return string
436
	 */
437
	public function getVal( $name, $default = null ) {
438
		$val = $this->getGPCVal( $this->data, $name, $default );
439
		if ( is_array( $val ) ) {
440
			$val = $default;
441
		}
442
		if ( is_null( $val ) ) {
443
			return $val;
444
		} else {
445
			return (string)$val;
446
		}
447
	}
448
449
	/**
450
	 * Set an arbitrary value into our get/post data.
451
	 *
452
	 * @param string $key Key name to use
453
	 * @param mixed $value Value to set
454
	 * @return mixed Old value if one was present, null otherwise
455
	 */
456
	public function setVal( $key, $value ) {
457
		$ret = isset( $this->data[$key] ) ? $this->data[$key] : null;
458
		$this->data[$key] = $value;
459
		return $ret;
460
	}
461
462
	/**
463
	 * Unset an arbitrary value from our get/post data.
464
	 *
465
	 * @param string $key Key name to use
466
	 * @return mixed Old value if one was present, null otherwise
467
	 */
468
	public function unsetVal( $key ) {
469 View Code Duplication
		if ( !isset( $this->data[$key] ) ) {
470
			$ret = null;
471
		} else {
472
			$ret = $this->data[$key];
473
			unset( $this->data[$key] );
474
		}
475
		return $ret;
476
	}
477
478
	/**
479
	 * Fetch an array from the input or return $default if it's not set.
480
	 * If source was scalar, will return an array with a single element.
481
	 * If no source and no default, returns null.
482
	 *
483
	 * @param string $name
484
	 * @param array $default Optional default (or null)
485
	 * @return array
486
	 */
487
	public function getArray( $name, $default = null ) {
488
		$val = $this->getGPCVal( $this->data, $name, $default );
489
		if ( is_null( $val ) ) {
490
			return null;
491
		} else {
492
			return (array)$val;
493
		}
494
	}
495
496
	/**
497
	 * Fetch an array of integers, or return $default if it's not set.
498
	 * If source was scalar, will return an array with a single element.
499
	 * If no source and no default, returns null.
500
	 * If an array is returned, contents are guaranteed to be integers.
501
	 *
502
	 * @param string $name
503
	 * @param array $default Option default (or null)
504
	 * @return array Array of ints
505
	 */
506
	public function getIntArray( $name, $default = null ) {
507
		$val = $this->getArray( $name, $default );
508
		if ( is_array( $val ) ) {
509
			$val = array_map( 'intval', $val );
510
		}
511
		return $val;
512
	}
513
514
	/**
515
	 * Fetch an integer value from the input or return $default if not set.
516
	 * Guaranteed to return an integer; non-numeric input will typically
517
	 * return 0.
518
	 *
519
	 * @param string $name
520
	 * @param int $default
521
	 * @return int
522
	 */
523
	public function getInt( $name, $default = 0 ) {
524
		return intval( $this->getRawVal( $name, $default ) );
525
	}
526
527
	/**
528
	 * Fetch an integer value from the input or return null if empty.
529
	 * Guaranteed to return an integer or null; non-numeric input will
530
	 * typically return null.
531
	 *
532
	 * @param string $name
533
	 * @return int|null
534
	 */
535
	public function getIntOrNull( $name ) {
536
		$val = $this->getRawVal( $name );
537
		return is_numeric( $val )
538
			? intval( $val )
539
			: null;
540
	}
541
542
	/**
543
	 * Fetch a floating point value from the input or return $default if not set.
544
	 * Guaranteed to return a float; non-numeric input will typically
545
	 * return 0.
546
	 *
547
	 * @since 1.23
548
	 * @param string $name
549
	 * @param float $default
550
	 * @return float
551
	 */
552
	public function getFloat( $name, $default = 0.0 ) {
553
		return floatval( $this->getRawVal( $name, $default ) );
554
	}
555
556
	/**
557
	 * Fetch a boolean value from the input or return $default if not set.
558
	 * Guaranteed to return true or false, with normal PHP semantics for
559
	 * boolean interpretation of strings.
560
	 *
561
	 * @param string $name
562
	 * @param bool $default
563
	 * @return bool
564
	 */
565
	public function getBool( $name, $default = false ) {
566
		return (bool)$this->getRawVal( $name, $default );
567
	}
568
569
	/**
570
	 * Fetch a boolean value from the input or return $default if not set.
571
	 * Unlike getBool, the string "false" will result in boolean false, which is
572
	 * useful when interpreting information sent from JavaScript.
573
	 *
574
	 * @param string $name
575
	 * @param bool $default
576
	 * @return bool
577
	 */
578
	public function getFuzzyBool( $name, $default = false ) {
579
		return $this->getBool( $name, $default )
580
			&& strcasecmp( $this->getRawVal( $name ), 'false' ) !== 0;
581
	}
582
583
	/**
584
	 * Return true if the named value is set in the input, whatever that
585
	 * value is (even "0"). Return false if the named value is not set.
586
	 * Example use is checking for the presence of check boxes in forms.
587
	 *
588
	 * @param string $name
589
	 * @return bool
590
	 */
591
	public function getCheck( $name ) {
592
		# Checkboxes and buttons are only present when clicked
593
		# Presence connotes truth, absence false
594
		return $this->getRawVal( $name, null ) !== null;
595
	}
596
597
	/**
598
	 * Fetch a text string from the given array or return $default if it's not
599
	 * set. Carriage returns are stripped from the text. This should generally
600
	 * be used for form "<textarea>" and "<input>" fields, and for
601
	 * user-supplied freeform text input.
602
	 *
603
	 * @param string $name
604
	 * @param string $default Optional
605
	 * @return string
606
	 */
607
	public function getText( $name, $default = '' ) {
608
		$val = $this->getVal( $name, $default );
609
		return str_replace( "\r\n", "\n", $val );
610
	}
611
612
	/**
613
	 * Extracts the given named values into an array.
614
	 * If no arguments are given, returns all input values.
615
	 * No transformation is performed on the values.
616
	 *
617
	 * @return array
618
	 */
619
	public function getValues() {
620
		$names = func_get_args();
621
		if ( count( $names ) == 0 ) {
622
			$names = array_keys( $this->data );
623
		}
624
625
		$retVal = [];
626
		foreach ( $names as $name ) {
627
			$value = $this->getGPCVal( $this->data, $name, null );
628
			if ( !is_null( $value ) ) {
629
				$retVal[$name] = $value;
630
			}
631
		}
632
		return $retVal;
633
	}
634
635
	/**
636
	 * Returns the names of all input values excluding those in $exclude.
637
	 *
638
	 * @param array $exclude
639
	 * @return array
640
	 */
641
	public function getValueNames( $exclude = [] ) {
642
		return array_diff( array_keys( $this->getValues() ), $exclude );
643
	}
644
645
	/**
646
	 * Get the values passed in the query string.
647
	 * No transformation is performed on the values.
648
	 *
649
	 * @codeCoverageIgnore
650
	 * @return array
651
	 */
652
	public function getQueryValues() {
653
		return $_GET;
654
	}
655
656
	/**
657
	 * Return the contents of the Query with no decoding. Use when you need to
658
	 * know exactly what was sent, e.g. for an OAuth signature over the elements.
659
	 *
660
	 * @codeCoverageIgnore
661
	 * @return string
662
	 */
663
	public function getRawQueryString() {
0 ignored issues
show
getRawQueryString 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...
664
		return $_SERVER['QUERY_STRING'];
665
	}
666
667
	/**
668
	 * Return the contents of the POST with no decoding. Use when you need to
669
	 * know exactly what was sent, e.g. for an OAuth signature over the elements.
670
	 *
671
	 * @return string
672
	 */
673
	public function getRawPostString() {
674
		if ( !$this->wasPosted() ) {
675
			return '';
676
		}
677
		return $this->getRawInput();
678
	}
679
680
	/**
681
	 * Return the raw request body, with no processing. Cached since some methods
682
	 * disallow reading the stream more than once. As stated in the php docs, this
683
	 * does not work with enctype="multipart/form-data".
684
	 *
685
	 * @return string
686
	 */
687
	public function getRawInput() {
688
		static $input = null;
689
		if ( $input === null ) {
690
			$input = file_get_contents( 'php://input' );
691
		}
692
		return $input;
693
	}
694
695
	/**
696
	 * Get the HTTP method used for this request.
697
	 *
698
	 * @return string
699
	 */
700
	public function getMethod() {
701
		return isset( $_SERVER['REQUEST_METHOD'] ) ? $_SERVER['REQUEST_METHOD'] : 'GET';
702
	}
703
704
	/**
705
	 * Returns true if the present request was reached by a POST operation,
706
	 * false otherwise (GET, HEAD, or command-line).
707
	 *
708
	 * Note that values retrieved by the object may come from the
709
	 * GET URL etc even on a POST request.
710
	 *
711
	 * @return bool
712
	 */
713
	public function wasPosted() {
714
		return $this->getMethod() == 'POST';
715
	}
716
717
	/**
718
	 * Return the session for this request
719
	 *
720
	 * This might unpersist an existing session if it was invalid.
721
	 *
722
	 * @since 1.27
723
	 * @note For performance, keep the session locally if you will be making
724
	 *  much use of it instead of calling this method repeatedly.
725
	 * @return Session
726
	 */
727
	public function getSession() {
728
		if ( $this->sessionId !== null ) {
729
			$session = SessionManager::singleton()->getSessionById( (string)$this->sessionId, true, $this );
730
			if ( $session ) {
731
				return $session;
732
			}
733
		}
734
735
		$session = SessionManager::singleton()->getSessionForRequest( $this );
736
		$this->sessionId = $session->getSessionId();
737
		return $session;
738
	}
739
740
	/**
741
	 * Set the session for this request
742
	 * @since 1.27
743
	 * @private For use by MediaWiki\Session classes only
744
	 * @param SessionId $sessionId
745
	 */
746
	public function setSessionId( SessionId $sessionId ) {
747
		$this->sessionId = $sessionId;
748
	}
749
750
	/**
751
	 * Get the session id for this request, if any
752
	 * @since 1.27
753
	 * @private For use by MediaWiki\Session classes only
754
	 * @return SessionId|null
755
	 */
756
	public function getSessionId() {
757
		return $this->sessionId;
758
	}
759
760
	/**
761
	 * Returns true if the request has a persistent session.
762
	 * This does not necessarily mean that the user is logged in!
763
	 *
764
	 * @deprecated since 1.27, use
765
	 *  \MediaWiki\Session\SessionManager::singleton()->getPersistedSessionId()
766
	 *  instead.
767
	 * @return bool
768
	 */
769
	public function checkSessionCookie() {
770
		global $wgInitialSessionId;
771
		wfDeprecated( __METHOD__, '1.27' );
772
		return $wgInitialSessionId !== null &&
773
			$this->getSession()->getId() === (string)$wgInitialSessionId;
774
	}
775
776
	/**
777
	 * Get a cookie from the $_COOKIE jar
778
	 *
779
	 * @param string $key The name of the cookie
780
	 * @param string $prefix A prefix to use for the cookie name, if not $wgCookiePrefix
781
	 * @param mixed $default What to return if the value isn't found
782
	 * @return mixed Cookie value or $default if the cookie not set
783
	 */
784
	public function getCookie( $key, $prefix = null, $default = null ) {
785
		if ( $prefix === null ) {
786
			global $wgCookiePrefix;
787
			$prefix = $wgCookiePrefix;
788
		}
789
		return $this->getGPCVal( $_COOKIE, $prefix . $key, $default );
790
	}
791
792
	/**
793
	 * Return the path and query string portion of the main request URI.
794
	 * This will be suitable for use as a relative link in HTML output.
795
	 *
796
	 * @throws MWException
797
	 * @return string
798
	 */
799
	public static function getGlobalRequestURL() {
800
		if ( isset( $_SERVER['REQUEST_URI'] ) && strlen( $_SERVER['REQUEST_URI'] ) ) {
801
			$base = $_SERVER['REQUEST_URI'];
802
		} elseif ( isset( $_SERVER['HTTP_X_ORIGINAL_URL'] )
803
			&& strlen( $_SERVER['HTTP_X_ORIGINAL_URL'] )
804
		) {
805
			// Probably IIS; doesn't set REQUEST_URI
806
			$base = $_SERVER['HTTP_X_ORIGINAL_URL'];
807
		} elseif ( isset( $_SERVER['SCRIPT_NAME'] ) ) {
808
			$base = $_SERVER['SCRIPT_NAME'];
809
			if ( isset( $_SERVER['QUERY_STRING'] ) && $_SERVER['QUERY_STRING'] != '' ) {
810
				$base .= '?' . $_SERVER['QUERY_STRING'];
811
			}
812
		} else {
813
			// This shouldn't happen!
814
			throw new MWException( "Web server doesn't provide either " .
815
				"REQUEST_URI, HTTP_X_ORIGINAL_URL or SCRIPT_NAME. Report details " .
816
				"of your web server configuration to https://phabricator.wikimedia.org/" );
817
		}
818
		// User-agents should not send a fragment with the URI, but
819
		// if they do, and the web server passes it on to us, we
820
		// need to strip it or we get false-positive redirect loops
821
		// or weird output URLs
822
		$hash = strpos( $base, '#' );
823
		if ( $hash !== false ) {
824
			$base = substr( $base, 0, $hash );
825
		}
826
827
		if ( $base[0] == '/' ) {
828
			// More than one slash will look like it is protocol relative
829
			return preg_replace( '!^/+!', '/', $base );
830
		} else {
831
			// We may get paths with a host prepended; strip it.
832
			return preg_replace( '!^[^:]+://[^/]+/+!', '/', $base );
833
		}
834
	}
835
836
	/**
837
	 * Return the path and query string portion of the request URI.
838
	 * This will be suitable for use as a relative link in HTML output.
839
	 *
840
	 * @throws MWException
841
	 * @return string
842
	 */
843
	public function getRequestURL() {
844
		return self::getGlobalRequestURL();
845
	}
846
847
	/**
848
	 * Return the request URI with the canonical service and hostname, path,
849
	 * and query string. This will be suitable for use as an absolute link
850
	 * in HTML or other output.
851
	 *
852
	 * If $wgServer is protocol-relative, this will return a fully
853
	 * qualified URL with the protocol that was used for this request.
854
	 *
855
	 * @return string
856
	 */
857
	public function getFullRequestURL() {
858
		return wfExpandUrl( $this->getRequestURL(), PROTO_CURRENT );
859
	}
860
861
	/**
862
	 * @param string $key
863
	 * @param string $value
864
	 * @return string
865
	 */
866
	public function appendQueryValue( $key, $value ) {
867
		return $this->appendQueryArray( [ $key => $value ] );
868
	}
869
870
	/**
871
	 * Appends or replaces value of query variables.
872
	 *
873
	 * @param array $array Array of values to replace/add to query
874
	 * @return string
875
	 */
876
	public function appendQueryArray( $array ) {
877
		$newquery = $this->getQueryValues();
878
		unset( $newquery['title'] );
879
		$newquery = array_merge( $newquery, $array );
880
881
		return wfArrayToCgi( $newquery );
882
	}
883
884
	/**
885
	 * Check for limit and offset parameters on the input, and return sensible
886
	 * defaults if not given. The limit must be positive and is capped at 5000.
887
	 * Offset must be positive but is not capped.
888
	 *
889
	 * @param int $deflimit Limit to use if no input and the user hasn't set the option.
890
	 * @param string $optionname To specify an option other than rclimit to pull from.
891
	 * @return int[] First element is limit, second is offset
892
	 */
893
	public function getLimitOffset( $deflimit = 50, $optionname = 'rclimit' ) {
894
		global $wgUser;
895
896
		$limit = $this->getInt( 'limit', 0 );
897
		if ( $limit < 0 ) {
898
			$limit = 0;
899
		}
900
		if ( ( $limit == 0 ) && ( $optionname != '' ) ) {
901
			$limit = $wgUser->getIntOption( $optionname );
902
		}
903
		if ( $limit <= 0 ) {
904
			$limit = $deflimit;
905
		}
906
		if ( $limit > 5000 ) {
907
			$limit = 5000; # We have *some* limits...
908
		}
909
910
		$offset = $this->getInt( 'offset', 0 );
911
		if ( $offset < 0 ) {
912
			$offset = 0;
913
		}
914
915
		return [ $limit, $offset ];
916
	}
917
918
	/**
919
	 * Return the path to the temporary file where PHP has stored the upload.
920
	 *
921
	 * @param string $key
922
	 * @return string|null String or null if no such file.
923
	 */
924
	public function getFileTempname( $key ) {
925
		$file = new WebRequestUpload( $this, $key );
926
		return $file->getTempName();
927
	}
928
929
	/**
930
	 * Return the upload error or 0
931
	 *
932
	 * @param string $key
933
	 * @return int
934
	 */
935
	public function getUploadError( $key ) {
936
		$file = new WebRequestUpload( $this, $key );
937
		return $file->getError();
938
	}
939
940
	/**
941
	 * Return the original filename of the uploaded file, as reported by
942
	 * the submitting user agent. HTML-style character entities are
943
	 * interpreted and normalized to Unicode normalization form C, in part
944
	 * to deal with weird input from Safari with non-ASCII filenames.
945
	 *
946
	 * Other than this the name is not verified for being a safe filename.
947
	 *
948
	 * @param string $key
949
	 * @return string|null String or null if no such file.
950
	 */
951
	public function getFileName( $key ) {
952
		$file = new WebRequestUpload( $this, $key );
953
		return $file->getName();
954
	}
955
956
	/**
957
	 * Return a WebRequestUpload object corresponding to the key
958
	 *
959
	 * @param string $key
960
	 * @return WebRequestUpload
961
	 */
962
	public function getUpload( $key ) {
963
		return new WebRequestUpload( $this, $key );
964
	}
965
966
	/**
967
	 * Return a handle to WebResponse style object, for setting cookies,
968
	 * headers and other stuff, for Request being worked on.
969
	 *
970
	 * @return WebResponse
971
	 */
972
	public function response() {
973
		/* Lazy initialization of response object for this request */
974
		if ( !is_object( $this->response ) ) {
975
			$class = ( $this instanceof FauxRequest ) ? 'FauxResponse' : 'WebResponse';
976
			$this->response = new $class();
977
		}
978
		return $this->response;
979
	}
980
981
	/**
982
	 * Initialise the header list
983
	 */
984
	protected function initHeaders() {
985
		if ( count( $this->headers ) ) {
986
			return;
987
		}
988
989
		$apacheHeaders = function_exists( 'apache_request_headers' ) ? apache_request_headers() : false;
990
		if ( $apacheHeaders ) {
991
			foreach ( $apacheHeaders as $tempName => $tempValue ) {
992
				$this->headers[strtoupper( $tempName )] = $tempValue;
993
			}
994
		} else {
995
			foreach ( $_SERVER as $name => $value ) {
996
				if ( substr( $name, 0, 5 ) === 'HTTP_' ) {
997
					$name = str_replace( '_', '-', substr( $name, 5 ) );
998
					$this->headers[$name] = $value;
999
				} elseif ( $name === 'CONTENT_LENGTH' ) {
1000
					$this->headers['CONTENT-LENGTH'] = $value;
1001
				}
1002
			}
1003
		}
1004
	}
1005
1006
	/**
1007
	 * Get an array containing all request headers
1008
	 *
1009
	 * @return array Mapping header name to its value
1010
	 */
1011
	public function getAllHeaders() {
1012
		$this->initHeaders();
1013
		return $this->headers;
1014
	}
1015
1016
	/**
1017
	 * Get a request header, or false if it isn't set.
1018
	 *
1019
	 * @param string $name Case-insensitive header name
1020
	 * @param int $flags Bitwise combination of:
1021
	 *   WebRequest::GETHEADER_LIST  Treat the header as a comma-separated list
1022
	 *                               of values, as described in RFC 2616 § 4.2.
1023
	 *                               (since 1.26).
1024
	 * @return string|array|bool False if header is unset; otherwise the
1025
	 *  header value(s) as either a string (the default) or an array, if
1026
	 *  WebRequest::GETHEADER_LIST flag was set.
1027
	 */
1028
	public function getHeader( $name, $flags = 0 ) {
1029
		$this->initHeaders();
1030
		$name = strtoupper( $name );
1031
		if ( !isset( $this->headers[$name] ) ) {
1032
			return false;
1033
		}
1034
		$value = $this->headers[$name];
1035
		if ( $flags & self::GETHEADER_LIST ) {
1036
			$value = array_map( 'trim', explode( ',', $value ) );
1037
		}
1038
		return $value;
1039
	}
1040
1041
	/**
1042
	 * Get data from the session
1043
	 *
1044
	 * @note Prefer $this->getSession() instead if making multiple calls.
1045
	 * @param string $key Name of key in the session
1046
	 * @return mixed
1047
	 */
1048
	public function getSessionData( $key ) {
1049
		return $this->getSession()->get( $key );
1050
	}
1051
1052
	/**
1053
	 * Set session data
1054
	 *
1055
	 * @note Prefer $this->getSession() instead if making multiple calls.
1056
	 * @param string $key Name of key in the session
1057
	 * @param mixed $data
1058
	 */
1059
	public function setSessionData( $key, $data ) {
1060
		$this->getSession()->set( $key, $data );
1061
	}
1062
1063
	/**
1064
	 * Check if Internet Explorer will detect an incorrect cache extension in
1065
	 * PATH_INFO or QUERY_STRING. If the request can't be allowed, show an error
1066
	 * message or redirect to a safer URL. Returns true if the URL is OK, and
1067
	 * false if an error message has been shown and the request should be aborted.
1068
	 *
1069
	 * @param array $extWhitelist
1070
	 * @throws HttpError
1071
	 * @return bool
1072
	 */
1073
	public function checkUrlExtension( $extWhitelist = [] ) {
1074
		$extWhitelist[] = 'php';
1075
		if ( IEUrlExtension::areServerVarsBad( $_SERVER, $extWhitelist ) ) {
1076
			if ( !$this->wasPosted() ) {
1077
				$newUrl = IEUrlExtension::fixUrlForIE6(
1078
					$this->getFullRequestURL(), $extWhitelist );
1079
				if ( $newUrl !== false ) {
1080
					$this->doSecurityRedirect( $newUrl );
1081
					return false;
1082
				}
1083
			}
1084
			throw new HttpError( 403,
1085
				'Invalid file extension found in the path info or query string.' );
1086
		}
1087
		return true;
1088
	}
1089
1090
	/**
1091
	 * Attempt to redirect to a URL with a QUERY_STRING that's not dangerous in
1092
	 * IE 6. Returns true if it was successful, false otherwise.
1093
	 *
1094
	 * @param string $url
1095
	 * @return bool
1096
	 */
1097
	protected function doSecurityRedirect( $url ) {
1098
		header( 'Location: ' . $url );
1099
		header( 'Content-Type: text/html' );
1100
		$encUrl = htmlspecialchars( $url );
1101
		echo <<<HTML
1102
<html>
1103
<head>
1104
<title>Security redirect</title>
1105
</head>
1106
<body>
1107
<h1>Security redirect</h1>
1108
<p>
1109
We can't serve non-HTML content from the URL you have requested, because
1110
Internet Explorer would interpret it as an incorrect and potentially dangerous
1111
content type.</p>
1112
<p>Instead, please use <a href="$encUrl">this URL</a>, which is the same as the
1113
URL you have requested, except that "&amp;*" is appended. This prevents Internet
1114
Explorer from seeing a bogus file extension.
1115
</p>
1116
</body>
1117
</html>
1118
HTML;
1119
		echo "\n";
1120
		return true;
1121
	}
1122
1123
	/**
1124
	 * Parse the Accept-Language header sent by the client into an array
1125
	 *
1126
	 * @return array Array( languageCode => q-value ) sorted by q-value in
1127
	 *   descending order then appearing time in the header in ascending order.
1128
	 * May contain the "language" '*', which applies to languages other than those explicitly listed.
1129
	 * This is aligned with rfc2616 section 14.4
1130
	 * Preference for earlier languages appears in rfc3282 as an extension to HTTP/1.1.
1131
	 */
1132
	public function getAcceptLang() {
1133
		// Modified version of code found at
1134
		// http://www.thefutureoftheweb.com/blog/use-accept-language-header
1135
		$acceptLang = $this->getHeader( 'Accept-Language' );
1136
		if ( !$acceptLang ) {
1137
			return [];
1138
		}
1139
1140
		// Return the language codes in lower case
1141
		$acceptLang = strtolower( $acceptLang );
1142
1143
		// Break up string into pieces (languages and q factors)
1144
		$lang_parse = null;
1145
		preg_match_all(
1146
			'/([a-z]{1,8}(-[a-z]{1,8})*|\*)\s*(;\s*q\s*=\s*(1(\.0{0,3})?|0(\.[0-9]{0,3})?)?)?/',
1147
			$acceptLang,
1148
			$lang_parse
1149
		);
1150
1151
		if ( !count( $lang_parse[1] ) ) {
1152
			return [];
1153
		}
1154
1155
		$langcodes = $lang_parse[1];
1156
		$qvalues = $lang_parse[4];
1157
		$indices = range( 0, count( $lang_parse[1] ) - 1 );
1158
1159
		// Set default q factor to 1
1160
		foreach ( $indices as $index ) {
1161
			if ( $qvalues[$index] === '' ) {
1162
				$qvalues[$index] = 1;
1163
			} elseif ( $qvalues[$index] == 0 ) {
1164
				unset( $langcodes[$index], $qvalues[$index], $indices[$index] );
1165
			}
1166
		}
1167
1168
		// Sort list. First by $qvalues, then by order. Reorder $langcodes the same way
1169
		array_multisort( $qvalues, SORT_DESC, SORT_NUMERIC, $indices, $langcodes );
1170
1171
		// Create a list like "en" => 0.8
1172
		$langs = array_combine( $langcodes, $qvalues );
1173
1174
		return $langs;
1175
	}
1176
1177
	/**
1178
	 * Fetch the raw IP from the request
1179
	 *
1180
	 * @since 1.19
1181
	 *
1182
	 * @throws MWException
1183
	 * @return string
1184
	 */
1185
	protected function getRawIP() {
1186
		if ( !isset( $_SERVER['REMOTE_ADDR'] ) ) {
1187
			return null;
1188
		}
1189
1190
		if ( is_array( $_SERVER['REMOTE_ADDR'] ) || strpos( $_SERVER['REMOTE_ADDR'], ',' ) !== false ) {
1191
			throw new MWException( __METHOD__
1192
				. " : Could not determine the remote IP address due to multiple values." );
1193
		} else {
1194
			$ipchain = $_SERVER['REMOTE_ADDR'];
1195
		}
1196
1197
		return IP::canonicalize( $ipchain );
1198
	}
1199
1200
	/**
1201
	 * Work out the IP address based on various globals
1202
	 * For trusted proxies, use the XFF client IP (first of the chain)
1203
	 *
1204
	 * @since 1.19
1205
	 *
1206
	 * @throws MWException
1207
	 * @return string
1208
	 */
1209
	public function getIP() {
1210
		global $wgUsePrivateIPs;
1211
1212
		# Return cached result
1213
		if ( $this->ip !== null ) {
1214
			return $this->ip;
1215
		}
1216
1217
		# collect the originating ips
1218
		$ip = $this->getRawIP();
1219
		if ( !$ip ) {
1220
			throw new MWException( 'Unable to determine IP.' );
1221
		}
1222
1223
		# Append XFF
1224
		$forwardedFor = $this->getHeader( 'X-Forwarded-For' );
1225
		if ( $forwardedFor !== false ) {
1226
			$proxyLookup = MediaWikiServices::getInstance()->getProxyLookup();
1227
			$isConfigured = $proxyLookup->isConfiguredProxy( $ip );
1228
			$ipchain = array_map( 'trim', explode( ',', $forwardedFor ) );
1229
			$ipchain = array_reverse( $ipchain );
1230
			array_unshift( $ipchain, $ip );
1231
1232
			# Step through XFF list and find the last address in the list which is a
1233
			# trusted server. Set $ip to the IP address given by that trusted server,
1234
			# unless the address is not sensible (e.g. private). However, prefer private
1235
			# IP addresses over proxy servers controlled by this site (more sensible).
1236
			# Note that some XFF values might be "unknown" with Squid/Varnish.
1237
			foreach ( $ipchain as $i => $curIP ) {
1238
				$curIP = IP::sanitizeIP( IP::canonicalize( $curIP ) );
1239
				if ( !$curIP || !isset( $ipchain[$i + 1] ) || $ipchain[$i + 1] === 'unknown'
1240
					|| !$proxyLookup->isTrustedProxy( $curIP )
1241
				) {
1242
					break; // IP is not valid/trusted or does not point to anything
1243
				}
1244
				if (
1245
					IP::isPublic( $ipchain[$i + 1] ) ||
1246
					$wgUsePrivateIPs ||
1247
					$proxyLookup->isConfiguredProxy( $curIP ) // bug 48919; treat IP as sane
1248
				) {
1249
					// Follow the next IP according to the proxy
1250
					$nextIP = IP::canonicalize( $ipchain[$i + 1] );
1251
					if ( !$nextIP && $isConfigured ) {
1252
						// We have not yet made it past CDN/proxy servers of this site,
1253
						// so either they are misconfigured or there is some IP spoofing.
1254
						throw new MWException( "Invalid IP given in XFF '$forwardedFor'." );
1255
					}
1256
					$ip = $nextIP;
1257
					// keep traversing the chain
1258
					continue;
1259
				}
1260
				break;
1261
			}
1262
		}
1263
1264
		# Allow extensions to improve our guess
1265
		Hooks::run( 'GetIP', [ &$ip ] );
1266
1267
		if ( !$ip ) {
1268
			throw new MWException( "Unable to determine IP." );
1269
		}
1270
1271
		wfDebug( "IP: $ip\n" );
1272
		$this->ip = $ip;
1273
		return $ip;
1274
	}
1275
1276
	/**
1277
	 * @param string $ip
1278
	 * @return void
1279
	 * @since 1.21
1280
	 */
1281
	public function setIP( $ip ) {
1282
		$this->ip = $ip;
1283
	}
1284
1285
	/**
1286
	 * Check if this request uses a "safe" HTTP method
1287
	 *
1288
	 * Safe methods are verbs (e.g. GET/HEAD/OPTIONS) used for obtaining content. Such requests
1289
	 * are not expected to mutate content, especially in ways attributable to the client. Verbs
1290
	 * like POST and PUT are typical of non-safe requests which often change content.
1291
	 *
1292
	 * @return bool
1293
	 * @see https://tools.ietf.org/html/rfc7231#section-4.2.1
1294
	 * @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html
1295
	 * @since 1.28
1296
	 */
1297
	public function hasSafeMethod() {
1298
		if ( !isset( $_SERVER['REQUEST_METHOD'] ) ) {
1299
			return false; // CLI mode
1300
		}
1301
1302
		return in_array( $_SERVER['REQUEST_METHOD'], [ 'GET', 'HEAD', 'OPTIONS', 'TRACE' ] );
1303
	}
1304
1305
	/**
1306
	 * Whether this request should be identified as being "safe"
1307
	 *
1308
	 * This means that the client is not requesting any state changes and that database writes
1309
	 * are not inherently required. Ideally, no visible updates would happen at all. If they
1310
	 * must, then they should not be publically attributed to the end user.
1311
	 *
1312
	 * In more detail:
1313
	 *   - Cache populations and refreshes MAY occur.
1314
	 *   - Private user session updates and private server logging MAY occur.
1315
	 *   - Updates to private viewing activity data MAY occur via DeferredUpdates.
1316
	 *   - Other updates SHOULD NOT occur (e.g. modifying content assets).
1317
	 *
1318
	 * @return bool
1319
	 * @see https://tools.ietf.org/html/rfc7231#section-4.2.1
1320
	 * @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html
1321
	 * @since 1.28
1322
	 */
1323
	public function isSafeRequest() {
1324
		if ( $this->markedAsSafe && $this->wasPosted() ) {
1325
			return true; // marked as a "safe" POST
1326
		}
1327
1328
		return $this->hasSafeMethod();
1329
	}
1330
1331
	/**
1332
	 * Mark this request as identified as being nullipotent even if it is a POST request
1333
	 *
1334
	 * POST requests are often used due to the need for a client payload, even if the request
1335
	 * is otherwise equivalent to a "safe method" request.
1336
	 *
1337
	 * @see https://tools.ietf.org/html/rfc7231#section-4.2.1
1338
	 * @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html
1339
	 * @since 1.28
1340
	 */
1341
	public function markAsSafeRequest() {
1342
		$this->markedAsSafe = true;
1343
	}
1344
}
1345