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