]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - includes/WebRequest.php
MediaWiki 1.17.0
[autoinstalls/mediawiki.git] / includes / WebRequest.php
1 <?php
2 /**
3  * Deal with importing all those nasssty globals and things
4  *
5  * Copyright © 2003 Brion Vibber <brion@pobox.com>
6  * http://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, handling remove of "magic quotes" slashes,
29  * stripping illegal input characters and 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         public function __construct() {
48                 /// @todo Fixme: this preemptive de-quoting can interfere with other web libraries
49                 ///        and increases our memory footprint. It would be cleaner to do on
50                 ///        demand; but currently we have no wrapper for $_SERVER etc.
51                 $this->checkMagicQuotes();
52
53                 // POST overrides GET data
54                 // We don't use $_REQUEST here to avoid interference from cookies...
55                 $this->data = $_POST + $_GET;
56         }
57
58         /**
59          * Check for title, action, and/or variant data in the URL
60          * and interpolate it into the GET variables.
61          * This should only be run after $wgContLang is available,
62          * as we may need the list of language variants to determine
63          * available variant URLs.
64          */
65         public function interpolateTitle() {
66                 global $wgUsePathInfo;
67
68                 // bug 16019: title interpolation on API queries is useless and possible harmful
69                 if ( defined( 'MW_API' ) ) {
70                         return;
71                 }
72
73                 if ( $wgUsePathInfo ) {
74                         // PATH_INFO is mangled due to http://bugs.php.net/bug.php?id=31892
75                         // And also by Apache 2.x, double slashes are converted to single slashes.
76                         // So we will use REQUEST_URI if possible.
77                         $matches = array();
78
79                         if ( !empty( $_SERVER['REQUEST_URI'] ) ) {
80                                 // Slurp out the path portion to examine...
81                                 $url = $_SERVER['REQUEST_URI'];
82                                 if ( !preg_match( '!^https?://!', $url ) ) {
83                                         $url = 'http://unused' . $url;
84                                 }
85                                 $a = parse_url( $url );
86                                 if( $a ) {
87                                         $path = isset( $a['path'] ) ? $a['path'] : '';
88
89                                         global $wgScript;
90                                         if( $path == $wgScript ) {
91                                                 // Script inside a rewrite path?
92                                                 // Abort to keep from breaking...
93                                                 return;
94                                         }
95                                         // Raw PATH_INFO style
96                                         $matches = $this->extractTitle( $path, "$wgScript/$1" );
97
98                                         global $wgArticlePath;
99                                         if( !$matches && $wgArticlePath ) {
100                                                 $matches = $this->extractTitle( $path, $wgArticlePath );
101                                         }
102
103                                         global $wgActionPaths;
104                                         if( !$matches && $wgActionPaths ) {
105                                                 $matches = $this->extractTitle( $path, $wgActionPaths, 'action' );
106                                         }
107
108                                         global $wgVariantArticlePath, $wgContLang;
109                                         if( !$matches && $wgVariantArticlePath ) {
110                                                 $variantPaths = array();
111                                                 foreach( $wgContLang->getVariants() as $variant ) {
112                                                         $variantPaths[$variant] =
113                                                                 str_replace( '$2', $variant, $wgVariantArticlePath );
114                                                 }
115                                                 $matches = $this->extractTitle( $path, $variantPaths, 'variant' );
116                                         }
117                                 }
118                         } elseif ( isset( $_SERVER['ORIG_PATH_INFO'] ) && $_SERVER['ORIG_PATH_INFO'] != '' ) {
119                                 // Mangled PATH_INFO
120                                 // http://bugs.php.net/bug.php?id=31892
121                                 // Also reported when ini_get('cgi.fix_pathinfo')==false
122                                 $matches['title'] = substr( $_SERVER['ORIG_PATH_INFO'], 1 );
123
124                         } elseif ( isset( $_SERVER['PATH_INFO'] ) && ($_SERVER['PATH_INFO'] != '') ) {
125                                 // Regular old PATH_INFO yay
126                                 $matches['title'] = substr( $_SERVER['PATH_INFO'], 1 );
127                         }
128                         foreach( $matches as $key => $val) {
129                                 $this->data[$key] = $_GET[$key] = $_REQUEST[$key] = $val;
130                         }
131                 }
132         }
133
134         /**
135          * Internal URL rewriting function; tries to extract page title and,
136          * optionally, one other fixed parameter value from a URL path.
137          *
138          * @param $path string: the URL path given from the client
139          * @param $bases array: one or more URLs, optionally with $1 at the end
140          * @param $key string: if provided, the matching key in $bases will be
141          *             passed on as the value of this URL parameter
142          * @return array of URL variables to interpolate; empty if no match
143          */
144         private function extractTitle( $path, $bases, $key=false ) {
145                 foreach( (array)$bases as $keyValue => $base ) {
146                         // Find the part after $wgArticlePath
147                         $base = str_replace( '$1', '', $base );
148                         $baseLen = strlen( $base );
149                         if( substr( $path, 0, $baseLen ) == $base ) {
150                                 $raw = substr( $path, $baseLen );
151                                 if( $raw !== '' ) {
152                                         $matches = array( 'title' => rawurldecode( $raw ) );
153                                         if( $key ) {
154                                                 $matches[$key] = $keyValue;
155                                         }
156                                         return $matches;
157                                 }
158                         }
159                 }
160                 return array();
161         }
162
163         /**
164          * Recursively strips slashes from the given array;
165          * used for undoing the evil that is magic_quotes_gpc.
166          *
167          * @param $arr array: will be modified
168          * @return array the original array
169          */
170         private function &fix_magic_quotes( &$arr ) {
171                 foreach( $arr as $key => $val ) {
172                         if( is_array( $val ) ) {
173                                 $this->fix_magic_quotes( $arr[$key] );
174                         } else {
175                                 $arr[$key] = stripslashes( $val );
176                         }
177                 }
178                 return $arr;
179         }
180
181         /**
182          * If magic_quotes_gpc option is on, run the global arrays
183          * through fix_magic_quotes to strip out the stupid slashes.
184          * WARNING: This should only be done once! Running a second
185          * time could damage the values.
186          */
187         private function checkMagicQuotes() {
188                 $mustFixQuotes = function_exists( 'get_magic_quotes_gpc' )
189                         && get_magic_quotes_gpc();
190                 if( $mustFixQuotes ) {
191                         $this->fix_magic_quotes( $_COOKIE );
192                         $this->fix_magic_quotes( $_ENV );
193                         $this->fix_magic_quotes( $_GET );
194                         $this->fix_magic_quotes( $_POST );
195                         $this->fix_magic_quotes( $_REQUEST );
196                         $this->fix_magic_quotes( $_SERVER );
197                 }
198         }
199
200         /**
201          * Recursively normalizes UTF-8 strings in the given array.
202          *
203          * @param $data string or array
204          * @return cleaned-up version of the given
205          * @private
206          */
207         function normalizeUnicode( $data ) {
208                 if( is_array( $data ) ) {
209                         foreach( $data as $key => $val ) {
210                                 $data[$key] = $this->normalizeUnicode( $val );
211                         }
212                 } else {
213                         global $wgContLang;
214                         $data = $wgContLang->normalize( $data );
215                 }
216                 return $data;
217         }
218
219         /**
220          * Fetch a value from the given array or return $default if it's not set.
221          *
222          * @param $arr Array
223          * @param $name String
224          * @param $default Mixed
225          * @return mixed
226          */
227         private function getGPCVal( $arr, $name, $default ) {
228                 # PHP is so nice to not touch input data, except sometimes:
229                 # http://us2.php.net/variables.external#language.variables.external.dot-in-names
230                 # Work around PHP *feature* to avoid *bugs* elsewhere.
231                 $name = strtr( $name, '.', '_' );
232                 if( isset( $arr[$name] ) ) {
233                         global $wgContLang;
234                         $data = $arr[$name];
235                         if( isset( $_GET[$name] ) && !is_array( $data ) ) {
236                                 # Check for alternate/legacy character encoding.
237                                 if( isset( $wgContLang ) ) {
238                                         $data = $wgContLang->checkTitleEncoding( $data );
239                                 }
240                         }
241                         $data = $this->normalizeUnicode( $data );
242                         return $data;
243                 } else {
244                         taint( $default );
245                         return $default;
246                 }
247         }
248
249         /**
250          * Fetch a scalar from the input or return $default if it's not set.
251          * Returns a string. Arrays are discarded. Useful for
252          * non-freeform text inputs (e.g. predefined internal text keys
253          * selected by a drop-down menu). For freeform input, see getText().
254          *
255          * @param $name String
256          * @param $default String: optional default (or NULL)
257          * @return String
258          */
259         public function getVal( $name, $default = null ) {
260                 $val = $this->getGPCVal( $this->data, $name, $default );
261                 if( is_array( $val ) ) {
262                         $val = $default;
263                 }
264                 if( is_null( $val ) ) {
265                         return $val;
266                 } else {
267                         return (string)$val;
268                 }
269         }
270
271         /**
272          * Set an aribtrary value into our get/post data.
273          *
274          * @param $key String: key name to use
275          * @param $value Mixed: value to set
276          * @return Mixed: old value if one was present, null otherwise
277          */
278         public function setVal( $key, $value ) {
279                 $ret = isset( $this->data[$key] ) ? $this->data[$key] : null;
280                 $this->data[$key] = $value;
281                 return $ret;
282         }
283
284         /**
285          * Fetch an array from the input or return $default if it's not set.
286          * If source was scalar, will return an array with a single element.
287          * If no source and no default, returns NULL.
288          *
289          * @param $name String
290          * @param $default Array: optional default (or NULL)
291          * @return Array
292          */
293         public function getArray( $name, $default = null ) {
294                 $val = $this->getGPCVal( $this->data, $name, $default );
295                 if( is_null( $val ) ) {
296                         return null;
297                 } else {
298                         return (array)$val;
299                 }
300         }
301
302         /**
303          * Fetch an array of integers, or return $default if it's not set.
304          * If source was scalar, will return an array with a single element.
305          * If no source and no default, returns NULL.
306          * If an array is returned, contents are guaranteed to be integers.
307          *
308          * @param $name String
309          * @param $default Array: option default (or NULL)
310          * @return Array of ints
311          */
312         public function getIntArray( $name, $default = null ) {
313                 $val = $this->getArray( $name, $default );
314                 if( is_array( $val ) ) {
315                         $val = array_map( 'intval', $val );
316                 }
317                 return $val;
318         }
319
320         /**
321          * Fetch an integer value from the input or return $default if not set.
322          * Guaranteed to return an integer; non-numeric input will typically
323          * return 0.
324          *
325          * @param $name String
326          * @param $default Integer
327          * @return Integer
328          */
329         public function getInt( $name, $default = 0 ) {
330                 return intval( $this->getVal( $name, $default ) );
331         }
332
333         /**
334          * Fetch an integer value from the input or return null if empty.
335          * Guaranteed to return an integer or null; non-numeric input will
336          * typically return null.
337          *
338          * @param $name String
339          * @return Integer
340          */
341         public function getIntOrNull( $name ) {
342                 $val = $this->getVal( $name );
343                 return is_numeric( $val )
344                         ? intval( $val )
345                         : null;
346         }
347
348         /**
349          * Fetch a boolean value from the input or return $default if not set.
350          * Guaranteed to return true or false, with normal PHP semantics for
351          * boolean interpretation of strings.
352          *
353          * @param $name String
354          * @param $default Boolean
355          * @return Boolean
356          */
357         public function getBool( $name, $default = false ) {
358                 return (bool)$this->getVal( $name, $default );
359         }
360         
361         /**
362          * Fetch a boolean value from the input or return $default if not set.
363          * Unlike getBool, the string "false" will result in boolean false, which is
364          * useful when interpreting information sent from JavaScript.
365          *
366          * @param $name String
367          * @param $default Boolean
368          * @return Boolean
369          */
370         public function getFuzzyBool( $name, $default = false ) {
371                 return $this->getBool( $name, $default ) && strcasecmp( $this->getVal( $name ), 'false' ) !== 0;
372         }
373
374         /**
375          * Return true if the named value is set in the input, whatever that
376          * value is (even "0"). Return false if the named value is not set.
377          * Example use is checking for the presence of check boxes in forms.
378          *
379          * @param $name String
380          * @return Boolean
381          */
382         public function getCheck( $name ) {
383                 # Checkboxes and buttons are only present when clicked
384                 # Presence connotes truth, abscense false
385                 $val = $this->getVal( $name, null );
386                 return isset( $val );
387         }
388
389         /**
390          * Fetch a text string from the given array or return $default if it's not
391          * set. Carriage returns are stripped from the text, and with some language
392          * modules there is an input transliteration applied. This should generally
393          * be used for form <textarea> and <input> fields. Used for user-supplied
394          * freeform text input (for which input transformations may be required - e.g.
395          * Esperanto x-coding).
396          *
397          * @param $name String
398          * @param $default String: optional
399          * @return String
400          */
401         public function getText( $name, $default = '' ) {
402                 global $wgContLang;
403                 $val = $this->getVal( $name, $default );
404                 return str_replace( "\r\n", "\n",
405                         $wgContLang->recodeInput( $val ) );
406         }
407
408         /**
409          * Extracts the given named values into an array.
410          * If no arguments are given, returns all input values.
411          * No transformation is performed on the values.
412          */
413         public function getValues() {
414                 $names = func_get_args();
415                 if ( count( $names ) == 0 ) {
416                         $names = array_keys( $this->data );
417                 }
418
419                 $retVal = array();
420                 foreach ( $names as $name ) {
421                         $value = $this->getVal( $name );
422                         if ( !is_null( $value ) ) {
423                                 $retVal[$name] = $value;
424                         }
425                 }
426                 return $retVal;
427         }
428
429         /**
430          * Returns true if the present request was reached by a POST operation,
431          * false otherwise (GET, HEAD, or command-line).
432          *
433          * Note that values retrieved by the object may come from the
434          * GET URL etc even on a POST request.
435          *
436          * @return Boolean
437          */
438         public function wasPosted() {
439                 return $_SERVER['REQUEST_METHOD'] == 'POST';
440         }
441
442         /**
443          * Returns true if there is a session cookie set.
444          * This does not necessarily mean that the user is logged in!
445          *
446          * If you want to check for an open session, use session_id()
447          * instead; that will also tell you if the session was opened
448          * during the current request (in which case the cookie will
449          * be sent back to the client at the end of the script run).
450          *
451          * @return Boolean
452          */
453         public function checkSessionCookie() {
454                 return isset( $_COOKIE[ session_name() ] );
455         }
456
457         /**
458          * Get a cookie from the $_COOKIE jar
459          *
460          * @param $key String: the name of the cookie
461          * @param $prefix String: a prefix to use for the cookie name, if not $wgCookiePrefix
462          * @param $default Mixed: what to return if the value isn't found
463          * @return Mixed: cookie value or $default if the cookie not set
464          */
465         public function getCookie( $key, $prefix = null, $default = null ) {
466                 if( $prefix === null ) {
467                         global $wgCookiePrefix;
468                         $prefix = $wgCookiePrefix;
469                 }
470                 return $this->getGPCVal( $_COOKIE, $prefix . $key , $default );
471         }
472
473         /**
474          * Return the path portion of the request URI.
475          *
476          * @return String
477          */
478         public function getRequestURL() {
479                 if( isset( $_SERVER['REQUEST_URI']) && strlen($_SERVER['REQUEST_URI']) ) {
480                         $base = $_SERVER['REQUEST_URI'];
481                 } elseif( isset( $_SERVER['SCRIPT_NAME'] ) ) {
482                         // Probably IIS; doesn't set REQUEST_URI
483                         $base = $_SERVER['SCRIPT_NAME'];
484                         if( isset( $_SERVER['QUERY_STRING'] ) && $_SERVER['QUERY_STRING'] != '' ) {
485                                 $base .= '?' . $_SERVER['QUERY_STRING'];
486                         }
487                 } else {
488                         // This shouldn't happen!
489                         throw new MWException( "Web server doesn't provide either " .
490                                 "REQUEST_URI or SCRIPT_NAME. Report details of your " .
491                                 "web server configuration to http://bugzilla.wikimedia.org/" );
492                 }
493                 // User-agents should not send a fragment with the URI, but
494                 // if they do, and the web server passes it on to us, we
495                 // need to strip it or we get false-positive redirect loops
496                 // or weird output URLs
497                 $hash = strpos( $base, '#' );
498                 if( $hash !== false ) {
499                         $base = substr( $base, 0, $hash );
500                 }
501                 if( $base{0} == '/' ) {
502                         return $base;
503                 } else {
504                         // We may get paths with a host prepended; strip it.
505                         return preg_replace( '!^[^:]+://[^/]+/!', '/', $base );
506                 }
507         }
508
509         /**
510          * Return the request URI with the canonical service and hostname.
511          *
512          * @return String
513          */
514         public function getFullRequestURL() {
515                 global $wgServer;
516                 return $wgServer . $this->getRequestURL();
517         }
518
519         /**
520          * Take an arbitrary query and rewrite the present URL to include it
521          * @param $query String: query string fragment; do not include initial '?'
522          *
523          * @return String
524          */
525         public function appendQuery( $query ) {
526                 global $wgTitle;
527                 $basequery = '';
528                 foreach( $_GET as $var => $val ) {
529                         if ( $var == 'title' )
530                                 continue;
531                         if ( is_array( $val ) )
532                                 /* This will happen given a request like
533                                  * http://en.wikipedia.org/w/index.php?title[]=Special:Userlogin&returnto[]=Main_Page
534                                  */
535                                 continue;
536                         $basequery .= '&' . urlencode( $var ) . '=' . urlencode( $val );
537                 }
538                 $basequery .= '&' . $query;
539
540                 # Trim the extra &
541                 $basequery = substr( $basequery, 1 );
542                 return $wgTitle->getLocalURL( $basequery );
543         }
544
545         /**
546          * HTML-safe version of appendQuery().
547          *
548          * @param $query String: query string fragment; do not include initial '?'
549          * @return String
550          */
551         public function escapeAppendQuery( $query ) {
552                 return htmlspecialchars( $this->appendQuery( $query ) );
553         }
554
555         public function appendQueryValue( $key, $value, $onlyquery = false ) {
556                 return $this->appendQueryArray( array( $key => $value ), $onlyquery );
557         }
558
559         /**
560          * Appends or replaces value of query variables.
561          *
562          * @param $array Array of values to replace/add to query
563          * @param $onlyquery Bool: whether to only return the query string and not
564          *                   the complete URL
565          * @return String
566          */
567         public function appendQueryArray( $array, $onlyquery = false ) {
568                 global $wgTitle;
569                 $newquery = $_GET;
570                 unset( $newquery['title'] );
571                 $newquery = array_merge( $newquery, $array );
572                 $query = wfArrayToCGI( $newquery );
573                 return $onlyquery ? $query : $wgTitle->getLocalURL( $query );
574         }
575
576         /**
577          * Check for limit and offset parameters on the input, and return sensible
578          * defaults if not given. The limit must be positive and is capped at 5000.
579          * Offset must be positive but is not capped.
580          *
581          * @param $deflimit Integer: limit to use if no input and the user hasn't set the option.
582          * @param $optionname String: to specify an option other than rclimit to pull from.
583          * @return array first element is limit, second is offset
584          */
585         public function getLimitOffset( $deflimit = 50, $optionname = 'rclimit' ) {
586                 global $wgUser;
587
588                 $limit = $this->getInt( 'limit', 0 );
589                 if( $limit < 0 ) {
590                         $limit = 0;
591                 }
592                 if( ( $limit == 0 ) && ( $optionname != '' ) ) {
593                         $limit = (int)$wgUser->getOption( $optionname );
594                 }
595                 if( $limit <= 0 ) {
596                         $limit = $deflimit;
597                 }
598                 if( $limit > 5000 ) {
599                         $limit = 5000; # We have *some* limits...
600                 }
601
602                 $offset = $this->getInt( 'offset', 0 );
603                 if( $offset < 0 ) {
604                         $offset = 0;
605                 }
606
607                 return array( $limit, $offset );
608         }
609
610         /**
611          * Return the path to the temporary file where PHP has stored the upload.
612          *
613          * @param $key String:
614          * @return string or NULL if no such file.
615          */
616         public function getFileTempname( $key ) {
617                 $file = new WebRequestUpload( $this, $key );
618                 return $file->getTempName();
619         }
620
621         /**
622          * Return the size of the upload, or 0.
623          *
624          * @deprecated
625          * @param $key String:
626          * @return integer
627          */
628         public function getFileSize( $key ) {
629                 $file = new WebRequestUpload( $this, $key );
630                 return $file->getSize();
631         }
632
633         /**
634          * Return the upload error or 0
635          *
636          * @param $key String:
637          * @return integer
638          */
639         public function getUploadError( $key ) {
640                 $file = new WebRequestUpload( $this, $key );
641                 return $file->getError();
642         }
643
644         /**
645          * Return the original filename of the uploaded file, as reported by
646          * the submitting user agent. HTML-style character entities are
647          * interpreted and normalized to Unicode normalization form C, in part
648          * to deal with weird input from Safari with non-ASCII filenames.
649          *
650          * Other than this the name is not verified for being a safe filename.
651          *
652          * @param $key String:
653          * @return string or NULL if no such file.
654          */
655         public function getFileName( $key ) {
656                 $file = new WebRequestUpload( $this, $key );
657                 return $file->getName();
658         }
659
660         /**
661          * Return a WebRequestUpload object corresponding to the key
662          *
663          * @param @key string
664          * @return WebRequestUpload
665          */
666         public function getUpload( $key ) {
667                 return new WebRequestUpload( $this, $key );
668         }
669
670         /**
671          * Return a handle to WebResponse style object, for setting cookies,
672          * headers and other stuff, for Request being worked on.
673          *
674          * @return WebResponse
675          */
676         public function response() {
677                 /* Lazy initialization of response object for this request */
678                 if ( !is_object( $this->response ) ) {
679                         $class = ( $this instanceof FauxRequest ) ? 'FauxResponse' : 'WebResponse';
680                         $this->response = new $class();
681                 }
682                 return $this->response;
683         }
684
685         /**
686          * Get a request header, or false if it isn't set
687          * @param $name String: case-insensitive header name
688          */
689         public function getHeader( $name ) {
690                 $name = strtoupper( $name );
691                 if ( function_exists( 'apache_request_headers' ) ) {
692                         if ( !$this->headers ) {
693                                 foreach ( apache_request_headers() as $tempName => $tempValue ) {
694                                         $this->headers[ strtoupper( $tempName ) ] = $tempValue;
695                                 }
696                         }
697                         if ( isset( $this->headers[$name] ) ) {
698                                 return $this->headers[$name];
699                         } else {
700                                 return false;
701                         }
702                 } else {
703                         $name = 'HTTP_' . str_replace( '-', '_', $name );
704                         if ( $name === 'HTTP_CONTENT_LENGTH' && !isset( $_SERVER[$name] ) ) {
705                                 $name = 'CONTENT_LENGTH';
706                         }
707                         if ( isset( $_SERVER[$name] ) ) {
708                                 return $_SERVER[$name];
709                         } else {
710                                 return false;
711                         }
712                 }
713         }
714
715         /**
716          * Get data from $_SESSION
717          *
718          * @param $key String: name of key in $_SESSION
719          * @return Mixed
720          */
721         public function getSessionData( $key ) {
722                 if( !isset( $_SESSION[$key] ) ) {
723                         return null;
724                 }
725                 return $_SESSION[$key];
726         }
727
728         /**
729          * Set session data
730          *
731          * @param $key String: name of key in $_SESSION
732          * @param $data Mixed
733          */
734         public function setSessionData( $key, $data ) {
735                 $_SESSION[$key] = $data;
736         }
737
738         /**
739          * Check if Internet Explorer will detect an incorrect cache extension in 
740          * PATH_INFO or QUERY_STRING. If the request can't be allowed, show an error
741          * message or redirect to a safer URL. Returns true if the URL is OK, and
742          * false if an error message has been shown and the request should be aborted.
743          */
744         public function checkUrlExtension( $extWhitelist = array() ) {
745                 global $wgScriptExtension;
746                 $extWhitelist[] = ltrim( $wgScriptExtension, '.' );
747                 if ( IEUrlExtension::areServerVarsBad( $_SERVER, $extWhitelist ) ) {
748                         if ( !$this->wasPosted() ) {
749                                 $newUrl = IEUrlExtension::fixUrlForIE6(
750                                         $this->getFullRequestURL(), $extWhitelist );
751                                 if ( $newUrl !== false ) {
752                                         $this->doSecurityRedirect( $newUrl );
753                                         return false;
754                                 }
755                         }
756                         wfHttpError( 403, 'Forbidden',
757                                 'Invalid file extension found in the path info or query string.' );
758                         
759                         return false;
760                 }
761                 return true;
762         }
763
764         /**
765          * Attempt to redirect to a URL with a QUERY_STRING that's not dangerous in 
766          * IE 6. Returns true if it was successful, false otherwise.
767          */
768         protected function doSecurityRedirect( $url ) {
769                 header( 'Location: ' . $url );
770                 header( 'Content-Type: text/html' );
771                 $encUrl = htmlspecialchars( $url );
772                 echo <<<HTML
773 <html>
774 <head>
775 <title>Security redirect</title>
776 </head>
777 <body>
778 <h1>Security redirect</h1>
779 <p>
780 We can't serve non-HTML content from the URL you have requested, because 
781 Internet Explorer would interpret it as an incorrect and potentially dangerous
782 content type.</p>
783 <p>Instead, please use <a href="$encUrl">this URL</a>, which is the same as the URL you have requested, except that 
784 "&amp;*" is appended. This prevents Internet Explorer from seeing a bogus file 
785 extension.
786 </p>
787 </body>
788 </html>
789 HTML;
790                 echo "\n";
791                 return true;
792         }
793
794         /**
795          * Returns true if the PATH_INFO ends with an extension other than a script
796          * extension. This could confuse IE for scripts that send arbitrary data which
797          * is not HTML but may be detected as such.
798          *
799          * Various past attempts to use the URL to make this check have generally
800          * run up against the fact that CGI does not provide a standard method to
801          * determine the URL. PATH_INFO may be mangled (e.g. if cgi.fix_pathinfo=0),
802          * but only by prefixing it with the script name and maybe some other stuff,
803          * the extension is not mangled. So this should be a reasonably portable
804          * way to perform this security check.
805          *
806          * Also checks for anything that looks like a file extension at the end of
807          * QUERY_STRING, since IE 6 and earlier will use this to get the file type
808          * if there was no dot before the question mark (bug 28235).
809          */
810         public function isPathInfoBad() {
811                 global $wgScriptExtension;
812                 $extWhitelist[] = ltrim( $wgScriptExtension, '.' );
813                 return IEUrlExtension::areServerVarsBad( $_SERVER, $extWhitelist );
814         }
815
816         /**
817          * Parse the Accept-Language header sent by the client into an array
818          * @return array( languageCode => q-value ) sorted by q-value in descending order
819          * May contain the "language" '*', which applies to languages other than those explicitly listed.
820          * This is aligned with rfc2616 section 14.4
821          */
822         public function getAcceptLang() {
823                 // Modified version of code found at http://www.thefutureoftheweb.com/blog/use-accept-language-header
824                 $acceptLang = $this->getHeader( 'Accept-Language' );
825                 if ( !$acceptLang ) {
826                         return array();
827                 }
828
829                 // Return the language codes in lower case
830                 $acceptLang = strtolower( $acceptLang );
831
832                 // Break up string into pieces (languages and q factors)
833                 $lang_parse = null;
834                 preg_match_all( '/([a-z]{1,8}(-[a-z]{1,8})?|\*)\s*(;\s*q\s*=\s*(1|0(\.[0-9]+)?)?)?/',
835                         $acceptLang, $lang_parse );
836
837                 if ( !count( $lang_parse[1] ) ) {
838                         return array();
839                 }
840
841                 // Create a list like "en" => 0.8
842                 $langs = array_combine( $lang_parse[1], $lang_parse[4] );
843                 // Set default q factor to 1
844                 foreach ( $langs as $lang => $val ) {
845                         if ( $val === '' ) {
846                                 $langs[$lang] = 1;
847                         } else if ( $val == 0 ) {
848                                 unset($langs[$lang]);
849                         }
850                 }
851
852                 // Sort list
853                 arsort( $langs, SORT_NUMERIC );
854                 return $langs;
855         }
856 }
857
858 /**
859  * Object to access the $_FILES array
860  */
861 class WebRequestUpload {
862         protected $request;
863         protected $doesExist;
864         protected $fileInfo;
865
866         /**
867          * Constructor. Should only be called by WebRequest
868          *
869          * @param $request WebRequest The associated request
870          * @param $key string Key in $_FILES array (name of form field)
871          */
872         public function __construct( $request, $key ) {
873                 $this->request = $request;
874                 $this->doesExist = isset( $_FILES[$key] );
875                 if ( $this->doesExist ) {
876                         $this->fileInfo = $_FILES[$key];
877                 }
878         }
879
880         /**
881          * Return whether a file with this name was uploaded.
882          *
883          * @return bool
884          */
885         public function exists() {
886                 return $this->doesExist;
887         }
888
889         /**
890          * Return the original filename of the uploaded file
891          *
892          * @return mixed Filename or null if non-existent
893          */
894         public function getName() {
895                 if ( !$this->exists() ) {
896                         return null;
897                 }
898
899                 global $wgContLang;
900                 $name = $this->fileInfo['name'];
901
902                 # Safari sends filenames in HTML-encoded Unicode form D...
903                 # Horrid and evil! Let's try to make some kind of sense of it.
904                 $name = Sanitizer::decodeCharReferences( $name );
905                 $name = $wgContLang->normalize( $name );
906                 wfDebug( __METHOD__ . ": {$this->fileInfo['name']} normalized to '$name'\n" );
907                 return $name;
908         }
909
910         /**
911          * Return the file size of the uploaded file
912          *
913          * @return int File size or zero if non-existent
914          */
915         public function getSize() {
916                 if ( !$this->exists() ) {
917                         return 0;
918                 }
919
920                 return $this->fileInfo['size'];
921         }
922
923         /**
924          * Return the path to the temporary file
925          *
926          * @return mixed Path or null if non-existent
927          */
928         public function getTempName() {
929                 if ( !$this->exists() ) {
930                         return null;
931                 }
932
933                 return $this->fileInfo['tmp_name'];
934         }
935
936         /**
937          * Return the upload error. See link for explanation
938          * http://www.php.net/manual/en/features.file-upload.errors.php
939          *
940          * @return int One of the UPLOAD_ constants, 0 if non-existent
941          */
942         public function getError() {
943                 if ( !$this->exists() ) {
944                         return 0; # UPLOAD_ERR_OK
945                 }
946
947                 return $this->fileInfo['error'];
948         }
949
950         /**
951          * Returns whether this upload failed because of overflow of a maximum set
952          * in php.ini
953          *
954          * @return bool
955          */
956         public function isIniSizeOverflow() {
957                 if ( $this->getError() == UPLOAD_ERR_INI_SIZE ) {
958                         # PHP indicated that upload_max_filesize is exceeded
959                         return true;
960                 }
961
962                 $contentLength = $this->request->getHeader( 'CONTENT_LENGTH' );
963                 if ( $contentLength > wfShorthandToInteger( ini_get( 'post_max_size' ) ) ) {
964                         # post_max_size is exceeded
965                         return true;
966                 }
967
968                 return false;
969         }
970 }
971
972 /**
973  * WebRequest clone which takes values from a provided array.
974  *
975  * @ingroup HTTP
976  */
977 class FauxRequest extends WebRequest {
978         private $wasPosted = false;
979         private $session = array();
980
981         /**
982          * @param $data Array of *non*-urlencoded key => value pairs, the
983          *   fake GET/POST values
984          * @param $wasPosted Bool: whether to treat the data as POST
985          * @param $session Mixed: session array or null
986          */
987         public function __construct( $data, $wasPosted = false, $session = null ) {
988                 if( is_array( $data ) ) {
989                         $this->data = $data;
990                 } else {
991                         throw new MWException( "FauxRequest() got bogus data" );
992                 }
993                 $this->wasPosted = $wasPosted;
994                 if( $session )
995                         $this->session = $session;
996         }
997
998         private function notImplemented( $method ) {
999                 throw new MWException( "{$method}() not implemented" );
1000         }
1001
1002         public function getText( $name, $default = '' ) {
1003                 # Override; don't recode since we're using internal data
1004                 return (string)$this->getVal( $name, $default );
1005         }
1006
1007         public function getValues() {
1008                 return $this->data;
1009         }
1010
1011         public function wasPosted() {
1012                 return $this->wasPosted;
1013         }
1014
1015         public function checkSessionCookie() {
1016                 return false;
1017         }
1018
1019         public function getRequestURL() {
1020                 $this->notImplemented( __METHOD__ );
1021         }
1022
1023         public function appendQuery( $query ) {
1024                 global $wgTitle;
1025                 $basequery = '';
1026                 foreach( $this->data as $var => $val ) {
1027                         if ( $var == 'title' ) {
1028                                 continue;
1029                         }
1030                         if ( is_array( $val ) ) {
1031                                 /* This will happen given a request like
1032                                  * http://en.wikipedia.org/w/index.php?title[]=Special:Userlogin&returnto[]=Main_Page
1033                                  */
1034                                 continue;
1035                         }
1036                         $basequery .= '&' . urlencode( $var ) . '=' . urlencode( $val );
1037                 }
1038                 $basequery .= '&' . $query;
1039
1040                 # Trim the extra &
1041                 $basequery = substr( $basequery, 1 );
1042                 return $wgTitle->getLocalURL( $basequery );
1043         }
1044
1045         public function getHeader( $name ) {
1046                 return isset( $this->headers[$name] ) ? $this->headers[$name] : false;
1047         }
1048
1049         public function setHeader( $name, $val ) {
1050                 $this->headers[$name] = $val;
1051         }
1052
1053         public function getSessionData( $key ) {
1054                 if( isset( $this->session[$key] ) )
1055                         return $this->session[$key];
1056         }
1057
1058         public function setSessionData( $key, $data ) {
1059                 $this->session[$key] = $data;
1060         }
1061
1062         public function isPathInfoBad( $extWhitelist = array() ) {
1063                 return false;
1064         }
1065
1066         public function checkUrlExtension( $extWhitelist = array() ) {
1067                 return true;
1068         }
1069 }