Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.
/lib/ -> webdavlib.php (source)

Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403]

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