1<?php 2/** 3 * Deal with importing all those nasty globals and things 4 * 5 * Copyright © 2003 Brion Vibber <brion@pobox.com> 6 * https://www.mediawiki.org/ 7 * 8 * This program is free software; you can redistribute it and/or modify 9 * it under the terms of the GNU General Public License as published by 10 * the Free Software Foundation; either version 2 of the License, or 11 * (at your option) any later version. 12 * 13 * This program is distributed in the hope that it will be useful, 14 * but WITHOUT ANY WARRANTY; without even the implied warranty of 15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 * GNU General Public License for more details. 17 * 18 * You should have received a copy of the GNU General Public License along 19 * with this program; if not, write to the Free Software Foundation, Inc., 20 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 21 * http://www.gnu.org/copyleft/gpl.html 22 * 23 * @file 24 */ 25 26use MediaWiki\MediaWikiServices; 27use MediaWiki\Session\Session; 28use MediaWiki\Session\SessionId; 29use MediaWiki\Session\SessionManager; 30use Wikimedia\IPUtils; 31 32// The point of this class is to be a wrapper around super globals 33// phpcs:disable MediaWiki.Usage.SuperGlobalsUsage.SuperGlobals 34 35/** 36 * The WebRequest class encapsulates getting at data passed in the 37 * URL or via a POSTed form stripping illegal input characters and 38 * normalizing Unicode sequences. 39 * 40 * @ingroup HTTP 41 */ 42class WebRequest { 43 /** 44 * The parameters from $_GET, $_POST and the path router 45 * @var array 46 */ 47 protected $data; 48 49 /** 50 * The parameters from $_GET. The parameters from the path router are 51 * added by interpolateTitle() during Setup.php. 52 * @var array 53 */ 54 protected $queryAndPathParams; 55 56 /** 57 * The parameters from $_GET only. 58 */ 59 protected $queryParams; 60 61 /** 62 * Lazy-initialized request headers indexed by upper-case header name 63 * @var array 64 */ 65 protected $headers = []; 66 67 /** 68 * Flag to make WebRequest::getHeader return an array of values. 69 * @since 1.26 70 */ 71 public const GETHEADER_LIST = 1; 72 73 /** 74 * The unique request ID. 75 * @var string 76 */ 77 private static $reqId; 78 79 /** 80 * Lazy-init response object 81 * @var WebResponse 82 */ 83 private $response; 84 85 /** 86 * Cached client IP address 87 * @var string 88 */ 89 private $ip; 90 91 /** 92 * The timestamp of the start of the request, with microsecond precision. 93 * @var float 94 */ 95 protected $requestTime; 96 97 /** 98 * Cached URL protocol 99 * @var string 100 */ 101 protected $protocol; 102 103 /** 104 * @var SessionId|null Session ID to use for this 105 * request. We can't save the session directly due to reference cycles not 106 * working too well (slow GC). 107 * 108 * TODO: Investigate whether this GC slowness concern (added in a73c5b7395 with regard to 109 * PHP 5.6) still applies in PHP 7.2+. 110 */ 111 protected $sessionId = null; 112 113 /** @var bool Whether this HTTP request is "safe" (even if it is an HTTP post) */ 114 protected $markedAsSafe = false; 115 116 /** 117 * @codeCoverageIgnore 118 */ 119 public function __construct() { 120 $this->requestTime = $_SERVER['REQUEST_TIME_FLOAT']; 121 122 // POST overrides GET data 123 // We don't use $_REQUEST here to avoid interference from cookies... 124 $this->data = $_POST + $_GET; 125 126 $this->queryAndPathParams = $this->queryParams = $_GET; 127 } 128 129 /** 130 * Extract relevant query arguments from the http request uri's path 131 * to be merged with the normal php provided query arguments. 132 * Tries to use the REQUEST_URI data if available and parses it 133 * according to the wiki's configuration looking for any known pattern. 134 * 135 * If the REQUEST_URI is not provided we'll fall back on the PATH_INFO 136 * provided by the server if any and use that to set a 'title' parameter. 137 * 138 * @internal This has many odd special cases and so should only be used by 139 * interpolateTitle() for index.php. Instead try getRequestPathSuffix(). 140 * 141 * @param string $want If this is not 'all', then the function 142 * will return an empty array if it determines that the URL is 143 * inside a rewrite path. 144 * 145 * @return array Any query arguments found in path matches. 146 * @throws FatalError If invalid routes are configured (T48998) 147 */ 148 public static function getPathInfo( $want = 'all' ) { 149 // PATH_INFO is mangled due to https://bugs.php.net/bug.php?id=31892 150 // And also by Apache 2.x, double slashes are converted to single slashes. 151 // So we will use REQUEST_URI if possible. 152 if ( isset( $_SERVER['REQUEST_URI'] ) ) { 153 // Slurp out the path portion to examine... 154 $url = $_SERVER['REQUEST_URI']; 155 if ( !preg_match( '!^https?://!', $url ) ) { 156 $url = 'http://unused' . $url; 157 } 158 $a = parse_url( $url ); 159 if ( !$a ) { 160 return []; 161 } 162 $path = $a['path'] ?? ''; 163 164 global $wgScript; 165 if ( $path == $wgScript && $want !== 'all' ) { 166 // Script inside a rewrite path? 167 // Abort to keep from breaking... 168 return []; 169 } 170 171 $router = new PathRouter; 172 173 // Raw PATH_INFO style 174 $router->add( "$wgScript/$1" ); 175 176 global $wgArticlePath; 177 if ( $wgArticlePath ) { 178 $router->validateRoute( $wgArticlePath, 'wgArticlePath' ); 179 $router->add( $wgArticlePath ); 180 } 181 182 global $wgActionPaths; 183 $articlePaths = PathRouter::getActionPaths( $wgActionPaths, $wgArticlePath ); 184 if ( $articlePaths ) { 185 $router->add( $articlePaths, [ 'action' => '$key' ] ); 186 } 187 188 global $wgVariantArticlePath; 189 if ( $wgVariantArticlePath ) { 190 $router->validateRoute( $wgVariantArticlePath, 'wgVariantArticlePath' ); 191 $router->add( $wgVariantArticlePath, 192 [ 'variant' => '$2' ], 193 [ '$2' => MediaWikiServices::getInstance()->getContentLanguage()-> 194 getVariants() ] 195 ); 196 } 197 198 Hooks::runner()->onWebRequestPathInfoRouter( $router ); 199 200 $matches = $router->parse( $path ); 201 } else { 202 global $wgUsePathInfo; 203 $matches = []; 204 if ( $wgUsePathInfo ) { 205 if ( !empty( $_SERVER['ORIG_PATH_INFO'] ) ) { 206 // Mangled PATH_INFO 207 // https://bugs.php.net/bug.php?id=31892 208 // Also reported when ini_get('cgi.fix_pathinfo')==false 209 $matches['title'] = substr( $_SERVER['ORIG_PATH_INFO'], 1 ); 210 } elseif ( !empty( $_SERVER['PATH_INFO'] ) ) { 211 // Regular old PATH_INFO yay 212 $matches['title'] = substr( $_SERVER['PATH_INFO'], 1 ); 213 } 214 } 215 } 216 217 return $matches; 218 } 219 220 /** 221 * If the request URL matches a given base path, extract the path part of 222 * the request URL after that base, and decode escape sequences in it. 223 * 224 * If the request URL does not match, false is returned. 225 * 226 * @since 1.35 227 * @param string $basePath The base URL path. Trailing slashes will be 228 * stripped. 229 * @return string|false 230 */ 231 public static function getRequestPathSuffix( $basePath ) { 232 $basePath = rtrim( $basePath, '/' ) . '/'; 233 $requestUrl = self::getGlobalRequestURL(); 234 $qpos = strpos( $requestUrl, '?' ); 235 if ( $qpos !== false ) { 236 $requestPath = substr( $requestUrl, 0, $qpos ); 237 } else { 238 $requestPath = $requestUrl; 239 } 240 if ( substr( $requestPath, 0, strlen( $basePath ) ) !== $basePath ) { 241 return false; 242 } 243 return rawurldecode( substr( $requestPath, strlen( $basePath ) ) ); 244 } 245 246 /** 247 * Work out an appropriate URL prefix containing scheme and host, based on 248 * information detected from $_SERVER 249 * 250 * @return string 251 */ 252 public static function detectServer() { 253 global $wgAssumeProxiesUseDefaultProtocolPorts; 254 255 $proto = self::detectProtocol(); 256 $stdPort = $proto === 'https' ? 443 : 80; 257 258 $varNames = [ 'HTTP_HOST', 'SERVER_NAME', 'HOSTNAME', 'SERVER_ADDR' ]; 259 $host = 'localhost'; 260 $port = $stdPort; 261 foreach ( $varNames as $varName ) { 262 if ( !isset( $_SERVER[$varName] ) ) { 263 continue; 264 } 265 266 $parts = IPUtils::splitHostAndPort( $_SERVER[$varName] ); 267 if ( !$parts ) { 268 // Invalid, do not use 269 continue; 270 } 271 272 $host = $parts[0]; 273 if ( $wgAssumeProxiesUseDefaultProtocolPorts && isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) ) { 274 // T72021: Assume that upstream proxy is running on the default 275 // port based on the protocol. We have no reliable way to determine 276 // the actual port in use upstream. 277 $port = $stdPort; 278 } elseif ( $parts[1] === false ) { 279 if ( isset( $_SERVER['SERVER_PORT'] ) ) { 280 $port = $_SERVER['SERVER_PORT']; 281 } // else leave it as $stdPort 282 } else { 283 $port = $parts[1]; 284 } 285 break; 286 } 287 288 return $proto . '://' . IPUtils::combineHostAndPort( $host, $port, $stdPort ); 289 } 290 291 /** 292 * Detect the protocol from $_SERVER. 293 * This is for use prior to Setup.php, when no WebRequest object is available. 294 * At other times, use the non-static function getProtocol(). 295 * 296 * @return string 297 */ 298 public static function detectProtocol() { 299 if ( ( !empty( $_SERVER['HTTPS'] ) && $_SERVER['HTTPS'] !== 'off' ) || 300 ( isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) && 301 $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https' ) ) { 302 return 'https'; 303 } else { 304 return 'http'; 305 } 306 } 307 308 /** 309 * Get the number of seconds to have elapsed since request start, 310 * in fractional seconds, with microsecond resolution. 311 * 312 * @return float 313 * @since 1.25 314 */ 315 public function getElapsedTime() { 316 return microtime( true ) - $this->requestTime; 317 } 318 319 /** 320 * Get the unique request ID. 321 * This is either the value of the UNIQUE_ID envvar (if present) or a 322 * randomly-generated 24-character string. 323 * 324 * @return string 325 * @since 1.27 326 */ 327 public static function getRequestId() { 328 // This method is called from various error handlers and should be kept simple. 329 330 if ( !self::$reqId ) { 331 global $wgAllowExternalReqID; 332 $id = $wgAllowExternalReqID 333 ? RequestContext::getMain()->getRequest()->getHeader( 'X-Request-Id' ) 334 : null; 335 if ( !$id ) { 336 $id = $_SERVER['UNIQUE_ID'] ?? wfRandomString( 24 ); 337 } 338 self::$reqId = $id; 339 } 340 341 return self::$reqId; 342 } 343 344 /** 345 * Override the unique request ID. This is for sub-requests, such as jobs, 346 * that wish to use the same id but are not part of the same execution context. 347 * 348 * @param string $id 349 * @since 1.27 350 */ 351 public static function overrideRequestId( $id ) { 352 self::$reqId = $id; 353 } 354 355 /** 356 * Get the current URL protocol (http or https) 357 * @return string 358 */ 359 public function getProtocol() { 360 if ( $this->protocol === null ) { 361 $this->protocol = self::detectProtocol(); 362 } 363 return $this->protocol; 364 } 365 366 /** 367 * Check for title, action, and/or variant data in the URL 368 * and interpolate it into the GET variables. 369 * This should only be run after the content language is available, 370 * as we may need the list of language variants to determine 371 * available variant URLs. 372 */ 373 public function interpolateTitle() { 374 // T18019: title interpolation on API queries is useless and sometimes harmful 375 if ( defined( 'MW_API' ) ) { 376 return; 377 } 378 379 $matches = self::getPathInfo( 'title' ); 380 foreach ( $matches as $key => $val ) { 381 $this->data[$key] = $this->queryAndPathParams[$key] = $val; 382 } 383 } 384 385 /** 386 * URL rewriting function; tries to extract page title and, 387 * optionally, one other fixed parameter value from a URL path. 388 * 389 * @param string $path The URL path given from the client 390 * @param array $bases One or more URLs, optionally with $1 at the end 391 * @param string|bool $key If provided, the matching key in $bases will be 392 * passed on as the value of this URL parameter 393 * @return array Array of URL variables to interpolate; empty if no match 394 */ 395 public static function extractTitle( $path, $bases, $key = false ) { 396 foreach ( (array)$bases as $keyValue => $base ) { 397 // Find the part after $wgArticlePath 398 $base = str_replace( '$1', '', $base ); 399 $baseLen = strlen( $base ); 400 if ( substr( $path, 0, $baseLen ) == $base ) { 401 $raw = substr( $path, $baseLen ); 402 if ( $raw !== '' ) { 403 $matches = [ 'title' => rawurldecode( $raw ) ]; 404 if ( $key ) { 405 $matches[$key] = $keyValue; 406 } 407 return $matches; 408 } 409 } 410 } 411 return []; 412 } 413 414 /** 415 * Recursively normalizes UTF-8 strings in the given array. 416 * 417 * @param string|array $data 418 * @return array|string Cleaned-up version of the given 419 * @internal 420 */ 421 public function normalizeUnicode( $data ) { 422 if ( is_array( $data ) ) { 423 foreach ( $data as $key => $val ) { 424 $data[$key] = $this->normalizeUnicode( $val ); 425 } 426 } else { 427 $contLang = MediaWikiServices::getInstance()->getContentLanguage(); 428 $data = $contLang->normalize( $data ); 429 } 430 return $data; 431 } 432 433 /** 434 * Fetch a value from the given array or return $default if it's not set. 435 * 436 * @param array $arr 437 * @param string $name 438 * @param mixed $default 439 * @return mixed 440 */ 441 private function getGPCVal( $arr, $name, $default ) { 442 # PHP is so nice to not touch input data, except sometimes: 443 # https://www.php.net/variables.external#language.variables.external.dot-in-names 444 # Work around PHP *feature* to avoid *bugs* elsewhere. 445 $name = strtr( $name, '.', '_' ); 446 447 if ( !isset( $arr[$name] ) ) { 448 return $default; 449 } 450 451 $data = $arr[$name]; 452 # Optimisation: Skip UTF-8 normalization and legacy transcoding for simple ASCII strings. 453 $isAsciiStr = ( is_string( $data ) && preg_match( '/[^\x20-\x7E]/', $data ) === 0 ); 454 if ( !$isAsciiStr ) { 455 if ( isset( $_GET[$name] ) && is_string( $data ) ) { 456 # Check for alternate/legacy character encoding. 457 $data = MediaWikiServices::getInstance() 458 ->getContentLanguage() 459 ->checkTitleEncoding( $data ); 460 } 461 $data = $this->normalizeUnicode( $data ); 462 } 463 464 return $data; 465 } 466 467 /** 468 * Fetch a scalar from the input without normalization, or return $default 469 * if it's not set. 470 * 471 * Unlike self::getVal(), this does not perform any normalization on the 472 * input value. 473 * 474 * @since 1.28 475 * @param string $name 476 * @param string|null $default 477 * @return string|null 478 */ 479 public function getRawVal( $name, $default = null ) { 480 $name = strtr( $name, '.', '_' ); // See comment in self::getGPCVal() 481 if ( isset( $this->data[$name] ) && !is_array( $this->data[$name] ) ) { 482 $val = $this->data[$name]; 483 } else { 484 $val = $default; 485 } 486 if ( $val === null ) { 487 return $val; 488 } else { 489 return (string)$val; 490 } 491 } 492 493 /** 494 * Fetch a scalar from the input or return $default if it's not set. 495 * Returns a string. Arrays are discarded. Useful for 496 * non-freeform text inputs (e.g. predefined internal text keys 497 * selected by a drop-down menu). For freeform input, see getText(). 498 * 499 * @param string $name 500 * @param string|null $default Optional default (or null) 501 * @return string|null 502 */ 503 public function getVal( $name, $default = null ) { 504 $val = $this->getGPCVal( $this->data, $name, $default ); 505 if ( is_array( $val ) ) { 506 $val = $default; 507 } 508 if ( $val === null ) { 509 return $val; 510 } else { 511 return (string)$val; 512 } 513 } 514 515 /** 516 * Set an arbitrary value into our get/post data. 517 * 518 * @param string $key Key name to use 519 * @param mixed $value Value to set 520 * @return mixed Old value if one was present, null otherwise 521 */ 522 public function setVal( $key, $value ) { 523 $ret = $this->data[$key] ?? null; 524 $this->data[$key] = $value; 525 return $ret; 526 } 527 528 /** 529 * Unset an arbitrary value from our get/post data. 530 * 531 * @param string $key Key name to use 532 * @return mixed Old value if one was present, null otherwise 533 */ 534 public function unsetVal( $key ) { 535 if ( !isset( $this->data[$key] ) ) { 536 $ret = null; 537 } else { 538 $ret = $this->data[$key]; 539 unset( $this->data[$key] ); 540 } 541 return $ret; 542 } 543 544 /** 545 * Fetch an array from the input or return $default if it's not set. 546 * If source was scalar, will return an array with a single element. 547 * If no source and no default, returns null. 548 * 549 * @param string $name 550 * @param array|null $default Optional default (or null) 551 * @return array|null 552 */ 553 public function getArray( $name, $default = null ) { 554 $val = $this->getGPCVal( $this->data, $name, $default ); 555 if ( $val === null ) { 556 return null; 557 } else { 558 return (array)$val; 559 } 560 } 561 562 /** 563 * Fetch an array of integers, or return $default if it's not set. 564 * If source was scalar, will return an array with a single element. 565 * If no source and no default, returns null. 566 * If an array is returned, contents are guaranteed to be integers. 567 * 568 * @param string $name 569 * @param array|null $default Option default (or null) 570 * @return int[]|null 571 */ 572 public function getIntArray( $name, $default = null ) { 573 $val = $this->getArray( $name, $default ); 574 if ( is_array( $val ) ) { 575 $val = array_map( 'intval', $val ); 576 } 577 return $val; 578 } 579 580 /** 581 * Fetch an integer value from the input or return $default if not set. 582 * Guaranteed to return an integer; non-numeric input will typically 583 * return 0. 584 * 585 * @param string $name 586 * @param int $default 587 * @return int 588 */ 589 public function getInt( $name, $default = 0 ) { 590 return intval( $this->getRawVal( $name, $default ) ); 591 } 592 593 /** 594 * Fetch an integer value from the input or return null if empty. 595 * Guaranteed to return an integer or null; non-numeric input will 596 * typically return null. 597 * 598 * @param string $name 599 * @return int|null 600 */ 601 public function getIntOrNull( $name ) { 602 $val = $this->getRawVal( $name ); 603 return is_numeric( $val ) 604 ? intval( $val ) 605 : null; 606 } 607 608 /** 609 * Fetch a floating point value from the input or return $default if not set. 610 * Guaranteed to return a float; non-numeric input will typically 611 * return 0. 612 * 613 * @since 1.23 614 * @param string $name 615 * @param float $default 616 * @return float 617 */ 618 public function getFloat( $name, $default = 0.0 ) { 619 return floatval( $this->getRawVal( $name, $default ) ); 620 } 621 622 /** 623 * Fetch a boolean value from the input or return $default if not set. 624 * Guaranteed to return true or false, with normal PHP semantics for 625 * boolean interpretation of strings. 626 * 627 * @param string $name 628 * @param bool $default 629 * @return bool 630 */ 631 public function getBool( $name, $default = false ) { 632 return (bool)$this->getRawVal( $name, $default ); 633 } 634 635 /** 636 * Fetch a boolean value from the input or return $default if not set. 637 * Unlike getBool, the string "false" will result in boolean false, which is 638 * useful when interpreting information sent from JavaScript. 639 * 640 * @param string $name 641 * @param bool $default 642 * @return bool 643 */ 644 public function getFuzzyBool( $name, $default = false ) { 645 return $this->getBool( $name, $default ) 646 && strcasecmp( $this->getRawVal( $name ), 'false' ) !== 0; 647 } 648 649 /** 650 * Return true if the named value is set in the input, whatever that 651 * value is (even "0"). Return false if the named value is not set. 652 * Example use is checking for the presence of check boxes in forms. 653 * 654 * @param string $name 655 * @return bool 656 */ 657 public function getCheck( $name ) { 658 # Checkboxes and buttons are only present when clicked 659 # Presence connotes truth, absence false 660 return $this->getRawVal( $name, null ) !== null; 661 } 662 663 /** 664 * Fetch a text string from the given array or return $default if it's not 665 * set. Carriage returns are stripped from the text. This should generally 666 * be used for form "<textarea>" and "<input>" fields, and for 667 * user-supplied freeform text input. 668 * 669 * @param string $name 670 * @param string $default Optional 671 * @return string 672 */ 673 public function getText( $name, $default = '' ) { 674 $val = $this->getVal( $name, $default ); 675 return str_replace( "\r\n", "\n", $val ); 676 } 677 678 /** 679 * Extracts the given named values into an array. 680 * If no arguments are given, returns all input values. 681 * No transformation is performed on the values. 682 * 683 * @return array 684 */ 685 public function getValues() { 686 $names = func_get_args(); 687 if ( count( $names ) == 0 ) { 688 $names = array_keys( $this->data ); 689 } 690 691 $retVal = []; 692 foreach ( $names as $name ) { 693 $value = $this->getGPCVal( $this->data, $name, null ); 694 if ( $value !== null ) { 695 $retVal[$name] = $value; 696 } 697 } 698 return $retVal; 699 } 700 701 /** 702 * Returns the names of all input values excluding those in $exclude. 703 * 704 * @param array $exclude 705 * @return array 706 */ 707 public function getValueNames( $exclude = [] ) { 708 return array_diff( array_keys( $this->getValues() ), $exclude ); 709 } 710 711 /** 712 * Get the values passed in the query string and the path router parameters. 713 * No transformation is performed on the values. 714 * 715 * @codeCoverageIgnore 716 * @return array 717 */ 718 public function getQueryValues() { 719 return $this->queryAndPathParams; 720 } 721 722 /** 723 * Get the values passed in the query string only, not including the path 724 * router parameters. This is less suitable for self-links to index.php but 725 * useful for other entry points. No transformation is performed on the 726 * values. 727 * 728 * @since 1.34 729 * @return array 730 */ 731 public function getQueryValuesOnly() { 732 return $this->queryParams; 733 } 734 735 /** 736 * Get the values passed via POST. 737 * No transformation is performed on the values. 738 * 739 * @since 1.32 740 * @codeCoverageIgnore 741 * @return array 742 */ 743 public function getPostValues() { 744 return $_POST; 745 } 746 747 /** 748 * Return the contents of the Query with no decoding. Use when you need to 749 * know exactly what was sent, e.g. for an OAuth signature over the elements. 750 * 751 * @codeCoverageIgnore 752 * @return string 753 */ 754 public function getRawQueryString() { 755 return $_SERVER['QUERY_STRING']; 756 } 757 758 /** 759 * Return the contents of the POST with no decoding. Use when you need to 760 * know exactly what was sent, e.g. for an OAuth signature over the elements. 761 * 762 * @return string 763 */ 764 public function getRawPostString() { 765 if ( !$this->wasPosted() ) { 766 return ''; 767 } 768 return $this->getRawInput(); 769 } 770 771 /** 772 * Return the raw request body, with no processing. Cached since some methods 773 * disallow reading the stream more than once. As stated in the php docs, this 774 * does not work with enctype="multipart/form-data". 775 * 776 * @return string 777 */ 778 public function getRawInput() { 779 static $input = null; 780 if ( $input === null ) { 781 $input = file_get_contents( 'php://input' ); 782 } 783 return $input; 784 } 785 786 /** 787 * Get the HTTP method used for this request. 788 * 789 * @return string 790 */ 791 public function getMethod() { 792 return $_SERVER['REQUEST_METHOD'] ?? 'GET'; 793 } 794 795 /** 796 * Returns true if the present request was reached by a POST operation, 797 * false otherwise (GET, HEAD, or command-line). 798 * 799 * Note that values retrieved by the object may come from the 800 * GET URL etc even on a POST request. 801 * 802 * @return bool 803 */ 804 public function wasPosted() { 805 return $this->getMethod() == 'POST'; 806 } 807 808 /** 809 * Return the session for this request 810 * 811 * This might unpersist an existing session if it was invalid. 812 * 813 * @since 1.27 814 * @note For performance, keep the session locally if you will be making 815 * much use of it instead of calling this method repeatedly. 816 * @return Session 817 */ 818 public function getSession() { 819 if ( $this->sessionId !== null ) { 820 $session = SessionManager::singleton()->getSessionById( (string)$this->sessionId, true, $this ); 821 if ( $session ) { 822 return $session; 823 } 824 } 825 826 $session = SessionManager::singleton()->getSessionForRequest( $this ); 827 $this->sessionId = $session->getSessionId(); 828 return $session; 829 } 830 831 /** 832 * Set the session for this request 833 * @since 1.27 834 * @internal For use by MediaWiki\Session classes only 835 * @param SessionId $sessionId 836 */ 837 public function setSessionId( SessionId $sessionId ) { 838 $this->sessionId = $sessionId; 839 } 840 841 /** 842 * Get the session id for this request, if any 843 * @since 1.27 844 * @internal For use by MediaWiki\Session classes only 845 * @return SessionId|null 846 */ 847 public function getSessionId() { 848 return $this->sessionId; 849 } 850 851 /** 852 * Get a cookie from the $_COOKIE jar 853 * 854 * @param string $key The name of the cookie 855 * @param string|null $prefix A prefix to use for the cookie name, if not $wgCookiePrefix 856 * @param mixed|null $default What to return if the value isn't found 857 * @return mixed Cookie value or $default if the cookie not set 858 */ 859 public function getCookie( $key, $prefix = null, $default = null ) { 860 if ( $prefix === null ) { 861 global $wgCookiePrefix; 862 $prefix = $wgCookiePrefix; 863 } 864 $name = $prefix . $key; 865 // Work around mangling of $_COOKIE 866 $name = strtr( $name, '.', '_' ); 867 if ( isset( $_COOKIE[$name] ) ) { 868 return $_COOKIE[$name]; 869 } else { 870 return $default; 871 } 872 } 873 874 /** 875 * Get a cookie set with SameSite=None possibly with a legacy fallback cookie. 876 * 877 * @param string $key The name of the cookie 878 * @param string $prefix A prefix to use, empty by default 879 * @param mixed|null $default What to return if the value isn't found 880 * @return mixed Cookie value or $default if the cookie is not set 881 */ 882 public function getCrossSiteCookie( $key, $prefix = '', $default = null ) { 883 global $wgUseSameSiteLegacyCookies; 884 $name = $prefix . $key; 885 // Work around mangling of $_COOKIE 886 $name = strtr( $name, '.', '_' ); 887 if ( isset( $_COOKIE[$name] ) ) { 888 return $_COOKIE[$name]; 889 } 890 if ( $wgUseSameSiteLegacyCookies ) { 891 $legacyName = $prefix . "ss0-" . $key; 892 $legacyName = strtr( $legacyName, '.', '_' ); 893 if ( isset( $_COOKIE[$legacyName] ) ) { 894 return $_COOKIE[$legacyName]; 895 } 896 } 897 return $default; 898 } 899 900 /** 901 * Return the path and query string portion of the main request URI. 902 * This will be suitable for use as a relative link in HTML output. 903 * 904 * @throws MWException 905 * @return string 906 */ 907 public static function getGlobalRequestURL() { 908 // This method is called on fatal errors; it should not depend on anything complex. 909 910 if ( isset( $_SERVER['REQUEST_URI'] ) && strlen( $_SERVER['REQUEST_URI'] ) ) { 911 $base = $_SERVER['REQUEST_URI']; 912 } elseif ( isset( $_SERVER['HTTP_X_ORIGINAL_URL'] ) 913 && strlen( $_SERVER['HTTP_X_ORIGINAL_URL'] ) 914 ) { 915 // Probably IIS; doesn't set REQUEST_URI 916 $base = $_SERVER['HTTP_X_ORIGINAL_URL']; 917 } elseif ( isset( $_SERVER['SCRIPT_NAME'] ) ) { 918 $base = $_SERVER['SCRIPT_NAME']; 919 if ( isset( $_SERVER['QUERY_STRING'] ) && $_SERVER['QUERY_STRING'] != '' ) { 920 $base .= '?' . $_SERVER['QUERY_STRING']; 921 } 922 } else { 923 // This shouldn't happen! 924 throw new MWException( "Web server doesn't provide either " . 925 "REQUEST_URI, HTTP_X_ORIGINAL_URL or SCRIPT_NAME. Report details " . 926 "of your web server configuration to https://phabricator.wikimedia.org/" ); 927 } 928 // User-agents should not send a fragment with the URI, but 929 // if they do, and the web server passes it on to us, we 930 // need to strip it or we get false-positive redirect loops 931 // or weird output URLs 932 $hash = strpos( $base, '#' ); 933 if ( $hash !== false ) { 934 $base = substr( $base, 0, $hash ); 935 } 936 937 if ( $base[0] == '/' ) { 938 // More than one slash will look like it is protocol relative 939 return preg_replace( '!^/+!', '/', $base ); 940 } else { 941 // We may get paths with a host prepended; strip it. 942 return preg_replace( '!^[^:]+://[^/]+/+!', '/', $base ); 943 } 944 } 945 946 /** 947 * Return the path and query string portion of the request URI. 948 * This will be suitable for use as a relative link in HTML output. 949 * 950 * @throws MWException 951 * @return string 952 */ 953 public function getRequestURL() { 954 return self::getGlobalRequestURL(); 955 } 956 957 /** 958 * Return the request URI with the canonical service and hostname, path, 959 * and query string. This will be suitable for use as an absolute link 960 * in HTML or other output. 961 * 962 * If $wgServer is protocol-relative, this will return a fully 963 * qualified URL with the protocol of this request object. 964 * 965 * @return string 966 */ 967 public function getFullRequestURL() { 968 // Pass an explicit PROTO constant instead of PROTO_CURRENT so that we 969 // do not rely on state from the global $wgRequest object (which it would, 970 // via wfGetServerUrl/wfExpandUrl/$wgRequest->protocol). 971 if ( $this->getProtocol() === 'http' ) { 972 return wfGetServerUrl( PROTO_HTTP ) . $this->getRequestURL(); 973 } else { 974 return wfGetServerUrl( PROTO_HTTPS ) . $this->getRequestURL(); 975 } 976 } 977 978 /** 979 * @param string $key 980 * @param string $value 981 * @return string 982 */ 983 public function appendQueryValue( $key, $value ) { 984 return $this->appendQueryArray( [ $key => $value ] ); 985 } 986 987 /** 988 * Appends or replaces value of query variables. 989 * 990 * @param array $array Array of values to replace/add to query 991 * @return string 992 */ 993 public function appendQueryArray( $array ) { 994 $newquery = $this->getQueryValues(); 995 unset( $newquery['title'] ); 996 $newquery = array_merge( $newquery, $array ); 997 998 return wfArrayToCgi( $newquery ); 999 } 1000 1001 /** 1002 * Same as ::getLimitOffsetForUser, but without a user parameter, instead using $wgUser 1003 * 1004 * @deprecated since 1.35, use ::getLimitOffsetForUser instead 1005 * 1006 * @param int $deflimit Limit to use if no input and the user hasn't set the option. 1007 * @param string $optionname To specify an option other than rclimit to pull from. 1008 * @return int[] First element is limit, second is offset 1009 */ 1010 public function getLimitOffset( $deflimit = 50, $optionname = 'rclimit' ) { 1011 wfDeprecated( __METHOD__, '1.35' ); 1012 1013 global $wgUser; 1014 return $this->getLimitOffsetForUser( $wgUser, $deflimit, $optionname ); 1015 } 1016 1017 /** 1018 * Check for limit and offset parameters on the input, and return sensible 1019 * defaults if not given. The limit must be positive and is capped at 5000. 1020 * Offset must be positive but is not capped. 1021 * 1022 * @param User $user User to get option for 1023 * @param int $deflimit Limit to use if no input and the user hasn't set the option. 1024 * @param string $optionname To specify an option other than rclimit to pull from. 1025 * @return int[] First element is limit, second is offset 1026 */ 1027 public function getLimitOffsetForUser( User $user, $deflimit = 50, $optionname = 'rclimit' ) { 1028 $limit = $this->getInt( 'limit', 0 ); 1029 if ( $limit < 0 ) { 1030 $limit = 0; 1031 } 1032 if ( ( $limit == 0 ) && ( $optionname != '' ) ) { 1033 $limit = $user->getIntOption( $optionname ); 1034 } 1035 if ( $limit <= 0 ) { 1036 $limit = $deflimit; 1037 } 1038 if ( $limit > 5000 ) { 1039 $limit = 5000; # We have *some* limits... 1040 } 1041 1042 $offset = $this->getInt( 'offset', 0 ); 1043 if ( $offset < 0 ) { 1044 $offset = 0; 1045 } 1046 1047 return [ $limit, $offset ]; 1048 } 1049 1050 /** 1051 * Return the path to the temporary file where PHP has stored the upload. 1052 * 1053 * @param string $key 1054 * @return string|null String or null if no such file. 1055 */ 1056 public function getFileTempname( $key ) { 1057 $file = new WebRequestUpload( $this, $key ); 1058 return $file->getTempName(); 1059 } 1060 1061 /** 1062 * Return the upload error or 0 1063 * 1064 * @param string $key 1065 * @return int 1066 */ 1067 public function getUploadError( $key ) { 1068 $file = new WebRequestUpload( $this, $key ); 1069 return $file->getError(); 1070 } 1071 1072 /** 1073 * Return the original filename of the uploaded file, as reported by 1074 * the submitting user agent. HTML-style character entities are 1075 * interpreted and normalized to Unicode normalization form C, in part 1076 * to deal with weird input from Safari with non-ASCII filenames. 1077 * 1078 * Other than this the name is not verified for being a safe filename. 1079 * 1080 * @param string $key 1081 * @return string|null String or null if no such file. 1082 */ 1083 public function getFileName( $key ) { 1084 $file = new WebRequestUpload( $this, $key ); 1085 return $file->getName(); 1086 } 1087 1088 /** 1089 * Return a WebRequestUpload object corresponding to the key 1090 * 1091 * @param string $key 1092 * @return WebRequestUpload 1093 */ 1094 public function getUpload( $key ) { 1095 return new WebRequestUpload( $this, $key ); 1096 } 1097 1098 /** 1099 * Return a handle to WebResponse style object, for setting cookies, 1100 * headers and other stuff, for Request being worked on. 1101 * 1102 * @return WebResponse 1103 */ 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 1113 /** 1114 * Initialise the header list 1115 */ 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 1124 /** 1125 * Get an array containing all request headers 1126 * 1127 * @return array Mapping header name to its value 1128 */ 1129 public function getAllHeaders() { 1130 $this->initHeaders(); 1131 return $this->headers; 1132 } 1133 1134 /** 1135 * Get a request header, or false if it isn't set. 1136 * 1137 * @param string $name Case-insensitive header name 1138 * @param int $flags Bitwise combination of: 1139 * WebRequest::GETHEADER_LIST Treat the header as a comma-separated list 1140 * of values, as described in RFC 2616 § 4.2. 1141 * (since 1.26). 1142 * @return string|array|bool False if header is unset; otherwise the 1143 * header value(s) as either a string (the default) or an array, if 1144 * WebRequest::GETHEADER_LIST flag was set. 1145 */ 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 1159 /** 1160 * Get data from the session 1161 * 1162 * @note Prefer $this->getSession() instead if making multiple calls. 1163 * @param string $key Name of key in the session 1164 * @return mixed 1165 */ 1166 public function getSessionData( $key ) { 1167 return $this->getSession()->get( $key ); 1168 } 1169 1170 /** 1171 * Set session data 1172 * 1173 * @note Prefer $this->getSession() instead if making multiple calls. 1174 * @param string $key Name of key in the session 1175 * @param mixed $data 1176 */ 1177 public function setSessionData( $key, $data ) { 1178 $this->getSession()->set( $key, $data ); 1179 } 1180 1181 /** 1182 * This function formerly did a security check to prevent an XSS 1183 * vulnerability in IE6, as documented in T30235. Since IE6 support has 1184 * been dropped, this function now returns true unconditionally. 1185 * 1186 * @deprecated since 1.35 1187 * @param array $extWhitelist 1188 * @return bool 1189 */ 1190 public function checkUrlExtension( $extWhitelist = [] ) { 1191 wfDeprecated( __METHOD__, '1.35' ); 1192 return true; 1193 } 1194 1195 /** 1196 * Parse the Accept-Language header sent by the client into an array 1197 * 1198 * @return array [ languageCode => q-value ] sorted by q-value in 1199 * descending order then appearing time in the header in ascending order. 1200 * May contain the "language" '*', which applies to languages other than those explicitly listed. 1201 * 1202 * This logic is aligned with RFC 7231 section 5 (previously RFC 2616 section 14), 1203 * at <https://tools.ietf.org/html/rfc7231#section-5.3.5>. 1204 * 1205 * Earlier languages in the list are preferred as per the RFC 23282 extension to HTTP/1.1, 1206 * at <https://tools.ietf.org/html/rfc3282>. 1207 */ 1208 public function getAcceptLang() { 1209 // Modified version of code found at 1210 // http://www.thefutureoftheweb.com/blog/use-accept-language-header 1211 $acceptLang = $this->getHeader( 'Accept-Language' ); 1212 if ( !$acceptLang ) { 1213 return []; 1214 } 1215 1216 // Return the language codes in lower case 1217 $acceptLang = strtolower( $acceptLang ); 1218 1219 // Break up string into pieces (languages and q factors) 1220 if ( !preg_match_all( 1221 '/([a-z]{1,8}(?:-[a-z]{1,8})*|\*)\s*(?:;\s*q\s*=\s*(1(?:\.0{0,3})?|0(?:\.[0-9]{0,3})?)?)?/', 1222 $acceptLang, 1223 $matches, 1224 PREG_SET_ORDER 1225 ) ) { 1226 return []; 1227 } 1228 1229 // Create a list like "en" => 0.8 1230 $langs = []; 1231 foreach ( $matches as $match ) { 1232 $languageCode = $match[1]; 1233 // When not present, the default value is 1 1234 $qValue = $match[2] ?? 1; 1235 if ( $qValue > 0 ) { 1236 $langs[$languageCode] = $qValue; 1237 } 1238 } 1239 1240 // Sort list by qValue 1241 arsort( $langs, SORT_NUMERIC ); 1242 return $langs; 1243 } 1244 1245 /** 1246 * Fetch the raw IP from the request 1247 * 1248 * @since 1.19 1249 * 1250 * @throws MWException 1251 * @return string|null 1252 */ 1253 protected function getRawIP() { 1254 if ( !isset( $_SERVER['REMOTE_ADDR'] ) ) { 1255 return null; 1256 } 1257 1258 if ( is_array( $_SERVER['REMOTE_ADDR'] ) || strpos( $_SERVER['REMOTE_ADDR'], ',' ) !== false ) { 1259 throw new MWException( __METHOD__ 1260 . " : Could not determine the remote IP address due to multiple values." ); 1261 } else { 1262 $ipchain = $_SERVER['REMOTE_ADDR']; 1263 } 1264 1265 return IPUtils::canonicalize( $ipchain ); 1266 } 1267 1268 /** 1269 * Work out the IP address based on various globals 1270 * For trusted proxies, use the XFF client IP (first of the chain) 1271 * 1272 * @since 1.19 1273 * 1274 * @throws MWException 1275 * @return string 1276 */ 1277 public function getIP() { 1278 global $wgUsePrivateIPs; 1279 1280 # Return cached result 1281 if ( $this->ip !== null ) { 1282 return $this->ip; 1283 } 1284 1285 # collect the originating ips 1286 $ip = $this->getRawIP(); 1287 if ( !$ip ) { 1288 throw new MWException( 'Unable to determine IP.' ); 1289 } 1290 1291 # Append XFF 1292 $forwardedFor = $this->getHeader( 'X-Forwarded-For' ); 1293 if ( $forwardedFor !== false ) { 1294 $proxyLookup = MediaWikiServices::getInstance()->getProxyLookup(); 1295 $isConfigured = $proxyLookup->isConfiguredProxy( $ip ); 1296 $ipchain = array_map( 'trim', explode( ',', $forwardedFor ) ); 1297 $ipchain = array_reverse( $ipchain ); 1298 array_unshift( $ipchain, $ip ); 1299 1300 # Step through XFF list and find the last address in the list which is a 1301 # trusted server. Set $ip to the IP address given by that trusted server, 1302 # unless the address is not sensible (e.g. private). However, prefer private 1303 # IP addresses over proxy servers controlled by this site (more sensible). 1304 # Note that some XFF values might be "unknown" with Squid/Varnish. 1305 foreach ( $ipchain as $i => $curIP ) { 1306 $curIP = IPUtils::sanitizeIP( IPUtils::canonicalize( $curIP ) ); 1307 if ( !$curIP || !isset( $ipchain[$i + 1] ) || $ipchain[$i + 1] === 'unknown' 1308 || !$proxyLookup->isTrustedProxy( $curIP ) 1309 ) { 1310 break; // IP is not valid/trusted or does not point to anything 1311 } 1312 if ( 1313 IPUtils::isPublic( $ipchain[$i + 1] ) || 1314 $wgUsePrivateIPs || 1315 $proxyLookup->isConfiguredProxy( $curIP ) // T50919; treat IP as sane 1316 ) { 1317 // Follow the next IP according to the proxy 1318 $nextIP = IPUtils::canonicalize( $ipchain[$i + 1] ); 1319 if ( !$nextIP && $isConfigured ) { 1320 // We have not yet made it past CDN/proxy servers of this site, 1321 // so either they are misconfigured or there is some IP spoofing. 1322 throw new MWException( "Invalid IP given in XFF '$forwardedFor'." ); 1323 } 1324 $ip = $nextIP; 1325 // keep traversing the chain 1326 continue; 1327 } 1328 break; 1329 } 1330 } 1331 1332 # Allow extensions to improve our guess 1333 Hooks::runner()->onGetIP( $ip ); 1334 1335 if ( !$ip ) { 1336 throw new MWException( "Unable to determine IP." ); 1337 } 1338 1339 $this->ip = $ip; 1340 return $ip; 1341 } 1342 1343 /** 1344 * @param string $ip 1345 * @return void 1346 * @since 1.21 1347 */ 1348 public function setIP( $ip ) { 1349 $this->ip = $ip; 1350 } 1351 1352 /** 1353 * Check if this request uses a "safe" HTTP method 1354 * 1355 * Safe methods are verbs (e.g. GET/HEAD/OPTIONS) used for obtaining content. Such requests 1356 * are not expected to mutate content, especially in ways attributable to the client. Verbs 1357 * like POST and PUT are typical of non-safe requests which often change content. 1358 * 1359 * @return bool 1360 * @see https://tools.ietf.org/html/rfc7231#section-4.2.1 1361 * @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html 1362 * @since 1.28 1363 */ 1364 public function hasSafeMethod() { 1365 if ( !isset( $_SERVER['REQUEST_METHOD'] ) ) { 1366 return false; // CLI mode 1367 } 1368 1369 return in_array( $_SERVER['REQUEST_METHOD'], [ 'GET', 'HEAD', 'OPTIONS', 'TRACE' ] ); 1370 } 1371 1372 /** 1373 * Whether this request should be identified as being "safe" 1374 * 1375 * This means that the client is not requesting any state changes and that database writes 1376 * are not inherently required. Ideally, no visible updates would happen at all. If they 1377 * must, then they should not be publicly attributed to the end user. 1378 * 1379 * In more detail: 1380 * - Cache populations and refreshes MAY occur. 1381 * - Private user session updates and private server logging MAY occur. 1382 * - Updates to private viewing activity data MAY occur via DeferredUpdates. 1383 * - Other updates SHOULD NOT occur (e.g. modifying content assets). 1384 * 1385 * @return bool 1386 * @see https://tools.ietf.org/html/rfc7231#section-4.2.1 1387 * @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html 1388 * @since 1.28 1389 */ 1390 public function isSafeRequest() { 1391 if ( $this->markedAsSafe && $this->wasPosted() ) { 1392 return true; // marked as a "safe" POST 1393 } 1394 1395 return $this->hasSafeMethod(); 1396 } 1397 1398 /** 1399 * Mark this request as identified as being nullipotent even if it is a POST request 1400 * 1401 * POST requests are often used due to the need for a client payload, even if the request 1402 * is otherwise equivalent to a "safe method" request. 1403 * 1404 * @see https://tools.ietf.org/html/rfc7231#section-4.2.1 1405 * @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html 1406 * @since 1.28 1407 */ 1408 public function markAsSafeRequest() { 1409 $this->markedAsSafe = true; 1410 } 1411} 1412