[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

/lib/ -> webdavlib.php (source)

   1  <?php
   2  
   3  // This file is part of Moodle - http://moodle.org/
   4  //
   5  // Moodle is free software: you can redistribute it and/or modify
   6  // it under the terms of the GNU General Public License as published by
   7  // the Free Software Foundation, either version 3 of the License, or
   8  // (at your option) any later version.
   9  //
  10  // Moodle is distributed in the hope that it will be useful,
  11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  13  // GNU General Public License for more details.
  14  //
  15  // You should have received a copy of the GNU General Public License
  16  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  17  
  18  /**
  19   * webdav_client v0.1.5, a php based webdav client class.
  20   * class webdav client. a php based nearly RFC 2518 conforming client.
  21   *
  22   * This class implements methods to get access to an webdav server.
  23   * Most of the methods are returning boolean false on error, an integer status (http response status) on success
  24   * or an array in case of a multistatus response (207) from the webdav server. Look at the code which keys are used in arrays.
  25   * It's your responsibility to handle the webdav server responses in an proper manner.
  26   * Please notice that all Filenames coming from or going to the webdav server should be UTF-8 encoded (see RFC 2518).
  27   * This class tries to convert all you filenames into utf-8 when it's needed.
  28   *
  29   * @package moodlecore
  30   * @author Christian Juerges <christian.juerges@xwave.ch>, Xwave GmbH, Josefstr. 92, 8005 Zuerich - Switzerland
  31   * @copyright (C) 2003/2004, Christian Juerges
  32   * @license http://opensource.org/licenses/lgpl-license.php GNU Lesser General Public License
  33   * @version 0.1.5
  34   */
  35  
  36  class webdav_client {
  37  
  38      /**#@+
  39       * @access private
  40       * @var string
  41       */
  42      private $_debug = false;
  43      private $sock;
  44      private $_server;
  45      private $_protocol = 'HTTP/1.1';
  46      private $_port = 80;
  47      private $_socket = '';
  48      private $_path ='/';
  49      private $_auth = false;
  50      private $_user;
  51      private $_pass;
  52  
  53      private $_socket_timeout = 5;
  54      private $_errno;
  55      private $_errstr;
  56      private $_user_agent = 'Moodle WebDav Client';
  57      private $_crlf = "\r\n";
  58      private $_req;
  59      private $_resp_status;
  60      private $_parser;
  61      private $_parserid;
  62      private $_xmltree;
  63      private $_tree;
  64      private $_ls = array();
  65      private $_ls_ref;
  66      private $_ls_ref_cdata;
  67      private $_delete = array();
  68      private $_delete_ref;
  69      private $_delete_ref_cdata;
  70      private $_lock = array();
  71      private $_lock_ref;
  72      private $_lock_rec_cdata;
  73      private $_null = NULL;
  74      private $_header='';
  75      private $_body='';
  76      private $_connection_closed = false;
  77      private $_maxheaderlenth = 1000;
  78      private $_digestchallenge = null;
  79      private $_cnonce = '';
  80      private $_nc = 0;
  81  
  82      /**#@-*/
  83  
  84      /**
  85       * Constructor - Initialise class variables
  86       */
  87      function __construct($server = '', $user = '', $pass = '', $auth = false, $socket = '') {
  88          if (!empty($server)) {
  89              $this->_server = $server;
  90          }
  91          if (!empty($user) && !empty($pass)) {
  92              $this->user = $user;
  93              $this->pass = $pass;
  94          }
  95          $this->_auth = $auth;
  96          $this->_socket = $socket;
  97      }
  98      public function __set($key, $value) {
  99          $property = '_' . $key;
 100          $this->$property = $value;
 101      }
 102  
 103      /**
 104       * Set which HTTP protocol will be used.
 105       * Value 1 defines that HTTP/1.1 should be used (Keeps Connection to webdav server alive).
 106       * Otherwise HTTP/1.0 will be used.
 107       * @param int version
 108       */
 109      function set_protocol($version) {
 110          if ($version == 1) {
 111              $this->_protocol = 'HTTP/1.1';
 112          } else {
 113              $this->_protocol = 'HTTP/1.0';
 114          }
 115      }
 116  
 117      /**
 118       * Convert ISO 8601 Date and Time Profile used in RFC 2518 to an unix timestamp.
 119       * @access private
 120       * @param string iso8601
 121       * @return unixtimestamp on sucess. Otherwise false.
 122       */
 123      function iso8601totime($iso8601) {
 124          /*
 125  
 126           date-time       = full-date "T" full-time
 127  
 128           full-date       = date-fullyear "-" date-month "-" date-mday
 129           full-time       = partial-time time-offset
 130  
 131           date-fullyear   = 4DIGIT
 132           date-month      = 2DIGIT  ; 01-12
 133           date-mday       = 2DIGIT  ; 01-28, 01-29, 01-30, 01-31 based on
 134           month/year
 135           time-hour       = 2DIGIT  ; 00-23
 136           time-minute     = 2DIGIT  ; 00-59
 137           time-second     = 2DIGIT  ; 00-59, 00-60 based on leap second rules
 138           time-secfrac    = "." 1*DIGIT
 139           time-numoffset  = ("+" / "-") time-hour ":" time-minute
 140           time-offset     = "Z" / time-numoffset
 141  
 142           partial-time    = time-hour ":" time-minute ":" time-second
 143                                              [time-secfrac]
 144           */
 145  
 146          $regs = array();
 147          /*         [1]        [2]        [3]        [4]        [5]        [6]  */
 148          if (preg_match('/^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})Z$/', $iso8601, $regs)) {
 149              return mktime($regs[4],$regs[5], $regs[6], $regs[2], $regs[3], $regs[1]);
 150          }
 151          // to be done: regex for partial-time...apache webdav mod never returns partial-time
 152  
 153          return false;
 154      }
 155  
 156      /**
 157       * Open's a socket to a webdav server
 158       * @return bool true on success. Otherwise false.
 159       */
 160      function open() {
 161          // let's try to open a socket
 162          $this->_error_log('open a socket connection');
 163          $this->sock = fsockopen($this->_socket . $this->_server, $this->_port, $this->_errno, $this->_errstr, $this->_socket_timeout);
 164          core_php_time_limit::raise(30);
 165          if (is_resource($this->sock)) {
 166              socket_set_blocking($this->sock, true);
 167              $this->_connection_closed = false;
 168              $this->_error_log('socket is open: ' . $this->sock);
 169              return true;
 170          } else {
 171              $this->_error_log("$this->_errstr ($this->_errno)\n");
 172              return false;
 173          }
 174      }
 175  
 176      /**
 177       * Closes an open socket.
 178       */
 179      function close() {
 180          $this->_error_log('closing socket ' . $this->sock);
 181          $this->_connection_closed = true;
 182          fclose($this->sock);
 183      }
 184  
 185      /**
 186       * Check's if server is a webdav compliant server.
 187       * True if server returns a DAV Element in Header and when
 188       * schema 1,2 is supported.
 189       * @return bool true if server is webdav server. Otherwise false.
 190       */
 191      function check_webdav() {
 192          $resp = $this->options();
 193          if (!$resp) {
 194              return false;
 195          }
 196          $this->_error_log($resp['header']['DAV']);
 197          // check schema
 198          if (preg_match('/1,2/', $resp['header']['DAV'])) {
 199              return true;
 200          }
 201          // otherwise return false
 202          return false;
 203      }
 204  
 205  
 206      /**
 207       * Get options from webdav server.
 208       * @return array with all header fields returned from webdav server. false if server does not speak http.
 209       */
 210      function options() {
 211          $this->header_unset();
 212          $this->create_basic_request('OPTIONS');
 213          $this->send_request();
 214          $this->get_respond();
 215          $response = $this->process_respond();
 216          // validate the response ...
 217          // check http-version
 218          if ($response['status']['http-version'] == 'HTTP/1.1' ||
 219              $response['status']['http-version'] == 'HTTP/1.0') {
 220                  return $response;
 221              }
 222          $this->_error_log('Response was not even http');
 223          return false;
 224  
 225      }
 226  
 227      /**
 228       * Public method mkcol
 229       *
 230       * Creates a new collection/directory on a webdav server
 231       * @param string path
 232       * @return int status code received as response from webdav server (see rfc 2518)
 233       */
 234      function mkcol($path) {
 235          $this->_path = $this->translate_uri($path);
 236          $this->header_unset();
 237          $this->create_basic_request('MKCOL');
 238          $this->send_request();
 239          $this->get_respond();
 240          $response = $this->process_respond();
 241          // validate the response ...
 242          // check http-version
 243          $http_version = $response['status']['http-version'];
 244          if ($http_version == 'HTTP/1.1' || $http_version == 'HTTP/1.0') {
 245              /** seems to be http ... proceed
 246               * just return what server gave us
 247               * rfc 2518 says:
 248               * 201 (Created) - The collection or structured resource was created in its entirety.
 249               * 403 (Forbidden) - This indicates at least one of two conditions:
 250               *    1) the server does not allow the creation of collections at the given location in its namespace, or
 251               *    2) the parent collection of the Request-URI exists but cannot accept members.
 252               * 405 (Method Not Allowed) - MKCOL can only be executed on a deleted/non-existent resource.
 253               * 409 (Conflict) - A collection cannot be made at the Request-URI until one or more intermediate
 254               *                  collections have been created.
 255               * 415 (Unsupported Media Type)- The server does not support the request type of the body.
 256               * 507 (Insufficient Storage) - The resource does not have sufficient space to record the state of the
 257               *                              resource after the execution of this method.
 258               */
 259              return $response['status']['status-code'];
 260          }
 261  
 262      }
 263  
 264      /**
 265       * Public method get
 266       *
 267       * Gets a file from a webdav collection.
 268       * @param string $path the path to the file on the webdav server
 269       * @param string &$buffer the buffer to store the data in
 270       * @param resource $fp optional if included, the data is written directly to this resource and not to the buffer
 271       * @return string|bool status code and &$buffer (by reference) with response data from server on success. False on error.
 272       */
 273      function get($path, &$buffer, $fp = null) {
 274          $this->_path = $this->translate_uri($path);
 275          $this->header_unset();
 276          $this->create_basic_request('GET');
 277          $this->send_request();
 278          $this->get_respond($fp);
 279          $response = $this->process_respond();
 280  
 281          $http_version = $response['status']['http-version'];
 282          // validate the response
 283          // check http-version
 284          if ($http_version == 'HTTP/1.1' || $http_version == 'HTTP/1.0') {
 285                  // seems to be http ... proceed
 286                  // We expect a 200 code
 287                  if ($response['status']['status-code'] == 200 ) {
 288                      if (!is_null($fp)) {
 289                          $stat = fstat($fp);
 290                          $this->_error_log('file created with ' . $stat['size'] . ' bytes.');
 291                      } else {
 292                          $this->_error_log('returning buffer with ' . strlen($response['body']) . ' bytes.');
 293                          $buffer = $response['body'];
 294                      }
 295                  }
 296                  return $response['status']['status-code'];
 297              }
 298          // ups: no http status was returned ?
 299          return false;
 300      }
 301  
 302      /**
 303       * Public method put
 304       *
 305       * Puts a file into a collection.
 306       *    Data is putted as one chunk!
 307       * @param string path, string data
 308       * @return int status-code read from webdavserver. False on error.
 309       */
 310      function put($path, $data ) {
 311          $this->_path = $this->translate_uri($path);
 312          $this->header_unset();
 313          $this->create_basic_request('PUT');
 314          // add more needed header information ...
 315          $this->header_add('Content-length: ' . strlen($data));
 316          $this->header_add('Content-type: application/octet-stream');
 317          // send header
 318          $this->send_request();
 319          // send the rest (data)
 320          fputs($this->sock, $data);
 321          $this->get_respond();
 322          $response = $this->process_respond();
 323  
 324          // validate the response
 325          // check http-version
 326          if ($response['status']['http-version'] == 'HTTP/1.1' ||
 327              $response['status']['http-version'] == 'HTTP/1.0') {
 328                  // seems to be http ... proceed
 329                  // We expect a 200 or 204 status code
 330                  // see rfc 2068 - 9.6 PUT...
 331                  // print 'http ok<br>';
 332                  return $response['status']['status-code'];
 333              }
 334          // ups: no http status was returned ?
 335          return false;
 336      }
 337  
 338      /**
 339       * Public method put_file
 340       *
 341       * Read a file as stream and puts it chunk by chunk into webdav server collection.
 342       *
 343       * Look at php documenation for legal filenames with fopen();
 344       * The filename will be translated into utf-8 if not allready in utf-8.
 345       *
 346       * @param string targetpath, string filename
 347       * @return int status code. False on error.
 348       */
 349      function put_file($path, $filename) {
 350          // try to open the file ...
 351  
 352  
 353          $handle = @fopen ($filename, 'r');
 354  
 355          if ($handle) {
 356              // $this->sock = pfsockopen ($this->_server, $this->_port, $this->_errno, $this->_errstr, $this->_socket_timeout);
 357              $this->_path = $this->translate_uri($path);
 358              $this->header_unset();
 359              $this->create_basic_request('PUT');
 360              // add more needed header information ...
 361              $this->header_add('Content-length: ' . filesize($filename));
 362              $this->header_add('Content-type: application/octet-stream');
 363              // send header
 364              $this->send_request();
 365              while (!feof($handle)) {
 366                  fputs($this->sock,fgets($handle,4096));
 367              }
 368              fclose($handle);
 369              $this->get_respond();
 370              $response = $this->process_respond();
 371  
 372              // validate the response
 373              // check http-version
 374              if ($response['status']['http-version'] == 'HTTP/1.1' ||
 375                  $response['status']['http-version'] == 'HTTP/1.0') {
 376                      // seems to be http ... proceed
 377                      // We expect a 200 or 204 status code
 378                      // see rfc 2068 - 9.6 PUT...
 379                      // print 'http ok<br>';
 380                      return $response['status']['status-code'];
 381                  }
 382              // ups: no http status was returned ?
 383              return false;
 384          } else {
 385              $this->_error_log('put_file: could not open ' . $filename);
 386              return false;
 387          }
 388  
 389      }
 390  
 391      /**
 392       * Public method get_file
 393       *
 394       * Gets a file from a collection into local filesystem.
 395       *
 396       * fopen() is used.
 397       * @param string $srcpath
 398       * @param string $localpath
 399       * @return bool true on success. false on error.
 400       */
 401      function get_file($srcpath, $localpath) {
 402  
 403          $localpath = $this->utf_decode_path($localpath);
 404  
 405          $handle = fopen($localpath, 'wb');
 406          if ($handle) {
 407              $unused = '';
 408              $ret = $this->get($srcpath, $unused, $handle);
 409              fclose($handle);
 410              if ($ret) {
 411                  return true;
 412              }
 413          }
 414          return false;
 415      }
 416  
 417      /**
 418       * Public method copy_file
 419       *
 420       * Copies a file on a webdav server
 421       *
 422       * Duplicates a file on the webdav server (serverside).
 423       * All work is done on the webdav server. If you set param overwrite as true,
 424       * the target will be overwritten.
 425       *
 426       * @param string src_path, string dest_path, bool overwrite
 427       * @return int status code (look at rfc 2518). false on error.
 428       */
 429      function copy_file($src_path, $dst_path, $overwrite) {
 430          $this->_path = $this->translate_uri($src_path);
 431          $this->header_unset();
 432          $this->create_basic_request('COPY');
 433          $this->header_add(sprintf('Destination: http://%s%s', $this->_server, $this->translate_uri($dst_path)));
 434          if ($overwrite) {
 435              $this->header_add('Overwrite: T');
 436          } else {
 437              $this->header_add('Overwrite: F');
 438          }
 439          $this->header_add('');
 440          $this->send_request();
 441          $this->get_respond();
 442          $response = $this->process_respond();
 443          // validate the response ...
 444          // check http-version
 445          if ($response['status']['http-version'] == 'HTTP/1.1' ||
 446              $response['status']['http-version'] == 'HTTP/1.0') {
 447           /* seems to be http ... proceed
 448               just return what server gave us (as defined in rfc 2518) :
 449               201 (Created) - The source resource was successfully copied. The copy operation resulted in the creation of a new resource.
 450               204 (No Content) - The source resource was successfully copied to a pre-existing destination resource.
 451               403 (Forbidden) - The source and destination URIs are the same.
 452               409 (Conflict) - A resource cannot be created at the destination until one or more intermediate collections have been created.
 453               412 (Precondition Failed) - The server was unable to maintain the liveness of the properties listed in the propertybehavior XML element
 454                       or the Overwrite header is "F" and the state of the destination resource is non-null.
 455               423 (Locked) - The destination resource was locked.
 456               502 (Bad Gateway) - This may occur when the destination is on another server and the destination server refuses to accept the resource.
 457               507 (Insufficient Storage) - The destination resource does not have sufficient space to record the state of the resource after the
 458                       execution of this method.
 459            */
 460                  return $response['status']['status-code'];
 461              }
 462          return false;
 463      }
 464  
 465      /**
 466       * Public method copy_coll
 467       *
 468       * Copies a collection on a webdav server
 469       *
 470       * Duplicates a collection on the webdav server (serverside).
 471       * All work is done on the webdav server. If you set param overwrite as true,
 472       * the target will be overwritten.
 473       *
 474       * @param string src_path, string dest_path, bool overwrite
 475       * @return int status code (look at rfc 2518). false on error.
 476       */
 477      function copy_coll($src_path, $dst_path, $overwrite) {
 478          $this->_path = $this->translate_uri($src_path);
 479          $this->header_unset();
 480          $this->create_basic_request('COPY');
 481          $this->header_add(sprintf('Destination: http://%s%s', $this->_server, $this->translate_uri($dst_path)));
 482          $this->header_add('Depth: Infinity');
 483  
 484          $xml  = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\r\n";
 485          $xml .= "<d:propertybehavior xmlns:d=\"DAV:\">\r\n";
 486          $xml .= "  <d:keepalive>*</d:keepalive>\r\n";
 487          $xml .= "</d:propertybehavior>\r\n";
 488  
 489          $this->header_add('Content-length: ' . strlen($xml));
 490          $this->header_add('Content-type: application/xml');
 491          $this->send_request();
 492          // send also xml
 493          fputs($this->sock, $xml);
 494          $this->get_respond();
 495          $response = $this->process_respond();
 496          // validate the response ...
 497          // check http-version
 498          if ($response['status']['http-version'] == 'HTTP/1.1' ||
 499              $response['status']['http-version'] == 'HTTP/1.0') {
 500           /* seems to be http ... proceed
 501               just return what server gave us (as defined in rfc 2518) :
 502               201 (Created) - The source resource was successfully copied. The copy operation resulted in the creation of a new resource.
 503               204 (No Content) - The source resource was successfully copied to a pre-existing destination resource.
 504               403 (Forbidden) - The source and destination URIs are the same.
 505               409 (Conflict) - A resource cannot be created at the destination until one or more intermediate collections have been created.
 506               412 (Precondition Failed) - The server was unable to maintain the liveness of the properties listed in the propertybehavior XML element
 507                       or the Overwrite header is "F" and the state of the destination resource is non-null.
 508               423 (Locked) - The destination resource was locked.
 509               502 (Bad Gateway) - This may occur when the destination is on another server and the destination server refuses to accept the resource.
 510               507 (Insufficient Storage) - The destination resource does not have sufficient space to record the state of the resource after the
 511                       execution of this method.
 512            */
 513                  return $response['status']['status-code'];
 514              }
 515          return false;
 516      }
 517  
 518      /**
 519       * Public method move
 520       *
 521       * Moves a file or collection on webdav server (serverside)
 522       *
 523       * If you set param overwrite as true, the target will be overwritten.
 524       *
 525       * @param string src_path, string dest_path, bool overwrite
 526       * @return int status code (look at rfc 2518). false on error.
 527       */
 528      // --------------------------------------------------------------------------
 529      // public method move
 530      // move/rename a file/collection on webdav server
 531      function move($src_path,$dst_path, $overwrite) {
 532  
 533          $this->_path = $this->translate_uri($src_path);
 534          $this->header_unset();
 535  
 536          $this->create_basic_request('MOVE');
 537          $this->header_add(sprintf('Destination: http://%s%s', $this->_server, $this->translate_uri($dst_path)));
 538          if ($overwrite) {
 539              $this->header_add('Overwrite: T');
 540          } else {
 541              $this->header_add('Overwrite: F');
 542          }
 543          $this->header_add('');
 544  
 545          $this->send_request();
 546          $this->get_respond();
 547          $response = $this->process_respond();
 548          // validate the response ...
 549          // check http-version
 550          if ($response['status']['http-version'] == 'HTTP/1.1' ||
 551              $response['status']['http-version'] == 'HTTP/1.0') {
 552              /* seems to be http ... proceed
 553                  just return what server gave us (as defined in rfc 2518) :
 554                  201 (Created) - The source resource was successfully moved, and a new resource was created at the destination.
 555                  204 (No Content) - The source resource was successfully moved to a pre-existing destination resource.
 556                  403 (Forbidden) - The source and destination URIs are the same.
 557                  409 (Conflict) - A resource cannot be created at the destination until one or more intermediate collections have been created.
 558                  412 (Precondition Failed) - The server was unable to maintain the liveness of the properties listed in the propertybehavior XML element
 559                           or the Overwrite header is "F" and the state of the destination resource is non-null.
 560                  423 (Locked) - The source or the destination resource was locked.
 561                  502 (Bad Gateway) - This may occur when the destination is on another server and the destination server refuses to accept the resource.
 562  
 563                  201 (Created) - The collection or structured resource was created in its entirety.
 564                  403 (Forbidden) - This indicates at least one of two conditions: 1) the server does not allow the creation of collections at the given
 565                                                   location in its namespace, or 2) the parent collection of the Request-URI exists but cannot accept members.
 566                  405 (Method Not Allowed) - MKCOL can only be executed on a deleted/non-existent resource.
 567                  409 (Conflict) - A collection cannot be made at the Request-URI until one or more intermediate collections have been created.
 568                  415 (Unsupported Media Type)- The server does not support the request type of the body.
 569                  507 (Insufficient Storage) - The resource does not have sufficient space to record the state of the resource after the execution of this method.
 570               */
 571                  return $response['status']['status-code'];
 572              }
 573          return false;
 574      }
 575  
 576      /**
 577       * Public method lock
 578       *
 579       * Locks a file or collection.
 580       *
 581       * Lock uses this->_user as lock owner.
 582       *
 583       * @param string path
 584       * @return int status code (look at rfc 2518). false on error.
 585       */
 586      function lock($path) {
 587          $this->_path = $this->translate_uri($path);
 588          $this->header_unset();
 589          $this->create_basic_request('LOCK');
 590          $this->header_add('Timeout: Infinite');
 591          $this->header_add('Content-type: text/xml');
 592          // create the xml request ...
 593          $xml =  "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\r\n";
 594          $xml .= "<D:lockinfo xmlns:D='DAV:'\r\n>";
 595          $xml .= "  <D:lockscope><D:exclusive/></D:lockscope>\r\n";
 596          $xml .= "  <D:locktype><D:write/></D:locktype>\r\n";
 597          $xml .= "  <D:owner>\r\n";
 598          $xml .= "    <D:href>".($this->_user)."</D:href>\r\n";
 599          $xml .= "  </D:owner>\r\n";
 600          $xml .= "</D:lockinfo>\r\n";
 601          $this->header_add('Content-length: ' . strlen($xml));
 602          $this->send_request();
 603          // send also xml
 604          fputs($this->sock, $xml);
 605          $this->get_respond();
 606          $response = $this->process_respond();
 607          // validate the response ... (only basic validation)
 608          // check http-version
 609          if ($response['status']['http-version'] == 'HTTP/1.1' ||
 610              $response['status']['http-version'] == 'HTTP/1.0') {
 611              /* seems to be http ... proceed
 612              rfc 2518 says:
 613              200 (OK) - The lock request succeeded and the value of the lockdiscovery property is included in the body.
 614              412 (Precondition Failed) - The included lock token was not enforceable on this resource or the server could not satisfy the
 615                       request in the lockinfo XML element.
 616              423 (Locked) - The resource is locked, so the method has been rejected.
 617               */
 618  
 619                  switch($response['status']['status-code']) {
 620                  case 200:
 621                      // collection was successfully locked... see xml response to get lock token...
 622                      if (strcmp($response['header']['Content-Type'], 'text/xml; charset="utf-8"') == 0) {
 623                          // ok let's get the content of the xml stuff
 624                          $this->_parser = xml_parser_create_ns();
 625                          $this->_parserid = (int) $this->_parser;
 626                          // forget old data...
 627                          unset($this->_lock[$this->_parserid]);
 628                          unset($this->_xmltree[$this->_parserid]);
 629                          xml_parser_set_option($this->_parser,XML_OPTION_SKIP_WHITE,0);
 630                          xml_parser_set_option($this->_parser,XML_OPTION_CASE_FOLDING,0);
 631                          xml_set_object($this->_parser, $this);
 632                          xml_set_element_handler($this->_parser, "_lock_startElement", "_endElement");
 633                          xml_set_character_data_handler($this->_parser, "_lock_cdata");
 634  
 635                          if (!xml_parse($this->_parser, $response['body'])) {
 636                              die(sprintf("XML error: %s at line %d",
 637                                  xml_error_string(xml_get_error_code($this->_parser)),
 638                                  xml_get_current_line_number($this->_parser)));
 639                          }
 640  
 641                          // Free resources
 642                          xml_parser_free($this->_parser);
 643                          // add status code to array
 644                          $this->_lock[$this->_parserid]['status'] = 200;
 645                          return $this->_lock[$this->_parserid];
 646  
 647                      } else {
 648                          print 'Missing Content-Type: text/xml header in response.<br>';
 649                      }
 650                      return false;
 651  
 652                  default:
 653                      // hmm. not what we expected. Just return what we got from webdav server
 654                      // someone else has to handle it.
 655                      $this->_lock['status'] = $response['status']['status-code'];
 656                      return $this->_lock;
 657                  }
 658              }
 659  
 660  
 661      }
 662  
 663  
 664      /**
 665       * Public method unlock
 666       *
 667       * Unlocks a file or collection.
 668       *
 669       * @param string path, string locktoken
 670       * @return int status code (look at rfc 2518). false on error.
 671       */
 672      function unlock($path, $locktoken) {
 673          $this->_path = $this->translate_uri($path);
 674          $this->header_unset();
 675          $this->create_basic_request('UNLOCK');
 676          $this->header_add(sprintf('Lock-Token: <%s>', $locktoken));
 677          $this->send_request();
 678          $this->get_respond();
 679          $response = $this->process_respond();
 680          if ($response['status']['http-version'] == 'HTTP/1.1' ||
 681              $response['status']['http-version'] == 'HTTP/1.0') {
 682              /* seems to be http ... proceed
 683              rfc 2518 says:
 684              204 (OK) - The 204 (No Content) status code is used instead of 200 (OK) because there is no response entity body.
 685               */
 686                  return $response['status']['status-code'];
 687              }
 688          return false;
 689      }
 690  
 691      /**
 692       * Public method delete
 693       *
 694       * deletes a collection/directory on a webdav server
 695       * @param string path
 696       * @return int status code (look at rfc 2518). false on error.
 697       */
 698      function delete($path) {
 699          $this->_path = $this->translate_uri($path);
 700          $this->header_unset();
 701          $this->create_basic_request('DELETE');
 702          /* $this->header_add('Content-Length: 0'); */
 703          $this->header_add('');
 704          $this->send_request();
 705          $this->get_respond();
 706          $response = $this->process_respond();
 707  
 708          // validate the response ...
 709          // check http-version
 710          if ($response['status']['http-version'] == 'HTTP/1.1' ||
 711              $response['status']['http-version'] == 'HTTP/1.0') {
 712                  // seems to be http ... proceed
 713                  // We expect a 207 Multi-Status status code
 714                  // print 'http ok<br>';
 715  
 716                  switch ($response['status']['status-code']) {
 717                  case 207:
 718                      // collection was NOT deleted... see xml response for reason...
 719                      // next there should be a Content-Type: text/xml; charset="utf-8" header line
 720                      if (strcmp($response['header']['Content-Type'], 'text/xml; charset="utf-8"') == 0) {
 721                          // ok let's get the content of the xml stuff
 722                          $this->_parser = xml_parser_create_ns();
 723                          $this->_parserid = (int) $this->_parser;
 724                          // forget old data...
 725                          unset($this->_delete[$this->_parserid]);
 726                          unset($this->_xmltree[$this->_parserid]);
 727                          xml_parser_set_option($this->_parser,XML_OPTION_SKIP_WHITE,0);
 728                          xml_parser_set_option($this->_parser,XML_OPTION_CASE_FOLDING,0);
 729                          xml_set_object($this->_parser, $this);
 730                          xml_set_element_handler($this->_parser, "_delete_startElement", "_endElement");
 731                          xml_set_character_data_handler($this->_parser, "_delete_cdata");
 732  
 733                          if (!xml_parse($this->_parser, $response['body'])) {
 734                              die(sprintf("XML error: %s at line %d",
 735                                  xml_error_string(xml_get_error_code($this->_parser)),
 736                                  xml_get_current_line_number($this->_parser)));
 737                          }
 738  
 739                          print "<br>";
 740  
 741                          // Free resources
 742                          xml_parser_free($this->_parser);
 743                          $this->_delete[$this->_parserid]['status'] = $response['status']['status-code'];
 744                          return $this->_delete[$this->_parserid];
 745  
 746                      } else {
 747                          print 'Missing Content-Type: text/xml header in response.<br>';
 748                      }
 749                      return false;
 750  
 751                  default:
 752                      // collection or file was successfully deleted
 753                      $this->_delete['status'] = $response['status']['status-code'];
 754                      return $this->_delete;
 755  
 756  
 757                  }
 758              }
 759  
 760      }
 761  
 762      /**
 763       * Public method ls
 764       *
 765       * Get's directory information from webdav server into flat a array using PROPFIND
 766       *
 767       * All filenames are UTF-8 encoded.
 768       * Have a look at _propfind_startElement what keys are used in array returned.
 769       * @param string path
 770       * @return array dirinfo, false on error
 771       */
 772      function ls($path) {
 773  
 774          if (trim($path) == '') {
 775              $this->_error_log('Missing a path in method ls');
 776              return false;
 777          }
 778          $this->_path = $this->translate_uri($path);
 779  
 780          $this->header_unset();
 781          $this->create_basic_request('PROPFIND');
 782          $this->header_add('Depth: 1');
 783          $this->header_add('Content-type: application/xml');
 784          // create profind xml request...
 785          $xml  = <<<EOD
 786  <?xml version="1.0" encoding="utf-8"?>
 787  <propfind xmlns="DAV:"><prop>
 788  <getcontentlength xmlns="DAV:"/>
 789  <getlastmodified xmlns="DAV:"/>
 790  <executable xmlns="http://apache.org/dav/props/"/>
 791  <resourcetype xmlns="DAV:"/>
 792  <checked-in xmlns="DAV:"/>
 793  <checked-out xmlns="DAV:"/>
 794  </prop></propfind>
 795  EOD;
 796          $this->header_add('Content-length: ' . strlen($xml));
 797          $this->send_request();
 798          $this->_error_log($xml);
 799          fputs($this->sock, $xml);
 800          $this->get_respond();
 801          $response = $this->process_respond();
 802          // validate the response ... (only basic validation)
 803          // check http-version
 804          if ($response['status']['http-version'] == 'HTTP/1.1' ||
 805              $response['status']['http-version'] == 'HTTP/1.0') {
 806                  // seems to be http ... proceed
 807                  // We expect a 207 Multi-Status status code
 808                  // print 'http ok<br>';
 809                  if (strcmp($response['status']['status-code'],'207') == 0 ) {
 810                      // ok so far
 811                      // next there should be a Content-Type: text/xml; charset="utf-8" header line
 812                      if (preg_match('#(application|text)/xml;\s?charset=[\'\"]?utf-8[\'\"]?#i', $response['header']['Content-Type'])) {
 813                          // ok let's get the content of the xml stuff
 814                          $this->_parser = xml_parser_create_ns('UTF-8');
 815                          $this->_parserid = (int) $this->_parser;
 816                          // forget old data...
 817                          unset($this->_ls[$this->_parserid]);
 818                          unset($this->_xmltree[$this->_parserid]);
 819                          xml_parser_set_option($this->_parser,XML_OPTION_SKIP_WHITE,0);
 820                          xml_parser_set_option($this->_parser,XML_OPTION_CASE_FOLDING,0);
 821                          // xml_parser_set_option($this->_parser,XML_OPTION_TARGET_ENCODING,'UTF-8');
 822                          xml_set_object($this->_parser, $this);
 823                          xml_set_element_handler($this->_parser, "_propfind_startElement", "_endElement");
 824                          xml_set_character_data_handler($this->_parser, "_propfind_cdata");
 825  
 826  
 827                          if (!xml_parse($this->_parser, $response['body'])) {
 828                              die(sprintf("XML error: %s at line %d",
 829                                  xml_error_string(xml_get_error_code($this->_parser)),
 830                                  xml_get_current_line_number($this->_parser)));
 831                          }
 832  
 833                          // Free resources
 834                          xml_parser_free($this->_parser);
 835                          $arr = $this->_ls[$this->_parserid];
 836                          return $arr;
 837                      } else {
 838                          $this->_error_log('Missing Content-Type: text/xml header in response!!');
 839                          return false;
 840                      }
 841                  } else {
 842                      // return status code ...
 843                      return $response['status']['status-code'];
 844                  }
 845              }
 846  
 847          // response was not http
 848          $this->_error_log('Ups in method ls: error in response from server');
 849          return false;
 850      }
 851  
 852  
 853      /**
 854       * Public method gpi
 855       *
 856       * Get's path information from webdav server for one element.
 857       *
 858       * @param string path
 859       * @return array dirinfo. false on error
 860       */
 861      function gpi($path) {
 862  
 863          // split path by last "/"
 864          $path = rtrim($path, "/");
 865          $item = basename($path);
 866          $dir  = dirname($path);
 867  
 868          $list = $this->ls($dir);
 869  
 870          // be sure it is an array
 871          if (is_array($list)) {
 872              foreach($list as $e) {
 873  
 874                  $fullpath = urldecode($e['href']);
 875                  $filename = basename($fullpath);
 876  
 877                  if ($filename == $item && $filename != "" and $fullpath != $dir."/") {
 878                      return $e;
 879                  }
 880              }
 881          }
 882          return false;
 883      }
 884  
 885      /**
 886       * Public method is_file
 887       *
 888       * Gathers whether a path points to a file or not.
 889       *
 890       * @param string path
 891       * @return bool true or false
 892       */
 893      function is_file($path) {
 894  
 895          $item = $this->gpi($path);
 896  
 897          if ($item === false) {
 898              return false;
 899          } else {
 900              return ($item['resourcetype'] != 'collection');
 901          }
 902      }
 903  
 904      /**
 905       * Public method is_dir
 906       *
 907       * Gather whether a path points to a directory
 908       * @param string path
 909       * return bool true or false
 910       */
 911      function is_dir($path) {
 912  
 913          // be sure path is utf-8
 914          $item = $this->gpi($path);
 915  
 916          if ($item === false) {
 917              return false;
 918          } else {
 919              return ($item['resourcetype'] == 'collection');
 920          }
 921      }
 922  
 923  
 924      /**
 925       * Public method mput
 926       *
 927       * Puts multiple files and/or directories onto a webdav server.
 928       *
 929       * Filenames should be allready UTF-8 encoded.
 930       * Param fileList must be in format array("localpath" => "destpath").
 931       *
 932       * @param array filelist
 933       * @return bool true on success. otherwise int status code on error
 934       */
 935      function mput($filelist) {
 936  
 937          $result = true;
 938  
 939          while (list($localpath, $destpath) = each($filelist)) {
 940  
 941              $localpath = rtrim($localpath, "/");
 942              $destpath  = rtrim($destpath, "/");
 943  
 944              // attempt to create target path
 945              if (is_dir($localpath)) {
 946                  $pathparts = explode("/", $destpath."/ "); // add one level, last level will be created as dir
 947              } else {
 948                  $pathparts = explode("/", $destpath);
 949              }
 950              $checkpath = "";
 951              for ($i=1; $i<sizeof($pathparts)-1; $i++) {
 952                  $checkpath .= "/" . $pathparts[$i];
 953                  if (!($this->is_dir($checkpath))) {
 954  
 955                      $result &= ($this->mkcol($checkpath) == 201 );
 956                  }
 957              }
 958  
 959              if ($result) {
 960                  // recurse directories
 961                  if (is_dir($localpath)) {
 962                      if (!$dp = opendir($localpath)) {
 963                          $this->_error_log("Could not open localpath for reading");
 964                          return false;
 965                      }
 966                      $fl = array();
 967                      while($filename = readdir($dp)) {
 968                          if ((is_file($localpath."/".$filename) || is_dir($localpath."/".$filename)) && $filename!="." && $filename != "..") {
 969                              $fl[$localpath."/".$filename] = $destpath."/".$filename;
 970                          }
 971                      }
 972                      $result &= $this->mput($fl);
 973                  } else {
 974                      $result &= ($this->put_file($destpath, $localpath) == 201);
 975                  }
 976              }
 977          }
 978          return $result;
 979      }
 980  
 981      /**
 982       * Public method mget
 983       *
 984       * Gets multiple files and directories.
 985       *
 986       * FileList must be in format array("remotepath" => "localpath").
 987       * Filenames are UTF-8 encoded.
 988       *
 989       * @param array filelist
 990       * @return bool true on succes, other int status code on error
 991       */
 992      function mget($filelist) {
 993  
 994          $result = true;
 995  
 996          while (list($remotepath, $localpath) = each($filelist)) {
 997  
 998              $localpath   = rtrim($localpath, "/");
 999              $remotepath  = rtrim($remotepath, "/");
1000  
1001              // attempt to create local path
1002              if ($this->is_dir($remotepath)) {
1003                  $pathparts = explode("/", $localpath."/ "); // add one level, last level will be created as dir
1004              } else {
1005                  $pathparts = explode("/", $localpath);
1006              }
1007              $checkpath = "";
1008              for ($i=1; $i<sizeof($pathparts)-1; $i++) {
1009                  $checkpath .= "/" . $pathparts[$i];
1010                  if (!is_dir($checkpath)) {
1011  
1012                      $result &= mkdir($checkpath);
1013                  }
1014              }
1015  
1016              if ($result) {
1017                  // recurse directories
1018                  if ($this->is_dir($remotepath)) {
1019                      $list = $this->ls($remotepath);
1020  
1021                      $fl = array();
1022                      foreach($list as $e) {
1023                          $fullpath = urldecode($e['href']);
1024                          $filename = basename($fullpath);
1025                          if ($filename != '' and $fullpath != $remotepath . '/') {
1026                              $fl[$remotepath."/".$filename] = $localpath."/".$filename;
1027                          }
1028                      }
1029                      $result &= $this->mget($fl);
1030                  } else {
1031                      $result &= ($this->get_file($remotepath, $localpath));
1032                  }
1033              }
1034          }
1035          return $result;
1036      }
1037  
1038      // --------------------------------------------------------------------------
1039      // private xml callback and helper functions starting here
1040      // --------------------------------------------------------------------------
1041  
1042  
1043      /**
1044       * Private method _endelement
1045       *
1046       * a generic endElement method  (used for all xml callbacks).
1047       *
1048       * @param resource parser, string name
1049       * @access private
1050       */
1051  
1052      private function _endElement($parser, $name) {
1053          // end tag was found...
1054          $parserid = (int) $parser;
1055          $this->_xmltree[$parserid] = substr($this->_xmltree[$parserid],0, strlen($this->_xmltree[$parserid]) - (strlen($name) + 1));
1056      }
1057  
1058      /**
1059       * Private method _propfind_startElement
1060       *
1061       * Is needed by public method ls.
1062       *
1063       * Generic method will called by php xml_parse when a xml start element tag has been detected.
1064       * The xml tree will translated into a flat php array for easier access.
1065       * @param resource parser, string name, string attrs
1066       * @access private
1067       */
1068      private function _propfind_startElement($parser, $name, $attrs) {
1069          // lower XML Names... maybe break a RFC, don't know ...
1070          $parserid = (int) $parser;
1071  
1072          $propname = strtolower($name);
1073          if (!empty($this->_xmltree[$parserid])) {
1074              $this->_xmltree[$parserid] .= $propname . '_';
1075          } else {
1076              $this->_xmltree[$parserid] = $propname . '_';
1077          }
1078  
1079          // translate xml tree to a flat array ...
1080          switch($this->_xmltree[$parserid]) {
1081          case 'dav::multistatus_dav::response_':
1082              // new element in mu
1083              $this->_ls_ref =& $this->_ls[$parserid][];
1084              break;
1085          case 'dav::multistatus_dav::response_dav::href_':
1086              $this->_ls_ref_cdata = &$this->_ls_ref['href'];
1087              break;
1088          case 'dav::multistatus_dav::response_dav::propstat_dav::prop_dav::creationdate_':
1089              $this->_ls_ref_cdata = &$this->_ls_ref['creationdate'];
1090              break;
1091          case 'dav::multistatus_dav::response_dav::propstat_dav::prop_dav::getlastmodified_':
1092              $this->_ls_ref_cdata = &$this->_ls_ref['lastmodified'];
1093              break;
1094          case 'dav::multistatus_dav::response_dav::propstat_dav::prop_dav::getcontenttype_':
1095              $this->_ls_ref_cdata = &$this->_ls_ref['getcontenttype'];
1096              break;
1097          case 'dav::multistatus_dav::response_dav::propstat_dav::prop_dav::getcontentlength_':
1098              $this->_ls_ref_cdata = &$this->_ls_ref['getcontentlength'];
1099              break;
1100          case 'dav::multistatus_dav::response_dav::propstat_dav::prop_dav::lockdiscovery_dav::activelock_dav::depth_':
1101              $this->_ls_ref_cdata = &$this->_ls_ref['activelock_depth'];
1102              break;
1103          case 'dav::multistatus_dav::response_dav::propstat_dav::prop_dav::lockdiscovery_dav::activelock_dav::owner_dav::href_':
1104              $this->_ls_ref_cdata = &$this->_ls_ref['activelock_owner'];
1105              break;
1106          case 'dav::multistatus_dav::response_dav::propstat_dav::prop_dav::lockdiscovery_dav::activelock_dav::owner_':
1107              $this->_ls_ref_cdata = &$this->_ls_ref['activelock_owner'];
1108              break;
1109          case 'dav::multistatus_dav::response_dav::propstat_dav::prop_dav::lockdiscovery_dav::activelock_dav::timeout_':
1110              $this->_ls_ref_cdata = &$this->_ls_ref['activelock_timeout'];
1111              break;
1112          case 'dav::multistatus_dav::response_dav::propstat_dav::prop_dav::lockdiscovery_dav::activelock_dav::locktoken_dav::href_':
1113              $this->_ls_ref_cdata = &$this->_ls_ref['activelock_token'];
1114              break;
1115          case 'dav::multistatus_dav::response_dav::propstat_dav::prop_dav::lockdiscovery_dav::activelock_dav::locktype_dav::write_':
1116              $this->_ls_ref_cdata = &$this->_ls_ref['activelock_type'];
1117              $this->_ls_ref_cdata = 'write';
1118              $this->_ls_ref_cdata = &$this->_null;
1119              break;
1120          case 'dav::multistatus_dav::response_dav::propstat_dav::prop_dav::resourcetype_dav::collection_':
1121              $this->_ls_ref_cdata = &$this->_ls_ref['resourcetype'];
1122              $this->_ls_ref_cdata = 'collection';
1123              $this->_ls_ref_cdata = &$this->_null;
1124              break;
1125          case 'dav::multistatus_dav::response_dav::propstat_dav::status_':
1126              $this->_ls_ref_cdata = &$this->_ls_ref['status'];
1127              break;
1128  
1129          default:
1130              // handle unknown xml elements...
1131              $this->_ls_ref_cdata = &$this->_ls_ref[$this->_xmltree[$parserid]];
1132          }
1133      }
1134  
1135      /**
1136       * Private method _propfind_cData
1137       *
1138       * Is needed by public method ls.
1139       *
1140       * Will be called by php xml_set_character_data_handler() when xml data has to be handled.
1141       * Stores data found into class var _ls_ref_cdata
1142       * @param resource parser, string cdata
1143       * @access private
1144       */
1145      private function _propfind_cData($parser, $cdata) {
1146          if (trim($cdata) <> '') {
1147              // cdata must be appended, because sometimes the php xml parser makes multiple calls
1148              // to _propfind_cData before the xml end tag was reached...
1149              $this->_ls_ref_cdata .= $cdata;
1150          } else {
1151              // do nothing
1152          }
1153      }
1154  
1155      /**
1156       * Private method _delete_startElement
1157       *
1158       * Is used by public method delete.
1159       *
1160       * Will be called by php xml_parse.
1161       * @param resource parser, string name, string attrs)
1162       * @access private
1163       */
1164      private function _delete_startElement($parser, $name, $attrs) {
1165          // lower XML Names... maybe break a RFC, don't know ...
1166          $parserid = (int) $parser;
1167          $propname = strtolower($name);
1168          $this->_xmltree[$parserid] .= $propname . '_';
1169  
1170          // translate xml tree to a flat array ...
1171          switch($this->_xmltree[$parserid]) {
1172          case 'dav::multistatus_dav::response_':
1173              // new element in mu
1174              $this->_delete_ref =& $this->_delete[$parserid][];
1175              break;
1176          case 'dav::multistatus_dav::response_dav::href_':
1177              $this->_delete_ref_cdata = &$this->_ls_ref['href'];
1178              break;
1179  
1180          default:
1181              // handle unknown xml elements...
1182              $this->_delete_cdata = &$this->_delete_ref[$this->_xmltree[$parserid]];
1183          }
1184      }
1185  
1186  
1187      /**
1188       * Private method _delete_cData
1189       *
1190       * Is used by public method delete.
1191       *
1192       * Will be called by php xml_set_character_data_handler() when xml data has to be handled.
1193       * Stores data found into class var _delete_ref_cdata
1194       * @param resource parser, string cdata
1195       * @access private
1196       */
1197      private function _delete_cData($parser, $cdata) {
1198          if (trim($cdata) <> '') {
1199              $this->_delete_ref_cdata .= $cdata;
1200          } else {
1201              // do nothing
1202          }
1203      }
1204  
1205  
1206      /**
1207       * Private method _lock_startElement
1208       *
1209       * Is needed by public method lock.
1210       *
1211       * Mmethod will called by php xml_parse when a xml start element tag has been detected.
1212       * The xml tree will translated into a flat php array for easier access.
1213       * @param resource parser, string name, string attrs
1214       * @access private
1215       */
1216      private function _lock_startElement($parser, $name, $attrs) {
1217          // lower XML Names... maybe break a RFC, don't know ...
1218          $parserid = (int) $parser;
1219          $propname = strtolower($name);
1220          $this->_xmltree[$parserid] .= $propname . '_';
1221  
1222          // translate xml tree to a flat array ...
1223          /*
1224          dav::prop_dav::lockdiscovery_dav::activelock_dav::depth_=
1225          dav::prop_dav::lockdiscovery_dav::activelock_dav::owner_dav::href_=
1226          dav::prop_dav::lockdiscovery_dav::activelock_dav::timeout_=
1227          dav::prop_dav::lockdiscovery_dav::activelock_dav::locktoken_dav::href_=
1228           */
1229          switch($this->_xmltree[$parserid]) {
1230          case 'dav::prop_dav::lockdiscovery_dav::activelock_':
1231              // new element
1232              $this->_lock_ref =& $this->_lock[$parserid][];
1233              break;
1234          case 'dav::prop_dav::lockdiscovery_dav::activelock_dav::locktype_dav::write_':
1235              $this->_lock_ref_cdata = &$this->_lock_ref['locktype'];
1236              $this->_lock_cdata = 'write';
1237              $this->_lock_cdata = &$this->_null;
1238              break;
1239          case 'dav::prop_dav::lockdiscovery_dav::activelock_dav::lockscope_dav::exclusive_':
1240              $this->_lock_ref_cdata = &$this->_lock_ref['lockscope'];
1241              $this->_lock_ref_cdata = 'exclusive';
1242              $this->_lock_ref_cdata = &$this->_null;
1243              break;
1244          case 'dav::prop_dav::lockdiscovery_dav::activelock_dav::depth_':
1245              $this->_lock_ref_cdata = &$this->_lock_ref['depth'];
1246              break;
1247          case 'dav::prop_dav::lockdiscovery_dav::activelock_dav::owner_dav::href_':
1248              $this->_lock_ref_cdata = &$this->_lock_ref['owner'];
1249              break;
1250          case 'dav::prop_dav::lockdiscovery_dav::activelock_dav::timeout_':
1251              $this->_lock_ref_cdata = &$this->_lock_ref['timeout'];
1252              break;
1253          case 'dav::prop_dav::lockdiscovery_dav::activelock_dav::locktoken_dav::href_':
1254              $this->_lock_ref_cdata = &$this->_lock_ref['locktoken'];
1255              break;
1256          default:
1257              // handle unknown xml elements...
1258              $this->_lock_cdata = &$this->_lock_ref[$this->_xmltree[$parserid]];
1259  
1260          }
1261      }
1262  
1263      /**
1264       * Private method _lock_cData
1265       *
1266       * Is used by public method lock.
1267       *
1268       * Will be called by php xml_set_character_data_handler() when xml data has to be handled.
1269       * Stores data found into class var _lock_ref_cdata
1270       * @param resource parser, string cdata
1271       * @access private
1272       */
1273      private function _lock_cData($parser, $cdata) {
1274          $parserid = (int) $parser;
1275          if (trim($cdata) <> '') {
1276              // $this->_error_log(($this->_xmltree[$parserid]) . '='. htmlentities($cdata));
1277              $this->_lock_ref_cdata .= $cdata;
1278          } else {
1279              // do nothing
1280          }
1281      }
1282  
1283  
1284      /**
1285       * Private method header_add
1286       *
1287       * extends class var array _req
1288       * @param string string
1289       * @access private
1290       */
1291      private function header_add($string) {
1292          $this->_req[] = $string;
1293      }
1294  
1295      /**
1296       * Private method header_unset
1297       *
1298       * unsets class var array _req
1299       * @access private
1300       */
1301  
1302      private function header_unset() {
1303          unset($this->_req);
1304      }
1305  
1306      /**
1307       * Private method create_basic_request
1308       *
1309       * creates by using private method header_add an general request header.
1310       * @param string method
1311       * @access private
1312       */
1313      private function create_basic_request($method) {
1314          $this->header_add(sprintf('%s %s %s', $method, $this->_path, $this->_protocol));
1315          $this->header_add(sprintf('Host: %s:%s', $this->_server, $this->_port));
1316          //$request .= sprintf('Connection: Keep-Alive');
1317          $this->header_add(sprintf('User-Agent: %s', $this->_user_agent));
1318          $this->header_add('Connection: TE');
1319          $this->header_add('TE: Trailers');
1320          if ($this->_auth == 'basic') {
1321              $this->header_add(sprintf('Authorization: Basic %s', base64_encode("$this->_user:$this->_pass")));
1322          } else if ($this->_auth == 'digest') {
1323              if ($signature = $this->digest_signature($method)){
1324                  $this->header_add($signature);
1325              }
1326          }
1327      }
1328  
1329      /**
1330       * Reads the header, stores the challenge information
1331       *
1332       * @return void
1333       */
1334      private function digest_auth() {
1335  
1336          $headers = array();
1337          $headers[] = sprintf('%s %s %s', 'HEAD', $this->_path, $this->_protocol);
1338          $headers[] = sprintf('Host: %s:%s', $this->_server, $this->_port);
1339          $headers[] = sprintf('User-Agent: %s', $this->_user_agent);
1340          $headers = implode("\r\n", $headers);
1341          $headers .= "\r\n\r\n";
1342          fputs($this->sock, $headers);
1343  
1344          // Reads the headers.
1345          $i = 0;
1346          $header = '';
1347          do {
1348              $header .= fread($this->sock, 1);
1349              $i++;
1350          } while (!preg_match('/\\r\\n\\r\\n$/', $header, $matches) && $i < $this->_maxheaderlenth);
1351  
1352          // Analyse the headers.
1353          $digest = array();
1354          $splitheaders = explode("\r\n", $header);
1355          foreach ($splitheaders as $line) {
1356              if (!preg_match('/^WWW-Authenticate: Digest/', $line)) {
1357                  continue;
1358              }
1359              $line = substr($line, strlen('WWW-Authenticate: Digest '));
1360              $params = explode(',', $line);
1361              foreach ($params as $param) {
1362                  list($key, $value) = explode('=', trim($param), 2);
1363                  $digest[$key] = trim($value, '"');
1364              }
1365              break;
1366          }
1367  
1368          $this->_digestchallenge = $digest;
1369      }
1370  
1371      /**
1372       * Generates the digest signature
1373       *
1374       * @return string signature to add to the headers
1375       * @access private
1376       */
1377      private function digest_signature($method) {
1378          if (!$this->_digestchallenge) {
1379              $this->digest_auth();
1380          }
1381  
1382          $signature = array();
1383          $signature['username'] = '"' . $this->_user . '"';
1384          $signature['realm'] = '"' . $this->_digestchallenge['realm'] . '"';
1385          $signature['nonce'] = '"' . $this->_digestchallenge['nonce'] . '"';
1386          $signature['uri'] = '"' . $this->_path . '"';
1387  
1388          if (isset($this->_digestchallenge['algorithm']) && $this->_digestchallenge['algorithm'] != 'MD5') {
1389              $this->_error_log('Algorithm other than MD5 are not supported');
1390              return false;
1391          }
1392  
1393          $a1 = $this->_user . ':' . $this->_digestchallenge['realm'] . ':' . $this->_pass;
1394          $a2 = $method . ':' . $this->_path;
1395  
1396          if (!isset($this->_digestchallenge['qop'])) {
1397              $signature['response'] = '"' . md5(md5($a1) . ':' . $this->_digestchallenge['nonce'] . ':' . md5($a2)) . '"';
1398          } else {
1399              // Assume QOP is auth
1400              if (empty($this->_cnonce)) {
1401                  $this->_cnonce = random_string();
1402                  $this->_nc = 0;
1403              }
1404              $this->_nc++;
1405              $nc = sprintf('%08d', $this->_nc);
1406              $signature['cnonce'] = '"' . $this->_cnonce . '"';
1407              $signature['nc'] = '"' . $nc . '"';
1408              $signature['qop'] = '"' . $this->_digestchallenge['qop'] . '"';
1409              $signature['response'] = '"' . md5(md5($a1) . ':' . $this->_digestchallenge['nonce'] . ':' .
1410                      $nc . ':' . $this->_cnonce . ':' . $this->_digestchallenge['qop'] . ':' . md5($a2)) . '"';
1411          }
1412  
1413          $response = array();
1414          foreach ($signature as $key => $value) {
1415              $response[] = "$key=$value";
1416          }
1417          return 'Authorization: Digest ' . implode(', ', $response);
1418      }
1419  
1420      /**
1421       * Private method send_request
1422       *
1423       * Sends a ready formed http/webdav request to webdav server.
1424       *
1425       * @access private
1426       */
1427      private function send_request() {
1428          // check if stream is declared to be open
1429          // only logical check we are not sure if socket is really still open ...
1430          if ($this->_connection_closed) {
1431              // reopen it
1432              // be sure to close the open socket.
1433              $this->close();
1434              $this->reopen();
1435          }
1436  
1437          // convert array to string
1438          $buffer = implode("\r\n", $this->_req);
1439          $buffer .= "\r\n\r\n";
1440          $this->_error_log($buffer);
1441          fputs($this->sock, $buffer);
1442      }
1443  
1444      /**
1445       * Private method get_respond
1446       *
1447       * Reads the response from the webdav server.
1448       *
1449       * Stores data into class vars _header for the header data and
1450       * _body for the rest of the response.
1451       * This routine is the weakest part of this class, because it very depends how php does handle a socket stream.
1452       * If the stream is blocked for some reason php is blocked as well.
1453       * @access private
1454       * @param resource $fp optional the file handle to write the body content to (stored internally in the '_body' if not set)
1455       */
1456      private function get_respond($fp = null) {
1457          $this->_error_log('get_respond()');
1458          // init vars (good coding style ;-)
1459          $buffer = '';
1460          $header = '';
1461          // attention: do not make max_chunk_size to big....
1462          $max_chunk_size = 8192;
1463          // be sure we got a open ressource
1464          if (! $this->sock) {
1465              $this->_error_log('socket is not open. Can not process response');
1466              return false;
1467          }
1468  
1469          // following code maybe helps to improve socket behaviour ... more testing needed
1470          // disabled at the moment ...
1471          // socket_set_timeout($this->sock,1 );
1472          // $socket_state = socket_get_status($this->sock);
1473  
1474          // read stream one byte by another until http header ends
1475          $i = 0;
1476          $matches = array();
1477          do {
1478              $header.=fread($this->sock, 1);
1479              $i++;
1480          } while (!preg_match('/\\r\\n\\r\\n$/',$header, $matches) && $i < $this->_maxheaderlenth);
1481  
1482          $this->_error_log($header);
1483  
1484          if (preg_match('/Connection: close\\r\\n/', $header)) {
1485              // This says that the server will close connection at the end of this stream.
1486              // Therefore we need to reopen the socket, before are sending the next request...
1487              $this->_error_log('Connection: close found');
1488              $this->_connection_closed = true;
1489          } else if (preg_match('@^HTTP/1\.(1|0) 401 @', $header)) {
1490              $this->_error_log('The server requires an authentication');
1491          }
1492  
1493          // check how to get the data on socket stream
1494          // chunked or content-length (HTTP/1.1) or
1495          // one block until feof is received (HTTP/1.0)
1496          switch(true) {
1497          case (preg_match('/Transfer\\-Encoding:\\s+chunked\\r\\n/',$header)):
1498              $this->_error_log('Getting HTTP/1.1 chunked data...');
1499              do {
1500                  $byte = '';
1501                  $chunk_size='';
1502                  do {
1503                      $chunk_size.=$byte;
1504                      $byte=fread($this->sock,1);
1505                      // check what happens while reading, because I do not really understand how php reads the socketstream...
1506                      // but so far - it seems to work here - tested with php v4.3.1 on apache 1.3.27 and Debian Linux 3.0 ...
1507                      if (strlen($byte) == 0) {
1508                          $this->_error_log('get_respond: warning --> read zero bytes');
1509                      }
1510                  } while ($byte!="\r" and strlen($byte)>0);      // till we match the Carriage Return
1511                  fread($this->sock, 1);                           // also drop off the Line Feed
1512                  $chunk_size=hexdec($chunk_size);                // convert to a number in decimal system
1513                  if ($chunk_size > 0) {
1514                      $read = 0;
1515                      // Reading the chunk in one bite is not secure, we read it byte by byte.
1516                      while ($read < $chunk_size) {
1517                          $chunk = fread($this->sock, 1);
1518                          self::update_file_or_buffer($chunk, $fp, $buffer);
1519                          $read++;
1520                      }
1521                  }
1522                  fread($this->sock, 2);                            // ditch the CRLF that trails the chunk
1523              } while ($chunk_size);                            // till we reach the 0 length chunk (end marker)
1524              break;
1525  
1526              // check for a specified content-length
1527          case preg_match('/Content\\-Length:\\s+([0-9]*)\\r\\n/',$header,$matches):
1528              $this->_error_log('Getting data using Content-Length '. $matches[1]);
1529  
1530              // check if we the content data size is small enough to get it as one block
1531              if ($matches[1] <= $max_chunk_size ) {
1532                  // only read something if Content-Length is bigger than 0
1533                  if ($matches[1] > 0 ) {
1534                      $chunk = fread($this->sock, $matches[1]);
1535                      $loadsize = strlen($chunk);
1536                      //did we realy get the full length?
1537                      if ($loadsize < $matches[1]) {
1538                          $max_chunk_size = $loadsize;
1539                          do {
1540                              $mod = $max_chunk_size % ($matches[1] - strlen($chunk));
1541                              $chunk_size = ($mod == $max_chunk_size ? $max_chunk_size : $matches[1] - strlen($chunk));
1542                              $chunk .= fread($this->sock, $chunk_size);
1543                              $this->_error_log('mod: ' . $mod . ' chunk: ' . $chunk_size . ' total: ' . strlen($chunk));
1544                          } while ($mod == $max_chunk_size);
1545                      }
1546                      self::update_file_or_buffer($chunk, $fp, $buffer);
1547                      break;
1548                  } else {
1549                      $buffer = '';
1550                      break;
1551                  }
1552              }
1553  
1554              // data is to big to handle it as one. Get it chunk per chunk...
1555              //trying to get the full length of max_chunk_size
1556              $chunk = fread($this->sock, $max_chunk_size);
1557              $loadsize = strlen($chunk);
1558              self::update_file_or_buffer($chunk, $fp, $buffer);
1559              if ($loadsize < $max_chunk_size) {
1560                  $max_chunk_size = $loadsize;
1561              }
1562              do {
1563                  $mod = $max_chunk_size % ($matches[1] - $loadsize);
1564                  $chunk_size = ($mod == $max_chunk_size ? $max_chunk_size : $matches[1] - $loadsize);
1565                  $chunk = fread($this->sock, $chunk_size);
1566                  self::update_file_or_buffer($chunk, $fp, $buffer);
1567                  $loadsize += strlen($chunk);
1568                  $this->_error_log('mod: ' . $mod . ' chunk: ' . $chunk_size . ' total: ' . $loadsize);
1569              } while ($mod == $max_chunk_size);
1570              if ($loadsize < $matches[1]) {
1571                  $chunk = fread($this->sock, $matches[1] - $loadsize);
1572                  self::update_file_or_buffer($chunk, $fp, $buffer);
1573              }
1574              break;
1575  
1576              // check for 204 No Content
1577              // 204 responds have no body.
1578              // Therefore we do not need to read any data from socket stream.
1579          case preg_match('/HTTP\/1\.1\ 204/',$header):
1580              // nothing to do, just proceed
1581              $this->_error_log('204 No Content found. No further data to read..');
1582              break;
1583          default:
1584              // just get the data until foef appears...
1585              $this->_error_log('reading until feof...' . $header);
1586              socket_set_timeout($this->sock, 0, 0);
1587              while (!feof($this->sock)) {
1588                  $chunk = fread($this->sock, 4096);
1589                  self::update_file_or_buffer($chunk, $fp, $buffer);
1590              }
1591              // renew the socket timeout...does it do something ???? Is it needed. More debugging needed...
1592              socket_set_timeout($this->sock, $this->_socket_timeout, 0);
1593          }
1594  
1595          $this->_header = $header;
1596          $this->_body = $buffer;
1597          // $this->_buffer = $header . "\r\n\r\n" . $buffer;
1598          $this->_error_log($this->_header);
1599          $this->_error_log($this->_body);
1600  
1601      }
1602  
1603      /**
1604       * Write the chunk to the file if $fp is set, otherwise append the data to the buffer
1605       * @param string $chunk the data to add
1606       * @param resource $fp the file handle to write to (or null)
1607       * @param string &$buffer the buffer to append to (if $fp is null)
1608       */
1609      static private function update_file_or_buffer($chunk, $fp, &$buffer) {
1610          if ($fp) {
1611              fwrite($fp, $chunk);
1612          } else {
1613              $buffer .= $chunk;
1614          }
1615      }
1616  
1617      /**
1618       * Private method process_respond
1619       *
1620       * Processes the webdav server respond and detects its components (header, body).
1621       * and returns data array structure.
1622       * @return array ret_struct
1623       * @access private
1624       */
1625      private function process_respond() {
1626          $lines = explode("\r\n", $this->_header);
1627          $header_done = false;
1628          // $this->_error_log($this->_buffer);
1629          // First line should be a HTTP status line (see http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6)
1630          // Format is: HTTP-Version SP Status-Code SP Reason-Phrase CRLF
1631          list($ret_struct['status']['http-version'],
1632              $ret_struct['status']['status-code'],
1633              $ret_struct['status']['reason-phrase']) = explode(' ', $lines[0],3);
1634  
1635          // print "HTTP Version: '$http_version' Status-Code: '$status_code' Reason Phrase: '$reason_phrase'<br>";
1636          // get the response header fields
1637          // See http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6
1638          for($i=1; $i<count($lines); $i++) {
1639              if (rtrim($lines[$i]) == '' && !$header_done) {
1640                  $header_done = true;
1641                  // print "--- response header end ---<br>";
1642  
1643              }
1644              if (!$header_done ) {
1645                  // store all found headers in array ...
1646                  list($fieldname, $fieldvalue) = explode(':', $lines[$i]);
1647                  // check if this header was allready set (apache 2.0 webdav module does this....).
1648                  // If so we add the the value to the end the fieldvalue, separated by comma...
1649                  if (empty($ret_struct['header'])) {
1650                      $ret_struct['header'] = array();
1651                  }
1652                  if (empty($ret_struct['header'][$fieldname])) {
1653                      $ret_struct['header'][$fieldname] = trim($fieldvalue);
1654                  } else {
1655                      $ret_struct['header'][$fieldname] .= ',' . trim($fieldvalue);
1656                  }
1657              }
1658          }
1659          // print 'string len of response_body:'. strlen($response_body);
1660          // print '[' . htmlentities($response_body) . ']';
1661          $ret_struct['body'] = $this->_body;
1662          $this->_error_log('process_respond: ' . var_export($ret_struct,true));
1663          return $ret_struct;
1664  
1665      }
1666  
1667      /**
1668       * Private method reopen
1669       *
1670       * Reopens a socket, if 'connection: closed'-header was received from server.
1671       *
1672       * Uses public method open.
1673       * @access private
1674       */
1675      private function reopen() {
1676          // let's try to reopen a socket
1677          $this->_error_log('reopen a socket connection');
1678          return $this->open();
1679      }
1680  
1681  
1682      /**
1683       * Private method translate_uri
1684       *
1685       * translates an uri to raw url encoded string.
1686       * Removes any html entity in uri
1687       * @param string uri
1688       * @return string translated_uri
1689       * @access private
1690       */
1691      private function translate_uri($uri) {
1692          // remove all html entities...
1693          $native_path = html_entity_decode($uri);
1694          $parts = explode('/', $native_path);
1695          for ($i = 0; $i < count($parts); $i++) {
1696              // check if part is allready utf8
1697              if (iconv('UTF-8', 'UTF-8', $parts[$i]) == $parts[$i]) {
1698                  $parts[$i] = rawurlencode($parts[$i]);
1699              } else {
1700                  $parts[$i] = rawurlencode(utf8_encode($parts[$i]));
1701              }
1702          }
1703          return implode('/', $parts);
1704      }
1705  
1706      /**
1707       * Private method utf_decode_path
1708       *
1709       * decodes a UTF-8 encoded string
1710       * @return string decodedstring
1711       * @access private
1712       */
1713      private function utf_decode_path($path) {
1714          $fullpath = $path;
1715          if (iconv('UTF-8', 'UTF-8', $fullpath) == $fullpath) {
1716              $this->_error_log("filename is utf-8. Needs conversion...");
1717              $fullpath = utf8_decode($fullpath);
1718          }
1719          return $fullpath;
1720      }
1721  
1722      /**
1723       * Private method _error_log
1724       *
1725       * a simple php error_log wrapper.
1726       * @param string err_string
1727       * @access private
1728       */
1729      private function _error_log($err_string) {
1730          if ($this->_debug) {
1731              error_log($err_string);
1732          }
1733      }
1734  }


Generated: Thu Aug 11 10:00:09 2016 Cross-referenced by PHPXref 0.7.1