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