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