<?php
/*
  api: PHP
  type: functions
  category: library
  priority: optional
  provides: http-request
  title: HTTP requests
  description: implements HTTP protocol, various request methods supported
  url: http://freshmeat.net/p/upgradephp
  version: 11.3

   This snippet implements HTTP queries, and allows for most request
   methods, content types and encodings. It is useful for contacting
   scripts made to serve HTML forms.
    - does neither depend upon wget or curl, nor any other extension
    - you can add ->$params (form variables) on the fly, it's a hash
    - if the initial URL contains a query string, the vars will be
      extracted first
    - set the ->$enc very carefully, because many CGI apps and HTTP
      servers can't deal with it (else "gzip" and "deflate" are nice)
    - there are abbreviations for the content ->$type values (namely
      "form" , "url" and "php")
    - user:password@ pairs may be included in the initially given URL
    - headers always get normalized to "Studly-Caps"
    - won't support keep-alive connections
    - for PUT and other methods, the ->$params var may just hold the
      request body
    - files can be added to the ->params array as hash with specially
      named fields: "content"/"data", and "filename"/"name" , "type"
    - you can add authentication information using the standard notation
      "http://user:passw@www.example.com/..." for ->$url and ->$proxy

   A response object will have a ->$content field, ->$headers[] and
   ->len, ->type attributes as well. You could also ->decode() the
   body, if it is app/vnd.php.serialized or app/x-www-form-urlencoded.
   
   Public Domain (use freely, transform into any other license, like
   LGPL, BSD, MPL, ...; but if you change this into GPL please be so
   kind and leave your users a hint where to find the free version).
*/


#-- request objects
class http_request {

   var $method = "GET";
   var $proto = "HTTP/1.1";
   var $url = "";
   var $params = array();   // URL/form post vars, or single request body str
   var $headers = array();
   var $cookies = array();
   var $type = "url";       // content-type, abbrv. for x-www-form-...
   var $enc = false;        // "gzip" or "deflate"
   var $error="", $io_err=0, $io_err_s="";
   var $active_client = 1;  // enables redirect-following
   var $redirects = 3;
   var $proxy = false;      // set to "http://host:NN/"
   var $timeout = 15;


   #-- constructor
   function http_request($method="GET", $url="", $params=NULL) {
      $this->headers["User-Agent"] = "http_query/17.2 {$GLOBALS[ewiki_config][ua]}";
      $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";
      $this->headers["Accept-Language"] = "en, eo, es;q=0.2, fr;q=0.1, nl;q=0.1, de;q=0.1";
      $this->headers["Accept-Charset"] = "iso-8859-1, utf-8";
      $this->headers["Accept-Feature"] = "textonly, tables, !tcpa, !javascript, !activex, !graphic";
      $this->headers["Accept-Encoding"] = "deflate, gzip, compress, x-gzip, x-bzip2";
      //$this->headers["Referer"] = '$google';
      $this->headers["TE"] = "identity, chunked, binary, base64";
      $this->headers["Connection"] = "close";
      //$this->headers["Content-Type"] = & $this->type;
      if (isset($params)) {
         $this->params = $params;
      }
      if (strpos($method, "://")) {
         $url = $method;  # glue for incompat PEAR::Http_Request
         $method = "GET";
      }
      $this->method($method);
      $this->setURL($url);
   }


   #-- sets request method
   function method($str = "GET") {
      $this->method = $str;
   }

   #-- special headers
   function setcookie($str="name=value", $add="") {
      $this->cookies[strtok($str,"=")] = strtok("\000").$add;
   }


   #-- deciphers URL into server+path and query string
   function setURL($url) {
      if ($this->method == "GET") {
         $this->url = strtok($url, "?");
         if ($uu = strtok("\000")) {
            $this->setQueryString($uu);
         }
      }
      else {
         $this->url = $url;
      }
   }
   
   
   #-- decodes a query strings vars into the $params hash
   function setQueryString($qs) {
      $qs = ltrim($qs, "?");
      parse_str($qs, $this->params);
   }


