]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - includes/HttpFunctions.php
MediaWiki 1.17.4
[autoinstalls/mediawiki.git] / includes / HttpFunctions.php
1 <?php
2 /**
3  * @defgroup HTTP HTTP
4  */
5
6 /**
7  * Various HTTP related functions
8  * @ingroup HTTP
9  */
10 class Http {
11         static $httpEngine = false;
12
13         /**
14          * Perform an HTTP request
15          *
16          * @param $method String: HTTP method. Usually GET/POST
17          * @param $url String: full URL to act on
18          * @param $options Array: options to pass to MWHttpRequest object.
19          *      Possible keys for the array:
20          *    - timeout             Timeout length in seconds
21          *    - postData            An array of key-value pairs or a url-encoded form data
22          *    - proxy               The proxy to use.
23          *                          Will use $wgHTTPProxy (if set) otherwise.
24          *    - noProxy             Override $wgHTTPProxy (if set) and don't use any proxy at all.
25          *    - sslVerifyHost       (curl only) Verify hostname against certificate
26          *    - sslVerifyCert       (curl only) Verify SSL certificate
27          *    - caInfo              (curl only) Provide CA information
28          *    - maxRedirects        Maximum number of redirects to follow (defaults to 5)
29          *    - followRedirects     Whether to follow redirects (defaults to false).
30          *                                  Note: this should only be used when the target URL is trusted,
31          *                                  to avoid attacks on intranet services accessible by HTTP.
32          * @return Mixed: (bool)false on failure or a string on success
33          */
34         public static function request( $method, $url, $options = array() ) {
35                 $url = wfExpandUrl( $url );
36                 wfDebug( "HTTP: $method: $url\n" );
37                 $options['method'] = strtoupper( $method );
38
39                 if ( !isset( $options['timeout'] ) ) {
40                         $options['timeout'] = 'default';
41                 }
42
43                 $req = MWHttpRequest::factory( $url, $options );
44                 $status = $req->execute();
45
46                 if ( $status->isOK() ) {
47                         return $req->getContent();
48                 } else {
49                         return false;
50                 }
51         }
52
53         /**
54          * Simple wrapper for Http::request( 'GET' )
55          * @see Http::request()
56          */
57         public static function get( $url, $timeout = 'default', $options = array() ) {
58                 $options['timeout'] = $timeout;
59                 return Http::request( 'GET', $url, $options );
60         }
61
62         /**
63          * Simple wrapper for Http::request( 'POST' )
64          * @see Http::request()
65          */
66         public static function post( $url, $options = array() ) {
67                 return Http::request( 'POST', $url, $options );
68         }
69
70         /**
71          * Check if the URL can be served by localhost
72          *
73          * @param $url String: full url to check
74          * @return Boolean
75          */
76         public static function isLocalURL( $url ) {
77                 global $wgCommandLineMode, $wgConf;
78
79                 if ( $wgCommandLineMode ) {
80                         return false;
81                 }
82
83                 // Extract host part
84                 $matches = array();
85                 if ( preg_match( '!^http://([\w.-]+)[/:].*$!', $url, $matches ) ) {
86                         $host = $matches[1];
87                         // Split up dotwise
88                         $domainParts = explode( '.', $host );
89                         // Check if this domain or any superdomain is listed in $wgConf as a local virtual host
90                         $domainParts = array_reverse( $domainParts );
91
92                         for ( $i = 0; $i < count( $domainParts ); $i++ ) {
93                                 $domainPart = $domainParts[$i];
94                                 if ( $i == 0 ) {
95                                         $domain = $domainPart;
96                                 } else {
97                                         $domain = $domainPart . '.' . $domain;
98                                 }
99
100                                 if ( $wgConf->isLocalVHost( $domain ) ) {
101                                         return true;
102                                 }
103                         }
104                 }
105
106                 return false;
107         }
108
109         /**
110          * A standard user-agent we can use for external requests.
111          * @return String
112          */
113         public static function userAgent() {
114                 global $wgVersion;
115                 return "MediaWiki/$wgVersion";
116         }
117
118         /**
119          * Checks that the given URI is a valid one. Hardcoding the
120          * protocols, because we only want protocols that both cURL
121          * and php support.
122          *
123          * @fixme this is wildly inaccurate and fails to actually check most stuff
124          *
125          * @param $uri Mixed: URI to check for validity
126          * @returns Boolean
127          */
128         public static function isValidURI( $uri ) {
129                 return preg_match(
130                         '/^https?:\/\/[^\/\s]\S*$/D',
131                         $uri
132                 );
133         }
134 }
135
136 /**
137  * This wrapper class will call out to curl (if available) or fallback
138  * to regular PHP if necessary for handling internal HTTP requests.
139  *
140  * Renamed from HttpRequest to MWHttpRequst to avoid conflict with
141  * php's HTTP extension.
142  */
143 class MWHttpRequest {
144         protected $content;
145         protected $timeout = 'default';
146         protected $headersOnly = null;
147         protected $postData = null;
148         protected $proxy = null;
149         protected $noProxy = false;
150         protected $sslVerifyHost = true;
151         protected $sslVerifyCert = true;
152         protected $caInfo = null;
153         protected $method = "GET";
154         protected $reqHeaders = array();
155         protected $url;
156         protected $parsedUrl;
157         protected $callback;
158         protected $maxRedirects = 5;
159         protected $followRedirects = false;
160
161         protected $cookieJar;
162
163         protected $headerList = array();
164         protected $respVersion = "0.9";
165         protected $respStatus = "200 Ok";
166         protected $respHeaders = array();
167
168         public $status;
169
170         /**
171          * @param $url String: url to use
172          * @param $options Array: (optional) extra params to pass (see Http::request())
173          */
174         function __construct( $url, $options = array() ) {
175                 global $wgHTTPTimeout;
176
177                 $this->url = $url;
178                 $this->parsedUrl = parse_url( $url );
179
180                 if ( !Http::isValidURI( $this->url ) ) {
181                         $this->status = Status::newFatal( 'http-invalid-url' );
182                 } else {
183                         $this->status = Status::newGood( 100 ); // continue
184                 }
185
186                 if ( isset( $options['timeout'] ) && $options['timeout'] != 'default' ) {
187                         $this->timeout = $options['timeout'];
188                 } else {
189                         $this->timeout = $wgHTTPTimeout;
190                 }
191
192                 $members = array( "postData", "proxy", "noProxy", "sslVerifyHost", "caInfo",
193                                   "method", "followRedirects", "maxRedirects", "sslVerifyCert" );
194
195                 foreach ( $members as $o ) {
196                         if ( isset( $options[$o] ) ) {
197                                 $this->$o = $options[$o];
198                         }
199                 }
200         }
201
202         /**
203          * Simple function to test if we can make any sort of requests at all, using
204          * cURL or fopen()
205          * @return bool
206          */
207         public static function canMakeRequests() {
208                 return function_exists( 'curl_init' ) || wfIniGetBool( 'allow_url_fopen' );
209         }
210
211         /**
212          * Generate a new request object
213          * @see MWHttpRequest::__construct
214          */
215         public static function factory( $url, $options = null ) {
216                 if ( !Http::$httpEngine ) {
217                         Http::$httpEngine = function_exists( 'curl_init' ) ? 'curl' : 'php';
218                 } elseif ( Http::$httpEngine == 'curl' && !function_exists( 'curl_init' ) ) {
219                         throw new MWException( __METHOD__ . ': curl (http://php.net/curl) is not installed, but' .
220                                                                    ' Http::$httpEngine is set to "curl"' );
221                 }
222
223                 switch( Http::$httpEngine ) {
224                         case 'curl':
225                                 return new CurlHttpRequest( $url, $options );
226                         case 'php':
227                                 if ( !wfIniGetBool( 'allow_url_fopen' ) ) {
228                                         throw new MWException( __METHOD__ . ': allow_url_fopen needs to be enabled for pure PHP' .
229                                                 ' http requests to work. If possible, curl should be used instead. See http://php.net/curl.' );
230                                 }
231                                 return new PhpHttpRequest( $url, $options );
232                         default:
233                                 throw new MWException( __METHOD__ . ': The setting of Http::$httpEngine is not valid.' );
234                 }
235         }
236
237         /**
238          * Get the body, or content, of the response to the request
239          *
240          * @return String
241          */
242         public function getContent() {
243                 return $this->content;
244         }
245
246         /**
247          * Set the parameters of the request
248
249          * @param $args Array
250          * @todo overload the args param
251          */
252         public function setData( $args ) {
253                 $this->postData = $args;
254         }
255
256         /**
257          * Take care of setting up the proxy
258          * (override in subclass)
259          *
260          * @return String
261          */
262         public function proxySetup() {
263                 global $wgHTTPProxy;
264
265                 if ( $this->proxy ) {
266                         return;
267                 }
268
269                 if ( Http::isLocalURL( $this->url ) ) {
270                         $this->proxy = 'http://localhost:80/';
271                 } elseif ( $wgHTTPProxy ) {
272                         $this->proxy = $wgHTTPProxy ;
273                 } elseif ( getenv( "http_proxy" ) ) {
274                         $this->proxy = getenv( "http_proxy" );
275                 }
276         }
277
278         /**
279          * Set the refererer header
280          */
281         public function setReferer( $url ) {
282                 $this->setHeader( 'Referer', $url );
283         }
284
285         /**
286          * Set the user agent
287          */
288         public function setUserAgent( $UA ) {
289                 $this->setHeader( 'User-Agent', $UA );
290         }
291
292         /**
293          * Set an arbitrary header
294          */
295         public function setHeader( $name, $value ) {
296                 // I feel like I should normalize the case here...
297                 $this->reqHeaders[$name] = $value;
298         }
299
300         /**
301          * Get an array of the headers
302          */
303         public function getHeaderList() {
304                 $list = array();
305
306                 if ( $this->cookieJar ) {
307                         $this->reqHeaders['Cookie'] =
308                                 $this->cookieJar->serializeToHttpRequest(
309                                         $this->parsedUrl['path'],
310                                         $this->parsedUrl['host']
311                                 );
312                 }
313
314                 foreach ( $this->reqHeaders as $name => $value ) {
315                         $list[] = "$name: $value";
316                 }
317
318                 return $list;
319         }
320
321         /**
322          * Set a read callback to accept data read from the HTTP request.
323          * By default, data is appended to an internal buffer which can be
324          * retrieved through $req->getContent().
325          *
326          * To handle data as it comes in -- especially for large files that
327          * would not fit in memory -- you can instead set your own callback,
328          * in the form function($resource, $buffer) where the first parameter
329          * is the low-level resource being read (implementation specific),
330          * and the second parameter is the data buffer.
331          *
332          * You MUST return the number of bytes handled in the buffer; if fewer
333          * bytes are reported handled than were passed to you, the HTTP fetch
334          * will be aborted.
335          *
336          * @param $callback Callback
337          */
338         public function setCallback( $callback ) {
339                 if ( !is_callable( $callback ) ) {
340                         throw new MWException( 'Invalid MwHttpRequest callback' );
341                 }
342                 $this->callback = $callback;
343         }
344
345         /**
346          * A generic callback to read the body of the response from a remote
347          * server.
348          *
349          * @param $fh handle
350          * @param $content String
351          */
352         public function read( $fh, $content ) {
353                 $this->content .= $content;
354                 return strlen( $content );
355         }
356
357         /**
358          * Take care of whatever is necessary to perform the URI request.
359          *
360          * @return Status
361          */
362         public function execute() {
363                 global $wgTitle;
364
365                 $this->content = "";
366
367                 if ( strtoupper( $this->method ) == "HEAD" ) {
368                         $this->headersOnly = true;
369                 }
370
371                 if ( is_array( $this->postData ) ) {
372                         $this->postData = wfArrayToCGI( $this->postData );
373                 }
374
375                 if ( is_object( $wgTitle ) && !isset( $this->reqHeaders['Referer'] ) ) {
376                         $this->setReferer( $wgTitle->getFullURL() );
377                 }
378
379                 if ( !$this->noProxy ) {
380                         $this->proxySetup();
381                 }
382
383                 if ( !$this->callback ) {
384                         $this->setCallback( array( $this, 'read' ) );
385                 }
386
387                 if ( !isset( $this->reqHeaders['User-Agent'] ) ) {
388                         $this->setUserAgent( Http::userAgent() );
389                 }
390         }
391
392         /**
393          * Parses the headers, including the HTTP status code and any
394          * Set-Cookie headers.  This function expectes the headers to be
395          * found in an array in the member variable headerList.
396          *
397          * @return nothing
398          */
399         protected function parseHeader() {
400                 $lastname = "";
401
402                 foreach ( $this->headerList as $header ) {
403                         if ( preg_match( "#^HTTP/([0-9.]+) (.*)#", $header, $match ) ) {
404                                 $this->respVersion = $match[1];
405                                 $this->respStatus = $match[2];
406                         } elseif ( preg_match( "#^[ \t]#", $header ) ) {
407                                 $last = count( $this->respHeaders[$lastname] ) - 1;
408                                 $this->respHeaders[$lastname][$last] .= "\r\n$header";
409                         } elseif ( preg_match( "#^([^:]*):[\t ]*(.*)#", $header, $match ) ) {
410                                 $this->respHeaders[strtolower( $match[1] )][] = $match[2];
411                                 $lastname = strtolower( $match[1] );
412                         }
413                 }
414
415                 $this->parseCookies();
416         }
417
418         /**
419          * Sets HTTPRequest status member to a fatal value with the error
420          * message if the returned integer value of the status code was
421          * not successful (< 300) or a redirect (>=300 and < 400).  (see
422          * RFC2616, section 10,
423          * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html for a
424          * list of status codes.)
425          *
426          * @return nothing
427          */
428         protected function setStatus() {
429                 if ( !$this->respHeaders ) {
430                         $this->parseHeader();
431                 }
432
433                 if ( (int)$this->respStatus > 399 ) {
434                         list( $code, $message ) = explode( " ", $this->respStatus, 2 );
435                         $this->status->fatal( "http-bad-status", $code, $message );
436                 }
437         }
438
439         /**
440          * Get the integer value of the HTTP status code (e.g. 200 for "200 Ok")
441          * (see RFC2616, section 10, http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
442          * for a list of status codes.)
443          *
444          * @return Integer
445          */
446         public function getStatus() {
447                 if ( !$this->respHeaders ) {
448                         $this->parseHeader();
449                 }
450
451                 return (int)$this->respStatus;
452         }
453
454
455         /**
456          * Returns true if the last status code was a redirect.
457          *
458          * @return Boolean
459          */
460         public function isRedirect() {
461                 if ( !$this->respHeaders ) {
462                         $this->parseHeader();
463                 }
464
465                 $status = (int)$this->respStatus;
466
467                 if ( $status >= 300 && $status <= 303 ) {
468                         return true;
469                 }
470
471                 return false;
472         }
473
474         /**
475          * Returns an associative array of response headers after the
476          * request has been executed.  Because some headers
477          * (e.g. Set-Cookie) can appear more than once the, each value of
478          * the associative array is an array of the values given.
479          *
480          * @return Array
481          */
482         public function getResponseHeaders() {
483                 if ( !$this->respHeaders ) {
484                         $this->parseHeader();
485                 }
486
487                 return $this->respHeaders;
488         }
489
490         /**
491          * Returns the value of the given response header.
492          *
493          * @param $header String
494          * @return String
495          */
496         public function getResponseHeader( $header ) {
497                 if ( !$this->respHeaders ) {
498                         $this->parseHeader();
499                 }
500
501                 if ( isset( $this->respHeaders[strtolower ( $header ) ] ) ) {
502                         $v = $this->respHeaders[strtolower ( $header ) ];
503                         return $v[count( $v ) - 1];
504                 }
505
506                 return null;
507         }
508
509         /**
510          * Tells the MWHttpRequest object to use this pre-loaded CookieJar.
511          *
512          * @param $jar CookieJar
513          */
514         public function setCookieJar( $jar ) {
515                 $this->cookieJar = $jar;
516         }
517
518         /**
519          * Returns the cookie jar in use.
520          *
521          * @returns CookieJar
522          */
523         public function getCookieJar() {
524                 if ( !$this->respHeaders ) {
525                         $this->parseHeader();
526                 }
527
528                 return $this->cookieJar;
529         }
530
531         /**
532          * Sets a cookie.  Used before a request to set up any individual
533          * cookies.      Used internally after a request to parse the
534          * Set-Cookie headers.
535          * @see Cookie::set
536          */
537         public function setCookie( $name, $value = null, $attr = null ) {
538                 if ( !$this->cookieJar ) {
539                         $this->cookieJar = new CookieJar;
540                 }
541
542                 $this->cookieJar->setCookie( $name, $value, $attr );
543         }
544
545         /**
546          * Parse the cookies in the response headers and store them in the cookie jar.
547          */
548         protected function parseCookies() {
549                 if ( !$this->cookieJar ) {
550                         $this->cookieJar = new CookieJar;
551                 }
552
553                 if ( isset( $this->respHeaders['set-cookie'] ) ) {
554                         $url = parse_url( $this->getFinalUrl() );
555                         foreach ( $this->respHeaders['set-cookie'] as $cookie ) {
556                                 $this->cookieJar->parseCookieResponseHeader( $cookie, $url['host'] );
557                         }
558                 }
559         }
560
561         /**
562          * Returns the final URL after all redirections.
563          *
564          * @return String
565          */
566         public function getFinalUrl() {
567                 $location = $this->getResponseHeader( "Location" );
568
569                 if ( $location ) {
570                         return $location;
571                 }
572
573                 return $this->url;
574         }
575
576         /**
577          * Returns true if the backend can follow redirects. Overridden by the
578          * child classes.
579          */
580         public function canFollowRedirects() {
581                 return true;
582         }
583 }
584
585
586 class Cookie {
587         protected $name;
588         protected $value;
589         protected $expires;
590         protected $path;
591         protected $domain;
592         protected $isSessionKey = true;
593         // TO IMPLEMENT  protected $secure
594         // TO IMPLEMENT? protected $maxAge (add onto expires)
595         // TO IMPLEMENT? protected $version
596         // TO IMPLEMENT? protected $comment
597
598         function __construct( $name, $value, $attr ) {
599                 $this->name = $name;
600                 $this->set( $value, $attr );
601         }
602
603         /**
604          * Sets a cookie.  Used before a request to set up any individual
605          * cookies.      Used internally after a request to parse the
606          * Set-Cookie headers.
607          *
608          * @param $value String: the value of the cookie
609          * @param $attr Array: possible key/values:
610          *              expires  A date string
611          *              path     The path this cookie is used on
612          *              domain   Domain this cookie is used on
613          */
614         public function set( $value, $attr ) {
615                 $this->value = $value;
616
617                 if ( isset( $attr['expires'] ) ) {
618                         $this->isSessionKey = false;
619                         $this->expires = strtotime( $attr['expires'] );
620                 }
621
622                 if ( isset( $attr['path'] ) ) {
623                         $this->path = $attr['path'];
624                 } else {
625                         $this->path = "/";
626                 }
627
628                 if ( isset( $attr['domain'] ) ) {
629                         if ( self::validateCookieDomain( $attr['domain'] ) ) {
630                                 $this->domain = $attr['domain'];
631                         }
632                 } else {
633                         throw new MWException( "You must specify a domain." );
634                 }
635         }
636
637         /**
638          * Return the true if the cookie is valid is valid.  Otherwise,
639          * false.  The uses a method similar to IE cookie security
640          * described here:
641          * http://kuza55.blogspot.com/2008/02/understanding-cookie-security.html
642          * A better method might be to use a blacklist like
643          * http://publicsuffix.org/
644          *
645          * @param $domain String: the domain to validate
646          * @param $originDomain String: (optional) the domain the cookie originates from
647          * @return Boolean
648          */
649         public static function validateCookieDomain( $domain, $originDomain = null ) {
650                 // Don't allow a trailing dot
651                 if ( substr( $domain, -1 ) == "." ) {
652                         return false;
653                 }
654
655                 $dc = explode( ".", $domain );
656
657                 // Only allow full, valid IP addresses
658                 if ( preg_match( '/^[0-9.]+$/', $domain ) ) {
659                         if ( count( $dc ) != 4 ) {
660                                 return false;
661                         }
662
663                         if ( ip2long( $domain ) === false ) {
664                                 return false;
665                         }
666
667                         if ( $originDomain == null || $originDomain == $domain ) {
668                                 return true;
669                         }
670
671                 }
672
673                 // Don't allow cookies for "co.uk" or "gov.uk", etc, but allow "supermarket.uk"
674                 if ( strrpos( $domain, "." ) - strlen( $domain )  == -3 ) {
675                         if ( ( count( $dc ) == 2 && strlen( $dc[0] ) <= 2 )
676                                 || ( count( $dc ) == 3 && strlen( $dc[0] ) == "" && strlen( $dc[1] ) <= 2 ) ) {
677                                 return false;
678                         }
679                         if ( ( count( $dc ) == 2 || ( count( $dc ) == 3 && $dc[0] == "" ) )
680                                 && preg_match( '/(com|net|org|gov|edu)\...$/', $domain ) ) {
681                                 return false;
682                         }
683                 }
684
685                 if ( $originDomain != null ) {
686                         if ( substr( $domain, 0, 1 ) != "." && $domain != $originDomain ) {
687                                 return false;
688                         }
689
690                         if ( substr( $domain, 0, 1 ) == "."
691                                 && substr_compare( $originDomain, $domain, -strlen( $domain ),
692                                                                    strlen( $domain ), TRUE ) != 0 ) {
693                                 return false;
694                         }
695                 }
696
697                 return true;
698         }
699
700         /**
701          * Serialize the cookie jar into a format useful for HTTP Request headers.
702          *
703          * @param $path String: the path that will be used. Required.
704          * @param $domain String: the domain that will be used. Required.
705          * @return String
706          */
707         public function serializeToHttpRequest( $path, $domain ) {
708                 $ret = "";
709
710                 if ( $this->canServeDomain( $domain )
711                                 && $this->canServePath( $path )
712                                 && $this->isUnExpired() ) {
713                         $ret = $this->name . "=" . $this->value;
714                 }
715
716                 return $ret;
717         }
718
719         protected function canServeDomain( $domain ) {
720                 if ( $domain == $this->domain
721                         || ( strlen( $domain ) > strlen( $this->domain )
722                                  && substr( $this->domain, 0, 1 ) == "."
723                                  && substr_compare( $domain, $this->domain, -strlen( $this->domain ),
724                                                                         strlen( $this->domain ), TRUE ) == 0 ) ) {
725                         return true;
726                 }
727
728                 return false;
729         }
730
731         protected function canServePath( $path ) {
732                 if ( $this->path && substr_compare( $this->path, $path, 0, strlen( $this->path ) ) == 0 ) {
733                         return true;
734                 }
735
736                 return false;
737         }
738
739         protected function isUnExpired() {
740                 if ( $this->isSessionKey || $this->expires > time() ) {
741                         return true;
742                 }
743
744                 return false;
745         }
746 }
747
748 class CookieJar {
749         private $cookie = array();
750
751         /**
752          * Set a cookie in the cookie jar.      Make sure only one cookie per-name exists.
753          * @see Cookie::set()
754          */
755         public function setCookie ( $name, $value, $attr ) {
756                 /* cookies: case insensitive, so this should work.
757                  * We'll still send the cookies back in the same case we got them, though.
758                  */
759                 $index = strtoupper( $name );
760
761                 if ( isset( $this->cookie[$index] ) ) {
762                         $this->cookie[$index]->set( $value, $attr );
763                 } else {
764                         $this->cookie[$index] = new Cookie( $name, $value, $attr );
765                 }
766         }
767
768         /**
769          * @see Cookie::serializeToHttpRequest
770          */
771         public function serializeToHttpRequest( $path, $domain ) {
772                 $cookies = array();
773
774                 foreach ( $this->cookie as $c ) {
775                         $serialized = $c->serializeToHttpRequest( $path, $domain );
776
777                         if ( $serialized ) {
778                                 $cookies[] = $serialized;
779                         }
780                 }
781
782                 return implode( "; ", $cookies );
783         }
784
785         /**
786          * Parse the content of an Set-Cookie HTTP Response header.
787          *
788          * @param $cookie String
789          * @param $domain String: cookie's domain
790          */
791         public function parseCookieResponseHeader ( $cookie, $domain ) {
792                 $len = strlen( "Set-Cookie:" );
793
794                 if ( substr_compare( "Set-Cookie:", $cookie, 0, $len, TRUE ) === 0 ) {
795                         $cookie = substr( $cookie, $len );
796                 }
797
798                 $bit = array_map( 'trim', explode( ";", $cookie ) );
799
800                 if ( count( $bit ) >= 1 ) {
801                         list( $name, $value ) = explode( "=", array_shift( $bit ), 2 );
802                         $attr = array();
803
804                         foreach ( $bit as $piece ) {
805                                 $parts = explode( "=", $piece );
806                                 if ( count( $parts ) > 1 ) {
807                                         $attr[strtolower( $parts[0] )] = $parts[1];
808                                 } else {
809                                         $attr[strtolower( $parts[0] )] = true;
810                                 }
811                         }
812
813                         if ( !isset( $attr['domain'] ) ) {
814                                 $attr['domain'] = $domain;
815                         } elseif ( !Cookie::validateCookieDomain( $attr['domain'], $domain ) ) {
816                                 return null;
817                         }
818
819                         $this->setCookie( $name, $value, $attr );
820                 }
821         }
822 }
823
824 /**
825  * MWHttpRequest implemented using internal curl compiled into PHP
826  */
827 class CurlHttpRequest extends MWHttpRequest {
828         static $curlMessageMap = array(
829                 6 => 'http-host-unreachable',
830                 28 => 'http-timed-out'
831         );
832
833         protected $curlOptions = array();
834         protected $headerText = "";
835
836         protected function readHeader( $fh, $content ) {
837                 $this->headerText .= $content;
838                 return strlen( $content );
839         }
840
841         public function execute() {
842                 parent::execute();
843
844                 if ( !$this->status->isOK() ) {
845                         return $this->status;
846                 }
847
848                 $this->curlOptions[CURLOPT_PROXY] = $this->proxy;
849                 $this->curlOptions[CURLOPT_TIMEOUT] = $this->timeout;
850                 $this->curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_0;
851                 $this->curlOptions[CURLOPT_WRITEFUNCTION] = $this->callback;
852                 $this->curlOptions[CURLOPT_HEADERFUNCTION] = array( $this, "readHeader" );
853                 $this->curlOptions[CURLOPT_MAXREDIRS] = $this->maxRedirects;
854                 $this->curlOptions[CURLOPT_ENCODING] = ""; # Enable compression
855
856                 /* not sure these two are actually necessary */
857                 if ( isset( $this->reqHeaders['Referer'] ) ) {
858                         $this->curlOptions[CURLOPT_REFERER] = $this->reqHeaders['Referer'];
859                 }
860                 $this->curlOptions[CURLOPT_USERAGENT] = $this->reqHeaders['User-Agent'];
861
862                 if ( isset( $this->sslVerifyHost ) ) {
863                         $this->curlOptions[CURLOPT_SSL_VERIFYHOST] = $this->sslVerifyHost;
864                 }
865
866                 if ( isset( $this->sslVerifyCert ) ) {
867                         $this->curlOptions[CURLOPT_SSL_VERIFYPEER] = $this->sslVerifyCert;
868                 }
869
870                 if ( $this->caInfo ) {
871                         $this->curlOptions[CURLOPT_CAINFO] = $this->caInfo;
872                 }
873
874                 if ( $this->headersOnly ) {
875                         $this->curlOptions[CURLOPT_NOBODY] = true;
876                         $this->curlOptions[CURLOPT_HEADER] = true;
877                 } elseif ( $this->method == 'POST' ) {
878                         $this->curlOptions[CURLOPT_POST] = true;
879                         $this->curlOptions[CURLOPT_POSTFIELDS] = $this->postData;
880                         // Suppress 'Expect: 100-continue' header, as some servers
881                         // will reject it with a 417 and Curl won't auto retry
882                         // with HTTP 1.0 fallback
883                         $this->reqHeaders['Expect'] = '';
884                 } else {
885                         $this->curlOptions[CURLOPT_CUSTOMREQUEST] = $this->method;
886                 }
887
888                 $this->curlOptions[CURLOPT_HTTPHEADER] = $this->getHeaderList();
889
890                 $curlHandle = curl_init( $this->url );
891
892                 if ( !curl_setopt_array( $curlHandle, $this->curlOptions ) ) {
893                         throw new MWException( "Error setting curl options." );
894                 }
895
896                 if ( $this->followRedirects && $this->canFollowRedirects() ) {
897                         if ( ! @curl_setopt( $curlHandle, CURLOPT_FOLLOWLOCATION, true ) ) {
898                                 wfDebug( __METHOD__ . ": Couldn't set CURLOPT_FOLLOWLOCATION. " .
899                                         "Probably safe_mode or open_basedir is set.\n" );
900                                 // Continue the processing. If it were in curl_setopt_array,
901                                 // processing would have halted on its entry
902                         }
903                 }
904
905                 if ( false === curl_exec( $curlHandle ) ) {
906                         $code = curl_error( $curlHandle );
907
908                         if ( isset( self::$curlMessageMap[$code] ) ) {
909                                 $this->status->fatal( self::$curlMessageMap[$code] );
910                         } else {
911                                 $this->status->fatal( 'http-curl-error', curl_error( $curlHandle ) );
912                         }
913                 } else {
914                         $this->headerList = explode( "\r\n", $this->headerText );
915                 }
916
917                 curl_close( $curlHandle );
918
919                 $this->parseHeader();
920                 $this->setStatus();
921
922                 return $this->status;
923         }
924
925         public function canFollowRedirects() {
926                 if ( strval( ini_get( 'open_basedir' ) ) !== '' || wfIniGetBool( 'safe_mode' ) ) {
927                         wfDebug( "Cannot follow redirects in safe mode\n" );
928                         return false;
929                 }
930
931                 if ( !defined( 'CURLOPT_REDIR_PROTOCOLS' ) ) {
932                         wfDebug( "Cannot follow redirects with libcurl < 7.19.4 due to CVE-2009-0037\n" );
933                         return false;
934                 }
935
936                 return true;
937         }
938 }
939
940 class PhpHttpRequest extends MWHttpRequest {
941         protected function urlToTcp( $url ) {
942                 $parsedUrl = parse_url( $url );
943
944                 return 'tcp://' . $parsedUrl['host'] . ':' . $parsedUrl['port'];
945         }
946
947         public function execute() {
948                 parent::execute();
949
950                 // At least on Centos 4.8 with PHP 5.1.6, using max_redirects to follow redirects
951                 // causes a segfault
952                 $manuallyRedirect = version_compare( phpversion(), '5.1.7', '<' );
953
954                 if ( $this->parsedUrl['scheme'] != 'http' &&
955                          $this->parsedUrl['scheme'] != 'https' ) {
956                         $this->status->fatal( 'http-invalid-scheme', $this->parsedUrl['scheme'] );
957                 }
958
959                 $this->reqHeaders['Accept'] = "*/*";
960                 if ( $this->method == 'POST' ) {
961                         // Required for HTTP 1.0 POSTs
962                         $this->reqHeaders['Content-Length'] = strlen( $this->postData );
963                         $this->reqHeaders['Content-type'] = "application/x-www-form-urlencoded";
964                 }
965
966                 $options = array();
967                 if ( $this->proxy && !$this->noProxy ) {
968                         $options['proxy'] = $this->urlToTCP( $this->proxy );
969                         $options['request_fulluri'] = true;
970                 }
971
972                 if ( !$this->followRedirects || $manuallyRedirect ) {
973                         $options['max_redirects'] = 0;
974                 } else {
975                         $options['max_redirects'] = $this->maxRedirects;
976                 }
977
978                 $options['method'] = $this->method;
979                 $options['header'] = implode( "\r\n", $this->getHeaderList() );
980                 // Note that at some future point we may want to support
981                 // HTTP/1.1, but we'd have to write support for chunking
982                 // in version of PHP < 5.3.1
983                 $options['protocol_version'] = "1.0";
984
985                 // This is how we tell PHP we want to deal with 404s (for example) ourselves.
986                 // Only works on 5.2.10+
987                 $options['ignore_errors'] = true;
988
989                 if ( $this->postData ) {
990                         $options['content'] = $this->postData;
991                 }
992
993                 $oldTimeout = false;
994                 if ( version_compare( '5.2.1', phpversion(), '>' ) ) {
995                         $oldTimeout = ini_set( 'default_socket_timeout', $this->timeout );
996                 } else {
997                         $options['timeout'] = $this->timeout;
998                 }
999
1000                 $context = stream_context_create( array( 'http' => $options ) );
1001
1002                 $this->headerList = array();
1003                 $reqCount = 0;
1004                 $url = $this->url;
1005
1006                 do {
1007                         $reqCount++;
1008                         wfSuppressWarnings();
1009                         $fh = fopen( $url, "r", false, $context );
1010                         wfRestoreWarnings();
1011
1012                         if ( !$fh ) {
1013                                 break;
1014                         }
1015
1016                         $result = stream_get_meta_data( $fh );
1017                         $this->headerList = $result['wrapper_data'];
1018                         $this->parseHeader();
1019
1020                         if ( !$manuallyRedirect || !$this->followRedirects ) {
1021                                 break;
1022                         }
1023
1024                         # Handle manual redirection
1025                         if ( !$this->isRedirect() || $reqCount > $this->maxRedirects ) {
1026                                 break;
1027                         }
1028                         # Check security of URL
1029                         $url = $this->getResponseHeader( "Location" );
1030
1031                         if ( substr( $url, 0, 7 ) !== 'http://' ) {
1032                                 wfDebug( __METHOD__ . ": insecure redirection\n" );
1033                                 break;
1034                         }
1035                 } while ( true );
1036
1037                 if ( $oldTimeout !== false ) {
1038                         ini_set( 'default_socket_timeout', $oldTimeout );
1039                 }
1040
1041                 $this->setStatus();
1042
1043                 if ( $fh === false ) {
1044                         $this->status->fatal( 'http-request-error' );
1045                         return $this->status;
1046                 }
1047
1048                 if ( $result['timed_out'] ) {
1049                         $this->status->fatal( 'http-timed-out', $this->url );
1050                         return $this->status;
1051                 }
1052
1053                 if ( $this->status->isOK() ) {
1054                         while ( !feof( $fh ) ) {
1055                                 $buf = fread( $fh, 8192 );
1056
1057                                 if ( $buf === false ) {
1058                                         $this->status->fatal( 'http-read-error' );
1059                                         break;
1060                                 }
1061
1062                                 if ( strlen( $buf ) ) {
1063                                         call_user_func( $this->callback, $fh, $buf );
1064                                 }
1065                         }
1066                 }
1067                 fclose( $fh );
1068
1069                 return $this->status;
1070         }
1071 }