MediaWiki  master
WebRequest.php
Go to the documentation of this file.
1 <?php
35 use Wikimedia\IPUtils;
36 
37 // The point of this class is to be a wrapper around super globals
38 // phpcs:disable MediaWiki.Usage.SuperGlobalsUsage.SuperGlobals
39 
47 class WebRequest {
52  protected $data;
53 
60 
65  protected $queryParams;
66 
71  protected $headers = [];
72 
77  public const GETHEADER_LIST = 1;
78 
83  private static $reqId;
84 
89  private $response;
90 
95  private $ip;
96 
101  protected $requestTime;
102 
107  protected $protocol;
108 
117  protected $sessionId = null;
118 
120  protected $markedAsSafe = false;
121 
125  public function __construct() {
126  $this->requestTime = $_SERVER['REQUEST_TIME_FLOAT'];
127 
128  // POST overrides GET data
129  // We don't use $_REQUEST here to avoid interference from cookies...
130  $this->data = $_POST + $_GET;
131 
132  $this->queryAndPathParams = $this->queryParams = $_GET;
133  }
134 
155  protected static function getPathInfo( $want = 'all' ) {
156  // PATH_INFO is mangled due to https://bugs.php.net/bug.php?id=31892
157  // And also by Apache 2.x, double slashes are converted to single slashes.
158  // So we will use REQUEST_URI if possible.
159  if ( isset( $_SERVER['REQUEST_URI'] ) ) {
160  // Slurp out the path portion to examine...
161  $url = $_SERVER['REQUEST_URI'];
162  if ( !preg_match( '!^https?://!', $url ) ) {
163  $url = 'http://unused' . $url;
164  }
165  $a = parse_url( $url );
166  if ( !$a ) {
167  return [];
168  }
169  $path = $a['path'] ?? '';
170 
171  global $wgScript;
172  if ( $path == $wgScript && $want !== 'all' ) {
173  // Script inside a rewrite path?
174  // Abort to keep from breaking...
175  return [];
176  }
177 
178  $router = new PathRouter;
179 
180  // Raw PATH_INFO style
181  $router->add( "$wgScript/$1" );
182 
183  global $wgArticlePath;
184  if ( $wgArticlePath ) {
185  $router->validateRoute( $wgArticlePath, 'wgArticlePath' );
186  $router->add( $wgArticlePath );
187  }
188 
189  global $wgActionPaths;
190  $articlePaths = PathRouter::getActionPaths( $wgActionPaths, $wgArticlePath );
191  if ( $articlePaths ) {
192  $router->add( $articlePaths, [ 'action' => '$key' ] );
193  }
194 
195  global $wgVariantArticlePath;
196  if ( $wgVariantArticlePath ) {
197  $services = MediaWikiServices::getInstance();
198  $router->validateRoute( $wgVariantArticlePath, 'wgVariantArticlePath' );
199  $router->add( $wgVariantArticlePath,
200  [ 'variant' => '$2' ],
201  [ '$2' => $services->getLanguageConverterFactory()
202  ->getLanguageConverter( $services->getContentLanguage() )
203  ->getVariants() ]
204  );
205  }
206 
207  Hooks::runner()->onWebRequestPathInfoRouter( $router );
208 
209  $matches = $router->parse( $path );
210  } else {
211  global $wgUsePathInfo;
212  $matches = [];
213  if ( $wgUsePathInfo ) {
214  if ( !empty( $_SERVER['ORIG_PATH_INFO'] ) ) {
215  // Mangled PATH_INFO
216  // https://bugs.php.net/bug.php?id=31892
217  // Also reported when ini_get('cgi.fix_pathinfo')==false
218  $matches['title'] = substr( $_SERVER['ORIG_PATH_INFO'], 1 );
219  } elseif ( !empty( $_SERVER['PATH_INFO'] ) ) {
220  // Regular old PATH_INFO yay
221  $matches['title'] = substr( $_SERVER['PATH_INFO'], 1 );
222  }
223  }
224  }
225 
226  return $matches;
227  }
228 
240  public static function getRequestPathSuffix( $basePath ) {
241  $basePath = rtrim( $basePath, '/' ) . '/';
242  $requestUrl = self::getGlobalRequestURL();
243  $qpos = strpos( $requestUrl, '?' );
244  if ( $qpos !== false ) {
245  $requestPath = substr( $requestUrl, 0, $qpos );
246  } else {
247  $requestPath = $requestUrl;
248  }
249  if ( !str_starts_with( $requestPath, $basePath ) ) {
250  return false;
251  }
252  return rawurldecode( substr( $requestPath, strlen( $basePath ) ) );
253  }
254 
266  public static function detectServer( $assumeProxiesUseDefaultProtocolPorts = null ) {
267  $assumeProxiesUseDefaultProtocolPorts ??= $GLOBALS['wgAssumeProxiesUseDefaultProtocolPorts'];
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  $this->protocol ??= self::detectProtocol();
374  return $this->protocol;
375  }
376 
384  public function interpolateTitle() {
385  $matches = self::getPathInfo( 'title' );
386  foreach ( $matches as $key => $val ) {
387  $this->data[$key] = $this->queryAndPathParams[$key] = $val;
388  }
389  }
390 
401  public static function extractTitle( $path, $bases, $key = false ) {
402  foreach ( (array)$bases as $keyValue => $base ) {
403  // Find the part after $wgArticlePath
404  $base = str_replace( '$1', '', $base );
405  $baseLen = strlen( $base );
406  if ( substr( $path, 0, $baseLen ) == $base ) {
407  $raw = substr( $path, $baseLen );
408  if ( $raw !== '' ) {
409  $matches = [ 'title' => rawurldecode( $raw ) ];
410  if ( $key ) {
411  $matches[$key] = $keyValue;
412  }
413  return $matches;
414  }
415  }
416  }
417  return [];
418  }
419 
427  public function normalizeUnicode( $data ) {
428  if ( is_array( $data ) ) {
429  foreach ( $data as $key => $val ) {
430  $data[$key] = $this->normalizeUnicode( $val );
431  }
432  } else {
433  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
434  $data = $contLang->normalize( $data );
435  }
436  return $data;
437  }
438 
447  private function getGPCVal( $arr, $name, $default ) {
448  # PHP is so nice to not touch input data, except sometimes:
449  # https://www.php.net/variables.external#language.variables.external.dot-in-names
450  # Work around PHP *feature* to avoid *bugs* elsewhere.
451  $name = strtr( $name, '.', '_' );
452 
453  if ( !isset( $arr[$name] ) ) {
454  return $default;
455  }
456 
457  $data = $arr[$name];
458  # Optimisation: Skip UTF-8 normalization and legacy transcoding for simple ASCII strings.
459  $isAsciiStr = ( is_string( $data ) && preg_match( '/[^\x20-\x7E]/', $data ) === 0 );
460  if ( !$isAsciiStr ) {
461  if ( isset( $_GET[$name] ) && is_string( $data ) ) {
462  # Check for alternate/legacy character encoding.
463  $data = MediaWikiServices::getInstance()
464  ->getContentLanguage()
465  ->checkTitleEncoding( $data );
466  }
467  $data = $this->normalizeUnicode( $data );
468  }
469 
470  return $data;
471  }
472 
485  public function getRawVal( $name, $default = null ) {
486  $name = strtr( $name, '.', '_' ); // See comment in self::getGPCVal()
487  if ( isset( $this->data[$name] ) && !is_array( $this->data[$name] ) ) {
488  $val = $this->data[$name];
489  } else {
490  $val = $default;
491  }
492 
493  return $val === null ? null : (string)$val;
494  }
495 
512  public function getVal( $name, $default = null ) {
513  $val = $this->getGPCVal( $this->data, $name, $default );
514  if ( is_array( $val ) ) {
515  $val = $default;
516  }
517 
518  return $val === null ? null : (string)$val;
519  }
520 
537  public function getText( $name, $default = '' ) {
538  $val = $this->getVal( $name, $default );
539  return str_replace( "\r\n", "\n", $val );
540  }
541 
549  public function setVal( $key, $value ) {
550  $ret = $this->data[$key] ?? null;
551  $this->data[$key] = $value;
552  return $ret;
553  }
554 
561  public function unsetVal( $key ) {
562  if ( !isset( $this->data[$key] ) ) {
563  $ret = null;
564  } else {
565  $ret = $this->data[$key];
566  unset( $this->data[$key] );
567  }
568  return $ret;
569  }
570 
580  public function getArray( $name, $default = null ) {
581  $val = $this->getGPCVal( $this->data, $name, $default );
582  if ( $val === null ) {
583  return null;
584  } else {
585  return (array)$val;
586  }
587  }
588 
599  public function getIntArray( $name, $default = null ) {
600  $val = $this->getArray( $name, $default );
601  if ( is_array( $val ) ) {
602  $val = array_map( 'intval', $val );
603  }
604  return $val;
605  }
606 
616  public function getInt( $name, $default = 0 ) {
617  // @phan-suppress-next-line PhanTypeMismatchArgument getRawVal does not return null here
618  return intval( $this->getRawVal( $name, $default ) );
619  }
620 
629  public function getIntOrNull( $name ) {
630  $val = $this->getRawVal( $name );
631  return is_numeric( $val )
632  ? intval( $val )
633  : null;
634  }
635 
646  public function getFloat( $name, $default = 0.0 ) {
647  // @phan-suppress-next-line PhanTypeMismatchArgument getRawVal does not return null here
648  return floatval( $this->getRawVal( $name, $default ) );
649  }
650 
660  public function getBool( $name, $default = false ) {
661  // @phan-suppress-next-line PhanTypeMismatchArgument getRawVal does not return null here
662  return (bool)$this->getRawVal( $name, $default );
663  }
664 
674  public function getFuzzyBool( $name, $default = false ) {
675  return $this->getBool( $name, $default )
676  && strcasecmp( $this->getRawVal( $name ), 'false' ) !== 0;
677  }
678 
687  public function getCheck( $name ) {
688  # Checkboxes and buttons are only present when clicked
689  # Presence connotes truth, absence false
690  return $this->getRawVal( $name, null ) !== null;
691  }
692 
700  public function getValues( ...$names ) {
701  if ( $names === [] ) {
702  $names = array_keys( $this->data );
703  }
704 
705  $retVal = [];
706  foreach ( $names as $name ) {
707  $value = $this->getGPCVal( $this->data, $name, null );
708  if ( $value !== null ) {
709  $retVal[$name] = $value;
710  }
711  }
712  return $retVal;
713  }
714 
721  public function getValueNames( $exclude = [] ) {
722  return array_diff( array_keys( $this->getValues() ), $exclude );
723  }
724 
732  public function getQueryValues() {
734  }
735 
745  public function getQueryValuesOnly() {
746  return $this->queryParams;
747  }
748 
757  public function getPostValues() {
758  return $_POST;
759  }
760 
768  public function getRawQueryString() {
769  return $_SERVER['QUERY_STRING'];
770  }
771 
778  public function getRawPostString() {
779  if ( !$this->wasPosted() ) {
780  return '';
781  }
782  return $this->getRawInput();
783  }
784 
792  public function getRawInput() {
793  static $input = null;
794  $input ??= file_get_contents( 'php://input' );
795  return $input;
796  }
797 
803  public function getMethod() {
804  return $_SERVER['REQUEST_METHOD'] ?? 'GET';
805  }
806 
816  public function wasPosted() {
817  return $this->getMethod() == 'POST';
818  }
819 
830  public function getSession() {
831  if ( $this->sessionId !== null ) {
832  $session = SessionManager::singleton()->getSessionById( (string)$this->sessionId, true, $this );
833  if ( $session ) {
834  return $session;
835  }
836  }
837 
838  $session = SessionManager::singleton()->getSessionForRequest( $this );
839  $this->sessionId = $session->getSessionId();
840  return $session;
841  }
842 
849  public function setSessionId( SessionId $sessionId ) {
850  $this->sessionId = $sessionId;
851  }
852 
859  public function getSessionId() {
860  return $this->sessionId;
861  }
862 
871  public function getCookie( $key, $prefix = null, $default = null ) {
872  if ( $prefix === null ) {
873  global $wgCookiePrefix;
874  $prefix = $wgCookiePrefix;
875  }
876  $name = $prefix . $key;
877  // Work around mangling of $_COOKIE
878  $name = strtr( $name, '.', '_' );
879  if ( isset( $_COOKIE[$name] ) ) {
880  return $_COOKIE[$name];
881  } else {
882  return $default;
883  }
884  }
885 
894  public function getCrossSiteCookie( $key, $prefix = '', $default = null ) {
896  $name = $prefix . $key;
897  // Work around mangling of $_COOKIE
898  $name = strtr( $name, '.', '_' );
899  if ( isset( $_COOKIE[$name] ) ) {
900  return $_COOKIE[$name];
901  }
903  $legacyName = $prefix . "ss0-" . $key;
904  $legacyName = strtr( $legacyName, '.', '_' );
905  if ( isset( $_COOKIE[$legacyName] ) ) {
906  return $_COOKIE[$legacyName];
907  }
908  }
909  return $default;
910  }
911 
919  public static function getGlobalRequestURL() {
920  // This method is called on fatal errors; it should not depend on anything complex.
921 
922  if ( isset( $_SERVER['REQUEST_URI'] ) && strlen( $_SERVER['REQUEST_URI'] ) ) {
923  $base = $_SERVER['REQUEST_URI'];
924  } elseif ( isset( $_SERVER['HTTP_X_ORIGINAL_URL'] )
925  && strlen( $_SERVER['HTTP_X_ORIGINAL_URL'] )
926  ) {
927  // Probably IIS; doesn't set REQUEST_URI
928  $base = $_SERVER['HTTP_X_ORIGINAL_URL'];
929  } elseif ( isset( $_SERVER['SCRIPT_NAME'] ) ) {
930  $base = $_SERVER['SCRIPT_NAME'];
931  if ( isset( $_SERVER['QUERY_STRING'] ) && $_SERVER['QUERY_STRING'] != '' ) {
932  $base .= '?' . $_SERVER['QUERY_STRING'];
933  }
934  } else {
935  // This shouldn't happen!
936  throw new MWException( "Web server doesn't provide either " .
937  "REQUEST_URI, HTTP_X_ORIGINAL_URL or SCRIPT_NAME. Report details " .
938  "of your web server configuration to https://phabricator.wikimedia.org/" );
939  }
940  // User-agents should not send a fragment with the URI, but
941  // if they do, and the web server passes it on to us, we
942  // need to strip it or we get false-positive redirect loops
943  // or weird output URLs
944  $hash = strpos( $base, '#' );
945  if ( $hash !== false ) {
946  $base = substr( $base, 0, $hash );
947  }
948 
949  if ( $base[0] == '/' ) {
950  // More than one slash will look like it is protocol relative
951  return preg_replace( '!^/+!', '/', $base );
952  } else {
953  // We may get paths with a host prepended; strip it.
954  return preg_replace( '!^[^:]+://[^/]+/+!', '/', $base );
955  }
956  }
957 
965  public function getRequestURL() {
966  return self::getGlobalRequestURL();
967  }
968 
979  public function getFullRequestURL() {
980  // Pass an explicit PROTO constant instead of PROTO_CURRENT so that we
981  // do not rely on state from the global $wgRequest object (which it would,
982  // via wfGetServerUrl/wfExpandUrl/$wgRequest->protocol).
983  if ( $this->getProtocol() === 'http' ) {
984  return wfGetServerUrl( PROTO_HTTP ) . $this->getRequestURL();
985  } else {
986  return wfGetServerUrl( PROTO_HTTPS ) . $this->getRequestURL();
987  }
988  }
989 
995  public function appendQueryValue( $key, $value ) {
996  return $this->appendQueryArray( [ $key => $value ] );
997  }
998 
1005  public function appendQueryArray( $array ) {
1006  $newquery = $this->getQueryValues();
1007  unset( $newquery['title'] );
1008  $newquery = array_merge( $newquery, $array );
1009 
1010  return wfArrayToCgi( $newquery );
1011  }
1012 
1023  public function getLimitOffsetForUser( UserIdentity $user, $deflimit = 50, $optionname = 'rclimit' ) {
1024  $limit = $this->getInt( 'limit', 0 );
1025  if ( $limit < 0 ) {
1026  $limit = 0;
1027  }
1028  if ( ( $limit == 0 ) && ( $optionname != '' ) ) {
1029  $limit = MediaWikiServices::getInstance()
1030  ->getUserOptionsLookup()
1031  ->getIntOption( $user, $optionname );
1032  }
1033  if ( $limit <= 0 ) {
1034  $limit = $deflimit;
1035  }
1036  if ( $limit > 5000 ) {
1037  $limit = 5000; # We have *some* limits...
1038  }
1039 
1040  $offset = $this->getInt( 'offset', 0 );
1041  if ( $offset < 0 ) {
1042  $offset = 0;
1043  }
1044 
1045  return [ $limit, $offset ];
1046  }
1047 
1054  public function getFileTempname( $key ) {
1055  return $this->getUpload( $key )->getTempName();
1056  }
1057 
1064  public function getUploadError( $key ) {
1065  return $this->getUpload( $key )->getError();
1066  }
1067 
1079  public function getFileName( $key ) {
1080  return $this->getUpload( $key )->getName();
1081  }
1082 
1089  public function getUpload( $key ) {
1090  return new WebRequestUpload( $this, $key );
1091  }
1092 
1099  public function response() {
1100  /* Lazy initialization of response object for this request */
1101  if ( !is_object( $this->response ) ) {
1102  $class = ( $this instanceof FauxRequest ) ? FauxResponse::class : WebResponse::class;
1103  $this->response = new $class();
1104  }
1105  return $this->response;
1106  }
1107 
1111  protected function initHeaders() {
1112  if ( count( $this->headers ) ) {
1113  return;
1114  }
1115 
1116  $this->headers = array_change_key_case( getallheaders(), CASE_UPPER );
1117  }
1118 
1124  public function getAllHeaders() {
1125  $this->initHeaders();
1126  return $this->headers;
1127  }
1128 
1141  public function getHeader( $name, $flags = 0 ) {
1142  $this->initHeaders();
1143  $name = strtoupper( $name );
1144  if ( !isset( $this->headers[$name] ) ) {
1145  return false;
1146  }
1147  $value = $this->headers[$name];
1148  if ( $flags & self::GETHEADER_LIST ) {
1149  $value = array_map( 'trim', explode( ',', $value ) );
1150  }
1151  return $value;
1152  }
1153 
1161  public function getSessionData( $key ) {
1162  return $this->getSession()->get( $key );
1163  }
1164 
1170  public function setSessionData( $key, $data ) {
1171  $this->getSession()->set( $key, $data );
1172  }
1173 
1187  public function getAcceptLang() {
1188  // Modified version of code found at
1189  // http://www.thefutureoftheweb.com/blog/use-accept-language-header
1190  $acceptLang = $this->getHeader( 'Accept-Language' );
1191  if ( !$acceptLang ) {
1192  return [];
1193  }
1194 
1195  // Return the language codes in lower case
1196  $acceptLang = strtolower( $acceptLang );
1197 
1198  // Break up string into pieces (languages and q factors)
1199  if ( !preg_match_all(
1200  '/
1201  # a language code or a star is required
1202  ([a-z]{1,8}(?:-[a-z]{1,8})*|\*)
1203  # from here everything is optional
1204  \s*
1205  (?:
1206  # this accepts only numbers in the range ;q=0.000 to ;q=1.000
1207  ;\s*q\s*=\s*
1208  (1(?:\.0{0,3})?|0(?:\.\d{0,3})?)?
1209  )?
1210  /x',
1211  $acceptLang,
1212  $matches,
1213  PREG_SET_ORDER
1214  ) ) {
1215  return [];
1216  }
1217 
1218  // Create a list like "en" => 0.8
1219  $langs = [];
1220  foreach ( $matches as $match ) {
1221  $languageCode = $match[1];
1222  // When not present, the default value is 1
1223  $qValue = (float)( $match[2] ?? 1.0 );
1224  if ( $qValue ) {
1225  $langs[$languageCode] = $qValue;
1226  }
1227  }
1228 
1229  // Sort list by qValue
1230  arsort( $langs, SORT_NUMERIC );
1231  return $langs;
1232  }
1233 
1240  protected function getRawIP() {
1241  $remoteAddr = $_SERVER['REMOTE_ADDR'] ?? null;
1242  if ( !$remoteAddr ) {
1243  return null;
1244  }
1245  if ( is_array( $remoteAddr ) || str_contains( $remoteAddr, ',' ) ) {
1246  throw new MWException( 'Remote IP must not contain multiple values' );
1247  }
1248 
1249  return IPUtils::canonicalize( $remoteAddr );
1250  }
1251 
1259  public function getIP() {
1260  global $wgUsePrivateIPs;
1261 
1262  # Return cached result
1263  if ( $this->ip !== null ) {
1264  return $this->ip;
1265  }
1266 
1267  # collect the originating IPs
1268  $ip = $this->getRawIP();
1269  if ( !$ip ) {
1270  throw new MWException( 'Unable to determine IP.' );
1271  }
1272 
1273  # Append XFF
1274  $forwardedFor = $this->getHeader( 'X-Forwarded-For' );
1275  if ( $forwardedFor !== false ) {
1276  $proxyLookup = MediaWikiServices::getInstance()->getProxyLookup();
1277  $isConfigured = $proxyLookup->isConfiguredProxy( $ip );
1278  $ipchain = array_map( 'trim', explode( ',', $forwardedFor ) );
1279  $ipchain = array_reverse( $ipchain );
1280  array_unshift( $ipchain, $ip );
1281 
1282  # Step through XFF list and find the last address in the list which is a
1283  # trusted server. Set $ip to the IP address given by that trusted server,
1284  # unless the address is not sensible (e.g. private). However, prefer private
1285  # IP addresses over proxy servers controlled by this site (more sensible).
1286  # Note that some XFF values might be "unknown" with Squid/Varnish.
1287  foreach ( $ipchain as $i => $curIP ) {
1288  $curIP = IPUtils::sanitizeIP(
1289  IPUtils::canonicalize(
1290  self::canonicalizeIPv6LoopbackAddress( $curIP )
1291  )
1292  );
1293  if ( !$curIP || !isset( $ipchain[$i + 1] ) || $ipchain[$i + 1] === 'unknown'
1294  || !$proxyLookup->isTrustedProxy( $curIP )
1295  ) {
1296  break; // IP is not valid/trusted or does not point to anything
1297  }
1298  if (
1299  IPUtils::isPublic( $ipchain[$i + 1] ) ||
1300  $wgUsePrivateIPs ||
1301  // T50919; treat IP as valid
1302  $proxyLookup->isConfiguredProxy( $curIP )
1303  ) {
1304  $nextIP = $ipchain[$i + 1];
1305 
1306  // Follow the next IP according to the proxy
1307  $nextIP = IPUtils::canonicalize(
1308  self::canonicalizeIPv6LoopbackAddress( $nextIP )
1309  );
1310  if ( !$nextIP && $isConfigured ) {
1311  // We have not yet made it past CDN/proxy servers of this site,
1312  // so either they are misconfigured or there is some IP spoofing.
1313  throw new MWException( "Invalid IP given in XFF '$forwardedFor'." );
1314  }
1315  $ip = $nextIP;
1316 
1317  // keep traversing the chain
1318  continue;
1319  }
1320  break;
1321  }
1322  }
1323 
1324  // Allow extensions to modify the result
1325  // Optimisation: Hot code called on most requests (T85805).
1326  if ( Hooks::isRegistered( 'GetIP' ) ) {
1327  // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
1328  Hooks::runner()->onGetIP( $ip );
1329  }
1330 
1331  if ( !$ip ) {
1332  throw new MWException( 'Unable to determine IP.' );
1333  }
1334 
1335  $this->ip = $ip;
1336  return $ip;
1337  }
1338 
1347  public static function canonicalizeIPv6LoopbackAddress( $ip ) {
1348  // Code moved from IPUtils library. See T248237#6614927
1349  $m = [];
1350  if ( preg_match( '/^0*' . IPUtils::RE_IPV6_GAP . '1$/', $ip, $m ) ) {
1351  return '127.0.0.1';
1352  }
1353  return $ip;
1354  }
1355 
1361  public function setIP( $ip ) {
1362  $this->ip = $ip;
1363  }
1364 
1377  public function hasSafeMethod() {
1378  if ( !isset( $_SERVER['REQUEST_METHOD'] ) ) {
1379  return false; // CLI mode
1380  }
1381 
1382  return in_array( $_SERVER['REQUEST_METHOD'], [ 'GET', 'HEAD', 'OPTIONS', 'TRACE' ] );
1383  }
1384 
1403  public function isSafeRequest() {
1404  if ( $this->markedAsSafe && $this->wasPosted() ) {
1405  return true; // marked as a "safe" POST
1406  }
1407 
1408  return $this->hasSafeMethod();
1409  }
1410 
1421  public function markAsSafeRequest() {
1422  $this->markedAsSafe = true;
1423  }
1424 
1436  public function matchURLForCDN( array $cdnUrls ) {
1437  $reqUrl = wfExpandUrl( $this->getRequestURL(), PROTO_INTERNAL );
1438  $config = MediaWikiServices::getInstance()->getMainConfig();
1439  if ( $config->get( MainConfigNames::CdnMatchParameterOrder ) ) {
1440  // Strict matching
1441  return in_array( $reqUrl, $cdnUrls, true );
1442  }
1443 
1444  // Loose matching (order of query parameters is ignored)
1445  $reqUrlParts = explode( '?', $reqUrl, 2 );
1446  $reqUrlBase = $reqUrlParts[0];
1447  $reqUrlParams = count( $reqUrlParts ) === 2 ? explode( '&', $reqUrlParts[1] ) : [];
1448  // The order of parameters after the sort() call below does not match
1449  // the order set by the CDN, and does not need to. The CDN needs to
1450  // take special care to preserve the relative order of duplicate keys
1451  // and array-like parameters.
1452  sort( $reqUrlParams );
1453  foreach ( $cdnUrls as $cdnUrl ) {
1454  if ( strlen( $reqUrl ) !== strlen( $cdnUrl ) ) {
1455  continue;
1456  }
1457  $cdnUrlParts = explode( '?', $cdnUrl, 2 );
1458  $cdnUrlBase = $cdnUrlParts[0];
1459  if ( $reqUrlBase !== $cdnUrlBase ) {
1460  continue;
1461  }
1462  $cdnUrlParams = count( $cdnUrlParts ) === 2 ? explode( '&', $cdnUrlParts[1] ) : [];
1463  sort( $cdnUrlParams );
1464  if ( $reqUrlParams === $cdnUrlParams ) {
1465  return true;
1466  }
1467  }
1468  return false;
1469  }
1470 }
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....
$matches
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:30
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
WebRequest clone which takes values from a provided array.
Definition: FauxRequest.php:42
MediaWiki\Request\PathRouter class.
Definition: PathRouter.php:78
add( $path, $params=[], $options=[])
Add a new path pattern to the path router.
Definition: PathRouter.php:163
Object to access the $_FILES array.
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
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form stripping il...
Definition: WebRequest.php:47
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.
Definition: WebRequest.php:629
string[] $queryParams
The parameters from $_GET only.
Definition: WebRequest.php:65
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:721
bool $markedAsSafe
Whether this HTTP request is "safe" (even if it is an HTTP post)
Definition: WebRequest.php:120
getUpload( $key)
Return a MediaWiki\Request\WebRequestUpload object corresponding to the key.
string $protocol
Cached URL protocol.
Definition: WebRequest.php:107
getArray( $name, $default=null)
Fetch an array from the input or return $default if it's not set.
Definition: WebRequest.php:580
interpolateTitle()
Check for title, action, and/or variant data in the URL and interpolate it into the GET variables.
Definition: WebRequest.php:384
getPostValues()
Get the values passed via POST.
Definition: WebRequest.php:757
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:830
getRawInput()
Return the raw request body, with no processing.
Definition: WebRequest.php:792
getValues(... $names)
Extracts the (given) named values into an array.
Definition: WebRequest.php:700
getRawQueryString()
Return the contents of the Query with no decoding.
Definition: WebRequest.php:768
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:512
getFloat( $name, $default=0.0)
Fetch a floating point value from the input or return $default if not set.
Definition: WebRequest.php:646
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:674
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:803
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:660
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:240
static getGlobalRequestURL()
Return the path and query string portion of the main request URI.
Definition: WebRequest.php:919
setVal( $key, $value)
Set an arbitrary value into our get/post data.
Definition: WebRequest.php:549
getFullRequestURL()
Return the request URI with the canonical service and hostname, path, and query string.
Definition: WebRequest.php:979
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:101
string[] $headers
Lazy-initialized request headers indexed by upper-case header name.
Definition: WebRequest.php:71
getCrossSiteCookie( $key, $prefix='', $default=null)
Get a cookie set with SameSite=None possibly with a legacy fallback cookie.
Definition: WebRequest.php:894
getCheck( $name)
Return true if the named value is set in the input, whatever that value is (even "0").
Definition: WebRequest.php:687
appendQueryArray( $array)
Appends or replaces value of query variables.
getSessionId()
Get the session id for this request, if any.
Definition: WebRequest.php:859
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:778
getQueryValues()
Get the values passed in the query string and the path router parameters.
Definition: WebRequest.php:732
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:266
getInt( $name, $default=0)
Fetch an integer value from the input or return $default if not set.
Definition: WebRequest.php:616
wasPosted()
Returns true if the present request was reached by a POST operation, false otherwise (GET,...
Definition: WebRequest.php:816
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:77
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:485
getIntArray( $name, $default=null)
Fetch an array of integers, or return $default if it's not set.
Definition: WebRequest.php:599
appendQueryValue( $key, $value)
Definition: WebRequest.php:995
normalizeUnicode( $data)
Recursively normalizes UTF-8 strings in the given array.
Definition: WebRequest.php:427
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:561
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:155
SessionId null $sessionId
Session ID to use for this request.
Definition: WebRequest.php:117
getRawIP()
Fetch the raw IP from the request.
setSessionId(SessionId $sessionId)
Set the session for this request.
Definition: WebRequest.php:849
getCookie( $key, $prefix=null, $default=null)
Get a cookie from the $_COOKIE jar.
Definition: WebRequest.php:871
array $data
The parameters from $_GET, $_POST and the path router.
Definition: WebRequest.php:52
static extractTitle( $path, $bases, $key=false)
URL rewriting function; tries to extract page title and, optionally, one other fixed parameter value ...
Definition: WebRequest.php:401
getText( $name, $default='')
Fetch a text string and return it in normalized form.
Definition: WebRequest.php:537
getRequestURL()
Return the path and query string portion of the request URI.
Definition: WebRequest.php:965
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:59
getQueryValuesOnly()
Get the values passed in the query string only, not including the path router parameters.
Definition: WebRequest.php:745
$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.