   #-- returns params as querystring for GET requests
   function getQueryString() {
      $qs = "";
      if (function_exists("http_build_query")) {
         $qs = http_build_query($this->params);
      }
      else {
         foreach ($this->params as $n=>$v) {
            $qs .= "&" . urlencode($n) . "=" . urlencode($v);
         }
         $qs = substr($qs, 1);
      }
      return($qs);
   }


   #-- transforms $params into request body
   function pack(&$path) {
      $m = strtoupper($this->method);

      #-- GET, HEAD
      if (($m == "GET") || ($m == "HEAD")) {
         $BODY = "";
         $path .= (strpos($path, "?") ? "&" : "?") . $this->getQueryString();
      }

      #-- POST
      elseif (($m == "POST") && is_array($this->params)) {

         #-- known encoding types
         $type = $this->type($this->type, 0);
         if ($type == "url") {
            $BODY = $this->getQueryString($prep="");
         }
         elseif ($type == "php") {
            $BODY = serialize($this->params);
         }
         elseif ($type == "form") {
            // boundary doesn't need checking, unique enough
            $bnd = "snip-".dechex(time())."-".md5(serialize($this->params))
                 . "-".dechex(rand())."-snap";
            $BODY = "";
            foreach ($this->params as $i=>$v) {
               $ct = "text/plain";
               $inj = "";
               if (is_array($v)) {
                  ($ct = $v["ct"].$v["type"].$v["content-type"]) || ($ct = "application/octet-stream");
                  $inj = ' filename="' . urlencode($v["name"].$v["file"].$v["filename"]) . '"';
                  $v = $v["data"].$v["content"].$v["body"];
               }
               $BODY .= "--$bnd\015\012"
                     . "Content-Disposition: form-data; name=\"".urlencode($i)."\"$inj\015\012"
                     . "Content-Type: $ct\015\012"
                     . "Content-Length: " . strlen($v) . "\015\012"
                     . "\015\012$v\015\012";
            }
            $BODY .= "--$bnd--\015\012";
            $ct = $this->type("form") . "; boundary=$bnd";
         }
         #-- ignore
         else {
            $this->error = "unsupported POST encoding";
          // return(false);
            $BODY = & $this->params;
         }

         $this->headers["Content-Type"] = isset($ct) ? $ct : $this->type($type, 1);
      }

      #-- PUT, POST, PUSH, P*
      elseif ($m[0] == "P") {
         $BODY = & $this->$params;
      }

      #-- ERROR (but don't complain)
      else {
         $this->error = "unsupported request method '{$this->method}'";
       //  return(false);
         $BODY = & $this->params;
      }

      return($BODY);
   }


   #-- converts content-type strings from/to shortened nick
   function type($str, $long=1) {
      $trans = array(
         "form" => "multipart/form-data",
         "url" => "application/x-www-form-urlencoded",
         "php" => "application/vnd.php.serialized",
      );
      $trans["multi"] = &$trans["form"];
      if ($long) {
         $new = $trans[$str];
      }
      else {
         $new = array_search($str, $trans);
      }
      return( $new ? $new : $str );
   }


   #-- initiate the configured HTTP request ------------------------------
   function go($force=0, $asis=0) {

      #-- prepare parts
      $url = $this->prepare_url();
      if (!$url && !$force) { return; }
      $BODY = $this->body($url);
      if (($BODY===false) && !$force) { return; }
      $HEAD = $this->head($url);

      #-- open socket
      if (!$this->connect($url)) {
         return;
      }

      #-- send request data
      fwrite($this->socket, $HEAD);
      fwrite($this->socket, $BODY);
      $HEAD = false;
      $BODY = false;

      #-- read response, end connection
      while (!feof($this->socket) && (strlen($DATA) <= 1<<22)) {
         $DATA .= fread($this->socket, 32<<10);
#echo "fread(".strlen($DATA).") ";
      }
      fclose($this->socket);
      unset($this->socket);

      #-- for raw http pings
      if ($asis) { 
         return($DATA);
      }

      #-- decode response
      $r = new http_response();
      $r->from($DATA);        // should auto-unset $DATA

      #-- handle redirects
      if ($this->active_client) {
         $this->auto_actions($r);
      }

      #-- fin      
      return($r);
   }

