7 * Various HTTP related functions
11 static $httpEngine = false;
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 * @returns mixed (bool)false on failure or a string on success
20 public static function request( $method, $url, $options = array() ) {
21 wfDebug( "HTTP: $method: $url" );
22 $options['method'] = strtoupper( $method );
23 if ( !isset( $options['timeout'] ) ) {
24 $options['timeout'] = 'default';
26 $req = HttpRequest
::factory( $url, $options );
27 $status = $req->execute();
28 if ( $status->isOK() ) {
29 return $req->getContent();
36 * Simple wrapper for Http::request( 'GET' )
37 * @see Http::request()
39 public static function get( $url, $timeout = 'default', $options = array() ) {
40 $options['timeout'] = $timeout;
41 return Http
::request( 'GET', $url, $options );
45 * Simple wrapper for Http::request( 'POST' )
46 * @see Http::request()
48 public static function post( $url, $options = array() ) {
49 return Http
::request( 'POST', $url, $options );
53 * Check if the URL can be served by localhost
54 * @param $url string Full url to check
57 public static function isLocalURL( $url ) {
58 global $wgCommandLineMode, $wgConf;
59 if ( $wgCommandLineMode ) {
65 if ( preg_match( '!^http://([\w.-]+)[/:].*$!', $url, $matches ) ) {
68 $domainParts = explode( '.', $host );
69 // Check if this domain or any superdomain is listed in $wgConf as a local virtual host
70 $domainParts = array_reverse( $domainParts );
71 for ( $i = 0; $i < count( $domainParts ); $i++
) {
72 $domainPart = $domainParts[$i];
74 $domain = $domainPart;
76 $domain = $domainPart . '.' . $domain;
78 if ( $wgConf->isLocalVHost( $domain ) ) {
87 * A standard user-agent we can use for external requests.
90 public static function userAgent() {
92 return "MediaWiki/$wgVersion";
96 * Checks that the given URI is a valid one
97 * @param $uri Mixed: URI to check for validity
100 public static function isValidURI( $uri ) {
102 '/(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/',
110 * This wrapper class will call out to curl (if available) or fallback
111 * to regular PHP if necessary for handling internal HTTP requests.
115 protected $timeout = 'default';
116 protected $headersOnly = null;
117 protected $postData = null;
118 protected $proxy = null;
119 protected $noProxy = false;
120 protected $sslVerifyHost = true;
121 protected $caInfo = null;
122 protected $method = "GET";
123 protected $reqHeaders = array();
125 protected $parsedUrl;
130 * @param $url string url to use
131 * @param $options array (optional) extra params to pass
132 * Possible keys for the array:
143 function __construct( $url, $options = array() ) {
144 global $wgHTTPTimeout;
147 $this->parsedUrl
= parse_url( $url );
149 if ( !Http
::isValidURI( $this->url
) ) {
150 $this->status
= Status
::newFromFatal('http-invalid-url');
152 $this->status
= Status
::newGood( 100 ); // continue
155 if ( isset($options['timeout']) && $options['timeout'] != 'default' ) {
156 $this->timeout
= $options['timeout'];
158 $this->timeout
= $wgHTTPTimeout;
161 $members = array( "targetFilePath", "requestKey", "postData",
162 "proxy", "noProxy", "sslVerifyHost", "caInfo", "method" );
163 foreach ( $members as $o ) {
164 if ( isset($options[$o]) ) {
165 $this->$o = $options[$o];
171 * Generate a new request object
172 * @see HttpRequest::__construct
174 public static function factory( $url, $options = null ) {
175 if ( !Http
::$httpEngine ) {
176 Http
::$httpEngine = function_exists( 'curl_init' ) ?
'curl' : 'php';
177 } elseif ( Http
::$httpEngine == 'curl' && !function_exists( 'curl_init' ) ) {
178 throw new MWException( __METHOD__
.': curl (http://php.net/curl) is not installed, but Http::$httpEngine is set to "curl"' );
181 switch( Http
::$httpEngine ) {
183 return new CurlHttpRequest( $url, $options );
185 if ( !wfIniGetBool( 'allow_url_fopen' ) ) {
186 throw new MWException( __METHOD__
.': allow_url_fopen needs to be enabled for pure PHP http requests to work. '.
187 'If possible, curl should be used instead. See http://php.net/curl.' );
189 return new PhpHttpRequest( $url, $options );
191 throw new MWException( __METHOD__
.': The setting of Http::$httpEngine is not valid.' );
196 * Get the body, or content, of the response to the request
199 public function getContent() {
200 return $this->content
;
204 * Take care of setting up the proxy
205 * (override in subclass)
208 public function proxySetup() {
212 if ( $this->proxy
) {
215 if ( Http
::isLocalURL( $this->url
) ) {
216 $this->proxy
= 'http://localhost:80/';
217 } elseif ( $wgHTTPProxy ) {
218 $this->proxy
= $wgHTTPProxy ;
223 * Set the refererer header
225 public function setReferer( $url ) {
226 $this->setHeader('Referer', $url);
232 public function setUserAgent( $UA ) {
233 $this->setHeader('User-Agent', $UA);
237 * Set an arbitrary header
239 public function setHeader($name, $value) {
240 // I feel like I should normalize the case here...
241 $this->reqHeaders
[$name] = $value;
245 * Get an array of the headers
247 public function getHeaderList() {
250 foreach($this->reqHeaders
as $name => $value) {
251 $list[] = "$name: $value";
258 * @param $callback callback
260 public function setCallback( $callback ) {
261 $this->callback
= $callback;
265 * A generic callback to read in the response from a remote server
267 * @param $content string
269 public function read( $fh, $content ) {
270 $this->content
.= $content;
271 return strlen( $content );
275 * Take care of whatever is necessary to perform the URI request.
278 public function execute() {
281 if( strtoupper($this->method
) == "HEAD" ) {
282 $this->headersOnly
= true;
285 if ( is_array( $this->postData
) ) {
286 $this->postData
= wfArrayToCGI( $this->postData
);
289 if ( is_object( $wgTitle ) && !isset($this->reqHeaders
['Referer']) ) {
290 $this->setReferer( $wgTitle->getFullURL() );
293 if ( !$this->noProxy
) {
297 if ( !$this->callback
) {
298 $this->setCallback( array( $this, 'read' ) );
301 if ( !isset($this->reqHeaders
['User-Agent']) ) {
302 $this->setUserAgent(Http
::userAgent());
308 * HttpRequest implemented using internal curl compiled into PHP
310 class CurlHttpRequest
extends HttpRequest
{
311 protected $curlOptions = array();
313 public function execute() {
315 if ( !$this->status
->isOK() ) {
316 return $this->status
;
318 $this->curlOptions
[CURLOPT_PROXY
] = $this->proxy
;
319 $this->curlOptions
[CURLOPT_TIMEOUT
] = $this->timeout
;
320 $this->curlOptions
[CURLOPT_HTTP_VERSION
] = CURL_HTTP_VERSION_1_0
;
321 $this->curlOptions
[CURLOPT_WRITEFUNCTION
] = $this->callback
;
323 /* not sure these two are actually necessary */
324 if(isset($this->reqHeaders
['Referer'])) {
325 $this->curlOptions
[CURLOPT_REFERER
] = $this->reqHeaders
['Referer'];
327 $this->curlOptions
[CURLOPT_USERAGENT
] = $this->reqHeaders
['User-Agent'];
329 if ( $this->sslVerifyHost
) {
330 $this->curlOptions
[CURLOPT_SSL_VERIFYHOST
] = $this->sslVerifyHost
;
333 if ( $this->caInfo
) {
334 $this->curlOptions
[CURLOPT_CAINFO
] = $this->caInfo
;
337 if ( $this->headersOnly
) {
338 $this->curlOptions
[CURLOPT_NOBODY
] = true;
339 $this->curlOptions
[CURLOPT_HEADER
] = true;
340 } elseif ( $this->method
== 'POST' ) {
341 $this->curlOptions
[CURLOPT_POST
] = true;
342 $this->curlOptions
[CURLOPT_POSTFIELDS
] = $this->postData
;
343 // Suppress 'Expect: 100-continue' header, as some servers
344 // will reject it with a 417 and Curl won't auto retry
345 // with HTTP 1.0 fallback
346 $this->reqHeaders
['Expect'] = '';
348 $this->curlOptions
[CURLOPT_CUSTOMREQUEST
] = $this->method
;
351 $this->curlOptions
[CURLOPT_HTTPHEADER
] = $this->getHeaderList();
353 $curlHandle = curl_init( $this->url
);
354 curl_setopt_array( $curlHandle, $this->curlOptions
);
356 if ( false === curl_exec( $curlHandle ) ) {
357 // re-using already translated error messages
358 $this->status
->fatal( 'upload-curl-error'.curl_errno( $curlHandle ).'-text' );
361 curl_close( $curlHandle );
363 return $this->status
;
367 class PhpHttpRequest
extends HttpRequest
{
370 protected function urlToTcp( $url ) {
371 $parsedUrl = parse_url( $url );
373 return 'tcp://' . $parsedUrl['host'] . ':' . $parsedUrl['port'];
376 public function execute() {
377 if ( $this->parsedUrl
['scheme'] != 'http' ) {
378 $this->status
->fatal( 'http-invalid-scheme', $this->parsedURL
['scheme'] );
382 if ( !$this->status
->isOK() ) {
383 return $this->status
;
386 $this->reqHeaders
['Accept'] = "*/*";
387 if ( $this->method
== 'POST' ) {
388 // Required for HTTP 1.0 POSTs
389 $this->reqHeaders
['Content-Length'] = strlen( $this->postData
);
390 $this->reqHeaders
['Content-type'] = "application/x-www-form-urlencoded";
394 if ( $this->proxy
&& !$this->noProxy
) {
395 $options['proxy'] = $this->urlToTCP( $this->proxy
);
396 $options['request_fulluri'] = true;
399 $options['method'] = $this->method
;
400 $options['timeout'] = $this->timeout
;
401 $options['header'] = implode("\r\n", $this->getHeaderList());
402 // FOR NOW: Force everyone to HTTP 1.0
403 /* if ( version_compare( "5.3.0", phpversion(), ">" ) ) { */
404 $options['protocol_version'] = "1.0";
406 /* $options['protocol_version'] = "1.1"; */
409 if ( $this->postData
) {
410 $options['content'] = $this->postData
;
413 $context = stream_context_create( array( 'http' => $options ) );
415 $this->fh
= fopen( $this->url
, "r", false, $context );
416 } catch ( Exception
$e ) {
417 $this->status
->fatal( $e->getMessage() ); /* need some l10n help */
418 return $this->status
;
421 $result = stream_get_meta_data( $this->fh
);
422 if ( $result['timed_out'] ) {
423 $this->status
->fatal( 'http-timed-out', $this->url
);
424 return $this->status
;
427 $this->headers
= $result['wrapper_data'];
431 $contents = fread( $this->fh
, 8192 );
434 $size = call_user_func_array( $this->callback
, array( $this->fh
, $contents ) );
436 $end = ( $size == 0 ) ||
feof( $this->fh
);
440 return $this->status
;