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