   #-- alias
   function start($a=0, $b=0) { 
      return $this->go($a, $b);
   }
   
   
   #-- creates socket connection
   function connect(&$url) {
      if ((isset($this->socket) and !feof($this->socket))
      or ($this->socket = fsockopen($url["host"], $url["port"], $this->io_err, $this->io_err_s, $this->timeout))) {
         socket_set_blocking($this->socket, true);
         socket_set_timeout($this->socket, $this->timeout, 555);
         return(true);
      }
      else {
         $this->error = "no socket/connection";
         return(false);
      }
   }


   #-- separate URL into pieces, prepare special headers
   function prepare_url() {
      $this->setURL($this->url);
      if (!$this->proxy) {
         $url = parse_url($this->url);
         if (strtolower($url["scheme"]) != "http") {
            $this->error = "unsupported protocol/scheme";
            return(false);
         }
         if (!$url["host"]) { return; }
         if (!$url["port"]) { $url["port"] = 80; }
         if (!$url["path"]) { $url["path"] = "/"; }
         if ($url["query"]) { $url["path"] .= "?" . $url["query"]; }
         $proxy = "";
      }
      else {
         $url = parse_url($this->proxy);
         $url["path"] = $this->url;
         $proxy = "Proxy-";
         $this->headers["Proxy-Connection"] = $this->headers["Connection"];
      }

      #-- inj auth headers
      if ($url["user"] || $url["pass"]) {
         $this->headers[$proxy."Authorization"] = "Basic " . base64_encode("$url[user]:$url[pass]");
      }
      
      return($url);
   }


   #-- generates request body (if any), must be called before ->head()
   function body(&$url) {

      #-- encoding of variable $params as request body (according to reqmethod)
      $BODY = $this->pack($url["path"]);
      if ($BODY === false) {
         return false;
      }
      elseif ($len = strlen($BODY)) {
         $this->headers["Content-Length"] = $len;
      }
      $enc_funcs = array("gzip"=>"gzencode", "deflate"=>"gzinflate", "bzip2"=>"bzcompress", "x-bzip2"=>"bzcompress", "compress"=>"gzcompress");
      if ((strlen($BODY) >= 1024) && ($f = $enc_funcs[$this->enc]) && function_exists($f)) {
         $BODY = $f($BODY);
         $this->headers["Content-Encoding"] = $this->enc;
         $this->headers["Content-Length"] = strlen($BODY);
      }
      return($BODY);
   }


   #-- generates request head part
   function head(&$url) {
   
      #-- inject cookie header (if any)
      if ($this->cookies) {
         $c = "";
         foreach ($this->cookies as $i=>$v) {
            $c .= "; " . urlencode($i) . "=" . urlencode($v);
         }
         $this->headers["Cookie"] = substr($c, 2);
         $this->headers["Cookie2"] = '$Version="1"';
      }
      
      #-- request head
      $CRLF = "\015\012";
      $HEAD  = "{$this->method} {$url[path]} {$this->proto}$CRLF";
      $HEAD .= "Host: {$url[host]}$CRLF";
      foreach ($this->headers as $h=>$v) {
         $HEAD .= trim($h) . ": " . strtr(trim($v), "\n", " ") . $CRLF;
      }
      $HEAD .= $CRLF;
      return($HEAD);
   }

