Merge "registration: Make it easier for other code to get extension metadata"
[lhc/web/wiklou.git] / includes / WebRequest.php
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
26 /**
27 * The WebRequest class encapsulates getting at data passed in the
28 * URL or via a POSTed form stripping illegal input characters and
29 * normalizing Unicode sequences.
30 *
31 * Usually this is used via a global singleton, $wgRequest. You should
32 * not create a second WebRequest object; make a FauxRequest object if
33 * you want to pass arbitrary data to some function in place of the web
34 * input.
35 *
36 * @ingroup HTTP
37 */
38 class WebRequest {
39 protected $data, $headers = array();
40
41 /**
42 * Lazy-init response object
43 * @var WebResponse
44 */
45 private $response;
46
47 /**
48 * Cached client IP address
49 * @var string
50 */
51 private $ip;
52
53 /**
54 * The timestamp of the start of the request, with microsecond precision.
55 * @var float
56 */
57 protected $requestTime;
58
59 /**
60 * Cached URL protocol
61 * @var string
62 */
63 protected $protocol;
64
65 public function __construct() {
66 $this->requestTime = isset( $_SERVER['REQUEST_TIME_FLOAT'] )
67 ? $_SERVER['REQUEST_TIME_FLOAT'] : microtime( true );
68
69 // POST overrides GET data
70 // We don't use $_REQUEST here to avoid interference from cookies...
71 $this->data = $_POST + $_GET;
72 }
73
74 /**
75 * Extract relevant query arguments from the http request uri's path
76 * to be merged with the normal php provided query arguments.
77 * Tries to use the REQUEST_URI data if available and parses it
78 * according to the wiki's configuration looking for any known pattern.
79 *
80 * If the REQUEST_URI is not provided we'll fall back on the PATH_INFO
81 * provided by the server if any and use that to set a 'title' parameter.
82 *
83 * @param string $want If this is not 'all', then the function
84 * will return an empty array if it determines that the URL is
85 * inside a rewrite path.
86 *
87 * @return array Any query arguments found in path matches.
88 */
89 public static function getPathInfo( $want = 'all' ) {
90 global $wgUsePathInfo;
91 // PATH_INFO is mangled due to http://bugs.php.net/bug.php?id=31892
92 // And also by Apache 2.x, double slashes are converted to single slashes.
93 // So we will use REQUEST_URI if possible.
94 $matches = array();
95 if ( !empty( $_SERVER['REQUEST_URI'] ) ) {
96 // Slurp out the path portion to examine...
97 $url = $_SERVER['REQUEST_URI'];
98 if ( !preg_match( '!^https?://!', $url ) ) {
99 $url = 'http://unused' . $url;
100 }
101 wfSuppressWarnings();
102 $a = parse_url( $url );
103 wfRestoreWarnings();
104 if ( $a ) {
105 $path = isset( $a['path'] ) ? $a['path'] : '';
106
107 global $wgScript;
108 if ( $path == $wgScript && $want !== 'all' ) {
109 // Script inside a rewrite path?
110 // Abort to keep from breaking...
111 return $matches;
112 }
113
114 $router = new PathRouter;
115
116 // Raw PATH_INFO style
117 $router->add( "$wgScript/$1" );
118
119 if ( isset( $_SERVER['SCRIPT_NAME'] )
120 && preg_match( '/\.php5?/', $_SERVER['SCRIPT_NAME'] )
121 ) {
122 # Check for SCRIPT_NAME, we handle index.php explicitly
123 # But we do have some other .php files such as img_auth.php
124 # Don't let root article paths clober the parsing for them
125 $router->add( $_SERVER['SCRIPT_NAME'] . "/$1" );
126 }
127
128 global $wgArticlePath;
129 if ( $wgArticlePath ) {
130 $router->add( $wgArticlePath );
131 }
132
133 global $wgActionPaths;
134 if ( $wgActionPaths ) {
135 $router->add( $wgActionPaths, array( 'action' => '$key' ) );
136 }
137
138 global $wgVariantArticlePath, $wgContLang;
139 if ( $wgVariantArticlePath ) {
140 $router->add( $wgVariantArticlePath,
141 array( 'variant' => '$2' ),
142 array( '$2' => $wgContLang->getVariants() )
143 );
144 }
145
146 Hooks::run( 'WebRequestPathInfoRouter', array( $router ) );
147
148 $matches = $router->parse( $path );
149 }
150 } elseif ( $wgUsePathInfo ) {
151 if ( isset( $_SERVER['ORIG_PATH_INFO'] ) && $_SERVER['ORIG_PATH_INFO'] != '' ) {
152 // Mangled PATH_INFO
153 // http://bugs.php.net/bug.php?id=31892
154 // Also reported when ini_get('cgi.fix_pathinfo')==false
155 $matches['title'] = substr( $_SERVER['ORIG_PATH_INFO'], 1 );
156
157 } elseif ( isset( $_SERVER['PATH_INFO'] ) && $_SERVER['PATH_INFO'] != '' ) {
158 // Regular old PATH_INFO yay
159 $matches['title'] = substr( $_SERVER['PATH_INFO'], 1 );
160 }
161 }
162
163 return $matches;
164 }
165
166 /**
167 * Work out an appropriate URL prefix containing scheme and host, based on
168 * information detected from $_SERVER
169 *
170 * @return string
171 */
172 public static function detectServer() {
173 $proto = self::detectProtocol();
174 $stdPort = $proto === 'https' ? 443 : 80;
175
176 $varNames = array( 'HTTP_HOST', 'SERVER_NAME', 'HOSTNAME', 'SERVER_ADDR' );
177 $host = 'localhost';
178 $port = $stdPort;
179 foreach ( $varNames as $varName ) {
180 if ( !isset( $_SERVER[$varName] ) ) {
181 continue;
182 }
183 $parts = IP::splitHostAndPort( $_SERVER[$varName] );
184 if ( !$parts ) {
185 // Invalid, do not use
186 continue;
187 }
188 $host = $parts[0];
189 if ( isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) ) {
190 // Bug 70021: Assume that upstream proxy is running on the default
191 // port based on the protocol. We have no reliable way to determine
192 // the actual port in use upstream.
193 $port = $stdPort;
194 } elseif ( $parts[1] === false ) {
195 if ( isset( $_SERVER['SERVER_PORT'] ) ) {
196 $port = $_SERVER['SERVER_PORT'];
197 } // else leave it as $stdPort
198 } else {
199 $port = $parts[1];
200 }
201 break;
202 }
203
204 return $proto . '://' . IP::combineHostAndPort( $host, $port, $stdPort );
205 }
206
207 /**
208 * Detect the protocol from $_SERVER.
209 * This is for use prior to Setup.php, when no WebRequest object is available.
210 * At other times, use the non-static function getProtocol().
211 *
212 * @return array
213 */
214 public static function detectProtocol() {
215 if ( ( !empty( $_SERVER['HTTPS'] ) && $_SERVER['HTTPS'] !== 'off' ) ||
216 ( isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) &&
217 $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https' ) ) {
218 return 'https';
219 } else {
220 return 'http';
221 }
222 }
223
224 /**
225 * Get the number of seconds to have elapsed since request start,
226 * in fractional seconds, with microsecond resolution.
227 *
228 * @return float
229 * @since 1.25
230 */
231 public function getElapsedTime() {
232 return microtime( true ) - $this->requestTime;
233 }
234
235 /**
236 * Get the current URL protocol (http or https)
237 * @return string
238 */
239 public function getProtocol() {
240 if ( $this->protocol === null ) {
241 $this->protocol = self::detectProtocol();
242 }
243 return $this->protocol;
244 }
245
246 /**
247 * Check for title, action, and/or variant data in the URL
248 * and interpolate it into the GET variables.
249 * This should only be run after $wgContLang is available,
250 * as we may need the list of language variants to determine
251 * available variant URLs.
252 */
253 public function interpolateTitle() {
254 // bug 16019: title interpolation on API queries is useless and sometimes harmful
255 if ( defined( 'MW_API' ) ) {
256 return;
257 }
258
259 $matches = self::getPathInfo( 'title' );
260 foreach ( $matches as $key => $val ) {
261 $this->data[$key] = $_GET[$key] = $_REQUEST[$key] = $val;
262 }
263 }
264
265 /**
266 * URL rewriting function; tries to extract page title and,
267 * optionally, one other fixed parameter value from a URL path.
268 *
269 * @param string $path The URL path given from the client
270 * @param array $bases One or more URLs, optionally with $1 at the end
271 * @param string $key If provided, the matching key in $bases will be
272 * passed on as the value of this URL parameter
273 * @return array Array of URL variables to interpolate; empty if no match
274 */
275 static function extractTitle( $path, $bases, $key = false ) {
276 foreach ( (array)$bases as $keyValue => $base ) {
277 // Find the part after $wgArticlePath
278 $base = str_replace( '$1', '', $base );
279 $baseLen = strlen( $base );
280 if ( substr( $path, 0, $baseLen ) == $base ) {
281 $raw = substr( $path, $baseLen );
282 if ( $raw !== '' ) {
283 $matches = array( 'title' => rawurldecode( $raw ) );
284 if ( $key ) {
285 $matches[$key] = $keyValue;
286 }
287 return $matches;
288 }
289 }
290 }
291 return array();
292 }
293
294 /**
295 * Recursively normalizes UTF-8 strings in the given array.
296 *
297 * @param string|array $data
298 * @return array|string Cleaned-up version of the given
299 * @private
300 */
301 function normalizeUnicode( $data ) {
302 if ( is_array( $data ) ) {
303 foreach ( $data as $key => $val ) {
304 $data[$key] = $this->normalizeUnicode( $val );
305 }
306 } else {
307 global $wgContLang;
308 $data = isset( $wgContLang ) ? $wgContLang->normalize( $data ) : UtfNormal\Validator::cleanUp( $data );
309 }
310 return $data;
311 }
312
313 /**
314 * Fetch a value from the given array or return $default if it's not set.
315 *
316 * @param array $arr
317 * @param string $name
318 * @param mixed $default
319 * @return mixed
320 */
321 private function getGPCVal( $arr, $name, $default ) {
322 # PHP is so nice to not touch input data, except sometimes:
323 # http://us2.php.net/variables.external#language.variables.external.dot-in-names
324 # Work around PHP *feature* to avoid *bugs* elsewhere.
325 $name = strtr( $name, '.', '_' );
326 if ( isset( $arr[$name] ) ) {
327 global $wgContLang;
328 $data = $arr[$name];
329 if ( isset( $_GET[$name] ) && !is_array( $data ) ) {
330 # Check for alternate/legacy character encoding.
331 if ( isset( $wgContLang ) ) {
332 $data = $wgContLang->checkTitleEncoding( $data );
333 }
334 }
335 $data = $this->normalizeUnicode( $data );
336 return $data;
337 } else {
338 return $default;
339 }
340 }
341
342 /**
343 * Fetch a scalar from the input or return $default if it's not set.
344 * Returns a string. Arrays are discarded. Useful for
345 * non-freeform text inputs (e.g. predefined internal text keys
346 * selected by a drop-down menu). For freeform input, see getText().
347 *
348 * @param string $name
349 * @param string $default Optional default (or null)
350 * @return string
351 */
352 public function getVal( $name, $default = null ) {
353 $val = $this->getGPCVal( $this->data, $name, $default );
354 if ( is_array( $val ) ) {
355 $val = $default;
356 }
357 if ( is_null( $val ) ) {
358 return $val;
359 } else {
360 return (string)$val;
361 }
362 }
363
364 /**
365 * Set an arbitrary value into our get/post data.
366 *
367 * @param string $key Key name to use
368 * @param mixed $value Value to set
369 * @return mixed Old value if one was present, null otherwise
370 */
371 public function setVal( $key, $value ) {
372 $ret = isset( $this->data[$key] ) ? $this->data[$key] : null;
373 $this->data[$key] = $value;
374 return $ret;
375 }
376
377 /**
378 * Unset an arbitrary value from our get/post data.
379 *
380 * @param string $key Key name to use
381 * @return mixed Old value if one was present, null otherwise
382 */
383 public function unsetVal( $key ) {
384 if ( !isset( $this->data[$key] ) ) {
385 $ret = null;
386 } else {
387 $ret = $this->data[$key];
388 unset( $this->data[$key] );
389 }
390 return $ret;
391 }
392
393 /**
394 * Fetch an array from the input or return $default if it's not set.
395 * If source was scalar, will return an array with a single element.
396 * If no source and no default, returns null.
397 *
398 * @param string $name
399 * @param array $default Optional default (or null)
400 * @return array
401 */
402 public function getArray( $name, $default = null ) {
403 $val = $this->getGPCVal( $this->data, $name, $default );
404 if ( is_null( $val ) ) {
405 return null;
406 } else {
407 return (array)$val;
408 }
409 }
410
411 /**
412 * Fetch an array of integers, or return $default if it's not set.
413 * If source was scalar, will return an array with a single element.
414 * If no source and no default, returns null.
415 * If an array is returned, contents are guaranteed to be integers.
416 *
417 * @param string $name
418 * @param array $default Option default (or null)
419 * @return array Array of ints
420 */
421 public function getIntArray( $name, $default = null ) {
422 $val = $this->getArray( $name, $default );
423 if ( is_array( $val ) ) {
424 $val = array_map( 'intval', $val );
425 }
426 return $val;
427 }
428
429 /**
430 * Fetch an integer value from the input or return $default if not set.
431 * Guaranteed to return an integer; non-numeric input will typically
432 * return 0.
433 *
434 * @param string $name
435 * @param int $default
436 * @return int
437 */
438 public function getInt( $name, $default = 0 ) {
439 return intval( $this->getVal( $name, $default ) );
440 }
441
442 /**
443 * Fetch an integer value from the input or return null if empty.
444 * Guaranteed to return an integer or null; non-numeric input will
445 * typically return null.
446 *
447 * @param string $name
448 * @return int|null
449 */
450 public function getIntOrNull( $name ) {
451 $val = $this->getVal( $name );
452 return is_numeric( $val )
453 ? intval( $val )
454 : null;
455 }
456
457 /**
458 * Fetch a floating point value from the input or return $default if not set.
459 * Guaranteed to return a float; non-numeric input will typically
460 * return 0.
461 *
462 * @since 1.23
463 * @param string $name
464 * @param float $default
465 * @return float
466 */
467 public function getFloat( $name, $default = 0.0 ) {
468 return floatval( $this->getVal( $name, $default ) );
469 }
470
471 /**
472 * Fetch a boolean value from the input or return $default if not set.
473 * Guaranteed to return true or false, with normal PHP semantics for
474 * boolean interpretation of strings.
475 *
476 * @param string $name
477 * @param bool $default
478 * @return bool
479 */
480 public function getBool( $name, $default = false ) {
481 return (bool)$this->getVal( $name, $default );
482 }
483
484 /**
485 * Fetch a boolean value from the input or return $default if not set.
486 * Unlike getBool, the string "false" will result in boolean false, which is
487 * useful when interpreting information sent from JavaScript.
488 *
489 * @param string $name
490 * @param bool $default
491 * @return bool
492 */
493 public function getFuzzyBool( $name, $default = false ) {
494 return $this->getBool( $name, $default ) && strcasecmp( $this->getVal( $name ), 'false' ) !== 0;
495 }
496
497 /**
498 * Return true if the named value is set in the input, whatever that
499 * value is (even "0"). Return false if the named value is not set.
500 * Example use is checking for the presence of check boxes in forms.
501 *
502 * @param string $name
503 * @return bool
504 */
505 public function getCheck( $name ) {
506 # Checkboxes and buttons are only present when clicked
507 # Presence connotes truth, absence false
508 return $this->getVal( $name, null ) !== null;
509 }
510
511 /**
512 * Fetch a text string from the given array or return $default if it's not
513 * set. Carriage returns are stripped from the text, and with some language
514 * modules there is an input transliteration applied. This should generally
515 * be used for form "<textarea>" and "<input>" fields. Used for
516 * user-supplied freeform text input (for which input transformations may
517 * be required - e.g. Esperanto x-coding).
518 *
519 * @param string $name
520 * @param string $default Optional
521 * @return string
522 */
523 public function getText( $name, $default = '' ) {
524 global $wgContLang;
525 $val = $this->getVal( $name, $default );
526 return str_replace( "\r\n", "\n",
527 $wgContLang->recodeInput( $val ) );
528 }
529
530 /**
531 * Extracts the given named values into an array.
532 * If no arguments are given, returns all input values.
533 * No transformation is performed on the values.
534 *
535 * @return array
536 */
537 public function getValues() {
538 $names = func_get_args();
539 if ( count( $names ) == 0 ) {
540 $names = array_keys( $this->data );
541 }
542
543 $retVal = array();
544 foreach ( $names as $name ) {
545 $value = $this->getGPCVal( $this->data, $name, null );
546 if ( !is_null( $value ) ) {
547 $retVal[$name] = $value;
548 }
549 }
550 return $retVal;
551 }
552
553 /**
554 * Returns the names of all input values excluding those in $exclude.
555 *
556 * @param array $exclude
557 * @return array
558 */
559 public function getValueNames( $exclude = array() ) {
560 return array_diff( array_keys( $this->getValues() ), $exclude );
561 }
562
563 /**
564 * Get the values passed in the query string.
565 * No transformation is performed on the values.
566 *
567 * @return array
568 */
569 public function getQueryValues() {
570 return $_GET;
571 }
572
573 /**
574 * Return the contents of the Query with no decoding. Use when you need to
575 * know exactly what was sent, e.g. for an OAuth signature over the elements.
576 *
577 * @return string
578 */
579 public function getRawQueryString() {
580 return $_SERVER['QUERY_STRING'];
581 }
582
583 /**
584 * Return the contents of the POST with no decoding. Use when you need to
585 * know exactly what was sent, e.g. for an OAuth signature over the elements.
586 *
587 * @return string
588 */
589 public function getRawPostString() {
590 if ( !$this->wasPosted() ) {
591 return '';
592 }
593 return $this->getRawInput();
594 }
595
596 /**
597 * Return the raw request body, with no processing. Cached since some methods
598 * disallow reading the stream more than once. As stated in the php docs, this
599 * does not work with enctype="multipart/form-data".
600 *
601 * @return string
602 */
603 public function getRawInput() {
604 static $input = null;
605 if ( $input === null ) {
606 $input = file_get_contents( 'php://input' );
607 }
608 return $input;
609 }
610
611 /**
612 * Get the HTTP method used for this request.
613 *
614 * @return string
615 */
616 public function getMethod() {
617 return isset( $_SERVER['REQUEST_METHOD'] ) ? $_SERVER['REQUEST_METHOD'] : 'GET';
618 }
619
620 /**
621 * Returns true if the present request was reached by a POST operation,
622 * false otherwise (GET, HEAD, or command-line).
623 *
624 * Note that values retrieved by the object may come from the
625 * GET URL etc even on a POST request.
626 *
627 * @return bool
628 */
629 public function wasPosted() {
630 return $this->getMethod() == 'POST';
631 }
632
633 /**
634 * Returns true if there is a session cookie set.
635 * This does not necessarily mean that the user is logged in!
636 *
637 * If you want to check for an open session, use session_id()
638 * instead; that will also tell you if the session was opened
639 * during the current request (in which case the cookie will
640 * be sent back to the client at the end of the script run).
641 *
642 * @return bool
643 */
644 public function checkSessionCookie() {
645 return isset( $_COOKIE[session_name()] );
646 }
647
648 /**
649 * Get a cookie from the $_COOKIE jar
650 *
651 * @param string $key The name of the cookie
652 * @param string $prefix A prefix to use for the cookie name, if not $wgCookiePrefix
653 * @param mixed $default What to return if the value isn't found
654 * @return mixed Cookie value or $default if the cookie not set
655 */
656 public function getCookie( $key, $prefix = null, $default = null ) {
657 if ( $prefix === null ) {
658 global $wgCookiePrefix;
659 $prefix = $wgCookiePrefix;
660 }
661 return $this->getGPCVal( $_COOKIE, $prefix . $key, $default );
662 }
663
664 /**
665 * Return the path and query string portion of the request URI.
666 * This will be suitable for use as a relative link in HTML output.
667 *
668 * @throws MWException
669 * @return string
670 */
671 public function getRequestURL() {
672 if ( isset( $_SERVER['REQUEST_URI'] ) && strlen( $_SERVER['REQUEST_URI'] ) ) {
673 $base = $_SERVER['REQUEST_URI'];
674 } elseif ( isset( $_SERVER['HTTP_X_ORIGINAL_URL'] )
675 && strlen( $_SERVER['HTTP_X_ORIGINAL_URL'] )
676 ) {
677 // Probably IIS; doesn't set REQUEST_URI
678 $base = $_SERVER['HTTP_X_ORIGINAL_URL'];
679 } elseif ( isset( $_SERVER['SCRIPT_NAME'] ) ) {
680 $base = $_SERVER['SCRIPT_NAME'];
681 if ( isset( $_SERVER['QUERY_STRING'] ) && $_SERVER['QUERY_STRING'] != '' ) {
682 $base .= '?' . $_SERVER['QUERY_STRING'];
683 }
684 } else {
685 // This shouldn't happen!
686 throw new MWException( "Web server doesn't provide either " .
687 "REQUEST_URI, HTTP_X_ORIGINAL_URL or SCRIPT_NAME. Report details " .
688 "of your web server configuration to http://bugzilla.wikimedia.org/" );
689 }
690 // User-agents should not send a fragment with the URI, but
691 // if they do, and the web server passes it on to us, we
692 // need to strip it or we get false-positive redirect loops
693 // or weird output URLs
694 $hash = strpos( $base, '#' );
695 if ( $hash !== false ) {
696 $base = substr( $base, 0, $hash );
697 }
698
699 if ( $base[0] == '/' ) {
700 // More than one slash will look like it is protocol relative
701 return preg_replace( '!^/+!', '/', $base );
702 } else {
703 // We may get paths with a host prepended; strip it.
704 return preg_replace( '!^[^:]+://[^/]+/+!', '/', $base );
705 }
706 }
707
708 /**
709 * Return the request URI with the canonical service and hostname, path,
710 * and query string. This will be suitable for use as an absolute link
711 * in HTML or other output.
712 *
713 * If $wgServer is protocol-relative, this will return a fully
714 * qualified URL with the protocol that was used for this request.
715 *
716 * @return string
717 */
718 public function getFullRequestURL() {
719 return wfExpandUrl( $this->getRequestURL(), PROTO_CURRENT );
720 }
721
722 /**
723 * Take an arbitrary query and rewrite the present URL to include it
724 * @deprecated Use appendQueryValue/appendQueryArray instead
725 * @param string $query Query string fragment; do not include initial '?'
726 * @return string
727 */
728 public function appendQuery( $query ) {
729 wfDeprecated( __METHOD__, '1.25' );
730 return $this->appendQueryArray( wfCgiToArray( $query ) );
731 }
732
733 /**
734 * @param string $key
735 * @param string $value
736 * @param bool $onlyquery [deprecated]
737 * @return string
738 */
739 public function appendQueryValue( $key, $value, $onlyquery = true ) {
740 return $this->appendQueryArray( array( $key => $value ), $onlyquery );
741 }
742
743 /**
744 * Appends or replaces value of query variables.
745 *
746 * @param array $array Array of values to replace/add to query
747 * @param bool $onlyquery Whether to only return the query string and not the complete URL [deprecated]
748 * @return string
749 */
750 public function appendQueryArray( $array, $onlyquery = true ) {
751 global $wgTitle;
752 $newquery = $this->getQueryValues();
753 unset( $newquery['title'] );
754 $newquery = array_merge( $newquery, $array );
755 $query = wfArrayToCgi( $newquery );
756 if ( !$onlyquery ) {
757 wfDeprecated( __METHOD__, '1.25' );
758 return $wgTitle->getLocalURL( $query );
759 }
760
761 return $query;
762 }
763
764 /**
765 * Check for limit and offset parameters on the input, and return sensible
766 * defaults if not given. The limit must be positive and is capped at 5000.
767 * Offset must be positive but is not capped.
768 *
769 * @param int $deflimit Limit to use if no input and the user hasn't set the option.
770 * @param string $optionname To specify an option other than rclimit to pull from.
771 * @return array First element is limit, second is offset
772 */
773 public function getLimitOffset( $deflimit = 50, $optionname = 'rclimit' ) {
774 global $wgUser;
775
776 $limit = $this->getInt( 'limit', 0 );
777 if ( $limit < 0 ) {
778 $limit = 0;
779 }
780 if ( ( $limit == 0 ) && ( $optionname != '' ) ) {
781 $limit = $wgUser->getIntOption( $optionname );
782 }
783 if ( $limit <= 0 ) {
784 $limit = $deflimit;
785 }
786 if ( $limit > 5000 ) {
787 $limit = 5000; # We have *some* limits...
788 }
789
790 $offset = $this->getInt( 'offset', 0 );
791 if ( $offset < 0 ) {
792 $offset = 0;
793 }
794
795 return array( $limit, $offset );
796 }
797
798 /**
799 * Return the path to the temporary file where PHP has stored the upload.
800 *
801 * @param string $key
802 * @return string|null String or null if no such file.
803 */
804 public function getFileTempname( $key ) {
805 $file = new WebRequestUpload( $this, $key );
806 return $file->getTempName();
807 }
808
809 /**
810 * Return the upload error or 0
811 *
812 * @param string $key
813 * @return int
814 */
815 public function getUploadError( $key ) {
816 $file = new WebRequestUpload( $this, $key );
817 return $file->getError();
818 }
819
820 /**
821 * Return the original filename of the uploaded file, as reported by
822 * the submitting user agent. HTML-style character entities are
823 * interpreted and normalized to Unicode normalization form C, in part
824 * to deal with weird input from Safari with non-ASCII filenames.
825 *
826 * Other than this the name is not verified for being a safe filename.
827 *
828 * @param string $key
829 * @return string|null String or null if no such file.
830 */
831 public function getFileName( $key ) {
832 $file = new WebRequestUpload( $this, $key );
833 return $file->getName();
834 }
835
836 /**
837 * Return a WebRequestUpload object corresponding to the key
838 *
839 * @param string $key
840 * @return WebRequestUpload
841 */
842 public function getUpload( $key ) {
843 return new WebRequestUpload( $this, $key );
844 }
845
846 /**
847 * Return a handle to WebResponse style object, for setting cookies,
848 * headers and other stuff, for Request being worked on.
849 *
850 * @return WebResponse
851 */
852 public function response() {
853 /* Lazy initialization of response object for this request */
854 if ( !is_object( $this->response ) ) {
855 $class = ( $this instanceof FauxRequest ) ? 'FauxResponse' : 'WebResponse';
856 $this->response = new $class();
857 }
858 return $this->response;
859 }
860
861 /**
862 * Initialise the header list
863 */
864 private function initHeaders() {
865 if ( count( $this->headers ) ) {
866 return;
867 }
868
869 $apacheHeaders = function_exists( 'apache_request_headers' ) ? apache_request_headers() : false;
870 if ( $apacheHeaders ) {
871 foreach ( $apacheHeaders as $tempName => $tempValue ) {
872 $this->headers[strtoupper( $tempName )] = $tempValue;
873 }
874 } else {
875 foreach ( $_SERVER as $name => $value ) {
876 if ( substr( $name, 0, 5 ) === 'HTTP_' ) {
877 $name = str_replace( '_', '-', substr( $name, 5 ) );
878 $this->headers[$name] = $value;
879 } elseif ( $name === 'CONTENT_LENGTH' ) {
880 $this->headers['CONTENT-LENGTH'] = $value;
881 }
882 }
883 }
884 }
885
886 /**
887 * Get an array containing all request headers
888 *
889 * @return array Mapping header name to its value
890 */
891 public function getAllHeaders() {
892 $this->initHeaders();
893 return $this->headers;
894 }
895
896 /**
897 * Get a request header, or false if it isn't set
898 * @param string $name Case-insensitive header name
899 *
900 * @return string|bool False on failure
901 */
902 public function getHeader( $name ) {
903 $this->initHeaders();
904 $name = strtoupper( $name );
905 if ( isset( $this->headers[$name] ) ) {
906 return $this->headers[$name];
907 } else {
908 return false;
909 }
910 }
911
912 /**
913 * Get data from $_SESSION
914 *
915 * @param string $key Name of key in $_SESSION
916 * @return mixed
917 */
918 public function getSessionData( $key ) {
919 if ( !isset( $_SESSION[$key] ) ) {
920 return null;
921 }
922 return $_SESSION[$key];
923 }
924
925 /**
926 * Set session data
927 *
928 * @param string $key Name of key in $_SESSION
929 * @param mixed $data
930 */
931 public function setSessionData( $key, $data ) {
932 $_SESSION[$key] = $data;
933 }
934
935 /**
936 * Check if Internet Explorer will detect an incorrect cache extension in
937 * PATH_INFO or QUERY_STRING. If the request can't be allowed, show an error
938 * message or redirect to a safer URL. Returns true if the URL is OK, and
939 * false if an error message has been shown and the request should be aborted.
940 *
941 * @param array $extWhitelist
942 * @throws HttpError
943 * @return bool
944 */
945 public function checkUrlExtension( $extWhitelist = array() ) {
946 global $wgScriptExtension;
947 $extWhitelist[] = ltrim( $wgScriptExtension, '.' );
948 if ( IEUrlExtension::areServerVarsBad( $_SERVER, $extWhitelist ) ) {
949 if ( !$this->wasPosted() ) {
950 $newUrl = IEUrlExtension::fixUrlForIE6(
951 $this->getFullRequestURL(), $extWhitelist );
952 if ( $newUrl !== false ) {
953 $this->doSecurityRedirect( $newUrl );
954 return false;
955 }
956 }
957 throw new HttpError( 403,
958 'Invalid file extension found in the path info or query string.' );
959 }
960 return true;
961 }
962
963 /**
964 * Attempt to redirect to a URL with a QUERY_STRING that's not dangerous in
965 * IE 6. Returns true if it was successful, false otherwise.
966 *
967 * @param string $url
968 * @return bool
969 */
970 protected function doSecurityRedirect( $url ) {
971 header( 'Location: ' . $url );
972 header( 'Content-Type: text/html' );
973 $encUrl = htmlspecialchars( $url );
974 echo <<<HTML
975 <html>
976 <head>
977 <title>Security redirect</title>
978 </head>
979 <body>
980 <h1>Security redirect</h1>
981 <p>
982 We can't serve non-HTML content from the URL you have requested, because
983 Internet Explorer would interpret it as an incorrect and potentially dangerous
984 content type.</p>
985 <p>Instead, please use <a href="$encUrl">this URL</a>, which is the same as the
986 URL you have requested, except that "&amp;*" is appended. This prevents Internet
987 Explorer from seeing a bogus file extension.
988 </p>
989 </body>
990 </html>
991 HTML;
992 echo "\n";
993 return true;
994 }
995
996 /**
997 * Parse the Accept-Language header sent by the client into an array
998 *
999 * @return array Array( languageCode => q-value ) sorted by q-value in
1000 * descending order then appearing time in the header in ascending order.
1001 * May contain the "language" '*', which applies to languages other than those explicitly listed.
1002 * This is aligned with rfc2616 section 14.4
1003 * Preference for earlier languages appears in rfc3282 as an extension to HTTP/1.1.
1004 */
1005 public function getAcceptLang() {
1006 // Modified version of code found at
1007 // http://www.thefutureoftheweb.com/blog/use-accept-language-header
1008 $acceptLang = $this->getHeader( 'Accept-Language' );
1009 if ( !$acceptLang ) {
1010 return array();
1011 }
1012
1013 // Return the language codes in lower case
1014 $acceptLang = strtolower( $acceptLang );
1015
1016 // Break up string into pieces (languages and q factors)
1017 $lang_parse = null;
1018 preg_match_all(
1019 '/([a-z]{1,8}(-[a-z]{1,8})*|\*)\s*(;\s*q\s*=\s*(1(\.0{0,3})?|0(\.[0-9]{0,3})?)?)?/',
1020 $acceptLang,
1021 $lang_parse
1022 );
1023
1024 if ( !count( $lang_parse[1] ) ) {
1025 return array();
1026 }
1027
1028 $langcodes = $lang_parse[1];
1029 $qvalues = $lang_parse[4];
1030 $indices = range( 0, count( $lang_parse[1] ) - 1 );
1031
1032 // Set default q factor to 1
1033 foreach ( $indices as $index ) {
1034 if ( $qvalues[$index] === '' ) {
1035 $qvalues[$index] = 1;
1036 } elseif ( $qvalues[$index] == 0 ) {
1037 unset( $langcodes[$index], $qvalues[$index], $indices[$index] );
1038 }
1039 }
1040
1041 // Sort list. First by $qvalues, then by order. Reorder $langcodes the same way
1042 array_multisort( $qvalues, SORT_DESC, SORT_NUMERIC, $indices, $langcodes );
1043
1044 // Create a list like "en" => 0.8
1045 $langs = array_combine( $langcodes, $qvalues );
1046
1047 return $langs;
1048 }
1049
1050 /**
1051 * Fetch the raw IP from the request
1052 *
1053 * @since 1.19
1054 *
1055 * @throws MWException
1056 * @return string
1057 */
1058 protected function getRawIP() {
1059 if ( !isset( $_SERVER['REMOTE_ADDR'] ) ) {
1060 return null;
1061 }
1062
1063 if ( is_array( $_SERVER['REMOTE_ADDR'] ) || strpos( $_SERVER['REMOTE_ADDR'], ',' ) !== false ) {
1064 throw new MWException( __METHOD__
1065 . " : Could not determine the remote IP address due to multiple values." );
1066 } else {
1067 $ipchain = $_SERVER['REMOTE_ADDR'];
1068 }
1069
1070 return IP::canonicalize( $ipchain );
1071 }
1072
1073 /**
1074 * Work out the IP address based on various globals
1075 * For trusted proxies, use the XFF client IP (first of the chain)
1076 *
1077 * @since 1.19
1078 *
1079 * @throws MWException
1080 * @return string
1081 */
1082 public function getIP() {
1083 global $wgUsePrivateIPs;
1084
1085 # Return cached result
1086 if ( $this->ip !== null ) {
1087 return $this->ip;
1088 }
1089
1090 # collect the originating ips
1091 $ip = $this->getRawIP();
1092 if ( !$ip ) {
1093 throw new MWException( 'Unable to determine IP.' );
1094 }
1095
1096 # Append XFF
1097 $forwardedFor = $this->getHeader( 'X-Forwarded-For' );
1098 if ( $forwardedFor !== false ) {
1099 $isConfigured = IP::isConfiguredProxy( $ip );
1100 $ipchain = array_map( 'trim', explode( ',', $forwardedFor ) );
1101 $ipchain = array_reverse( $ipchain );
1102 array_unshift( $ipchain, $ip );
1103
1104 # Step through XFF list and find the last address in the list which is a
1105 # trusted server. Set $ip to the IP address given by that trusted server,
1106 # unless the address is not sensible (e.g. private). However, prefer private
1107 # IP addresses over proxy servers controlled by this site (more sensible).
1108 # Note that some XFF values might be "unknown" with Squid/Varnish.
1109 foreach ( $ipchain as $i => $curIP ) {
1110 $curIP = IP::sanitizeIP( IP::canonicalize( $curIP ) );
1111 if ( !$curIP || !isset( $ipchain[$i + 1] ) || $ipchain[$i + 1] === 'unknown'
1112 || !IP::isTrustedProxy( $curIP )
1113 ) {
1114 break; // IP is not valid/trusted or does not point to anything
1115 }
1116 if (
1117 IP::isPublic( $ipchain[$i + 1] ) ||
1118 $wgUsePrivateIPs ||
1119 IP::isConfiguredProxy( $curIP ) // bug 48919; treat IP as sane
1120 ) {
1121 // Follow the next IP according to the proxy
1122 $nextIP = IP::canonicalize( $ipchain[$i + 1] );
1123 if ( !$nextIP && $isConfigured ) {
1124 // We have not yet made it past CDN/proxy servers of this site,
1125 // so either they are misconfigured or there is some IP spoofing.
1126 throw new MWException( "Invalid IP given in XFF '$forwardedFor'." );
1127 }
1128 $ip = $nextIP;
1129 // keep traversing the chain
1130 continue;
1131 }
1132 break;
1133 }
1134 }
1135
1136 # Allow extensions to improve our guess
1137 Hooks::run( 'GetIP', array( &$ip ) );
1138
1139 if ( !$ip ) {
1140 throw new MWException( "Unable to determine IP." );
1141 }
1142
1143 wfDebug( "IP: $ip\n" );
1144 $this->ip = $ip;
1145 return $ip;
1146 }
1147
1148 /**
1149 * @param string $ip
1150 * @return void
1151 * @since 1.21
1152 */
1153 public function setIP( $ip ) {
1154 $this->ip = $ip;
1155 }
1156 }
1157
1158 /**
1159 * Object to access the $_FILES array
1160 */
1161 class WebRequestUpload {
1162 protected $request;
1163 protected $doesExist;
1164 protected $fileInfo;
1165
1166 /**
1167 * Constructor. Should only be called by WebRequest
1168 *
1169 * @param WebRequest $request The associated request
1170 * @param string $key Key in $_FILES array (name of form field)
1171 */
1172 public function __construct( $request, $key ) {
1173 $this->request = $request;
1174 $this->doesExist = isset( $_FILES[$key] );
1175 if ( $this->doesExist ) {
1176 $this->fileInfo = $_FILES[$key];
1177 }
1178 }
1179
1180 /**
1181 * Return whether a file with this name was uploaded.
1182 *
1183 * @return bool
1184 */
1185 public function exists() {
1186 return $this->doesExist;
1187 }
1188
1189 /**
1190 * Return the original filename of the uploaded file
1191 *
1192 * @return string|null Filename or null if non-existent
1193 */
1194 public function getName() {
1195 if ( !$this->exists() ) {
1196 return null;
1197 }
1198
1199 global $wgContLang;
1200 $name = $this->fileInfo['name'];
1201
1202 # Safari sends filenames in HTML-encoded Unicode form D...
1203 # Horrid and evil! Let's try to make some kind of sense of it.
1204 $name = Sanitizer::decodeCharReferences( $name );
1205 $name = $wgContLang->normalize( $name );
1206 wfDebug( __METHOD__ . ": {$this->fileInfo['name']} normalized to '$name'\n" );
1207 return $name;
1208 }
1209
1210 /**
1211 * Return the file size of the uploaded file
1212 *
1213 * @return int File size or zero if non-existent
1214 */
1215 public function getSize() {
1216 if ( !$this->exists() ) {
1217 return 0;
1218 }
1219
1220 return $this->fileInfo['size'];
1221 }
1222
1223 /**
1224 * Return the path to the temporary file
1225 *
1226 * @return string|null Path or null if non-existent
1227 */
1228 public function getTempName() {
1229 if ( !$this->exists() ) {
1230 return null;
1231 }
1232
1233 return $this->fileInfo['tmp_name'];
1234 }
1235
1236 /**
1237 * Return the upload error. See link for explanation
1238 * http://www.php.net/manual/en/features.file-upload.errors.php
1239 *
1240 * @return int One of the UPLOAD_ constants, 0 if non-existent
1241 */
1242 public function getError() {
1243 if ( !$this->exists() ) {
1244 return 0; # UPLOAD_ERR_OK
1245 }
1246
1247 return $this->fileInfo['error'];
1248 }
1249
1250 /**
1251 * Returns whether this upload failed because of overflow of a maximum set
1252 * in php.ini
1253 *
1254 * @return bool
1255 */
1256 public function isIniSizeOverflow() {
1257 if ( $this->getError() == UPLOAD_ERR_INI_SIZE ) {
1258 # PHP indicated that upload_max_filesize is exceeded
1259 return true;
1260 }
1261
1262 $contentLength = $this->request->getHeader( 'CONTENT_LENGTH' );
1263 if ( $contentLength > wfShorthandToInteger( ini_get( 'post_max_size' ) ) ) {
1264 # post_max_size is exceeded
1265 return true;
1266 }
1267
1268 return false;
1269 }
1270 }
1271
1272 /**
1273 * WebRequest clone which takes values from a provided array.
1274 *
1275 * @ingroup HTTP
1276 */
1277 class FauxRequest extends WebRequest {
1278 private $wasPosted = false;
1279 private $session = array();
1280 private $requestUrl;
1281
1282 /**
1283 * @param array $data Array of *non*-urlencoded key => value pairs, the
1284 * fake GET/POST values
1285 * @param bool $wasPosted Whether to treat the data as POST
1286 * @param array|null $session Session array or null
1287 * @param string $protocol 'http' or 'https'
1288 * @throws MWException
1289 */
1290 public function __construct( $data = array(), $wasPosted = false,
1291 $session = null, $protocol = 'http'
1292 ) {
1293 $this->requestTime = microtime( true );
1294
1295 if ( is_array( $data ) ) {
1296 $this->data = $data;
1297 } else {
1298 throw new MWException( "FauxRequest() got bogus data" );
1299 }
1300 $this->wasPosted = $wasPosted;
1301 if ( $session ) {
1302 $this->session = $session;
1303 }
1304 $this->protocol = $protocol;
1305 }
1306
1307 /**
1308 * @param string $method
1309 * @throws MWException
1310 */
1311 private function notImplemented( $method ) {
1312 throw new MWException( "{$method}() not implemented" );
1313 }
1314
1315 /**
1316 * @param string $name
1317 * @param string $default
1318 * @return string
1319 */
1320 public function getText( $name, $default = '' ) {
1321 # Override; don't recode since we're using internal data
1322 return (string)$this->getVal( $name, $default );
1323 }
1324
1325 /**
1326 * @return array
1327 */
1328 public function getValues() {
1329 return $this->data;
1330 }
1331
1332 /**
1333 * @return array
1334 */
1335 public function getQueryValues() {
1336 if ( $this->wasPosted ) {
1337 return array();
1338 } else {
1339 return $this->data;
1340 }
1341 }
1342
1343 public function getMethod() {
1344 return $this->wasPosted ? 'POST' : 'GET';
1345 }
1346
1347 /**
1348 * @return bool
1349 */
1350 public function wasPosted() {
1351 return $this->wasPosted;
1352 }
1353
1354 public function getCookie( $key, $prefix = null, $default = null ) {
1355 return $default;
1356 }
1357
1358 public function checkSessionCookie() {
1359 return false;
1360 }
1361
1362 public function setRequestURL( $url ) {
1363 $this->requestUrl = $url;
1364 }
1365
1366 public function getRequestURL() {
1367 if ( $this->requestUrl === null ) {
1368 throw new MWException( 'Request URL not set' );
1369 }
1370 return $this->requestUrl;
1371 }
1372
1373 public function getProtocol() {
1374 return $this->protocol;
1375 }
1376
1377 /**
1378 * @param string $name The name of the header to get (case insensitive).
1379 * @return bool|string
1380 */
1381 public function getHeader( $name ) {
1382 $name = strtoupper( $name );
1383 return isset( $this->headers[$name] ) ? $this->headers[$name] : false;
1384 }
1385
1386 /**
1387 * @param string $name
1388 * @param string $val
1389 */
1390 public function setHeader( $name, $val ) {
1391 $name = strtoupper( $name );
1392 $this->headers[$name] = $val;
1393 }
1394
1395 /**
1396 * @param string $key
1397 * @return array|null
1398 */
1399 public function getSessionData( $key ) {
1400 if ( isset( $this->session[$key] ) ) {
1401 return $this->session[$key];
1402 }
1403 return null;
1404 }
1405
1406 /**
1407 * @param string $key
1408 * @param array $data
1409 */
1410 public function setSessionData( $key, $data ) {
1411 $this->session[$key] = $data;
1412 }
1413
1414 /**
1415 * @return array|mixed|null
1416 */
1417 public function getSessionArray() {
1418 return $this->session;
1419 }
1420
1421 /**
1422 * FauxRequests shouldn't depend on raw request data (but that could be implemented here)
1423 * @return string
1424 */
1425 public function getRawQueryString() {
1426 return '';
1427 }
1428
1429 /**
1430 * FauxRequests shouldn't depend on raw request data (but that could be implemented here)
1431 * @return string
1432 */
1433 public function getRawPostString() {
1434 return '';
1435 }
1436
1437 /**
1438 * FauxRequests shouldn't depend on raw request data (but that could be implemented here)
1439 * @return string
1440 */
1441 public function getRawInput() {
1442 return '';
1443 }
1444
1445 /**
1446 * @param array $extWhitelist
1447 * @return bool
1448 */
1449 public function checkUrlExtension( $extWhitelist = array() ) {
1450 return true;
1451 }
1452
1453 /**
1454 * @return string
1455 */
1456 protected function getRawIP() {
1457 return '127.0.0.1';
1458 }
1459 }
1460
1461 /**
1462 * Similar to FauxRequest, but only fakes URL parameters and method
1463 * (POST or GET) and use the base request for the remaining stuff
1464 * (cookies, session and headers).
1465 *
1466 * @ingroup HTTP
1467 * @since 1.19
1468 */
1469 class DerivativeRequest extends FauxRequest {
1470 private $base;
1471
1472 /**
1473 * @param WebRequest $base
1474 * @param array $data Array of *non*-urlencoded key => value pairs, the
1475 * fake GET/POST values
1476 * @param bool $wasPosted Whether to treat the data as POST
1477 */
1478 public function __construct( WebRequest $base, $data, $wasPosted = false ) {
1479 $this->base = $base;
1480 parent::__construct( $data, $wasPosted );
1481 }
1482
1483 public function getCookie( $key, $prefix = null, $default = null ) {
1484 return $this->base->getCookie( $key, $prefix, $default );
1485 }
1486
1487 public function checkSessionCookie() {
1488 return $this->base->checkSessionCookie();
1489 }
1490
1491 public function getHeader( $name ) {
1492 return $this->base->getHeader( $name );
1493 }
1494
1495 public function getAllHeaders() {
1496 return $this->base->getAllHeaders();
1497 }
1498
1499 public function getSessionData( $key ) {
1500 return $this->base->getSessionData( $key );
1501 }
1502
1503 public function setSessionData( $key, $data ) {
1504 $this->base->setSessionData( $key, $data );
1505 }
1506
1507 public function getAcceptLang() {
1508 return $this->base->getAcceptLang();
1509 }
1510
1511 public function getIP() {
1512 return $this->base->getIP();
1513 }
1514
1515 public function getProtocol() {
1516 return $this->base->getProtocol();
1517 }
1518
1519 public function getElapsedTime() {
1520 return $this->base->getElapsedTime();
1521 }
1522 }