MDL-36538 repository_webdav - save files directly to local filesystem, rather than...
[moodle.git] / lib / webdavlib.php
1 <?php
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/>.
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  */
36 class webdav_client {
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;
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;
82     /**#@-*/
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     }
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     }
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         /*
126          date-time       = full-date "T" full-time
128          full-date       = date-fullyear "-" date-month "-" date-mday
129          full-time       = partial-time time-offset
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
142          partial-time    = time-hour ":" time-minute ":" time-second
143                                             [time-secfrac]
144          */
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
153         return false;
154     }
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         set_time_limit(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     }
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     }
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     }
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;
225     }
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 reponse 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         }
262     }
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();
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     }
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();
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     }
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 ...
353         $handle = @fopen ($filename, 'r');
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();
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         }
389     }
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) {
403         $localpath = $this->utf_decode_path($localpath);
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     }
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     }
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');
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";
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     }
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) {
533         $this->_path = $this->translate_uri($src_path);
534         $this->header_unset();
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('');
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.
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     }
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              */
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");
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                         }
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];
647                     } else {
648                         print 'Missing Content-Type: text/xml header in response.<br>';
649                     }
650                     return false;
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             }
661     }
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     }
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();
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>';
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");
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                         }
739                         print "<br>";
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];
746                     } else {
747                         print 'Missing Content-Type: text/xml header in response.<br>';
748                     }
749                     return false;
751                 default:
752                     // collection or file was successfully deleted
753                     $this->_delete['status'] = $response['status']['status-code'];
754                     return $this->_delete;
757                 }
758             }
760     }
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) {
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);
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");
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                         }
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             }
847         // response was not http
848         $this->_error_log('Ups in method ls: error in response from server');
849         return false;
850     }
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) {
863         // split path by last "/"
864         $path = rtrim($path, "/");
865         $item = basename($path);
866         $dir  = dirname($path);
868         $list = $this->ls($dir);
870         // be sure it is an array
871         if (is_array($list)) {
872             foreach($list as $e) {
874                 $fullpath = urldecode($e['href']);
875                 $filename = basename($fullpath);
877                 if ($filename == $item && $filename != "" and $fullpath != $dir."/") {
878                     return $e;
879                 }
880             }
881         }
882         return false;
883     }
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) {
895         $item = $this->gpi($path);
897         if ($item === false) {
898             return false;
899         } else {
900             return ($item['resourcetype'] != 'collection');
901         }
902     }
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) {
913         // be sure path is utf-8
914         $item = $this->gpi($path);
916         if ($item === false) {
917             return false;
918         } else {
919             return ($item['resourcetype'] == 'collection');
920         }
921     }
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) {
937         $result = true;
939         while (list($localpath, $destpath) = each($filelist)) {
941             $localpath = rtrim($localpath, "/");
942             $destpath  = rtrim($destpath, "/");
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))) {
955                     $result &= ($this->mkcol($checkpath) == 201 );
956                 }
957             }
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     }
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) {
994         $result = true;
996         while (list($remotepath, $localpath) = each($filelist)) {
998             $localpath   = rtrim($localpath, "/");
999             $remotepath  = rtrim($remotepath, "/");
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)) {
1012                     $result &= mkdir($checkpath);
1013                 }
1014             }
1016             if ($result) {
1017                 // recurse directories
1018                 if ($this->is_dir($remotepath)) {
1019                     $list = $this->ls($remotepath);
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     }
1038     // --------------------------------------------------------------------------
1039     // private xml callback and helper functions starting here
1040     // --------------------------------------------------------------------------
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      */
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     }
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;
1072         $propname = strtolower($name);
1073         if (!empty($this->_xmltree[$parserid])) {
1074             $this->_xmltree[$parserid] .= $propname . '_';
1075         } else {
1076             $this->_xmltree[$parserid] = $propname . '_';
1077         }
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;
1129         default:
1130             // handle unknown xml elements...
1131             $this->_ls_ref_cdata = &$this->_ls_ref[$this->_xmltree[$parserid]];
1132         }
1133     }
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     }
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 . '_';
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;
1180         default:
1181             // handle unknown xml elements...
1182             $this->_delete_cdata = &$this->_delete_ref[$this->_xmltree[$parserid]];
1183         }
1184     }
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     }
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 . '_';
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]];
1260         }
1261     }
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     }
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     }
1295     /**
1296      * Private method header_unset
1297      *
1298      * unsets class var array _req
1299      * @access private
1300      */
1302     private function header_unset() {
1303         unset($this->_req);
1304     }
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     }
1329     /**
1330      * Reads the header, stores the challenge information
1331      *
1332      * @return void
1333      */
1334     private function digest_auth() {
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);
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);
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         }
1368         $this->_digestchallenge = $digest;
1369     }
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         }
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 . '"';
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         }
1393         $a1 = $this->_user . ':' . $this->_digestchallenge['realm'] . ':' . $this->_pass;
1394         $a2 = $method . ':' . $this->_path;
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         }
1413         $response = array();
1414         foreach ($signature as $key => $value) {
1415             $response[] = "$key=$value";
1416         }
1417         return 'Authorization: Digest ' . implode(', ', $response);
1418     }
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         }
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     }
1444     /**
1445      * Private method get_respond
1446      *
1447      * Reads the reponse 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         }
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);
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);
1482         $this->_error_log($header);
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         }
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;
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]);
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             }
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;
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         }
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);
1601     }
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     }
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);
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>";
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;
1665     }
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     }
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     }
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     }
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     }