MediaWiki  master
WebRequest.php
Go to the documentation of this file.
1 <?php
38 use Wikimedia\IPUtils;
39 
40 // The point of this class is to be a wrapper around super globals
41 // phpcs:disable MediaWiki.Usage.SuperGlobalsUsage.SuperGlobals
42 
50 class WebRequest {
55  protected $data;
56 
63 
68  protected $queryParams;
69 
74  protected $headers = [];
75 
80  public const GETHEADER_LIST = 1;
81 
86  private static $reqId;
87 
92  private $response;
93 
98  private $ip;
99 
104  protected $requestTime;
105 
110  protected $protocol;
111 
120  protected $sessionId = null;
121 
123  protected $markedAsSafe = false;
124 
128  public function __construct() {
129  $this->requestTime = $_SERVER['REQUEST_TIME_FLOAT'];
130 
131  // POST overrides GET data
132  // We don't use $_REQUEST here to avoid interference from cookies...
133  $this->data = $_POST + $_GET;
134 
135  $this->queryAndPathParams = $this->queryParams = $_GET;
136  }
137 
158  protected static function getPathInfo( $want = 'all' ) {
159  // PATH_INFO is mangled due to https://bugs.php.net/bug.php?id=31892
160  // And also by Apache 2.x, double slashes are converted to single slashes.
161  // So we will use REQUEST_URI if possible.
162  if ( isset( $_SERVER['REQUEST_URI'] ) ) {
163  // Slurp out the path portion to examine...
164  $url = $_SERVER['REQUEST_URI'];
165  if ( !preg_match( '!^https?://!', $url ) ) {
166  $url = 'http://unused' . $url;
167  }
168  $a = parse_url( $url );
169  if ( !$a ) {
170  return [];
171  }
172  $path = $a['path'] ?? '';
173 
174  global $wgScript;
175  if ( $path == $wgScript && $want !== 'all' ) {
176  // Script inside a rewrite path?
177  // Abort to keep from breaking...
178  return [];
179  }
180 
181  $router = new PathRouter;
182 
183  // Raw PATH_INFO style
184  $router->add( "$wgScript/$1" );
185 
186  global $wgArticlePath;
187  if ( $wgArticlePath ) {
188  $router->validateRoute( $wgArticlePath, 'wgArticlePath' );
189  $router->add( $wgArticlePath );
190  }
191 
192  global $wgActionPaths;
193  $articlePaths = PathRouter::getActionPaths( $wgActionPaths, $wgArticlePath );
194  if ( $articlePaths ) {
195  $router->add( $articlePaths, [ 'action' => '$key' ] );
196  }
197 
198  $services = MediaWikiServices::getInstance();
199  global $wgVariantArticlePath;
200  if ( $wgVariantArticlePath ) {
201  $router->validateRoute( $wgVariantArticlePath, 'wgVariantArticlePath' );
202  $router->add( $wgVariantArticlePath,
203  [ 'variant' => '$2' ],
204  [ '$2' => $services->getLanguageConverterFactory()
205  ->getLanguageConverter( $services->getContentLanguage() )
206  ->getVariants() ]
207  );
208  }
209 
210  ( new HookRunner( $services->getHookContainer() ) )->onWebRequestPathInfoRouter( $router );
211 
212  $matches = $router->parse( $path );
213  } else {
214  global $wgUsePathInfo;
215  $matches = [];
216  if ( $wgUsePathInfo ) {
217  if ( !empty( $_SERVER['ORIG_PATH_INFO'] ) ) {
218  // Mangled PATH_INFO
219  // https://bugs.php.net/bug.php?id=31892
220  // Also reported when ini_get('cgi.fix_pathinfo')==false
221  $matches['title'] = substr( $_SERVER['ORIG_PATH_INFO'], 1 );
222  } elseif ( !empty( $_SERVER['PATH_INFO'] ) ) {
223  // Regular old PATH_INFO yay
224  $matches['title'] = substr( $_SERVER['PATH_INFO'], 1 );
225  }
226  }
227  }
228 
229  return $matches;
230  }
231 
243  public static function getRequestPathSuffix( $basePath ) {
244  $basePath = rtrim( $basePath, '/' ) . '/';
245  $requestUrl = self::getGlobalRequestURL();
246  $qpos = strpos( $requestUrl, '?' );
247  if ( $qpos !== false ) {
248  $requestPath = substr( $requestUrl, 0, $qpos );
249  } else {
250  $requestPath = $requestUrl;
251  }
252  if ( !str_starts_with( $requestPath, $basePath ) ) {
253  return false;
254  }
255  return rawurldecode( substr( $requestPath, strlen( $basePath ) ) );
256  }
257 
269  public static function detectServer( $assumeProxiesUseDefaultProtocolPorts = null ) {
270  $assumeProxiesUseDefaultProtocolPorts ??= $GLOBALS['wgAssumeProxiesUseDefaultProtocolPorts'];
271 
272  $proto = self::detectProtocol();
273  $stdPort = $proto === 'https' ? 443 : 80;
274 
275  $varNames = [ 'HTTP_HOST', 'SERVER_NAME', 'HOSTNAME', 'SERVER_ADDR' ];
276  $host = 'localhost';
277  $port = $stdPort;
278  foreach ( $varNames as $varName ) {
279  if ( !isset( $_SERVER[$varName] ) ) {
280  continue;
281  }
282 
283  $parts = IPUtils::splitHostAndPort( $_SERVER[$varName] );
284  if ( !$parts ) {
285  // Invalid, do not use
286  continue;
287  }
288 
289  $host = $parts[0];
290  if ( $assumeProxiesUseDefaultProtocolPorts && isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) ) {
291  // T72021: Assume that upstream proxy is running on the default
292  // port based on the protocol. We have no reliable way to determine
293  // the actual port in use upstream.
294  $port = $stdPort;
295  } elseif ( $parts[1] === false ) {
296  if ( isset( $_SERVER['SERVER_PORT'] ) ) {
297  $port = $_SERVER['SERVER_PORT'];
298  } // else leave it as $stdPort
299  } else {
300  $port = $parts[1];
301  }
302  break;
303  }
304 
305  return $proto . '://' . IPUtils::combineHostAndPort( $host, $port, $stdPort );
306  }
307 
315  public static function detectProtocol() {
316  if ( ( !empty( $_SERVER['HTTPS'] ) && $_SERVER['HTTPS'] !== 'off' ) ||
317  ( isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) &&
318  $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https' ) ) {
319  return 'https';
320  } else {
321  return 'http';
322  }
323  }
324 
332  public function getElapsedTime() {
333  return microtime( true ) - $this->requestTime;
334  }
335 
345  public static function getRequestId() {
346  // This method is called from various error handlers and MUST be kept simple and stateless.
347  if ( !self::$reqId ) {
348  global $wgAllowExternalReqID;
349  if ( $wgAllowExternalReqID ) {
350  $id = $_SERVER['HTTP_X_REQUEST_ID'] ?? $_SERVER['UNIQUE_ID'] ?? wfRandomString( 24 );
351  } else {
352  $id = $_SERVER['UNIQUE_ID'] ?? wfRandomString( 24 );
353  }
354  self::$reqId = $id;
355  }
356 
357  return self::$reqId;
358  }
359 
367  public static function overrideRequestId( $id ) {
368  self::$reqId = $id;
369  }
370 
375  public function getProtocol() {
376  $this->protocol ??= self::detectProtocol();
377  return $this->protocol;
378  }
379 
387  public function interpolateTitle() {
388  $matches = self::getPathInfo( 'title' );
389  foreach ( $matches as $key => $val ) {
390  $this->data[$key] = $this->queryAndPathParams[$key] = $val;
391  }
392  }
393 
404  public static function extractTitle( $path, $bases, $key = false ) {
405  foreach ( (array)$bases as $keyValue => $base ) {
406  // Find the part after $wgArticlePath
407  $base = str_replace( '$1', '', $base );
408  $baseLen = strlen( $base );
409  if ( substr( $path, 0, $baseLen ) == $base ) {
410  $raw = substr( $path, $baseLen );
411  if ( $raw !== '' ) {
412  $matches = [ 'title' => rawurldecode( $raw ) ];
413  if ( $key ) {
414  $matches[$key] = $keyValue;
415  }
416  return $matches;
417  }
418  }
419  }
420  return [];
421  }
422 
430  public function normalizeUnicode( $data ) {
431  if ( is_array( $data ) ) {
432  foreach ( $data as $key => $val ) {
433  $data[$key] = $this->normalizeUnicode( $val );
434  }
435  } else {
436  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
437  $data = $contLang->normalize( $data );
438  }
439  return $data;
440  }
441 
450  private function getGPCVal( $arr, $name, $default ) {
451  # PHP is so nice to not touch input data, except sometimes:
452  # https://www.php.net/variables.external#language.variables.external.dot-in-names
453  # Work around PHP *feature* to avoid *bugs* elsewhere.
454  $name = strtr( $name, '.', '_' );
455 
456  if ( !isset( $arr[$name] ) ) {
457  return $default;
458  }
459 
460  $data = $arr[$name];
461  # Optimisation: Skip UTF-8 normalization and legacy transcoding for simple ASCII strings.
462  $isAsciiStr = ( is_string( $data ) && preg_match( '/[^\x20-\x7E]/', $data ) === 0 );
463  if ( !$isAsciiStr ) {
464  if ( isset( $_GET[$name] ) && is_string( $data ) ) {
465  # Check for alternate/legacy character encoding.
466  $data = MediaWikiServices::getInstance()
467  ->getContentLanguage()
468  ->checkTitleEncoding( $data );
469  }
470  $data = $this->normalizeUnicode( $data );
471  }
472 
473  return $data;
474  }
475 
488  public function getRawVal( $name, $default = null ) {
489  $name = strtr( $name, '.', '_' ); // See comment in self::getGPCVal()
490  if ( isset( $this->data[$name] ) && !is_array( $this->data[$name] ) ) {
491  $val = $this->data[$name];
492  } else {
493  $val = $default;
494  }
495 
496  return $val === null ? null : (string)$val;
497  }
498 
515  public function getVal( $name, $default = null ) {
516  $val = $this->getGPCVal( $this->data, $name, $default );
517  if ( is_array( $val ) ) {
518  $val = $default;
519  }
520 
521  return $val === null ? null : (string)$val;
522  }
523 
540  public function getText( $name, $default = '' ) {
541  $val = $this->getVal( $name, $default );
542  return str_replace( "\r\n", "\n", $val );
543  }
544 
552  public function setVal( $key, $value ) {
553  $ret = $this->data[$key] ?? null;
554  $this->data[$key] = $value;
555  return $ret;
556  }
557 
564  public function unsetVal( $key ) {
565  if ( !isset( $this->data[$key] ) ) {
566  $ret = null;
567  } else {
568  $ret = $this->data[$key];
569  unset( $this->data[$key] );
570  }
571  return $ret;
572  }
573 
583  public function getArray( $name, $default = null ) {
584  $val = $this->getGPCVal( $this->data, $name, $default );
585  if ( $val === null ) {
586  return null;
587  } else {
588  return (array)$val;
589  }
590  }
591 
602  public function getIntArray( $name, $default = null ) {
603  $val = $this->getArray( $name, $default );
604  if ( is_array( $val ) ) {
605  $val = array_map( 'intval', $val );
606  }
607  return $val;
608  }
609 
619  public function getInt( $name, $default = 0 ) {
620  // @phan-suppress-next-line PhanTypeMismatchArgument getRawVal does not return null here
621  return intval( $this->getRawVal( $name, $default ) );
622  }
623 
632  public function getIntOrNull( $name ) {
633  $val = $this->getRawVal( $name );
634  return is_numeric( $val )
635  ? intval( $val )
636  : null;
637  }
638 
649  public function getFloat( $name, $default = 0.0 ) {
650  // @phan-suppress-next-line PhanTypeMismatchArgument getRawVal does not return null here
651  return floatval( $this->getRawVal( $name, $default ) );
652  }
653 
663  public function getBool( $name, $default = false ) {
664  // @phan-suppress-next-line PhanTypeMismatchArgument getRawVal does not return null here
665  return (bool)$this->getRawVal( $name, $default );
666  }
667 
677  public function getFuzzyBool( $name, $default = false ) {
678  return $this->getBool( $name, $default )
679  && strcasecmp( $this->getRawVal( $name ), 'false' ) !== 0;
680  }
681 
690  public function getCheck( $name ) {
691  # Checkboxes and buttons are only present when clicked
692  # Presence connotes truth, absence false
693  return $this->getRawVal( $name, null ) !== null;
694  }
695 
703  public function getValues( ...$names ) {
704  if ( $names === [] ) {
705  $names = array_keys( $this->data );
706  }
707 
708  $retVal = [];
709  foreach ( $names as $name ) {
710  $value = $this->getGPCVal( $this->data, $name, null );
711  if ( $value !== null ) {
712  $retVal[$name] = $value;
713  }
714  }
715  return $retVal;
716  }
717 
724  public function getValueNames( $exclude = [] ) {
725  return array_diff( array_keys( $this->getValues() ), $exclude );
726  }
727 
735  public function getQueryValues() {
737  }
738 
748  public function getQueryValuesOnly() {
749  return $this->queryParams;
750  }
751 
760  public function getPostValues() {
761  return $_POST;
762  }
763 
771  public function getRawQueryString() {
772  return $_SERVER['QUERY_STRING'];
773  }
774 
781  public function getRawPostString() {
782  if ( !$this->wasPosted() ) {
783  return '';
784  }
785  return $this->getRawInput();
786  }
787 
795  public function getRawInput() {
796  static $input = null;
797  $input ??= file_get_contents( 'php://input' );
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  $services = MediaWikiServices::getInstance();
1277  # Append XFF
1278  $forwardedFor = $this->getHeader( 'X-Forwarded-For' );
1279  if ( $forwardedFor !== false ) {
1280  $proxyLookup = $services->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  $hookContainer = $services->getHookContainer();
1330  // Optimisation: Hot code called on most requests (T85805).
1331  if ( $hookContainer->isRegistered( 'GetIP' ) ) {
1332  // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
1333  ( new HookRunner( $hookContainer ) )->onGetIP( $ip );
1334  }
1335 
1336  if ( !$ip ) {
1337  throw new MWException( 'Unable to determine IP.' );
1338  }
1339 
1340  $this->ip = $ip;
1341  return $ip;
1342  }
1343 
1352  public static function canonicalizeIPv6LoopbackAddress( $ip ) {
1353  // Code moved from IPUtils library. See T248237#6614927
1354  if ( preg_match( '/^0*' . IPUtils::RE_IPV6_GAP . '1$/', $ip ) ) {
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 
1440  public function matchURLForCDN( array $cdnUrls ) {
1441  $reqUrl = wfExpandUrl( $this->getRequestURL(), PROTO_INTERNAL );
1442  $config = MediaWikiServices::getInstance()->getMainConfig();
1443  if ( $config->get( MainConfigNames::CdnMatchParameterOrder ) ) {
1444  // Strict matching
1445  return in_array( $reqUrl, $cdnUrls, true );
1446  }
1447 
1448  // Loose matching (order of query parameters is ignored)
1449  $reqUrlParts = explode( '?', $reqUrl, 2 );
1450  $reqUrlBase = $reqUrlParts[0];
1451  $reqUrlParams = count( $reqUrlParts ) === 2 ? explode( '&', $reqUrlParts[1] ) : [];
1452  // The order of parameters after the sort() call below does not match
1453  // the order set by the CDN, and does not need to. The CDN needs to
1454  // take special care to preserve the relative order of duplicate keys
1455  // and array-like parameters.
1456  sort( $reqUrlParams );
1457  foreach ( $cdnUrls as $cdnUrl ) {
1458  if ( strlen( $reqUrl ) !== strlen( $cdnUrl ) ) {
1459  continue;
1460  }
1461  $cdnUrlParts = explode( '?', $cdnUrl, 2 );
1462  $cdnUrlBase = $cdnUrlParts[0];
1463  if ( $reqUrlBase !== $cdnUrlBase ) {
1464  continue;
1465  }
1466  $cdnUrlParams = count( $cdnUrlParts ) === 2 ? explode( '&', $cdnUrlParts[1] ) : [];
1467  sort( $cdnUrlParams );
1468  if ( $reqUrlParams === $cdnUrlParams ) {
1469  return true;
1470  }
1471  }
1472  return false;
1473  }
1474 }
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 using $wgServer (or one of its alternatives).
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
MediaWiki exception.
Definition: MWException.php:32
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:565
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.
Allow programs to request this object from WebRequest::response() and handle all outputting (or lack ...
Definition: WebResponse.php:36
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:50
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:632
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:724
bool $markedAsSafe
Whether this HTTP request is "safe" (even if it is an HTTP post)
Definition: WebRequest.php:123
getUpload( $key)
Return a MediaWiki\Request\WebRequestUpload object corresponding to the key.
string $protocol
Cached URL protocol.
Definition: WebRequest.php:110
getArray( $name, $default=null)
Fetch an array from the input or return $default if it's not set.
Definition: WebRequest.php:583
interpolateTitle()
Check for title, action, and/or variant data in the URL and interpolate it into the GET variables.
Definition: WebRequest.php:387
getPostValues()
Get the values passed via POST.
Definition: WebRequest.php:760
static detectProtocol()
Detect the protocol from $_SERVER.
Definition: WebRequest.php:315
isSafeRequest()
Whether this request should be identified as being "safe".
getSession()
Return the session for this request.
Definition: WebRequest.php:833
getRawInput()
Return the raw request body, with no processing.
Definition: WebRequest.php:795
getValues(... $names)
Extracts the (given) named values into an array.
Definition: WebRequest.php:703
getRawQueryString()
Return the contents of the Query with no decoding.
Definition: WebRequest.php:771
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:515
getFloat( $name, $default=0.0)
Fetch a floating point value from the input or return $default if not set.
Definition: WebRequest.php:649
string string[][] $queryParams
The parameters from $_GET only.
Definition: WebRequest.php:68
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:677
static getRequestId()
Get the current request ID.
Definition: WebRequest.php:345
getProtocol()
Get the current URL protocol (http or https)
Definition: WebRequest.php:375
getMethod()
Get the HTTP method used for this request.
Definition: WebRequest.php:806
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:663
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:243
string string[][] $queryAndPathParams
The parameters from $_GET.
Definition: WebRequest.php:62
static getGlobalRequestURL()
Return the path and query string portion of the main request URI.
Definition: WebRequest.php:922
setVal( $key, $value)
Set an arbitrary value into our get/post data.
Definition: WebRequest.php:552
getFullRequestURL()
Return the request URI with the canonical service and hostname, path, and query string.
Definition: WebRequest.php:982
getElapsedTime()
Get the number of seconds to have elapsed since request start, in fractional seconds,...
Definition: WebRequest.php:332
float $requestTime
The timestamp of the start of the request, with microsecond precision.
Definition: WebRequest.php:104
string[] $headers
Lazy-initialized request headers indexed by upper-case header name.
Definition: WebRequest.php:74
getCrossSiteCookie( $key, $prefix='', $default=null)
Get a cookie set with SameSite=None possibly with a legacy fallback cookie.
Definition: WebRequest.php:897
getCheck( $name)
Return true if the named value is set in the input, whatever that value is (even "0").
Definition: WebRequest.php:690
appendQueryArray( $array)
Appends or replaces value of query variables.
getSessionId()
Get the session id for this request, if any.
Definition: WebRequest.php:862
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:781
getQueryValues()
Get the values passed in the query string and the path router parameters.
Definition: WebRequest.php:735
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:269
getInt( $name, $default=0)
Fetch an integer value from the input or return $default if not set.
Definition: WebRequest.php:619
wasPosted()
Returns true if the present request was reached by a POST operation, false otherwise (GET,...
Definition: WebRequest.php:819
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:80
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:488
getIntArray( $name, $default=null)
Fetch an array of integers, or return $default if it's not set.
Definition: WebRequest.php:602
appendQueryValue( $key, $value)
Definition: WebRequest.php:998
normalizeUnicode( $data)
Recursively normalizes UTF-8 strings in the given array.
Definition: WebRequest.php:430
static overrideRequestId( $id)
Override the unique request ID.
Definition: WebRequest.php:367
unsetVal( $key)
Unset an arbitrary value from our get/post data.
Definition: WebRequest.php:564
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:158
SessionId null $sessionId
Session ID to use for this request.
Definition: WebRequest.php:120
getRawIP()
Fetch the raw IP from the request.
setSessionId(SessionId $sessionId)
Set the session for this request.
Definition: WebRequest.php:852
getCookie( $key, $prefix=null, $default=null)
Get a cookie from the $_COOKIE jar.
Definition: WebRequest.php:874
array $data
The parameters from $_GET, $_POST and the path router.
Definition: WebRequest.php:55
static extractTitle( $path, $bases, $key=false)
URL rewriting function; tries to extract page title and, optionally, one other fixed parameter value ...
Definition: WebRequest.php:404
getText( $name, $default='')
Fetch a text string and return it in normalized form.
Definition: WebRequest.php:540
getRequestURL()
Return the path and query string portion of the request URI.
Definition: WebRequest.php:968
getHeader( $name, $flags=0)
Get a request header, or false if it isn't set.
getSessionData( $key)
Get data from the session.
getQueryValuesOnly()
Get the values passed in the query string only, not including the path router parameters.
Definition: WebRequest.php:748
$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.