MediaWiki  master
WebRequest.php
Go to the documentation of this file.
1 <?php
32 use Wikimedia\IPUtils;
33 
34 // The point of this class is to be a wrapper around super globals
35 // phpcs:disable MediaWiki.Usage.SuperGlobalsUsage.SuperGlobals
36 
44 class WebRequest {
49  protected $data;
50 
57 
62  protected $queryParams;
63 
68  protected $headers = [];
69 
74  public const GETHEADER_LIST = 1;
75 
80  private static $reqId;
81 
86  private $response;
87 
92  private $ip;
93 
98  protected $requestTime;
99 
104  protected $protocol;
105 
114  protected $sessionId = null;
115 
117  protected $markedAsSafe = false;
118 
122  public function __construct() {
123  $this->requestTime = $_SERVER['REQUEST_TIME_FLOAT'];
124 
125  // POST overrides GET data
126  // We don't use $_REQUEST here to avoid interference from cookies...
127  $this->data = $_POST + $_GET;
128 
129  $this->queryAndPathParams = $this->queryParams = $_GET;
130  }
131 
152  protected static function getPathInfo( $want = 'all' ) {
153  // PATH_INFO is mangled due to https://bugs.php.net/bug.php?id=31892
154  // And also by Apache 2.x, double slashes are converted to single slashes.
155  // So we will use REQUEST_URI if possible.
156  if ( isset( $_SERVER['REQUEST_URI'] ) ) {
157  // Slurp out the path portion to examine...
158  $url = $_SERVER['REQUEST_URI'];
159  if ( !preg_match( '!^https?://!', $url ) ) {
160  $url = 'http://unused' . $url;
161  }
162  $a = parse_url( $url );
163  if ( !$a ) {
164  return [];
165  }
166  $path = $a['path'] ?? '';
167 
168  global $wgScript;
169  // @phan-suppress-next-line PhanPossiblyUndeclaredVariable False positive
170  if ( $path == $wgScript && $want !== 'all' ) {
171  // Script inside a rewrite path?
172  // Abort to keep from breaking...
173  return [];
174  }
175 
176  $router = new PathRouter;
177 
178  // Raw PATH_INFO style
179  // @phan-suppress-next-line PhanPossiblyUndeclaredVariable False positive
180  $router->add( "$wgScript/$1" );
181 
182  global $wgArticlePath;
183  if ( $wgArticlePath ) {
184  $router->validateRoute( $wgArticlePath, 'wgArticlePath' );
185  $router->add( $wgArticlePath );
186  }
187 
188  global $wgActionPaths;
190  if ( $articlePaths ) {
191  $router->add( $articlePaths, [ 'action' => '$key' ] );
192  }
193 
194  global $wgVariantArticlePath;
195  if ( $wgVariantArticlePath ) {
196  $services = MediaWikiServices::getInstance();
197  $router->validateRoute( $wgVariantArticlePath, 'wgVariantArticlePath' );
198  $router->add( $wgVariantArticlePath,
199  [ 'variant' => '$2' ],
200  [ '$2' => $services->getLanguageConverterFactory()
201  ->getLanguageConverter( $services->getContentLanguage() )
202  ->getVariants() ]
203  );
204  }
205 
206  Hooks::runner()->onWebRequestPathInfoRouter( $router );
207 
208  $matches = $router->parse( $path );
209  } else {
210  global $wgUsePathInfo;
211  $matches = [];
212  if ( $wgUsePathInfo ) {
213  if ( !empty( $_SERVER['ORIG_PATH_INFO'] ) ) {
214  // Mangled PATH_INFO
215  // https://bugs.php.net/bug.php?id=31892
216  // Also reported when ini_get('cgi.fix_pathinfo')==false
217  $matches['title'] = substr( $_SERVER['ORIG_PATH_INFO'], 1 );
218  } elseif ( !empty( $_SERVER['PATH_INFO'] ) ) {
219  // Regular old PATH_INFO yay
220  $matches['title'] = substr( $_SERVER['PATH_INFO'], 1 );
221  }
222  }
223  }
224 
225  return $matches;
226  }
227 
239  public static function getRequestPathSuffix( $basePath ) {
240  $basePath = rtrim( $basePath, '/' ) . '/';
241  $requestUrl = self::getGlobalRequestURL();
242  $qpos = strpos( $requestUrl, '?' );
243  if ( $qpos !== false ) {
244  $requestPath = substr( $requestUrl, 0, $qpos );
245  } else {
246  $requestPath = $requestUrl;
247  }
248  if ( !str_starts_with( $requestPath, $basePath ) ) {
249  return false;
250  }
251  return rawurldecode( substr( $requestPath, strlen( $basePath ) ) );
252  }
253 
265  public static function detectServer( $assumeProxiesUseDefaultProtocolPorts = null ) {
266  if ( $assumeProxiesUseDefaultProtocolPorts === null ) {
267  $assumeProxiesUseDefaultProtocolPorts = $GLOBALS['wgAssumeProxiesUseDefaultProtocolPorts'];
268  }
269 
270  $proto = self::detectProtocol();
271  $stdPort = $proto === 'https' ? 443 : 80;
272 
273  $varNames = [ 'HTTP_HOST', 'SERVER_NAME', 'HOSTNAME', 'SERVER_ADDR' ];
274  $host = 'localhost';
275  $port = $stdPort;
276  foreach ( $varNames as $varName ) {
277  if ( !isset( $_SERVER[$varName] ) ) {
278  continue;
279  }
280 
281  $parts = IPUtils::splitHostAndPort( $_SERVER[$varName] );
282  if ( !$parts ) {
283  // Invalid, do not use
284  continue;
285  }
286 
287  $host = $parts[0];
288  if ( $assumeProxiesUseDefaultProtocolPorts && isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) ) {
289  // T72021: Assume that upstream proxy is running on the default
290  // port based on the protocol. We have no reliable way to determine
291  // the actual port in use upstream.
292  $port = $stdPort;
293  } elseif ( $parts[1] === false ) {
294  if ( isset( $_SERVER['SERVER_PORT'] ) ) {
295  $port = $_SERVER['SERVER_PORT'];
296  } // else leave it as $stdPort
297  } else {
298  $port = $parts[1];
299  }
300  break;
301  }
302 
303  return $proto . '://' . IPUtils::combineHostAndPort( $host, $port, $stdPort );
304  }
305 
313  public static function detectProtocol() {
314  if ( ( !empty( $_SERVER['HTTPS'] ) && $_SERVER['HTTPS'] !== 'off' ) ||
315  ( isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) &&
316  $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https' ) ) {
317  return 'https';
318  } else {
319  return 'http';
320  }
321  }
322 
330  public function getElapsedTime() {
331  return microtime( true ) - $this->requestTime;
332  }
333 
343  public static function getRequestId() {
344  // This method is called from various error handlers and MUST be kept simple and stateless.
345  if ( !self::$reqId ) {
346  global $wgAllowExternalReqID;
347  if ( $wgAllowExternalReqID ) {
348  $id = $_SERVER['HTTP_X_REQUEST_ID'] ?? $_SERVER['UNIQUE_ID'] ?? wfRandomString( 24 );
349  } else {
350  $id = $_SERVER['UNIQUE_ID'] ?? wfRandomString( 24 );
351  }
352  self::$reqId = $id;
353  }
354 
355  return self::$reqId;
356  }
357 
365  public static function overrideRequestId( $id ) {
366  self::$reqId = $id;
367  }
368 
373  public function getProtocol() {
374  if ( $this->protocol === null ) {
375  $this->protocol = self::detectProtocol();
376  }
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  if ( $input === null ) {
798  $input = file_get_contents( 'php://input' );
799  }
800  return $input;
801  }
802 
808  public function getMethod() {
809  return $_SERVER['REQUEST_METHOD'] ?? 'GET';
810  }
811 
821  public function wasPosted() {
822  return $this->getMethod() == 'POST';
823  }
824 
835  public function getSession() {
836  if ( $this->sessionId !== null ) {
837  $session = SessionManager::singleton()->getSessionById( (string)$this->sessionId, true, $this );
838  if ( $session ) {
839  return $session;
840  }
841  }
842 
843  $session = SessionManager::singleton()->getSessionForRequest( $this );
844  $this->sessionId = $session->getSessionId();
845  return $session;
846  }
847 
854  public function setSessionId( SessionId $sessionId ) {
855  $this->sessionId = $sessionId;
856  }
857 
864  public function getSessionId() {
865  return $this->sessionId;
866  }
867 
876  public function getCookie( $key, $prefix = null, $default = null ) {
877  if ( $prefix === null ) {
878  global $wgCookiePrefix;
879  $prefix = $wgCookiePrefix;
880  }
881  $name = $prefix . $key;
882  // Work around mangling of $_COOKIE
883  $name = strtr( $name, '.', '_' );
884  if ( isset( $_COOKIE[$name] ) ) {
885  return $_COOKIE[$name];
886  } else {
887  return $default;
888  }
889  }
890 
899  public function getCrossSiteCookie( $key, $prefix = '', $default = null ) {
901  $name = $prefix . $key;
902  // Work around mangling of $_COOKIE
903  $name = strtr( $name, '.', '_' );
904  if ( isset( $_COOKIE[$name] ) ) {
905  return $_COOKIE[$name];
906  }
908  $legacyName = $prefix . "ss0-" . $key;
909  $legacyName = strtr( $legacyName, '.', '_' );
910  if ( isset( $_COOKIE[$legacyName] ) ) {
911  return $_COOKIE[$legacyName];
912  }
913  }
914  return $default;
915  }
916 
924  public static function getGlobalRequestURL() {
925  // This method is called on fatal errors; it should not depend on anything complex.
926 
927  if ( isset( $_SERVER['REQUEST_URI'] ) && strlen( $_SERVER['REQUEST_URI'] ) ) {
928  $base = $_SERVER['REQUEST_URI'];
929  } elseif ( isset( $_SERVER['HTTP_X_ORIGINAL_URL'] )
930  && strlen( $_SERVER['HTTP_X_ORIGINAL_URL'] )
931  ) {
932  // Probably IIS; doesn't set REQUEST_URI
933  $base = $_SERVER['HTTP_X_ORIGINAL_URL'];
934  } elseif ( isset( $_SERVER['SCRIPT_NAME'] ) ) {
935  $base = $_SERVER['SCRIPT_NAME'];
936  if ( isset( $_SERVER['QUERY_STRING'] ) && $_SERVER['QUERY_STRING'] != '' ) {
937  $base .= '?' . $_SERVER['QUERY_STRING'];
938  }
939  } else {
940  // This shouldn't happen!
941  throw new MWException( "Web server doesn't provide either " .
942  "REQUEST_URI, HTTP_X_ORIGINAL_URL or SCRIPT_NAME. Report details " .
943  "of your web server configuration to https://phabricator.wikimedia.org/" );
944  }
945  // User-agents should not send a fragment with the URI, but
946  // if they do, and the web server passes it on to us, we
947  // need to strip it or we get false-positive redirect loops
948  // or weird output URLs
949  $hash = strpos( $base, '#' );
950  if ( $hash !== false ) {
951  $base = substr( $base, 0, $hash );
952  }
953 
954  if ( $base[0] == '/' ) {
955  // More than one slash will look like it is protocol relative
956  return preg_replace( '!^/+!', '/', $base );
957  } else {
958  // We may get paths with a host prepended; strip it.
959  return preg_replace( '!^[^:]+://[^/]+/+!', '/', $base );
960  }
961  }
962 
970  public function getRequestURL() {
971  return self::getGlobalRequestURL();
972  }
973 
984  public function getFullRequestURL() {
985  // Pass an explicit PROTO constant instead of PROTO_CURRENT so that we
986  // do not rely on state from the global $wgRequest object (which it would,
987  // via wfGetServerUrl/wfExpandUrl/$wgRequest->protocol).
988  if ( $this->getProtocol() === 'http' ) {
989  return wfGetServerUrl( PROTO_HTTP ) . $this->getRequestURL();
990  } else {
991  return wfGetServerUrl( PROTO_HTTPS ) . $this->getRequestURL();
992  }
993  }
994 
1000  public function appendQueryValue( $key, $value ) {
1001  return $this->appendQueryArray( [ $key => $value ] );
1002  }
1003 
1010  public function appendQueryArray( $array ) {
1011  $newquery = $this->getQueryValues();
1012  unset( $newquery['title'] );
1013  $newquery = array_merge( $newquery, $array );
1014 
1015  return wfArrayToCgi( $newquery );
1016  }
1017 
1028  public function getLimitOffsetForUser( UserIdentity $user, $deflimit = 50, $optionname = 'rclimit' ) {
1029  $limit = $this->getInt( 'limit', 0 );
1030  if ( $limit < 0 ) {
1031  $limit = 0;
1032  }
1033  if ( ( $limit == 0 ) && ( $optionname != '' ) ) {
1034  $limit = MediaWikiServices::getInstance()
1035  ->getUserOptionsLookup()
1036  ->getIntOption( $user, $optionname );
1037  }
1038  if ( $limit <= 0 ) {
1039  $limit = $deflimit;
1040  }
1041  if ( $limit > 5000 ) {
1042  $limit = 5000; # We have *some* limits...
1043  }
1044 
1045  $offset = $this->getInt( 'offset', 0 );
1046  if ( $offset < 0 ) {
1047  $offset = 0;
1048  }
1049 
1050  return [ $limit, $offset ];
1051  }
1052 
1059  public function getFileTempname( $key ) {
1060  return $this->getUpload( $key )->getTempName();
1061  }
1062 
1069  public function getUploadError( $key ) {
1070  return $this->getUpload( $key )->getError();
1071  }
1072 
1084  public function getFileName( $key ) {
1085  return $this->getUpload( $key )->getName();
1086  }
1087 
1094  public function getUpload( $key ) {
1095  return new WebRequestUpload( $this, $key );
1096  }
1097 
1104  public function response() {
1105  /* Lazy initialization of response object for this request */
1106  if ( !is_object( $this->response ) ) {
1107  $class = ( $this instanceof FauxRequest ) ? FauxResponse::class : WebResponse::class;
1108  $this->response = new $class();
1109  }
1110  return $this->response;
1111  }
1112 
1116  protected function initHeaders() {
1117  if ( count( $this->headers ) ) {
1118  return;
1119  }
1120 
1121  $this->headers = array_change_key_case( getallheaders(), CASE_UPPER );
1122  }
1123 
1129  public function getAllHeaders() {
1130  $this->initHeaders();
1131  return $this->headers;
1132  }
1133 
1146  public function getHeader( $name, $flags = 0 ) {
1147  $this->initHeaders();
1148  $name = strtoupper( $name );
1149  if ( !isset( $this->headers[$name] ) ) {
1150  return false;
1151  }
1152  $value = $this->headers[$name];
1153  if ( $flags & self::GETHEADER_LIST ) {
1154  $value = array_map( 'trim', explode( ',', $value ) );
1155  }
1156  return $value;
1157  }
1158 
1166  public function getSessionData( $key ) {
1167  return $this->getSession()->get( $key );
1168  }
1169 
1175  public function setSessionData( $key, $data ) {
1176  $this->getSession()->set( $key, $data );
1177  }
1178 
1192  public function getAcceptLang() {
1193  // Modified version of code found at
1194  // http://www.thefutureoftheweb.com/blog/use-accept-language-header
1195  $acceptLang = $this->getHeader( 'Accept-Language' );
1196  if ( !$acceptLang ) {
1197  return [];
1198  }
1199 
1200  // Return the language codes in lower case
1201  $acceptLang = strtolower( $acceptLang );
1202 
1203  // Break up string into pieces (languages and q factors)
1204  if ( !preg_match_all(
1205  '/
1206  # a language code or a star is required
1207  ([a-z]{1,8}(?:-[a-z]{1,8})*|\*)
1208  # from here everything is optional
1209  \s*
1210  (?:
1211  # this accepts only numbers in the range ;q=0.000 to ;q=1.000
1212  ;\s*q\s*=\s*
1213  (1(?:\.0{0,3})?|0(?:\.\d{0,3})?)?
1214  )?
1215  /x',
1216  $acceptLang,
1217  $matches,
1218  PREG_SET_ORDER
1219  ) ) {
1220  return [];
1221  }
1222 
1223  // Create a list like "en" => 0.8
1224  $langs = [];
1225  foreach ( $matches as $match ) {
1226  $languageCode = $match[1];
1227  // When not present, the default value is 1
1228  $qValue = (float)( $match[2] ?? 1.0 );
1229  if ( $qValue ) {
1230  $langs[$languageCode] = $qValue;
1231  }
1232  }
1233 
1234  // Sort list by qValue
1235  arsort( $langs, SORT_NUMERIC );
1236  return $langs;
1237  }
1238 
1245  protected function getRawIP() {
1246  $remoteAddr = $_SERVER['REMOTE_ADDR'] ?? null;
1247  if ( !$remoteAddr ) {
1248  return null;
1249  }
1250  if ( is_array( $remoteAddr ) || str_contains( $remoteAddr, ',' ) ) {
1251  throw new MWException( 'Remote IP must not contain multiple values' );
1252  }
1253 
1254  return IPUtils::canonicalize( $remoteAddr );
1255  }
1256 
1264  public function getIP() {
1265  global $wgUsePrivateIPs;
1266 
1267  # Return cached result
1268  if ( $this->ip !== null ) {
1269  return $this->ip;
1270  }
1271 
1272  # collect the originating IPs
1273  $ip = $this->getRawIP();
1274  if ( !$ip ) {
1275  throw new MWException( 'Unable to determine IP.' );
1276  }
1277 
1278  # Append XFF
1279  $forwardedFor = $this->getHeader( 'X-Forwarded-For' );
1280  if ( $forwardedFor !== false ) {
1281  $proxyLookup = MediaWikiServices::getInstance()->getProxyLookup();
1282  $isConfigured = $proxyLookup->isConfiguredProxy( $ip );
1283  $ipchain = array_map( 'trim', explode( ',', $forwardedFor ) );
1284  $ipchain = array_reverse( $ipchain );
1285  array_unshift( $ipchain, $ip );
1286 
1287  # Step through XFF list and find the last address in the list which is a
1288  # trusted server. Set $ip to the IP address given by that trusted server,
1289  # unless the address is not sensible (e.g. private). However, prefer private
1290  # IP addresses over proxy servers controlled by this site (more sensible).
1291  # Note that some XFF values might be "unknown" with Squid/Varnish.
1292  foreach ( $ipchain as $i => $curIP ) {
1293  $curIP = IPUtils::sanitizeIP(
1294  IPUtils::canonicalize(
1295  self::canonicalizeIPv6LoopbackAddress( $curIP )
1296  )
1297  );
1298  if ( !$curIP || !isset( $ipchain[$i + 1] ) || $ipchain[$i + 1] === 'unknown'
1299  || !$proxyLookup->isTrustedProxy( $curIP )
1300  ) {
1301  break; // IP is not valid/trusted or does not point to anything
1302  }
1303  if (
1304  IPUtils::isPublic( $ipchain[$i + 1] ) ||
1305  $wgUsePrivateIPs ||
1306  // T50919; treat IP as valid
1307  $proxyLookup->isConfiguredProxy( $curIP )
1308  ) {
1309  $nextIP = $ipchain[$i + 1];
1310 
1311  // Follow the next IP according to the proxy
1312  $nextIP = IPUtils::canonicalize(
1313  self::canonicalizeIPv6LoopbackAddress( $nextIP )
1314  );
1315  if ( !$nextIP && $isConfigured ) {
1316  // We have not yet made it past CDN/proxy servers of this site,
1317  // so either they are misconfigured or there is some IP spoofing.
1318  throw new MWException( "Invalid IP given in XFF '$forwardedFor'." );
1319  }
1320  $ip = $nextIP;
1321 
1322  // keep traversing the chain
1323  continue;
1324  }
1325  break;
1326  }
1327  }
1328 
1329  // Allow extensions to modify the result
1330  // Optimisation: Hot code called on most requests (T85805).
1331  if ( Hooks::isRegistered( 'GetIP' ) ) {
1332  // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
1333  Hooks::runner()->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  $m = [];
1355  if ( preg_match( '/^0*' . IPUtils::RE_IPV6_GAP . '1$/', $ip, $m ) ) {
1356  return '127.0.0.1';
1357  }
1358  return $ip;
1359  }
1360 
1366  public function setIP( $ip ) {
1367  $this->ip = $ip;
1368  }
1369 
1382  public function hasSafeMethod() {
1383  if ( !isset( $_SERVER['REQUEST_METHOD'] ) ) {
1384  return false; // CLI mode
1385  }
1386 
1387  return in_array( $_SERVER['REQUEST_METHOD'], [ 'GET', 'HEAD', 'OPTIONS', 'TRACE' ] );
1388  }
1389 
1408  public function isSafeRequest() {
1409  if ( $this->markedAsSafe && $this->wasPosted() ) {
1410  return true; // marked as a "safe" POST
1411  }
1412 
1413  return $this->hasSafeMethod();
1414  }
1415 
1426  public function markAsSafeRequest() {
1427  $this->markedAsSafe = true;
1428  }
1429 
1441  public function matchURLForCDN( array $cdnUrls ) {
1442  $reqUrl = wfExpandUrl( $this->getRequestURL(), PROTO_INTERNAL );
1443  $config = MediaWikiServices::getInstance()->getMainConfig();
1444  if ( $config->get( MainConfigNames::CdnMatchParameterOrder ) ) {
1445  // Strict matching
1446  return in_array( $reqUrl, $cdnUrls, true );
1447  }
1448 
1449  // Loose matching (order of query parameters is ignored)
1450  $reqUrlParts = explode( '?', $reqUrl, 2 );
1451  $reqUrlBase = $reqUrlParts[0];
1452  $reqUrlParams = count( $reqUrlParts ) === 2 ? explode( '&', $reqUrlParts[1] ) : [];
1453  // The order of parameters after the sort() call below does not match
1454  // the order set by the CDN, and does not need to. The CDN needs to
1455  // take special care to preserve the relative order of duplicate keys
1456  // and array-like parameters.
1457  sort( $reqUrlParams );
1458  foreach ( $cdnUrls as $cdnUrl ) {
1459  if ( strlen( $reqUrl ) !== strlen( $cdnUrl ) ) {
1460  continue;
1461  }
1462  $cdnUrlParts = explode( '?', $cdnUrl, 2 );
1463  $cdnUrlBase = $cdnUrlParts[0];
1464  if ( $reqUrlBase !== $cdnUrlBase ) {
1465  continue;
1466  }
1467  $cdnUrlParams = count( $cdnUrlParts ) === 2 ? explode( '&', $cdnUrlParts[1] ) : [];
1468  sort( $cdnUrlParams );
1469  if ( $reqUrlParams === $cdnUrlParams ) {
1470  return true;
1471  }
1472  }
1473  return false;
1474  }
1475 }
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
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
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
Value object holding the session ID in a manner that can be globally updated.
Definition: SessionId.php:40
This serves as the entry point to the MediaWiki session handling system.
Manages data for an authenticated session.
Definition: Session.php:50
PathRouter class.
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:44
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
string[] $queryParams
The parameters from $_GET only.
Definition: WebRequest.php:62
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:117
getUpload( $key)
Return a WebRequestUpload object corresponding to the key.
string $protocol
Cached URL protocol.
Definition: WebRequest.php:104
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:313
isSafeRequest()
Whether this request should be identified as being "safe".
getSession()
Return the session for this request.
Definition: WebRequest.php:835
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
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:343
getProtocol()
Get the current URL protocol (http or https)
Definition: WebRequest.php:373
getMethod()
Get the HTTP method used for this request.
Definition: WebRequest.php:808
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:239
static getGlobalRequestURL()
Return the path and query string portion of the main request URI.
Definition: WebRequest.php:924
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:984
getElapsedTime()
Get the number of seconds to have elapsed since request start, in fractional seconds,...
Definition: WebRequest.php:330
float $requestTime
The timestamp of the start of the request, with microsecond precision.
Definition: WebRequest.php:98
string[] $headers
Lazy-initialized request headers indexed by upper-case header name.
Definition: WebRequest.php:68
getCrossSiteCookie( $key, $prefix='', $default=null)
Get a cookie set with SameSite=None possibly with a legacy fallback cookie.
Definition: WebRequest.php:899
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:864
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:265
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:821
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:74
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)
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:365
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:152
SessionId null $sessionId
Session ID to use for this request.
Definition: WebRequest.php:114
getRawIP()
Fetch the raw IP from the request.
setSessionId(SessionId $sessionId)
Set the session for this request.
Definition: WebRequest.php:854
getCookie( $key, $prefix=null, $default=null)
Get a cookie from the $_COOKIE jar.
Definition: WebRequest.php:876
array $data
The parameters from $_GET, $_POST and the path router.
Definition: WebRequest.php:49
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:970
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:56
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.