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