MediaWiki REL1_39
WebRequest.php
Go to the documentation of this file.
1<?php
32use Wikimedia\IPUtils;
33
34// The point of this class is to be a wrapper around super globals
35// phpcs:disable MediaWiki.Usage.SuperGlobalsUsage.SuperGlobals
36
49 protected $data;
50
57
62 protected $queryParams;
63
68 protected $headers = [];
69
74 public const GETHEADER_LIST = 1;
75
80 private static $reqId;
81
86 private $response;
87
92 private $ip;
93
98 protected $requestTime;
99
104 protected $protocol;
105
114 protected $sessionId = null;
115
117 protected $markedAsSafe = false;
118
122 public function __construct() {
123 $this->requestTime = $_SERVER['REQUEST_TIME_FLOAT'];
124
125 // POST overrides GET data
126 // We don't use $_REQUEST here to avoid interference from cookies...
127 $this->data = $_POST + $_GET;
128
129 $this->queryAndPathParams = $this->queryParams = $_GET;
130 }
131
152 protected static function getPathInfo( $want = 'all' ) {
153 // PATH_INFO is mangled due to https://bugs.php.net/bug.php?id=31892
154 // And also by Apache 2.x, double slashes are converted to single slashes.
155 // So we will use REQUEST_URI if possible.
156 if ( isset( $_SERVER['REQUEST_URI'] ) ) {
157 // Slurp out the path portion to examine...
158 $url = $_SERVER['REQUEST_URI'];
159 if ( !preg_match( '!^https?://!', $url ) ) {
160 $url = 'http://unused' . $url;
161 }
162 $a = parse_url( $url );
163 if ( !$a ) {
164 return [];
165 }
166 $path = $a['path'] ?? '';
167
168 global $wgScript;
169 if ( $path == $wgScript && $want !== 'all' ) {
170 // Script inside a rewrite path?
171 // Abort to keep from breaking...
172 return [];
173 }
174
175 $router = new PathRouter;
176
177 // Raw PATH_INFO style
178 $router->add( "$wgScript/$1" );
179
180 global $wgArticlePath;
181 if ( $wgArticlePath ) {
182 $router->validateRoute( $wgArticlePath, 'wgArticlePath' );
183 $router->add( $wgArticlePath );
184 }
185
186 global $wgActionPaths;
187 $articlePaths = PathRouter::getActionPaths( $wgActionPaths, $wgArticlePath );
188 if ( $articlePaths ) {
189 $router->add( $articlePaths, [ 'action' => '$key' ] );
190 }
191
193 if ( $wgVariantArticlePath ) {
194 $services = MediaWikiServices::getInstance();
195 $router->validateRoute( $wgVariantArticlePath, 'wgVariantArticlePath' );
196 $router->add( $wgVariantArticlePath,
197 [ 'variant' => '$2' ],
198 [ '$2' => $services->getLanguageConverterFactory()
199 ->getLanguageConverter( $services->getContentLanguage() )
200 ->getVariants() ]
201 );
202 }
203
204 Hooks::runner()->onWebRequestPathInfoRouter( $router );
205
206 $matches = $router->parse( $path );
207 } else {
208 global $wgUsePathInfo;
209 $matches = [];
210 if ( $wgUsePathInfo ) {
211 if ( !empty( $_SERVER['ORIG_PATH_INFO'] ) ) {
212 // Mangled PATH_INFO
213 // https://bugs.php.net/bug.php?id=31892
214 // Also reported when ini_get('cgi.fix_pathinfo')==false
215 $matches['title'] = substr( $_SERVER['ORIG_PATH_INFO'], 1 );
216 } elseif ( !empty( $_SERVER['PATH_INFO'] ) ) {
217 // Regular old PATH_INFO yay
218 $matches['title'] = substr( $_SERVER['PATH_INFO'], 1 );
219 }
220 }
221 }
222
223 return $matches;
224 }
225
237 public static function getRequestPathSuffix( $basePath ) {
238 $basePath = rtrim( $basePath, '/' ) . '/';
239 $requestUrl = self::getGlobalRequestURL();
240 $qpos = strpos( $requestUrl, '?' );
241 if ( $qpos !== false ) {
242 $requestPath = substr( $requestUrl, 0, $qpos );
243 } else {
244 $requestPath = $requestUrl;
245 }
246 if ( !str_starts_with( $requestPath, $basePath ) ) {
247 return false;
248 }
249 return rawurldecode( substr( $requestPath, strlen( $basePath ) ) );
250 }
251
263 public static function detectServer( $assumeProxiesUseDefaultProtocolPorts = null ) {
264 if ( $assumeProxiesUseDefaultProtocolPorts === null ) {
265 $assumeProxiesUseDefaultProtocolPorts = $GLOBALS['wgAssumeProxiesUseDefaultProtocolPorts'];
266 }
267
268 $proto = self::detectProtocol();
269 $stdPort = $proto === 'https' ? 443 : 80;
270
271 $varNames = [ 'HTTP_HOST', 'SERVER_NAME', 'HOSTNAME', 'SERVER_ADDR' ];
272 $host = 'localhost';
273 $port = $stdPort;
274 foreach ( $varNames as $varName ) {
275 if ( !isset( $_SERVER[$varName] ) ) {
276 continue;
277 }
278
279 $parts = IPUtils::splitHostAndPort( $_SERVER[$varName] );
280 if ( !$parts ) {
281 // Invalid, do not use
282 continue;
283 }
284
285 $host = $parts[0];
286 if ( $assumeProxiesUseDefaultProtocolPorts && isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) ) {
287 // T72021: Assume that upstream proxy is running on the default
288 // port based on the protocol. We have no reliable way to determine
289 // the actual port in use upstream.
290 $port = $stdPort;
291 } elseif ( $parts[1] === false ) {
292 if ( isset( $_SERVER['SERVER_PORT'] ) ) {
293 $port = $_SERVER['SERVER_PORT'];
294 } // else leave it as $stdPort
295 } else {
296 $port = $parts[1];
297 }
298 break;
299 }
300
301 return $proto . '://' . IPUtils::combineHostAndPort( $host, $port, $stdPort );
302 }
303
311 public static function detectProtocol() {
312 if ( ( !empty( $_SERVER['HTTPS'] ) && $_SERVER['HTTPS'] !== 'off' ) ||
313 ( isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) &&
314 $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https' ) ) {
315 return 'https';
316 } else {
317 return 'http';
318 }
319 }
320
328 public function getElapsedTime() {
329 return microtime( true ) - $this->requestTime;
330 }
331
341 public static function getRequestId() {
342 // This method is called from various error handlers and MUST be kept simple and stateless.
343 if ( !self::$reqId ) {
345 if ( $wgAllowExternalReqID ) {
346 $id = $_SERVER['HTTP_X_REQUEST_ID'] ?? $_SERVER['UNIQUE_ID'] ?? wfRandomString( 24 );
347 } else {
348 $id = $_SERVER['UNIQUE_ID'] ?? wfRandomString( 24 );
349 }
350 self::$reqId = $id;
351 }
352
353 return self::$reqId;
354 }
355
363 public static function overrideRequestId( $id ) {
364 self::$reqId = $id;
365 }
366
371 public function getProtocol() {
372 if ( $this->protocol === null ) {
373 $this->protocol = self::detectProtocol();
374 }
375 return $this->protocol;
376 }
377
385 public function interpolateTitle() {
386 $matches = self::getPathInfo( 'title' );
387 foreach ( $matches as $key => $val ) {
388 $this->data[$key] = $this->queryAndPathParams[$key] = $val;
389 }
390 }
391
402 public static function extractTitle( $path, $bases, $key = false ) {
403 foreach ( (array)$bases as $keyValue => $base ) {
404 // Find the part after $wgArticlePath
405 $base = str_replace( '$1', '', $base );
406 $baseLen = strlen( $base );
407 if ( substr( $path, 0, $baseLen ) == $base ) {
408 $raw = substr( $path, $baseLen );
409 if ( $raw !== '' ) {
410 $matches = [ 'title' => rawurldecode( $raw ) ];
411 if ( $key ) {
412 $matches[$key] = $keyValue;
413 }
414 return $matches;
415 }
416 }
417 }
418 return [];
419 }
420
428 public function normalizeUnicode( $data ) {
429 if ( is_array( $data ) ) {
430 foreach ( $data as $key => $val ) {
431 $data[$key] = $this->normalizeUnicode( $val );
432 }
433 } else {
434 $contLang = MediaWikiServices::getInstance()->getContentLanguage();
435 $data = $contLang->normalize( $data );
436 }
437 return $data;
438 }
439
448 private function getGPCVal( $arr, $name, $default ) {
449 # PHP is so nice to not touch input data, except sometimes:
450 # https://www.php.net/variables.external#language.variables.external.dot-in-names
451 # Work around PHP *feature* to avoid *bugs* elsewhere.
452 $name = strtr( $name, '.', '_' );
453
454 if ( !isset( $arr[$name] ) ) {
455 return $default;
456 }
457
458 $data = $arr[$name];
459 # Optimisation: Skip UTF-8 normalization and legacy transcoding for simple ASCII strings.
460 $isAsciiStr = ( is_string( $data ) && preg_match( '/[^\x20-\x7E]/', $data ) === 0 );
461 if ( !$isAsciiStr ) {
462 if ( isset( $_GET[$name] ) && is_string( $data ) ) {
463 # Check for alternate/legacy character encoding.
464 $data = MediaWikiServices::getInstance()
465 ->getContentLanguage()
466 ->checkTitleEncoding( $data );
467 }
468 $data = $this->normalizeUnicode( $data );
469 }
470
471 return $data;
472 }
473
486 public function getRawVal( $name, $default = null ) {
487 $name = strtr( $name, '.', '_' ); // See comment in self::getGPCVal()
488 if ( isset( $this->data[$name] ) && !is_array( $this->data[$name] ) ) {
489 $val = $this->data[$name];
490 } else {
491 $val = $default;
492 }
493
494 return $val === null ? null : (string)$val;
495 }
496
513 public function getVal( $name, $default = null ) {
514 $val = $this->getGPCVal( $this->data, $name, $default );
515 if ( is_array( $val ) ) {
516 $val = $default;
517 }
518
519 return $val === null ? null : (string)$val;
520 }
521
538 public function getText( $name, $default = '' ) {
539 $val = $this->getVal( $name, $default );
540 return str_replace( "\r\n", "\n", $val );
541 }
542
550 public function setVal( $key, $value ) {
551 $ret = $this->data[$key] ?? null;
552 $this->data[$key] = $value;
553 return $ret;
554 }
555
562 public function unsetVal( $key ) {
563 if ( !isset( $this->data[$key] ) ) {
564 $ret = null;
565 } else {
566 $ret = $this->data[$key];
567 unset( $this->data[$key] );
568 }
569 return $ret;
570 }
571
581 public function getArray( $name, $default = null ) {
582 $val = $this->getGPCVal( $this->data, $name, $default );
583 if ( $val === null ) {
584 return null;
585 } else {
586 return (array)$val;
587 }
588 }
589
600 public function getIntArray( $name, $default = null ) {
601 $val = $this->getArray( $name, $default );
602 if ( is_array( $val ) ) {
603 $val = array_map( 'intval', $val );
604 }
605 return $val;
606 }
607
617 public function getInt( $name, $default = 0 ) {
618 // @phan-suppress-next-line PhanTypeMismatchArgument getRawVal does not return null here
619 return intval( $this->getRawVal( $name, $default ) );
620 }
621
630 public function getIntOrNull( $name ) {
631 $val = $this->getRawVal( $name );
632 return is_numeric( $val )
633 ? intval( $val )
634 : null;
635 }
636
647 public function getFloat( $name, $default = 0.0 ) {
648 // @phan-suppress-next-line PhanTypeMismatchArgument getRawVal does not return null here
649 return floatval( $this->getRawVal( $name, $default ) );
650 }
651
661 public function getBool( $name, $default = false ) {
662 // @phan-suppress-next-line PhanTypeMismatchArgument getRawVal does not return null here
663 return (bool)$this->getRawVal( $name, $default );
664 }
665
675 public function getFuzzyBool( $name, $default = false ) {
676 return $this->getBool( $name, $default )
677 && strcasecmp( $this->getRawVal( $name ), 'false' ) !== 0;
678 }
679
688 public function getCheck( $name ) {
689 # Checkboxes and buttons are only present when clicked
690 # Presence connotes truth, absence false
691 return $this->getRawVal( $name, null ) !== null;
692 }
693
701 public function getValues( ...$names ) {
702 if ( $names === [] ) {
703 $names = array_keys( $this->data );
704 }
705
706 $retVal = [];
707 foreach ( $names as $name ) {
708 $value = $this->getGPCVal( $this->data, $name, null );
709 if ( $value !== null ) {
710 $retVal[$name] = $value;
711 }
712 }
713 return $retVal;
714 }
715
722 public function getValueNames( $exclude = [] ) {
723 return array_diff( array_keys( $this->getValues() ), $exclude );
724 }
725
733 public function getQueryValues() {
734 return $this->queryAndPathParams;
735 }
736
746 public function getQueryValuesOnly() {
747 return $this->queryParams;
748 }
749
758 public function getPostValues() {
759 return $_POST;
760 }
761
769 public function getRawQueryString() {
770 return $_SERVER['QUERY_STRING'];
771 }
772
779 public function getRawPostString() {
780 if ( !$this->wasPosted() ) {
781 return '';
782 }
783 return $this->getRawInput();
784 }
785
793 public function getRawInput() {
794 static $input = null;
795 if ( $input === null ) {
796 $input = file_get_contents( 'php://input' );
797 }
798 return $input;
799 }
800
806 public function getMethod() {
807 return $_SERVER['REQUEST_METHOD'] ?? 'GET';
808 }
809
819 public function wasPosted() {
820 return $this->getMethod() == 'POST';
821 }
822
833 public function getSession() {
834 if ( $this->sessionId !== null ) {
835 $session = SessionManager::singleton()->getSessionById( (string)$this->sessionId, true, $this );
836 if ( $session ) {
837 return $session;
838 }
839 }
840
841 $session = SessionManager::singleton()->getSessionForRequest( $this );
842 $this->sessionId = $session->getSessionId();
843 return $session;
844 }
845
852 public function setSessionId( SessionId $sessionId ) {
853 $this->sessionId = $sessionId;
854 }
855
862 public function getSessionId() {
863 return $this->sessionId;
864 }
865
874 public function getCookie( $key, $prefix = null, $default = null ) {
875 if ( $prefix === null ) {
876 global $wgCookiePrefix;
877 $prefix = $wgCookiePrefix;
878 }
879 $name = $prefix . $key;
880 // Work around mangling of $_COOKIE
881 $name = strtr( $name, '.', '_' );
882 if ( isset( $_COOKIE[$name] ) ) {
883 return $_COOKIE[$name];
884 } else {
885 return $default;
886 }
887 }
888
897 public function getCrossSiteCookie( $key, $prefix = '', $default = null ) {
899 $name = $prefix . $key;
900 // Work around mangling of $_COOKIE
901 $name = strtr( $name, '.', '_' );
902 if ( isset( $_COOKIE[$name] ) ) {
903 return $_COOKIE[$name];
904 }
906 $legacyName = $prefix . "ss0-" . $key;
907 $legacyName = strtr( $legacyName, '.', '_' );
908 if ( isset( $_COOKIE[$legacyName] ) ) {
909 return $_COOKIE[$legacyName];
910 }
911 }
912 return $default;
913 }
914
922 public static function getGlobalRequestURL() {
923 // This method is called on fatal errors; it should not depend on anything complex.
924
925 if ( isset( $_SERVER['REQUEST_URI'] ) && strlen( $_SERVER['REQUEST_URI'] ) ) {
926 $base = $_SERVER['REQUEST_URI'];
927 } elseif ( isset( $_SERVER['HTTP_X_ORIGINAL_URL'] )
928 && strlen( $_SERVER['HTTP_X_ORIGINAL_URL'] )
929 ) {
930 // Probably IIS; doesn't set REQUEST_URI
931 $base = $_SERVER['HTTP_X_ORIGINAL_URL'];
932 } elseif ( isset( $_SERVER['SCRIPT_NAME'] ) ) {
933 $base = $_SERVER['SCRIPT_NAME'];
934 if ( isset( $_SERVER['QUERY_STRING'] ) && $_SERVER['QUERY_STRING'] != '' ) {
935 $base .= '?' . $_SERVER['QUERY_STRING'];
936 }
937 } else {
938 // This shouldn't happen!
939 throw new MWException( "Web server doesn't provide either " .
940 "REQUEST_URI, HTTP_X_ORIGINAL_URL or SCRIPT_NAME. Report details " .
941 "of your web server configuration to https://phabricator.wikimedia.org/" );
942 }
943 // User-agents should not send a fragment with the URI, but
944 // if they do, and the web server passes it on to us, we
945 // need to strip it or we get false-positive redirect loops
946 // or weird output URLs
947 $hash = strpos( $base, '#' );
948 if ( $hash !== false ) {
949 $base = substr( $base, 0, $hash );
950 }
951
952 if ( $base[0] == '/' ) {
953 // More than one slash will look like it is protocol relative
954 return preg_replace( '!^/+!', '/', $base );
955 } else {
956 // We may get paths with a host prepended; strip it.
957 return preg_replace( '!^[^:]+://[^/]+/+!', '/', $base );
958 }
959 }
960
968 public function getRequestURL() {
969 return self::getGlobalRequestURL();
970 }
971
982 public function getFullRequestURL() {
983 // Pass an explicit PROTO constant instead of PROTO_CURRENT so that we
984 // do not rely on state from the global $wgRequest object (which it would,
985 // via wfGetServerUrl/wfExpandUrl/$wgRequest->protocol).
986 if ( $this->getProtocol() === 'http' ) {
987 return wfGetServerUrl( PROTO_HTTP ) . $this->getRequestURL();
988 } else {
989 return wfGetServerUrl( PROTO_HTTPS ) . $this->getRequestURL();
990 }
991 }
992
998 public function appendQueryValue( $key, $value ) {
999 return $this->appendQueryArray( [ $key => $value ] );
1000 }
1001
1008 public function appendQueryArray( $array ) {
1009 $newquery = $this->getQueryValues();
1010 unset( $newquery['title'] );
1011 $newquery = array_merge( $newquery, $array );
1012
1013 return wfArrayToCgi( $newquery );
1014 }
1015
1026 public function getLimitOffsetForUser( UserIdentity $user, $deflimit = 50, $optionname = 'rclimit' ) {
1027 $limit = $this->getInt( 'limit', 0 );
1028 if ( $limit < 0 ) {
1029 $limit = 0;
1030 }
1031 if ( ( $limit == 0 ) && ( $optionname != '' ) ) {
1032 $limit = MediaWikiServices::getInstance()
1033 ->getUserOptionsLookup()
1034 ->getIntOption( $user, $optionname );
1035 }
1036 if ( $limit <= 0 ) {
1037 $limit = $deflimit;
1038 }
1039 if ( $limit > 5000 ) {
1040 $limit = 5000; # We have *some* limits...
1041 }
1042
1043 $offset = $this->getInt( 'offset', 0 );
1044 if ( $offset < 0 ) {
1045 $offset = 0;
1046 }
1047
1048 return [ $limit, $offset ];
1049 }
1050
1057 public function getFileTempname( $key ) {
1058 return $this->getUpload( $key )->getTempName();
1059 }
1060
1067 public function getUploadError( $key ) {
1068 return $this->getUpload( $key )->getError();
1069 }
1070
1082 public function getFileName( $key ) {
1083 return $this->getUpload( $key )->getName();
1084 }
1085
1092 public function getUpload( $key ) {
1093 return new WebRequestUpload( $this, $key );
1094 }
1095
1102 public function response() {
1103 /* Lazy initialization of response object for this request */
1104 if ( !is_object( $this->response ) ) {
1105 $class = ( $this instanceof FauxRequest ) ? FauxResponse::class : WebResponse::class;
1106 $this->response = new $class();
1107 }
1108 return $this->response;
1109 }
1110
1114 protected function initHeaders() {
1115 if ( count( $this->headers ) ) {
1116 return;
1117 }
1118
1119 $this->headers = array_change_key_case( getallheaders(), CASE_UPPER );
1120 }
1121
1127 public function getAllHeaders() {
1128 $this->initHeaders();
1129 return $this->headers;
1130 }
1131
1144 public function getHeader( $name, $flags = 0 ) {
1145 $this->initHeaders();
1146 $name = strtoupper( $name );
1147 if ( !isset( $this->headers[$name] ) ) {
1148 return false;
1149 }
1150 $value = $this->headers[$name];
1151 if ( $flags & self::GETHEADER_LIST ) {
1152 $value = array_map( 'trim', explode( ',', $value ) );
1153 }
1154 return $value;
1155 }
1156
1164 public function getSessionData( $key ) {
1165 return $this->getSession()->get( $key );
1166 }
1167
1173 public function setSessionData( $key, $data ) {
1174 $this->getSession()->set( $key, $data );
1175 }
1176
1190 public function getAcceptLang() {
1191 // Modified version of code found at
1192 // http://www.thefutureoftheweb.com/blog/use-accept-language-header
1193 $acceptLang = $this->getHeader( 'Accept-Language' );
1194 if ( !$acceptLang ) {
1195 return [];
1196 }
1197
1198 // Return the language codes in lower case
1199 $acceptLang = strtolower( $acceptLang );
1200
1201 // Break up string into pieces (languages and q factors)
1202 if ( !preg_match_all(
1203 '/
1204 # a language code or a star is required
1205 ([a-z]{1,8}(?:-[a-z]{1,8})*|\*)
1206 # from here everything is optional
1207 \s*
1208 (?:
1209 # this accepts only numbers in the range ;q=0.000 to ;q=1.000
1210 ;\s*q\s*=\s*
1211 (1(?:\.0{0,3})?|0(?:\.\d{0,3})?)?
1212 )?
1213 /x',
1214 $acceptLang,
1215 $matches,
1216 PREG_SET_ORDER
1217 ) ) {
1218 return [];
1219 }
1220
1221 // Create a list like "en" => 0.8
1222 $langs = [];
1223 foreach ( $matches as $match ) {
1224 $languageCode = $match[1];
1225 // When not present, the default value is 1
1226 $qValue = (float)( $match[2] ?? 1.0 );
1227 if ( $qValue ) {
1228 $langs[$languageCode] = $qValue;
1229 }
1230 }
1231
1232 // Sort list by qValue
1233 arsort( $langs, SORT_NUMERIC );
1234 return $langs;
1235 }
1236
1243 protected function getRawIP() {
1244 $remoteAddr = $_SERVER['REMOTE_ADDR'] ?? null;
1245 if ( !$remoteAddr ) {
1246 return null;
1247 }
1248 if ( is_array( $remoteAddr ) || str_contains( $remoteAddr, ',' ) ) {
1249 throw new MWException( 'Remote IP must not contain multiple values' );
1250 }
1251
1252 return IPUtils::canonicalize( $remoteAddr );
1253 }
1254
1262 public function getIP() {
1263 global $wgUsePrivateIPs;
1264
1265 # Return cached result
1266 if ( $this->ip !== null ) {
1267 return $this->ip;
1268 }
1269
1270 # collect the originating IPs
1271 $ip = $this->getRawIP();
1272 if ( !$ip ) {
1273 throw new MWException( 'Unable to determine IP.' );
1274 }
1275
1276 # Append XFF
1277 $forwardedFor = $this->getHeader( 'X-Forwarded-For' );
1278 if ( $forwardedFor !== false ) {
1279 $proxyLookup = MediaWikiServices::getInstance()->getProxyLookup();
1280 $isConfigured = $proxyLookup->isConfiguredProxy( $ip );
1281 $ipchain = array_map( 'trim', explode( ',', $forwardedFor ) );
1282 $ipchain = array_reverse( $ipchain );
1283 array_unshift( $ipchain, $ip );
1284
1285 # Step through XFF list and find the last address in the list which is a
1286 # trusted server. Set $ip to the IP address given by that trusted server,
1287 # unless the address is not sensible (e.g. private). However, prefer private
1288 # IP addresses over proxy servers controlled by this site (more sensible).
1289 # Note that some XFF values might be "unknown" with Squid/Varnish.
1290 foreach ( $ipchain as $i => $curIP ) {
1291 $curIP = IPUtils::sanitizeIP(
1292 IPUtils::canonicalize(
1293 self::canonicalizeIPv6LoopbackAddress( $curIP )
1294 )
1295 );
1296 if ( !$curIP || !isset( $ipchain[$i + 1] ) || $ipchain[$i + 1] === 'unknown'
1297 || !$proxyLookup->isTrustedProxy( $curIP )
1298 ) {
1299 break; // IP is not valid/trusted or does not point to anything
1300 }
1301 if (
1302 IPUtils::isPublic( $ipchain[$i + 1] ) ||
1304 // T50919; treat IP as valid
1305 $proxyLookup->isConfiguredProxy( $curIP )
1306 ) {
1307 $nextIP = $ipchain[$i + 1];
1308
1309 // Follow the next IP according to the proxy
1310 $nextIP = IPUtils::canonicalize(
1311 self::canonicalizeIPv6LoopbackAddress( $nextIP )
1312 );
1313 if ( !$nextIP && $isConfigured ) {
1314 // We have not yet made it past CDN/proxy servers of this site,
1315 // so either they are misconfigured or there is some IP spoofing.
1316 throw new MWException( "Invalid IP given in XFF '$forwardedFor'." );
1317 }
1318 $ip = $nextIP;
1319
1320 // keep traversing the chain
1321 continue;
1322 }
1323 break;
1324 }
1325 }
1326
1327 // Allow extensions to modify the result
1328 // Optimisation: Hot code called on most requests (T85805).
1329 if ( Hooks::isRegistered( 'GetIP' ) ) {
1330 // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
1331 Hooks::runner()->onGetIP( $ip );
1332 }
1333
1334 if ( !$ip ) {
1335 throw new MWException( 'Unable to determine IP.' );
1336 }
1337
1338 $this->ip = $ip;
1339 return $ip;
1340 }
1341
1350 public static function canonicalizeIPv6LoopbackAddress( $ip ) {
1351 // Code moved from IPUtils library. See T248237#6614927
1352 $m = [];
1353 if ( preg_match( '/^0*' . IPUtils::RE_IPV6_GAP . '1$/', $ip, $m ) ) {
1354 return '127.0.0.1';
1355 }
1356 return $ip;
1357 }
1358
1364 public function setIP( $ip ) {
1365 $this->ip = $ip;
1366 }
1367
1380 public function hasSafeMethod() {
1381 if ( !isset( $_SERVER['REQUEST_METHOD'] ) ) {
1382 return false; // CLI mode
1383 }
1384
1385 return in_array( $_SERVER['REQUEST_METHOD'], [ 'GET', 'HEAD', 'OPTIONS', 'TRACE' ] );
1386 }
1387
1406 public function isSafeRequest() {
1407 if ( $this->markedAsSafe && $this->wasPosted() ) {
1408 return true; // marked as a "safe" POST
1409 }
1410
1411 return $this->hasSafeMethod();
1412 }
1413
1424 public function markAsSafeRequest() {
1425 $this->markedAsSafe = true;
1426 }
1427
1439 public function matchURLForCDN( array $cdnUrls ) {
1440 $reqUrl = wfExpandUrl( $this->getRequestURL(), PROTO_INTERNAL );
1441 $config = MediaWikiServices::getInstance()->getMainConfig();
1442 if ( $config->get( MainConfigNames::CdnMatchParameterOrder ) ) {
1443 // Strict matching
1444 return in_array( $reqUrl, $cdnUrls, true );
1445 }
1446
1447 // Loose matching (order of query parameters is ignored)
1448 $reqUrlParts = explode( '?', $reqUrl, 2 );
1449 $reqUrlBase = $reqUrlParts[0];
1450 $reqUrlParams = count( $reqUrlParts ) === 2 ? explode( '&', $reqUrlParts[1] ) : [];
1451 // The order of parameters after the sort() call below does not match
1452 // the order set by the CDN, and does not need to. The CDN needs to
1453 // take special care to preserve the relative order of duplicate keys
1454 // and array-like parameters.
1455 sort( $reqUrlParams );
1456 foreach ( $cdnUrls as $cdnUrl ) {
1457 if ( strlen( $reqUrl ) !== strlen( $cdnUrl ) ) {
1458 continue;
1459 }
1460 $cdnUrlParts = explode( '?', $cdnUrl, 2 );
1461 $cdnUrlBase = $cdnUrlParts[0];
1462 if ( $reqUrlBase !== $cdnUrlBase ) {
1463 continue;
1464 }
1465 $cdnUrlParams = count( $cdnUrlParts ) === 2 ? explode( '&', $cdnUrlParts[1] ) : [];
1466 sort( $cdnUrlParams );
1467 if ( $reqUrlParams === $cdnUrlParams ) {
1468 return true;
1469 }
1470 }
1471 return false;
1472 }
1473}
const PROTO_HTTPS
Definition Defines.php:194
const PROTO_INTERNAL
Definition Defines.php:200
const PROTO_HTTP
Definition Defines.php:193
wfRandomString( $length=32)
Get a random string containing a number of pseudo-random hex characters.
wfExpandUrl( $url, $defaultProto=PROTO_CURRENT)
Expand a potentially local URL to a fully-qualified URL.
wfGetServerUrl( $proto)
Get the wiki's "server", i.e.
wfArrayToCgi( $array1, $array2=null, $prefix='')
This function takes one or two arrays as input, and returns a CGI-style string, e....
WebRequest clone which takes values from a provided array.
MediaWiki exception.
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
Value object holding the session ID in a manner that can be globally updated.
Definition SessionId.php:40
This serves as the entry point to the MediaWiki session handling system.
Manages data for an authenticated session.
Definition Session.php:50
PathRouter class.
add( $path, $params=[], $options=[])
Add a new path pattern to the path router.
Object to access the $_FILES array.
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form stripping il...
matchURLForCDN(array $cdnUrls)
Determine if the request URL matches one of a given set of canonical CDN URLs.
markAsSafeRequest()
Mark this request as identified as being nullipotent even if it is a POST request.
getIntOrNull( $name)
Fetch an integer value from the input or return null if empty.
string[] $queryParams
The parameters from $_GET only.
getLimitOffsetForUser(UserIdentity $user, $deflimit=50, $optionname='rclimit')
Check for limit and offset parameters on the input, and return sensible defaults if not given.
getValueNames( $exclude=[])
Returns the names of all input values excluding those in $exclude.
bool $markedAsSafe
Whether this HTTP request is "safe" (even if it is an HTTP post)
getUpload( $key)
Return a WebRequestUpload object corresponding to the key.
string $protocol
Cached URL protocol.
getArray( $name, $default=null)
Fetch an array from the input or return $default if it's not set.
interpolateTitle()
Check for title, action, and/or variant data in the URL and interpolate it into the GET variables.
getPostValues()
Get the values passed via POST.
static detectProtocol()
Detect the protocol from $_SERVER.
isSafeRequest()
Whether this request should be identified as being "safe".
getSession()
Return the session for this request.
getRawInput()
Return the raw request body, with no processing.
getValues(... $names)
Extracts the (given) named values into an array.
getRawQueryString()
Return the contents of the Query with no decoding.
getFileTempname( $key)
Return the path to the temporary file where PHP has stored the upload.
getVal( $name, $default=null)
Fetch a text string and partially normalized it.
getFloat( $name, $default=0.0)
Fetch a floating point value from the input or return $default if not set.
getUploadError( $key)
Return the upload error or 0.
getAllHeaders()
Get an array containing all request headers.
getFuzzyBool( $name, $default=false)
Fetch a boolean value from the input or return $default if not set.
static getRequestId()
Get the current request ID.
getProtocol()
Get the current URL protocol (http or https)
getMethod()
Get the HTTP method used for this request.
initHeaders()
Initialise the header list.
getBool( $name, $default=false)
Fetch a boolean value from the input or return $default if not set.
static getRequestPathSuffix( $basePath)
If the request URL matches a given base path, extract the path part of the request URL after that bas...
static getGlobalRequestURL()
Return the path and query string portion of the main request URI.
setVal( $key, $value)
Set an arbitrary value into our get/post data.
getFullRequestURL()
Return the request URI with the canonical service and hostname, path, and query string.
getElapsedTime()
Get the number of seconds to have elapsed since request start, in fractional seconds,...
float $requestTime
The timestamp of the start of the request, with microsecond precision.
string[] $headers
Lazy-initialized request headers indexed by upper-case header name.
getCrossSiteCookie( $key, $prefix='', $default=null)
Get a cookie set with SameSite=None possibly with a legacy fallback cookie.
getCheck( $name)
Return true if the named value is set in the input, whatever that value is (even "0").
appendQueryArray( $array)
Appends or replaces value of query variables.
getSessionId()
Get the session id for this request, if any.
getAcceptLang()
Parse the Accept-Language header sent by the client into an array.
static canonicalizeIPv6LoopbackAddress( $ip)
Converts ::1 (IPv6 loopback address) to 127.0.0.1 (IPv4 loopback address); assists in matching truste...
getRawPostString()
Return the contents of the POST with no decoding.
getQueryValues()
Get the values passed in the query string and the path router parameters.
response()
Return a handle to WebResponse style object, for setting cookies, headers and other stuff,...
getIP()
Work out the IP address based on various globals For trusted proxies, use the XFF client IP (first of...
static detectServer( $assumeProxiesUseDefaultProtocolPorts=null)
Work out an appropriate URL prefix containing scheme and host, based on information detected from $_S...
getInt( $name, $default=0)
Fetch an integer value from the input or return $default if not set.
wasPosted()
Returns true if the present request was reached by a POST operation, false otherwise (GET,...
setSessionData( $key, $data)
getFileName( $key)
Return the original filename of the uploaded file, as reported by the submitting user agent.
const GETHEADER_LIST
Flag to make WebRequest::getHeader return an array of values.
hasSafeMethod()
Check if this request uses a "safe" HTTP method.
getRawVal( $name, $default=null)
Fetch a string WITHOUT any Unicode or line break normalization.
getIntArray( $name, $default=null)
Fetch an array of integers, or return $default if it's not set.
appendQueryValue( $key, $value)
normalizeUnicode( $data)
Recursively normalizes UTF-8 strings in the given array.
static overrideRequestId( $id)
Override the unique request ID.
unsetVal( $key)
Unset an arbitrary value from our get/post data.
static getPathInfo( $want='all')
Extract relevant query arguments from the http request uri's path to be merged with the normal php pr...
SessionId null $sessionId
Session ID to use for this request.
getRawIP()
Fetch the raw IP from the request.
setSessionId(SessionId $sessionId)
Set the session for this request.
getCookie( $key, $prefix=null, $default=null)
Get a cookie from the $_COOKIE jar.
array $data
The parameters from $_GET, $_POST and the path router.
static extractTitle( $path, $bases, $key=false)
URL rewriting function; tries to extract page title and, optionally, one other fixed parameter value ...
getText( $name, $default='')
Fetch a text string and return it in normalized form.
getRequestURL()
Return the path and query string portion of the request URI.
getHeader( $name, $flags=0)
Get a request header, or false if it isn't set.
getSessionData( $key)
Get data from the session.
string[] $queryAndPathParams
The parameters from $_GET.
getQueryValuesOnly()
Get the values passed in the query string only, not including the path router parameters.
Allow programs to request this object from WebRequest::response() and handle all outputting (or lack ...
$wgUsePathInfo
Config variable stub for the UsePathInfo setting, for use by phpdoc and IDEs.
$wgUseSameSiteLegacyCookies
Config variable stub for the UseSameSiteLegacyCookies setting, for use by phpdoc and IDEs.
$wgScript
Config variable stub for the Script setting, for use by phpdoc and IDEs.
$wgActionPaths
Config variable stub for the ActionPaths setting, for use by phpdoc and IDEs.
$wgArticlePath
Config variable stub for the ArticlePath setting, for use by phpdoc and IDEs.
$wgAllowExternalReqID
Config variable stub for the AllowExternalReqID setting, for use by phpdoc and IDEs.
$wgVariantArticlePath
Config variable stub for the VariantArticlePath setting, for use by phpdoc and IDEs.
$wgCookiePrefix
Config variable stub for the CookiePrefix setting, for use by phpdoc and IDEs.
$wgUsePrivateIPs
Config variable stub for the UsePrivateIPs setting, for use by phpdoc and IDEs.
Interface for objects representing user identity.