Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

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