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