   #-- perform some things automatically (redirects)
   function auto_actions(&$r) {

      #-- behaviour table
      static $bhv = array(
         "failure" => "204,300,304,305,306",
         "clean_::POST" => "300,301,302,303,307",
         "clean_::PUT" => "300,301,302,303,307",
         "clean_::GET" => "300",  // $params:=undef
         "GET_::POST" => "303",
         "GET_::PUT" => "303",    // downgrade $method:=GET
      );
   
      #-- failure
      if (strstr($this->behaviour_table["failure"], $r->status)) {
         return;
      }

      #-- HTTP redirects
      if (($pri_url=$r->headers["Location"]) || ($pri_url=$r->headers["Uri"])) {

         if ((($this->redirects--) >= 0) && ($r->status >= 300) && ($r->status < 400)) {
            $m = strtoupper($this->method);
            if (strstr($this->behaviour_table["clean_::$m"], $r->status)) {
               unset($this->params);
            }
            if (strstr($this->behaviour_table["GET_::$m"], $r->status)) {
               $this->method("GET");
            }
            $this->setURL($pri_url);
            $this->go();
         }
      }
   }
   
   #-- aliases for compatiblity to PEAR::HTTP_Request
   function sendRequest() {
      return $this->go();
   }
   function setBasicAuth($user, $pw) {
      $this->url = preg_replace("#//(.+?@)?#", "//$user@$pw", $this->url);
   }
   function setMethod($m) {
      $this->method($m);
   }
   function setProxy($host, $port=8080, $user="", $pw="") {
      $auth = ($pw ? "$user:$pw@" : ($user ? "$user@" : ""));
      $this->proxy = "http://$auth$server:$port";
   }
   function addHeader($h, $v) {
      $this->headers[$h] = $v;
   }
   function getResponseStatus() {
      $this->headers[$h] = $v;
   }
}
class http_query extends http_request {
   /* this is just an alias */
}




#-- every query result will be encoded in such an object --------------------
class http_response {

   var $status = 520;
   var $status_str = "";
   var $headers_str = "";
   var $headers = array();
   var $len = 0;
   var $type = "message/x-raw";
   var $content = "";
   
   
   function http_response() {
   }
   

   #-- fill object from given HTTP response BLOB   
   function from(&$SRC) {
      $this->breakHeaders($SRC);  // split data into body + headers
      $SRC = false;
      $this->decodeHeaders();     // normalize header names
      $this->headerMeta();
      $this->decodeTransferEncodings();    // chunked
      $this->decodeContentEncodings();     // gzip, deflate
      $this->len = strlen($this->content);
   }


   #-- separates headers block from response body part
   function breakHeaders(&$DATA) {
      $l = strpos($DATA, "\012\015\012"); $skip = 3;
      $r = strpos($DATA, "\012\012");
      if ($r && ($r<$l)) { $l = $r; $skip = 2; }
      if (!$l) { $l = strlen($DATA); }
      $this->headers_str = rtrim(substr($DATA, 0, $l), "\015");
      $this->content = substr($DATA, $l + $skip);
      $this->body = & $this->content;
      $this->data = & $this->content;  // aliases
      $this->ct = & $this->type;
   }


   #-- splits up the $headers_str into an array and normalizes header names
   function decodeHeaders() {

      #-- normalize linebreaks
      $str = & $this->headers_str;
//      $str = str_replace("\n ", " ", $str);
      $str = str_replace("\r", "", $str);
      
      #-- strip headline
      $nl = strpos($str, "\n") + 1;
      $this->proto = strtok(substr($str, 0, $nl), " ");
      $this->status = (int) strtok(" ");
      $this->status_str = strtok("\000\r\n");
      if ($this->status == 100) {
         $this->full_duplex = 1;
      }

      #-- go through lines, split name:value pairs
      foreach (explode("\n", substr($str, $nl)) as $line) {

         $i = trim(strtok($line, ":"));
         $v = trim(strtok("\000"));

         #-- normalize name look&feel
         $i = strtr(ucwords(strtolower(strtr($i, "-", " "))), " ", "-");

         #-- add to, if key exists
         if (!empty($this->headers[$i])) {
            $this->headers[$i] .= ", ".$v;
         }
         else {
            $this->headers[$i] = $v;
         }

      }
   }


