stylize, message fixups.
[lhc/web/wiklou.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 /**
12 * Perform an HTTP request
13 * @param $method string HTTP method. Usually GET/POST
14 * @param $url string Full URL to act on
15 * @param $opts options to pass to HttpRequest object
16 * @returns mixed (bool)false on failure or a string on success
17 */
18 public static function request( $method, $url, $opts = array() ) {
19 $opts['method'] = strtoupper( $method );
20 if ( !array_key_exists( 'timeout', $opts ) ) {
21 $opts['timeout'] = 'default';
22 }
23 $req = HttpRequest::factory( $url, $opts );
24 $status = $req->execute();
25 if ( $status->isOK() ) {
26 return $req;
27 } else {
28 return false;
29 }
30 }
31
32 /**
33 * Simple wrapper for Http::request( 'GET' )
34 * @see Http::request()
35 */
36 public static function get( $url, $timeout = 'default', $opts = array() ) {
37 $opts['timeout'] = $timeout;
38 return Http::request( 'GET', $url, $opts );
39 }
40
41 /**
42 * Simple wrapper for Http::request( 'POST' )
43 * @see Http::request()
44 */
45 public static function post( $url, $opts = array() ) {
46 return Http::request( 'POST', $url, $opts );
47 }
48
49 /**
50 * Check if the URL can be served by localhost
51 * @param $url string Full url to check
52 * @return bool
53 */
54 public static function isLocalURL( $url ) {
55 global $wgCommandLineMode, $wgConf;
56 if ( $wgCommandLineMode ) {
57 return false;
58 }
59
60 // Extract host part
61 $matches = array();
62 if ( preg_match( '!^http://([\w.-]+)[/:].*$!', $url, $matches ) ) {
63 $host = $matches[1];
64 // Split up dotwise
65 $domainParts = explode( '.', $host );
66 // Check if this domain or any superdomain is listed in $wgConf as a local virtual host
67 $domainParts = array_reverse( $domainParts );
68 for ( $i = 0; $i < count( $domainParts ); $i++ ) {
69 $domainPart = $domainParts[$i];
70 if ( $i == 0 ) {
71 $domain = $domainPart;
72 } else {
73 $domain = $domainPart . '.' . $domain;
74 }
75 if ( $wgConf->isLocalVHost( $domain ) ) {
76 return true;
77 }
78 }
79 }
80 return false;
81 }
82
83 /**
84 * A standard user-agent we can use for external requests.
85 * @returns string
86 */
87 public static function userAgent() {
88 global $wgVersion;
89 return "MediaWiki/$wgVersion";
90 }
91
92 /**
93 * Checks that the given URI is a valid one
94 * @param $uri Mixed: URI to check for validity
95 * @returns bool
96 */
97 public static function isValidURI( $uri ) {
98 return preg_match(
99 '/(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/',
100 $uri,
101 $matches
102 );
103 }
104 }
105
106 /**
107 * This wrapper class will call out to curl (if available) or fallback
108 * to regular PHP if necessary for handling internal HTTP requests.
109 */
110 class HttpRequest {
111 protected $content;
112 protected $timeout = 'default';
113 protected $headersOnly = null;
114 protected $postdata = null;
115 protected $proxy = null;
116 protected $no_proxy = false;
117 protected $sslVerifyHost = true;
118 protected $caInfo = null;
119 protected $method = "GET";
120 protected $url;
121 protected $parsed_url;
122 public $status;
123
124 /**
125 * @param $url string url to use
126 * @param $options array (optional) extra params to pass
127 * Possible keys for the array:
128 * method
129 * timeout
130 * targetFilePath
131 * requestKey
132 * headersOnly
133 * postdata
134 * proxy
135 * no_proxy
136 * sslVerifyHost
137 * caInfo
138 */
139 function __construct( $url = null, $opt = array() ) {
140 global $wgHTTPTimeout, $wgTitle;
141
142 $this->url = $url;
143 $this->parsed_url = parse_url( $url );
144
145 if ( !ini_get( 'allow_url_fopen' ) ) {
146 throw new MWException( 'allow_url_fopen needs to be enabled for http requests to work' );
147 } elseif ( !Http::isValidURI( $this->url ) ) {
148 throw new MWException( 'Invalid URL' );
149 } else {
150 $this->status = Status::newGood( 100 ); // continue
151 }
152
153 if ( array_key_exists( 'timeout', $opt ) && $opt['timeout'] != 'default' ) {
154 $this->timeout = $opt['timeout'];
155 } else {
156 $this->timeout = $wgHTTPTimeout;
157 }
158
159 $members = array( "targetFilePath", "requestKey", "headersOnly", "postdata",
160 "proxy", "no_proxy", "sslVerifyHost", "caInfo", "method" );
161 foreach ( $members as $o ) {
162 if ( array_key_exists( $o, $opt ) ) {
163 $this->$o = $opt[$o];
164 }
165 }
166
167 if ( is_array( $this->postdata ) ) {
168 $this->postdata = wfArrayToCGI( $this->postdata );
169 }
170
171 $this->initRequest();
172
173 if ( !$this->no_proxy ) {
174 $this->proxySetup();
175 }
176
177 # Set the referer to $wgTitle, even in command-line mode
178 # This is useful for interwiki transclusion, where the foreign
179 # server wants to know what the referring page is.
180 # $_SERVER['REQUEST_URI'] gives a less reliable indication of the
181 # referring page.
182 if ( is_object( $wgTitle ) ) {
183 $this->setReferrer( $wgTitle->getFullURL() );
184 }
185 }
186
187 /**
188 * For backwards compatibility, we provide a __toString method so
189 * that any code that expects a string result from Http::Get()
190 * will see the content of the request.
191 */
192 function __toString() {
193 return $this->content;
194 }
195
196 /**
197 * Generate a new request object
198 * @see HttpRequest::__construct
199 */
200 public static function factory( $url, $opt ) {
201 global $wgHTTPEngine;
202 $engine = $wgHTTPEngine;
203
204 if ( !$wgHTTPEngine ) {
205 $wgHTTPEngine = function_exists( 'curl_init' ) ? 'curl' : 'php';
206 } elseif ( $wgHTTPEngine == 'curl' && !function_exists( 'curl_init' ) ) {
207 throw new MWException( 'FIXME' );
208 }
209
210 switch( $wgHTTPEngine ) {
211 case 'curl':
212 return new CurlHttpRequest( $url, $opt );
213 case 'php':
214 return new PhpHttpRequest( $url, $opt );
215 default:
216 throw new MWException( 'The setting of $wgHTTPEngine is not valid.' );
217 }
218 }
219
220 public function getContent() {
221 return $this->content;
222 }
223
224 public function initRequest() { }
225 public function proxySetup() { }
226 public function setReferrer( $url ) { }
227 public function setCallback( $cb ) { }
228 public function read( $fh, $content ) { }
229 public function getCode() { }
230 public function execute() { }
231 }
232
233 /**
234 * HttpRequest implemented using internal curl compiled into PHP
235 */
236 class CurlHttpRequest extends HttpRequest {
237 protected $curlHandle;
238 protected $curlCBSet;
239
240 public function initRequest() {
241 $this->curlHandle = curl_init( $this->url );
242 }
243
244 public function proxySetup() {
245 global $wgHTTPProxy;
246
247 if ( is_string( $this->proxy ) ) {
248 curl_setopt( $this->curlHandle, CURLOPT_PROXY, $this->proxy );
249 } else if ( Http::isLocalURL( $this->url ) ) { /* Not sure this makes any sense. */
250 curl_setopt( $this->curlHandle, CURLOPT_PROXY, 'localhost:80' );
251 } else if ( $wgHTTPProxy ) {
252 curl_setopt( $this->curlHandle, CURLOPT_PROXY, $wgHTTPProxy );
253 }
254 }
255
256 public function setCallback( $cb ) {
257 if ( !$this->curlCBSet ) {
258 $this->curlCBSet = true;
259 curl_setopt( $this->curlHandle, CURLOPT_WRITEFUNCTION, $cb );
260 }
261 }
262
263 public function execute() {
264 if ( !$this->status->isOK() ) {
265 return $this->status;
266 }
267
268 $this->setCallback( array( $this, 'read' ) );
269
270 curl_setopt( $this->curlHandle, CURLOPT_TIMEOUT, $this->timeout );
271 curl_setopt( $this->curlHandle, CURLOPT_USERAGENT, Http::userAgent() );
272 curl_setopt( $this->curlHandle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0 );
273
274 if ( $this->sslVerifyHost ) {
275 curl_setopt( $this->curlHandle, CURLOPT_SSL_VERIFYHOST, $this->sslVerifyHost );
276 }
277
278 if ( $this->caInfo ) {
279 curl_setopt( $this->curlHandle, CURLOPT_CAINFO, $this->caInfo );
280 }
281
282 if ( $this->headersOnly ) {
283 curl_setopt( $this->curlHandle, CURLOPT_NOBODY, true );
284 curl_setopt( $this->curlHandle, CURLOPT_HEADER, true );
285 } elseif ( $this->method == 'POST' ) {
286 curl_setopt( $this->curlHandle, CURLOPT_POST, true );
287 curl_setopt( $this->curlHandle, CURLOPT_POSTFIELDS, $this->postdata );
288 // Suppress 'Expect: 100-continue' header, as some servers
289 // will reject it with a 417 and Curl won't auto retry
290 // with HTTP 1.0 fallback
291 curl_setopt( $this->curlHandle, CURLOPT_HTTPHEADER, array( 'Expect:' ) );
292 } else {
293 curl_setopt( $this->curlHandle, CURLOPT_CUSTOMREQUEST, $this->method );
294 }
295
296 try {
297 if ( false === curl_exec( $this->curlHandle ) ) {
298 $this->status->fatal( 'Error sending request (#$1): $2',
299 curl_errno( $this->curlHandle ),
300 curl_error( $this->curlHandle ) );
301 }
302 } catch ( Exception $e ) {
303 $errno = curl_errno( $this->curlHandle );
304 if ( $errno != CURLE_OK ) {
305 $errstr = curl_error( $this->curlHandle );
306 $this->status->fatal( 'CURL error code $1: $2', $errno, $errstr );
307 }
308 }
309
310 curl_close( $this->curlHandle );
311
312 return $this->status;
313 }
314
315 public function read( $curlH, $content ) {
316 $this->content .= $content;
317 return strlen( $content );
318 }
319
320 public function getCode() {
321 # Don't return truncated output
322 $code = curl_getinfo( $this->curlHandle, CURLINFO_HTTP_CODE );
323 if ( $code < 400 ) {
324 $this->status->setResult( true, $code );
325 } else {
326 $this->status->setResult( false, $code );
327 }
328 }
329 }
330
331 class PhpHttpRequest extends HttpRequest {
332 private $reqHeaders;
333 private $callback;
334 private $fh;
335
336 public function initRequest() {
337 $this->setCallback( array( $this, 'read' ) );
338
339 $this->reqHeaders[] = "User-Agent: " . Http::userAgent();
340 $this->reqHeaders[] = "Accept: */*";
341 if ( $this->method == 'POST' ) {
342 // Required for HTTP 1.0 POSTs
343 $this->reqHeaders[] = "Content-Length: " . strlen( $this->postdata );
344 $this->reqHeaders[] = "Content-type: application/x-www-form-urlencoded";
345 }
346
347 if ( $this->parsed_url['scheme'] != 'http' ) {
348 $this->status->fatal( "Only http:// is supported currently." );
349 }
350 }
351
352 protected function urlToTcp( $url ) {
353 $parsed_url = parse_url( $url );
354
355 return 'tcp://' . $parsed_url['host'] . ':' . $parsed_url['port'];
356 }
357
358 public function proxySetup() {
359 global $wgHTTPProxy;
360
361 if ( Http::isLocalURL( $this->url ) ) {
362 $this->proxy = 'http://localhost:80/';
363 } elseif ( $wgHTTPProxy ) {
364 $this->proxy = $wgHTTPProxy ;
365 }
366 }
367
368 public function setReferrer( $url ) {
369 $this->reqHeaders[] = "Referer: $url";
370 }
371
372 public function setCallback( $cb ) {
373 $this->callback = $cb;
374 }
375
376 public function read( $fh, $contents ) {
377 if ( $this->headersOnly ) {
378 return false;
379 }
380 $this->content .= $contents;
381
382 return strlen( $contents );
383 }
384
385 public function execute() {
386 if ( !$this->status->isOK() ) {
387 return $this->status;
388 }
389
390 $opts = array();
391 if ( $this->proxy && !$this->no_proxy ) {
392 $opts['proxy'] = $this->urlToTCP( $this->proxy );
393 $opts['request_fulluri'] = true;
394 }
395
396 $opts['method'] = $this->method;
397 $opts['timeout'] = $this->timeout;
398 $opts['header'] = implode( "\r\n", $this->reqHeaders );
399 // FOR NOW: Force everyone to HTTP 1.0
400 /* if ( version_compare( "5.3.0", phpversion(), ">" ) ) { */
401 $opts['protocol_version'] = "1.0";
402 /* } else { */
403 /* $opts['protocol_version'] = "1.1"; */
404 /* } */
405
406 if ( $this->postdata ) {
407 $opts['content'] = $this->postdata;
408 }
409
410 $context = stream_context_create( array( 'http' => $opts ) );
411 try {
412 $this->fh = fopen( $this->url, "r", false, $context );
413 } catch ( Exception $e ) {
414 $this->status->fatal( $e->getMessage() );
415 return $this->status;
416 }
417
418 $result = stream_get_meta_data( $this->fh );
419 if ( $result['timed_out'] ) {
420 $this->status->error( 'The request timed out' );
421 }
422
423 $this->headers = $result['wrapper_data'];
424
425 $end = false;
426 while ( !$end ) {
427 $contents = fread( $this->fh, 8192 );
428 $size = call_user_func_array( $this->callback, array( $this->fh, $contents ) );
429 $end = ( $size == 0 ) || feof( $this->fh );
430 }
431 fclose( $this->fh );
432
433 return $this->status;
434 }
435 }