changed git call from https to git readonly
[atutor.git] / mods / wiki / plugins / lib / http.php
1 <?php
2 /*
3    This snippet implements HTTP queries, and allows for most request
4    methods, content types and encodings. It is useful for contacting
5    scripts made to serve HTML forms.
6     - does neither depend upon wget or curl, nor any other extension
7     - you can add ->$params (form variables) on the fly, it's a hash
8     - if the initial URL contains a query string, the vars will be
9       extracted first
10     - set the ->$enc very carefully, because many CGI apps and HTTP
11       servers can't deal with it (else "gzip" and "deflate" are nice)
12     - there are abbreviations for the content ->$type values (namely
13       "form" , "url" and "php")
14     - user:password@ pairs may be included in the initially given URL
15     - headers always get normalized to "Studly-Caps"
16     - won't support keep-alive connections
17     - for PUT and other methods, the ->$params var may just hold the
18       request body
19     - files can be added to the ->params array as hash with specially
20       named fields: "content"/"data", and "filename"/"name" , "type"
21     - you can add authentication information using the standard notation
22       "http://user:passw@www.example.com/..." for ->$url and ->$proxy
23
24    A response object will have a ->$content field, ->$headers[] and
25    ->len, ->type attributes as well. You could also ->decode() the
26    body, if it is app/vnd.php.serialized or app/x-www-form-urlencoded.
27    
28    Public Domain (use freely, transform into any other license, like
29    LGPL, BSD, MPL, ...; but if you change this into GPL please be so
30    kind and leave your users a hint where to find the free version).
31 */
32
33
34 #-- request objects
35 class http_request {
36
37    var $method = "GET";
38    var $proto = "HTTP/1.1";
39    var $url = "";
40    var $params = array();   // URL/form post vars, or single request body str
41    var $headers = array();
42    var $cookies = array();
43    var $type = "url";       // content-type, abbrv. for x-www-form-...
44    var $enc = false;        // "gzip" or "deflate"
45    var $error="", $io_err=0, $io_err_s="";
46    var $active_client = 1;  // enables redirect-following
47    var $redirects = 3;
48    var $proxy = false;      // set to "http://host:NN/"
49    var $timeout = 15;
50
51
52    #-- constructor
53    function http_request($method="GET", $url="", $params=NULL) {
54       $this->headers["User-Agent"] = "http_query/17.2 {$GLOBALS[ewiki_config][ua]}";
55       $this->headers["Accept"] = "text/html, application/xml;q=0.9, text/xml;q=0.7, xml/*;q=0.6, text/plain;q=0.5, text/*;q=0.1, image/png;q=0.8, image/*;q=0.4, */*+xml;q=0.3; application/x-msword;q=0.001, */*;q=0.075";
56       $this->headers["Accept-Language"] = "en, eo, es;q=0.2, fr;q=0.1, nl;q=0.1, de;q=0.1";
57       $this->headers["Accept-Charset"] = "iso-8859-1, utf-8";
58       $this->headers["Accept-Feature"] = "textonly, tables, !tcpa, !javascript, !activex, !graphic";
59       $this->headers["Accept-Encoding"] = "deflate, gzip, compress, x-gzip, x-bzip2";
60       //$this->headers["Referer"] = '$google';
61       $this->headers["TE"] = "identity, chunked, binary, base64";
62       $this->headers["Connection"] = "close";
63       //$this->headers["Content-Type"] = & $this->type;
64       if (isset($params)) {
65          $this->params = $params;
66       }
67       if (strpos($method, "://")) {
68          $url = $method;  # glue for incompat PEAR::Http_Request
69          $method = "GET";
70       }
71       $this->method($method);
72       $this->setURL($url);
73    }
74
75
76    #-- sets request method
77    function method($str = "GET") {
78       $this->method = $str;
79    }
80
81    #-- special headers
82    function setcookie($str="name=value", $add="") {
83       $this->cookies[strtok($str,"=")] = strtok("\000").$add;
84    }
85
86
87    #-- deciphers URL into server+path and query string
88    function setURL($url) {
89       if ($this->method == "GET") {
90          $this->url = strtok($url, "?");
91          if ($uu = strtok("\000")) {
92             $this->setQueryString($uu);
93          }
94       }
95       else {
96          $this->url = $url;
97       }
98    }
99    
100    
101    #-- decodes a query strings vars into the $params hash
102    function setQueryString($qs) {
103       $qs = ltrim($qs, "?");
104       parse_str($qs, $this->params);
105    }
106
107
108    #-- returns params as querystring for GET requests
109    function getQueryString() {
110       $qs = "";
111       if (function_exists("http_build_query")) {
112          $qs = http_build_query($this->params);
113       }
114       else {
115          foreach ($this->params as $n=>$v) {
116             $qs .= "&" . urlencode($n) . "=" . urlencode($v);
117          }
118          $qs = substr($qs, 1);
119       }
120       return($qs);
121    }
122
123
124    #-- transforms $params into request body
125    function pack(&$path) {
126       $m = strtoupper($this->method);
127
128       #-- GET, HEAD
129       if (($m == "GET") || ($m == "HEAD")) {
130          $BODY = "";
131          $path .= (strpos($path, "?") ? "&" : "?") . $this->getQueryString();
132       }
133
134       #-- POST
135       elseif (($m == "POST") && is_array($this->params)) {
136
137          #-- known encoding types
138          $type = $this->type($this->type, 0);
139          if ($type == "url") {
140             $BODY = $this->getQueryString($prep="");
141          }
142          elseif ($type == "php") {
143             $BODY = serialize($this->params);
144          }
145          elseif ($type == "form") {
146             // boundary doesn't need checking, unique enough
147             $bnd = "snip-".dechex(time())."-".md5(serialize($this->params))
148                  . "-".dechex(rand())."-snap";
149             $BODY = "";
150             foreach ($this->params as $i=>$v) {
151                $ct = "text/plain";
152                $inj = "";
153                if (is_array($v)) {
154                   ($ct = $v["ct"].$v["type"].$v["content-type"]) || ($ct = "application/octet-stream");
155                   $inj = ' filename="' . urlencode($v["name"].$v["file"].$v["filename"]) . '"';
156                   $v = $v["data"].$v["content"].$v["body"];
157                }
158                $BODY .= "--$bnd\015\012"
159                      . "Content-Disposition: form-data; name=\"".urlencode($i)."\"$inj\015\012"
160                      . "Content-Type: $ct\015\012"
161                      . "Content-Length: " . strlen($v) . "\015\012"
162                      . "\015\012$v\015\012";
163             }
164             $BODY .= "--$bnd--\015\012";
165             $ct = $this->type("form") . "; boundary=$bnd";
166          }
167          #-- ignore
168          else {
169             $this->error = "unsupported POST encoding";
170           // return(false);
171             $BODY = & $this->params;
172          }
173
174          $this->headers["Content-Type"] = isset($ct) ? $ct : $this->type($type, 1);
175       }
176
177       #-- PUT, POST, PUSH, P*
178       elseif ($m[0] == "P") {
179          $BODY = & $this->$params;
180       }
181
182       #-- ERROR (but don't complain)
183       else {
184          $this->error = "unsupported request method '{$this->method}'";
185        //  return(false);
186          $BODY = & $this->params;
187       }
188
189       return($BODY);
190    }
191
192
193    #-- converts content-type strings from/to shortened nick
194    function type($str, $long=1) {
195       $trans = array(
196          "form" => "multipart/form-data",
197          "url" => "application/x-www-form-urlencoded",
198          "php" => "application/vnd.php.serialized",
199       );
200       $trans["multi"] = &$trans["form"];
201       if ($long) {
202          $new = $trans[$str];
203       }
204       else {
205          $new = array_search($str, $trans);
206       }
207       return( $new ? $new : $str );
208    }
209
210
211    #-- initiate the configured HTTP request ------------------------------
212    function go($force=0, $asis=0) {
213
214       #-- prepare parts
215       $url = $this->prepare_url();
216       if (!$url && !$force) { return; }
217       $BODY = $this->body($url);
218       if (($BODY===false) && !$force) { return; }
219       $HEAD = $this->head($url);
220
221       #-- open socket
222       if (!$this->connect($url)) {
223          return;
224       }
225
226       #-- send request data
227       fwrite($this->socket, $HEAD);
228       fwrite($this->socket, $BODY);
229       $HEAD = false;
230       $BODY = false;
231
232       #-- read response, end connection
233       while (!feof($this->socket) && (strlen($DATA) <= 1<<22)) {
234          $DATA .= fread($this->socket, 32<<10);
235       }
236       fclose($this->socket);
237       unset($this->socket);
238
239       #-- for raw http pings
240       if ($asis) { 
241          return($DATA);
242       }
243
244       #-- decode response
245       $r = new http_response();
246       $r->from($DATA);        // should auto-unset $DATA
247
248       #-- handle redirects
249       if ($this->active_client) {
250          $this->auto_actions($r);
251       }
252
253       #-- fin      
254       return($r);
255    }
256
257    #-- alias
258    function start($a=0, $b=0) { 
259       return $this->go($a, $b);
260    }
261    
262    
263    #-- creates socket connection
264    function connect(&$url) {
265       if ((isset($this->socket) and !feof($this->socket))
266       or ($this->socket = fsockopen($url["host"], $url["port"], $this->io_err, $this->io_err_s, $this->timeout))) {
267          socket_set_blocking($this->socket, true);
268          socket_set_timeout($this->socket, $this->timeout, 555);
269          return(true);
270       }
271       else {
272          $this->error = "no socket/connection";
273          return(false);
274       }
275    }
276
277
278    #-- separate URL into pieces, prepare special headers
279    function prepare_url() {
280       $this->setURL($this->url);
281       if (!$this->proxy) {
282          $url = parse_url($this->url);
283          if (strtolower($url["scheme"]) != "http") {
284             $this->error = "unsupported protocol/scheme";
285             return(false);
286          }
287          if (!$url["host"]) { return; }
288          if (!$url["port"]) { $url["port"] = 80; }
289          if (!$url["path"]) { $url["path"] = "/"; }
290          if ($url["query"]) { $url["path"] .= "?" . $url["query"]; }
291          $proxy = "";
292       }
293       else {
294          $url = parse_url($this->proxy);
295          $url["path"] = $this->url;
296          $proxy = "Proxy-";
297          $this->headers["Proxy-Connection"] = $this->headers["Connection"];
298       }
299
300       #-- inj auth headers
301       if ($url["user"] || $url["pass"]) {
302          $this->headers[$proxy."Authorization"] = "Basic " . base64_encode("$url[user]:$url[pass]");
303       }
304       
305       return($url);
306    }
307
308
309    #-- generates request body (if any), must be called before ->head()
310    function body(&$url) {
311
312       #-- encoding of variable $params as request body (according to reqmethod)
313       $BODY = $this->pack($url["path"]);
314       if ($BODY === false) {
315          return false;
316       }
317       elseif ($len = strlen($BODY)) {
318          $this->headers["Content-Length"] = $len;
319       }
320       $enc_funcs = array("gzip"=>"gzencode", "deflate"=>"gzinflate", "bzip2"=>"bzcompress", "x-bzip2"=>"bzcompress", "compress"=>"gzcompress");
321       if ((strlen($BODY) >= 1024) && ($f = $enc_funcs[$this->enc]) && function_exists($f)) {
322          $BODY = $f($BODY);
323          $this->headers["Content-Encoding"] = $this->enc;
324          $this->headers["Content-Length"] = strlen($BODY);
325       }
326       return($BODY);
327    }
328
329
330    #-- generates request head part
331    function head(&$url) {
332    
333       #-- inject cookie header (if any)
334       if ($this->cookies) {
335          $c = "";
336          foreach ($this->cookies as $i=>$v) {
337             $c .= "; " . urlencode($i) . "=" . urlencode($v);
338          }
339          $this->headers["Cookie"] = substr($c, 2);
340          $this->headers["Cookie2"] = '$Version="1"';
341       }
342       
343       #-- request head
344       $CRLF = "\015\012";
345       $HEAD  = "{$this->method} {$url[path]} {$this->proto}$CRLF";
346       $HEAD .= "Host: {$url[host]}$CRLF";
347       foreach ($this->headers as $h=>$v) {
348          $HEAD .= trim($h) . ": " . strtr(trim($v), "\n", " ") . $CRLF;
349       }
350       $HEAD .= $CRLF;
351       return($HEAD);
352    }
353
354    #-- perform some things automatically (redirects)
355    function auto_actions(&$r) {
356
357       #-- behaviour table
358       static $bhv = array(
359          "failure" => "204,300,304,305,306",
360          "clean_::POST" => "300,301,302,303,307",
361          "clean_::PUT" => "300,301,302,303,307",
362          "clean_::GET" => "300",  // $params:=undef
363          "GET_::POST" => "303",
364          "GET_::PUT" => "303",    // downgrade $method:=GET
365       );
366    
367       #-- failure
368       if (strstr($this->behaviour_table["failure"], $r->status)) {
369          return;
370       }
371
372       #-- HTTP redirects
373       if (($pri_url=$r->headers["Location"]) || ($pri_url=$r->headers["Uri"])) {
374
375          if ((($this->redirects--) >= 0) && ($r->status >= 300) && ($r->status < 400)) {
376             $m = strtoupper($this->method);
377             if (strstr($this->behaviour_table["clean_::$m"], $r->status)) {
378                unset($this->params);
379             }
380             if (strstr($this->behaviour_table["GET_::$m"], $r->status)) {
381                $this->method("GET");
382             }
383             $this->setURL($pri_url);
384             $this->go();
385          }
386       }
387    }
388    
389    #-- aliases for compatiblity to PEAR::HTTP_Request
390    function sendRequest() {
391       return $this->go();
392    }
393    function setBasicAuth($user, $pw) {
394       $this->url = preg_replace("#//(.+?@)?#", "//$user@$pw", $this->url);
395    }
396    function setMethod($m) {
397       $this->method($m);
398    }
399    function setProxy($host, $port=8080, $user="", $pw="") {
400       $auth = ($pw ? "$user:$pw@" : ($user ? "$user@" : ""));
401       $this->proxy = "http://$auth$server:$port";
402    }
403    function addHeader($h, $v) {
404       $this->headers[$h] = $v;
405    }
406    function getResponseStatus() {
407       $this->headers[$h] = $v;
408    }
409 }
410 class http_query extends http_request {
411    /* this is just an alias */
412 }
413
414
415
416
417 #-- every query result will be encoded in such an object --------------------
418 class http_response {
419
420    var $status = 520;
421    var $status_str = "";
422    var $headers_str = "";
423    var $headers = array();
424    var $len = 0;
425    var $type = "message/x-raw";
426    var $content = "";
427    
428    
429    function http_response() {
430    }
431    
432
433    #-- fill object from given HTTP response BLOB   
434    function from(&$SRC) {
435       $this->breakHeaders($SRC);  // split data into body + headers
436       $SRC = false;
437       $this->decodeHeaders();     // normalize header names
438       $this->headerMeta();
439       $this->decodeTransferEncodings();    // chunked
440       $this->decodeContentEncodings();     // gzip, deflate
441       $this->len = strlen($this->content);
442    }
443
444
445    #-- separates headers block from response body part
446    function breakHeaders(&$DATA) {
447       $l = strpos($DATA, "\012\015\012"); $skip = 3;
448       $r = strpos($DATA, "\012\012");
449       if ($r && ($r<$l)) { $l = $r; $skip = 2; }
450       if (!$l) { $l = strlen($DATA); }
451       $this->headers_str = rtrim(substr($DATA, 0, $l), "\015");
452       $this->content = substr($DATA, $l + $skip);
453       $this->body = & $this->content;
454       $this->data = & $this->content;  // aliases
455       $this->ct = & $this->type;
456    }
457
458
459    #-- splits up the $headers_str into an array and normalizes header names
460    function decodeHeaders() {
461
462       #-- normalize linebreaks
463       $str = & $this->headers_str;
464 //      $str = str_replace("\n ", " ", $str);
465       $str = str_replace("\r", "", $str);
466       
467       #-- strip headline
468       $nl = strpos($str, "\n") + 1;
469       $this->proto = strtok(substr($str, 0, $nl), " ");
470       $this->status = (int) strtok(" ");
471       $this->status_str = strtok("\000\r\n");
472       if ($this->status == 100) {
473          $this->full_duplex = 1;
474       }
475
476       #-- go through lines, split name:value pairs
477       foreach (explode("\n", substr($str, $nl)) as $line) {
478
479          $i = trim(strtok($line, ":"));
480          $v = trim(strtok("\000"));
481
482          #-- normalize name look&feel
483          $i = strtr(ucwords(strtolower(strtr($i, "-", " "))), " ", "-");
484
485          #-- add to, if key exists
486          if (!empty($this->headers[$i])) {
487             $this->headers[$i] .= ", ".$v;
488          }
489          else {
490             $this->headers[$i] = $v;
491          }
492
493       }
494    }
495
496
497    #-- extract interesting values
498    function headerMeta() {
499       $this->len = strlen($this->content);
500       $this->type = trim(strtok(strtolower($this->headers["Content-Type"]), ";"));
501    }
502    
503
504    #-- strip any content transformation
505    function decodeTransferEncodings() {
506       $enc = trim(strtok(strtolower($this->headers["Transfer-Encoding"]), ",;"));
507       if ($enc) {
508          switch ($enc) {
509             case "chunked":
510                $this->decodeChunkedEncoding();
511                break;
512             case "base64":
513                $this->content = base64_decode($this->content);
514                $this->len = strlen($this->content);
515                break;
516             case "identity": case "binary":
517             case "7bit": case "8bit":
518                break;
519             default:
520                trigger_error("http_response::decodeTransferEncodings: unkown TE of '$enc'\n", E_WARNING);
521          }
522       }
523    }
524
525
526    #-- scripts on HTTP/1.1 servers may send fragmented response
527    function decodeChunkedEncoding() {
528
529       $data = "";       # decoded data
530       $p = 0;           # current string position
531
532       while ($p < strlen($this->content)) {
533
534          #-- read len token
535          $n = strtok(substr($this->content, $p, 20), "\n");
536          $p += strlen($n)+1;
537
538          #-- make integer
539          $n = 0 + (int) (trim($n));
540          if (!$n) {
541             break;
542          }
543
544          #-- read data
545          $data .= substr($this->content, $p, $n);
546          $p += $n;
547       }
548
549       $this->content = $data;
550       unset($data);
551       $this->len = strlen($this->content);
552    }
553
554
555    #-- uncompress response body
556    function decodeContentEncodings() {
557       $enc = trim(strtok(strtolower($this->headers["Content-Encoding"]), ";,"));
558       $dat = &$this->content;
559       if ($enc == "deflate") {
560          $dat = gzinflate($dat);
561       }
562       elseif (($enc == "gzip") || ($enc == "x-gzip")) {
563          if (function_exists("gzdecode")) {
564             $dat = gzdecode($dat);
565          }
566          else {
567             $dat = gzinflate(substr($dat, 10, strlen($dat)-18));
568          }
569       }
570       elseif ($enc == "compress") {
571          $dat = gzuncompress($dat);
572       }
573       elseif (($enc == "x-bzip2") || ($enc == "bzip2")) {
574          if (function_exists("bzdecompress")) {
575             $dat = bzdecompress($dat);
576          }
577          else trigger_error("http_response::decodeContentEncoding: bzip2 decoding isn't supported with this PHP interpreter version", E_WARNING);
578       }
579       $this->len = strlen($this->content);
580    }
581
582
583    #-- can handle special content-types (multipart, serialized, form-data)
584    function decode() {
585       $t = http_request::type($this->type, 0);
586       if ($t == "php") {
587          return(unserialize($this->content));
588       }
589       elseif ($t == "url") {
590          parse_str($this->content, $r);
591          return($r);
592       }
593       elseif ($t == "form") {
594          // oh, not yet exactly
595       }
596    }
597
598    #-- aliases for compatiblity to PEAR::HTTP_Request
599    function getResponseBody() {
600       return $this->content;
601    }
602    function getResponseStatus() {
603       return $this->status;
604    }
605    function getResponseCode() {
606       return $this->status;
607    }
608    function getResponseHeader($i=NULL) {
609       if (!isset($i)) {
610          return $this->headers;
611       }
612       $i = strtolower($i);
613       foreach ($this->headers as $h=>$v) {
614          if (strtolower($h)==$i) {
615             return $v;
616          }
617       }
618    }
619 }
620
621
622
623 ?>