[ Index ]

PHP Cross Reference of DokuWiki

title

Body

[close]

/inc/ -> HTTPClient.php (source)

   1  <?php
   2  /**
   3   * HTTP Client
   4   *
   5   * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
   6   * @author     Andreas Goetz <cpuidle@gmx.de>
   7   */
   8  
   9  if(!defined('DOKU_INC')) define('DOKU_INC',fullpath(dirname(__FILE__).'/../').'/');
  10  require_once(DOKU_CONF.'dokuwiki.php');
  11  
  12  define('HTTP_NL',"\r\n");
  13  
  14  
  15  /**
  16   * Adds DokuWiki specific configs to the HTTP client
  17   *
  18   * @author Andreas Goetz <cpuidle@gmx.de>
  19   */
  20  class DokuHTTPClient extends HTTPClient {
  21  
  22      /**
  23       * Constructor.
  24       *
  25       * @author Andreas Gohr <andi@splitbrain.org>
  26       */
  27      function DokuHTTPClient(){
  28          global $conf;
  29  
  30          // call parent constructor
  31          $this->HTTPClient();
  32  
  33          // set some values from the config
  34          $this->proxy_host = $conf['proxy']['host'];
  35          $this->proxy_port = $conf['proxy']['port'];
  36          $this->proxy_user = $conf['proxy']['user'];
  37          $this->proxy_pass = $conf['proxy']['pass'];
  38          $this->proxy_ssl  = $conf['proxy']['ssl'];
  39      }
  40  }
  41  
  42  /**
  43   * This class implements a basic HTTP client
  44   *
  45   * It supports POST and GET, Proxy usage, basic authentication,
  46   * handles cookies and referers. It is based upon the httpclient
  47   * function from the VideoDB project.
  48   *
  49   * @link   http://www.splitbrain.org/go/videodb
  50   * @author Andreas Goetz <cpuidle@gmx.de>
  51   * @author Andreas Gohr <andi@splitbrain.org>
  52   */
  53  class HTTPClient {
  54      //set these if you like
  55      var $agent;         // User agent
  56      var $http;          // HTTP version defaults to 1.0
  57      var $timeout;       // read timeout (seconds)
  58      var $cookies;
  59      var $referer;
  60      var $max_redirect;
  61      var $max_bodysize;
  62      var $max_bodysize_abort = true;  // if set, abort if the response body is bigger than max_bodysize
  63      var $header_regexp; // if set this RE must match against the headers, else abort
  64      var $headers;
  65      var $debug;
  66      var $start = 0; // for timings
  67  
  68      // don't set these, read on error
  69      var $error;
  70      var $redirect_count;
  71  
  72      // read these after a successful request
  73      var $resp_status;
  74      var $resp_body;
  75      var $resp_headers;
  76  
  77      // set these to do basic authentication
  78      var $user;
  79      var $pass;
  80  
  81      // set these if you need to use a proxy
  82      var $proxy_host;
  83      var $proxy_port;
  84      var $proxy_user;
  85      var $proxy_pass;
  86      var $proxy_ssl; //boolean set to true if your proxy needs SSL
  87  
  88      /**
  89       * Constructor.
  90       *
  91       * @author Andreas Gohr <andi@splitbrain.org>
  92       */
  93      function HTTPClient(){
  94          $this->agent        = 'Mozilla/4.0 (compatible; DokuWiki HTTP Client; '.PHP_OS.')';
  95          $this->timeout      = 15;
  96          $this->cookies      = array();
  97          $this->referer      = '';
  98          $this->max_redirect = 3;
  99          $this->redirect_count = 0;
 100          $this->status       = 0;
 101          $this->headers      = array();
 102          $this->http         = '1.0';
 103          $this->debug        = false;
 104          $this->max_bodysize = 0;
 105          $this->header_regexp= '';
 106          if(extension_loaded('zlib')) $this->headers['Accept-encoding'] = 'gzip';
 107          $this->headers['Accept'] = 'text/xml,application/xml,application/xhtml+xml,'.
 108                                     'text/html,text/plain,image/png,image/jpeg,image/gif,*/*';
 109          $this->headers['Accept-Language'] = 'en-us';
 110      }
 111  
 112  
 113      /**
 114       * Simple function to do a GET request
 115       *
 116       * Returns the wanted page or false on an error;
 117       *
 118       * @param  string $url       The URL to fetch
 119       * @param  bool   $sloppy304 Return body on 304 not modified
 120       * @author Andreas Gohr <andi@splitbrain.org>
 121       */
 122      function get($url,$sloppy304=false){
 123          if(!$this->sendRequest($url)) return false;
 124          if($this->status == 304 && $sloppy304) return $this->resp_body;
 125          if($this->status != 200) return false;
 126          return $this->resp_body;
 127      }
 128  
 129      /**
 130       * Simple function to do a POST request
 131       *
 132       * Returns the resulting page or false on an error;
 133       *
 134       * @author Andreas Gohr <andi@splitbrain.org>
 135       */
 136      function post($url,$data){
 137          if(!$this->sendRequest($url,$data,'POST')) return false;
 138          if($this->status != 200) return false;
 139          return $this->resp_body;
 140      }
 141  
 142      /**
 143       * Send an HTTP request
 144       *
 145       * This method handles the whole HTTP communication. It respects set proxy settings,
 146       * builds the request headers, follows redirects and parses the response.
 147       *
 148       * Post data should be passed as associative array. When passed as string it will be
 149       * sent as is. You will need to setup your own Content-Type header then.
 150       *
 151       * @param  string $url    - the complete URL
 152       * @param  mixed  $data   - the post data either as array or raw data
 153       * @param  string $method - HTTP Method usually GET or POST.
 154       * @return bool - true on success
 155       * @author Andreas Goetz <cpuidle@gmx.de>
 156       * @author Andreas Gohr <andi@splitbrain.org>
 157       */
 158      function sendRequest($url,$data='',$method='GET'){
 159          $this->start  = $this->_time();
 160          $this->error  = '';
 161          $this->status = 0;
 162  
 163          // parse URL into bits
 164          $uri = parse_url($url);
 165          $server = $uri['host'];
 166          $path   = $uri['path'];
 167          if(empty($path)) $path = '/';
 168          if(!empty($uri['query'])) $path .= '?'.$uri['query'];
 169          $port = $uri['port'];
 170          if($uri['user']) $this->user = $uri['user'];
 171          if($uri['pass']) $this->pass = $uri['pass'];
 172  
 173          // proxy setup
 174          if($this->proxy_host){
 175              $request_url = $url;
 176              $server      = $this->proxy_host;
 177              $port        = $this->proxy_port;
 178              if (empty($port)) $port = 8080;
 179          }else{
 180              $request_url = $path;
 181              $server      = $server;
 182              if (empty($port)) $port = ($uri['scheme'] == 'https') ? 443 : 80;
 183          }
 184  
 185          // add SSL stream prefix if needed - needs SSL support in PHP
 186          if($port == 443 || $this->proxy_ssl) $server = 'ssl://'.$server;
 187  
 188          // prepare headers
 189          $headers               = $this->headers;
 190          $headers['Host']       = $uri['host'];
 191          $headers['User-Agent'] = $this->agent;
 192          $headers['Referer']    = $this->referer;
 193          $headers['Connection'] = 'Close';
 194          if($method == 'POST'){
 195              if(is_array($data)){
 196                  $headers['Content-Type']   = 'application/x-www-form-urlencoded';
 197                  $data = $this->_postEncode($data);
 198              }
 199              $headers['Content-Length'] = strlen($data);
 200              $rmethod = 'POST';
 201          }elseif($method == 'GET'){
 202              $data = ''; //no data allowed on GET requests
 203          }
 204          if($this->user) {
 205              $headers['Authorization'] = 'Basic '.base64_encode($this->user.':'.$this->pass);
 206          }
 207          if($this->proxy_user) {
 208              $headers['Proxy-Authorization'] = 'Basic '.base64_encode($this->proxy_user.':'.$this->proxy_pass);
 209          }
 210  
 211          // stop time
 212          $start = time();
 213  
 214          // open socket
 215          $socket = @fsockopen($server,$port,$errno, $errstr, $this->timeout);
 216          if (!$socket){
 217              $resp->status = '-100';
 218              $this->error = "Could not connect to $server:$port\n$errstr ($errno)";
 219              return false;
 220          }
 221          //set non blocking
 222          stream_set_blocking($socket,0);
 223  
 224          // build request
 225          $request  = "$method $request_url HTTP/".$this->http.HTTP_NL;
 226          $request .= $this->_buildHeaders($headers);
 227          $request .= $this->_getCookies();
 228          $request .= HTTP_NL;
 229          $request .= $data;
 230  
 231          $this->_debug('request',$request);
 232  
 233          // send request
 234          $towrite = strlen($request);
 235          $written = 0;
 236          while($written < $towrite){
 237              $ret = fwrite($socket, substr($request,$written));
 238              if($ret === false){
 239                  $this->status = -100;
 240                  $this->error = 'Failed writing to socket';
 241                  return false;
 242              }
 243              $written += $ret;
 244          }
 245  
 246  
 247          // read headers from socket
 248          $r_headers = '';
 249          do{
 250              if(time()-$start > $this->timeout){
 251                  $this->status = -100;
 252                  $this->error = sprintf('Timeout while reading headers (%.3fs)',$this->_time() - $this->start);
 253                  return false;
 254              }
 255              if(feof($socket)){
 256                  $this->error = 'Premature End of File (socket)';
 257                  return false;
 258              }
 259              $r_headers .= fgets($socket,1024);
 260          }while(!preg_match('/\r?\n\r?\n$/',$r_headers));
 261  
 262          $this->_debug('response headers',$r_headers);
 263  
 264          // check if expected body size exceeds allowance
 265          if($this->max_bodysize && preg_match('/\r?\nContent-Length:\s*(\d+)\r?\n/i',$r_headers,$match)){
 266              if($match[1] > $this->max_bodysize){
 267                  $this->error = 'Reported content length exceeds allowed response size';
 268                  if ($this->max_bodysize_abort)
 269                      return false;
 270              }
 271          }
 272  
 273          // get Status
 274          if (!preg_match('/^HTTP\/(\d\.\d)\s*(\d+).*?\n/', $r_headers, $m)) {
 275              $this->error = 'Server returned bad answer';
 276              return false;
 277          }
 278          $this->status = $m[2];
 279  
 280          // handle headers and cookies
 281          $this->resp_headers = $this->_parseHeaders($r_headers);
 282          if(isset($this->resp_headers['set-cookie'])){
 283              foreach ((array) $this->resp_headers['set-cookie'] as $c){
 284                  list($key, $value, $foo) = split('=', $cookie);
 285                  $this->cookies[$key] = $value;
 286              }
 287          }
 288  
 289          $this->_debug('Object headers',$this->resp_headers);
 290  
 291          // check server status code to follow redirect
 292          if($this->status == 301 || $this->status == 302 ){
 293              if (empty($this->resp_headers['location'])){
 294                  $this->error = 'Redirect but no Location Header found';
 295                  return false;
 296              }elseif($this->redirect_count == $this->max_redirect){
 297                  $this->error = 'Maximum number of redirects exceeded';
 298                  return false;
 299              }else{
 300                  $this->redirect_count++;
 301                  $this->referer = $url;
 302                  if (!preg_match('/^http/i', $this->resp_headers['location'])){
 303                      $this->resp_headers['location'] = $uri['scheme'].'://'.$uri['host'].
 304                                                        $this->resp_headers['location'];
 305                  }
 306                  // perform redirected request, always via GET (required by RFC)
 307                  return $this->sendRequest($this->resp_headers['location'],array(),'GET');
 308              }
 309          }
 310  
 311          // check if headers are as expected
 312          if($this->header_regexp && !preg_match($this->header_regexp,$r_headers)){
 313              $this->error = 'The received headers did not match the given regexp';
 314              return false;
 315          }
 316  
 317          //read body (with chunked encoding if needed)
 318          $r_body    = '';
 319          if(preg_match('/transfer\-(en)?coding:\s*chunked\r\n/i',$r_header)){
 320              do {
 321                  unset($chunk_size);
 322                  do {
 323                      if(feof($socket)){
 324                          $this->error = 'Premature End of File (socket)';
 325                          return false;
 326                      }
 327                      if(time()-$start > $this->timeout){
 328                          $this->status = -100;
 329                          $this->error = sprintf('Timeout while reading chunk (%.3fs)',$this->_time() - $this->start);
 330                          return false;
 331                      }
 332                      $byte = fread($socket,1);
 333                      $chunk_size .= $byte;
 334                  } while (preg_match('/[a-zA-Z0-9]/',$byte)); // read chunksize including \r
 335  
 336                  $byte = fread($socket,1);     // readtrailing \n
 337                  $chunk_size = hexdec($chunk_size);
 338                  $this_chunk = fread($socket,$chunk_size);
 339                  $r_body    .= $this_chunk;
 340                  if ($chunk_size) $byte = fread($socket,2); // read trailing \r\n
 341  
 342                  if($this->max_bodysize && strlen($r_body) > $this->max_bodysize){
 343                      $this->error = 'Allowed response size exceeded';
 344                      if ($this->max_bodysize_abort)
 345                          return false;
 346                      else
 347                          break;
 348                  }
 349              } while ($chunk_size);
 350          }else{
 351              // read entire socket
 352              while (!feof($socket)) {
 353                  if(time()-$start > $this->timeout){
 354                      $this->status = -100;
 355                      $this->error = sprintf('Timeout while reading response (%.3fs)',$this->_time() - $this->start);
 356                      return false;
 357                  }
 358                  $r_body .= fread($socket,4096);
 359                  $r_size = strlen($r_body);
 360                  if($this->max_bodysize && $r_size > $this->max_bodysize){
 361                      $this->error = 'Allowed response size exceeded';
 362                      if ($this->max_bodysize_abort)
 363                          return false;
 364                      else
 365                          break;
 366                  }
 367                  if($this->resp_headers['content-length'] && !$this->resp_headers['transfer-encoding'] &&
 368                     $this->resp_headers['content-length'] == $r_size){
 369                      // we read the content-length, finish here
 370                      break;
 371                  }
 372              }
 373          }
 374  
 375          // close socket
 376          $status = socket_get_status($socket);
 377          fclose($socket);
 378  
 379          // decode gzip if needed
 380          if($this->resp_headers['content-encoding'] == 'gzip'){
 381              $this->resp_body = gzinflate(substr($r_body, 10));
 382          }else{
 383              $this->resp_body = $r_body;
 384          }
 385  
 386          $this->_debug('response body',$this->resp_body);
 387          $this->redirect_count = 0;
 388          return true;
 389      }
 390  
 391      /**
 392       * print debug info
 393       *
 394       * @author Andreas Gohr <andi@splitbrain.org>
 395       */
 396      function _debug($info,$var=null){
 397          if(!$this->debug) return;
 398          print '<b>'.$info.'</b> '.($this->_time() - $this->start).'s<br />';
 399          if(!is_null($var)){
 400              ob_start();
 401              print_r($var);
 402              $content = htmlspecialchars(ob_get_contents());
 403              ob_end_clean();
 404              print '<pre>'.$content.'</pre>';
 405          }
 406      }
 407  
 408      /**
 409       * Return current timestamp in microsecond resolution
 410       */
 411      function _time(){
 412          list($usec, $sec) = explode(" ", microtime());
 413          return ((float)$usec + (float)$sec);
 414      }
 415  
 416      /**
 417       * convert given header string to Header array
 418       *
 419       * All Keys are lowercased.
 420       *
 421       * @author Andreas Gohr <andi@splitbrain.org>
 422       */
 423      function _parseHeaders($string){
 424          $headers = array();
 425          $lines = explode("\n",$string);
 426          foreach($lines as $line){
 427              list($key,$val) = explode(':',$line,2);
 428              $key = strtolower(trim($key));
 429              $val = trim($val);
 430              if(empty($val)) continue;
 431              if(isset($headers[$key])){
 432                  if(is_array($headers[$key])){
 433                      $headers[$key][] = $val;
 434                  }else{
 435                      $headers[$key] = array($headers[$key],$val);
 436                  }
 437              }else{
 438                  $headers[$key] = $val;
 439              }
 440          }
 441          return $headers;
 442      }
 443  
 444      /**
 445       * convert given header array to header string
 446       *
 447       * @author Andreas Gohr <andi@splitbrain.org>
 448       */
 449      function _buildHeaders($headers){
 450          $string = '';
 451          foreach($headers as $key => $value){
 452              if(empty($value)) continue;
 453              $string .= $key.': '.$value.HTTP_NL;
 454          }
 455          return $string;
 456      }
 457  
 458      /**
 459       * get cookies as http header string
 460       *
 461       * @author Andreas Goetz <cpuidle@gmx.de>
 462       */
 463      function _getCookies(){
 464          foreach ($this->cookies as $key => $val){
 465              if ($headers) $headers .= '; ';
 466              $headers .= $key.'='.$val;
 467          }
 468  
 469          if ($headers) $headers = "Cookie: $headers".HTTP_NL;
 470          return $headers;
 471      }
 472  
 473      /**
 474       * Encode data for posting
 475       *
 476       * @todo handle mixed encoding for file upoads
 477       * @author Andreas Gohr <andi@splitbrain.org>
 478       */
 479      function