   #-- extract interesting values
   function headerMeta() {
      $this->len = strlen($this->content);
      $this->type = trim(strtok(strtolower($this->headers["Content-Type"]), ";"));
   }
   

   #-- strip any content transformation
   function decodeTransferEncodings() {
      $enc = trim(strtok(strtolower($this->headers["Transfer-Encoding"]), ",;"));
      if ($enc) {
         switch ($enc) {
#echo "ENC($enc) ";
            case "chunked":
               $this->decodeChunkedEncoding();
               break;
            case "base64":
               $this->content = base64_decode($this->content);
               $this->len = strlen($this->content);
               break;
            case "identity": case "binary":
            case "7bit": case "8bit":
               break;
            default:
               trigger_error("http_response::decodeTransferEncodings: unkown TE of '$enc'\n", E_WARNING);
         }
      }
   }


   #-- scripts on HTTP/1.1 servers may send fragmented response
   function decodeChunkedEncoding() {

      $bin = "";	# decoded data
      $p = 0;		# current string position
#file_put_contents("/tmp/1", $this->content);

      while ($p < strlen($this->content)) {
         #-- read len token
         $n = strtok(substr($this->content, $p, 20), "\n");
#echo "CHUNK($p,$n) ";
         $p += strlen($n)+1;
#echo "CHUNK2($p,$n) ";

         #-- make integer
         $n = hexdec(trim($n));
         if ($n===0) {
            break;
         }
         elseif (!$n) {
            break; //WARN
         }
         $n += 1;

         #-- read data
         $bin .= substr($this->content, $p, $n);
         $p += $n + 1;
#echo "CHUNK3($p,$n) ";
      }

      $this->content = $bin;
      unset($bin);
      $this->len = strlen($this->content);
   }


   #-- uncompress response body
   function decodeContentEncodings() {
      $enc = trim(strtok(strtolower($this->headers["Content-Encoding"]), ";,"));
      $dat = &$this->content;
      if ($enc == "deflate") {
         $dat = gzinflate($dat);
      }
      elseif (($enc == "gzip") || ($enc == "x-gzip")) {
         if (function_exists("gzdecode")) {
            $dat = gzdecode($dat);
         }
         else {
            $dat = gzinflate(substr($dat, 10, strlen($dat)-18));
         }
      }
      elseif ($enc == "compress") {
         $dat = gzuncompress($dat);
      }
      elseif (($enc == "x-bzip2") || ($enc == "bzip2")) {
         if (function_exists("bzdecompress")) {
            $dat = bzdecompress($dat);
         }
         else trigger_error("http_response::decodeContentEncoding: bzip2 decoding isn't supported with this PHP interpreter version", E_WARNING);
      }
      $this->len = strlen($this->content);
   }


   #-- can handle special content-types (multipart, serialized, form-data)
   function decode() {
      $t = http_request::type($this->type, 0);
      if ($t == "php") {
         return(unserialize($this->content));
      }
      elseif ($t == "url") {
         parse_str($this->content, $r);
         return($r);
      }
      elseif ($t == "form") {
         // oh, not yet exactly
      }
   }

   #-- aliases for compatiblity to PEAR::HTTP_Request
   function getResponseBody() {
      return $this->content;
   }
   function getResponseStatus() {
      return $this->status;
   }
   function getResponseCode() {
      return $this->status;
   }
   function getResponseHeader($i=NULL) {
      if (!isset($i)) {
         return $this->headers;
      }
      $i = strtolower($i);
      foreach ($this->headers as $h=>$v) {
         if (strtolower($h)==$i) {
            return $v;
         }
      }
   }
}



?>