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