Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | 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);
1441 ariadna 665
                        xml_set_element_handler($this->_parser, [$this, "_lock_startElement"], [$this, "_endElement"]);
666
                        xml_set_character_data_handler($this->_parser, [$this, "_lock_cdata"]);
1 efrain 667
 
668
                        if (!xml_parse($this->_parser, $response['body'])) {
669
                            die(sprintf("XML error: %s at line %d",
670
                                xml_error_string(xml_get_error_code($this->_parser)),
671
                                xml_get_current_line_number($this->_parser)));
672
                        }
673
 
674
                        // Free resources
675
                        xml_parser_free($this->_parser);
676
                        // add status code to array
677
                        $this->_lock[$this->_parserid]['status'] = 200;
678
                        return $this->_lock[$this->_parserid];
679
 
680
                    } else {
681
                        print 'Missing Content-Type: text/xml header in response.<br>';
682
                    }
683
                    return false;
684
 
685
                default:
686
                    // hmm. not what we expected. Just return what we got from webdav server
687
                    // someone else has to handle it.
688
                    $this->_lock['status'] = $response['status']['status-code'];
689
                    return $this->_lock;
690
                }
691
            }
692
 
693
 
694
    }
695
 
696
 
697
    /**
698
     * Public method unlock
699
     *
700
     * Unlocks a file or collection.
701
     *
702
     * @param string path, string locktoken
703
     * @return int status code (look at rfc 2518). false on error.
704
     */
705
    function unlock($path, $locktoken) {
706
        $this->_path = $this->translate_uri($path);
707
        $this->header_unset();
708
        $this->create_basic_request('UNLOCK');
709
        $this->header_add(sprintf('Lock-Token: <%s>', $locktoken));
710
        $this->send_request();
711
        $this->get_respond();
712
        $response = $this->process_respond();
713
        if ($response['status']['http-version'] == 'HTTP/1.1' ||
714
            $response['status']['http-version'] == 'HTTP/1.0') {
715
            /* seems to be http ... proceed
716
            rfc 2518 says:
717
            204 (OK) - The 204 (No Content) status code is used instead of 200 (OK) because there is no response entity body.
718
             */
719
                return $response['status']['status-code'];
720
            }
721
        return false;
722
    }
723
 
724
    /**
725
     * Public method delete
726
     *
727
     * deletes a collection/directory on a webdav server
728
     * @param string path
729
     * @return int status code (look at rfc 2518). false on error.
730
     */
731
    function delete($path) {
732
        $this->_path = $this->translate_uri($path);
733
        $this->header_unset();
734
        $this->create_basic_request('DELETE');
735
        /* $this->header_add('Content-Length: 0'); */
736
        $this->header_add('');
737
        $this->send_request();
738
        $this->get_respond();
739
        $response = $this->process_respond();
740
 
741
        // validate the response ...
742
        // check http-version
743
        if ($response['status']['http-version'] == 'HTTP/1.1' ||
744
            $response['status']['http-version'] == 'HTTP/1.0') {
745
                // seems to be http ... proceed
746
                // We expect a 207 Multi-Status status code
747
                // print 'http ok<br>';
748
 
749
                switch ($response['status']['status-code']) {
750
                case 207:
751
                    // collection was NOT deleted... see xml response for reason...
752
                    // next there should be a Content-Type: text/xml; charset="utf-8" header line
753
                    if (strcmp($response['header']['Content-Type'], 'text/xml; charset="utf-8"') == 0) {
754
                        // ok let's get the content of the xml stuff
755
                        $this->_parser = xml_parser_create_ns();
756
                        $this->_parserid = $this->get_parser_id($this->_parser);
757
                        // forget old data...
758
                        unset($this->_delete[$this->_parserid]);
759
                        unset($this->_xmltree[$this->_parserid]);
760
                        xml_parser_set_option($this->_parser,XML_OPTION_SKIP_WHITE,0);
761
                        xml_parser_set_option($this->_parser,XML_OPTION_CASE_FOLDING,0);
1441 ariadna 762
                        xml_set_element_handler($this->_parser, [$this, "_delete_startElement"], [$this, "_endElement"]);
763
                        xml_set_character_data_handler($this->_parser, [$this, "_delete_cdata"]);
1 efrain 764
 
765
                        if (!xml_parse($this->_parser, $response['body'])) {
766
                            die(sprintf("XML error: %s at line %d",
767
                                xml_error_string(xml_get_error_code($this->_parser)),
768
                                xml_get_current_line_number($this->_parser)));
769
                        }
770
 
771
                        print "<br>";
772
 
773
                        // Free resources
774
                        xml_parser_free($this->_parser);
775
                        $this->_delete[$this->_parserid]['status'] = $response['status']['status-code'];
776
                        return $this->_delete[$this->_parserid];
777
 
778
                    } else {
779
                        print 'Missing Content-Type: text/xml header in response.<br>';
780
                    }
781
                    return false;
782
 
783
                default:
784
                    // collection or file was successfully deleted
785
                    $this->_delete['status'] = $response['status']['status-code'];
786
                    return $this->_delete;
787
 
788
 
789
                }
790
            }
791
 
792
    }
793
 
794
    /**
795
     * Public method ls
796
     *
797
     * Get's directory information from webdav server into flat a array using PROPFIND
798
     *
799
     * All filenames are UTF-8 encoded.
800
     * Have a look at _propfind_startElement what keys are used in array returned.
801
     * @param string path
802
     * @return array dirinfo, false on error
803
     */
804
    function ls($path) {
805
 
806
        if (trim($path) == '') {
807
            $this->_error_log('Missing a path in method ls');
808
            return false;
809
        }
810
        $this->_path = $this->translate_uri($path);
811
 
812
        $this->header_unset();
813
        $this->create_basic_request('PROPFIND');
814
        $this->header_add('Depth: 1');
815
        $this->header_add('Content-type: application/xml');
816
        // create profind xml request...
817
        $xml  = <<<EOD
818
<?xml version="1.0" encoding="utf-8"?>
819
<propfind xmlns="DAV:"><prop>
820
<getcontentlength xmlns="DAV:"/>
821
<getlastmodified xmlns="DAV:"/>
822
<executable xmlns="http://apache.org/dav/props/"/>
823
<resourcetype xmlns="DAV:"/>
824
<checked-in xmlns="DAV:"/>
825
<checked-out xmlns="DAV:"/>
826
</prop></propfind>
827
EOD;
828
        $this->header_add('Content-length: ' . strlen($xml));
829
        $this->send_request();
830
        $this->_error_log($xml);
831
        fputs($this->sock, $xml);
832
        $this->get_respond();
833
        $response = $this->process_respond();
834
        // validate the response ... (only basic validation)
835
        // check http-version
836
        if ($response['status']['http-version'] == 'HTTP/1.1' ||
837
            $response['status']['http-version'] == 'HTTP/1.0') {
838
                // seems to be http ... proceed
839
                // We expect a 207 Multi-Status status code
840
                // print 'http ok<br>';
841
                if (strcmp($response['status']['status-code'],'207') == 0 ) {
842
                    // ok so far
843
                    // next there should be a Content-Type: text/xml; charset="utf-8" header line
844
                    if (preg_match('#(application|text)/xml;\s?charset=[\'\"]?utf-8[\'\"]?#i', $response['header']['Content-Type'])) {
845
                        // ok let's get the content of the xml stuff
846
                        $this->_parser = xml_parser_create_ns('UTF-8');
847
                        $this->_parserid = $this->get_parser_id($this->_parser);
848
                        // forget old data...
849
                        unset($this->_ls[$this->_parserid]);
850
                        unset($this->_xmltree[$this->_parserid]);
851
                        xml_parser_set_option($this->_parser,XML_OPTION_SKIP_WHITE,0);
852
                        xml_parser_set_option($this->_parser,XML_OPTION_CASE_FOLDING,0);
853
                        // xml_parser_set_option($this->_parser,XML_OPTION_TARGET_ENCODING,'UTF-8');
1441 ariadna 854
                        xml_set_element_handler($this->_parser, [$this, "_propfind_startElement"], [$this, "_endElement"]);
855
                        xml_set_character_data_handler($this->_parser, [$this, "_propfind_cdata"]);
1 efrain 856
 
857
 
858
                        if (!xml_parse($this->_parser, $response['body'])) {
859
                            die(sprintf("XML error: %s at line %d",
860
                                xml_error_string(xml_get_error_code($this->_parser)),
861
                                xml_get_current_line_number($this->_parser)));
862
                        }
863
 
864
                        // Free resources
865
                        xml_parser_free($this->_parser);
866
                        $arr = $this->_ls[$this->_parserid];
867
                        return $arr;
868
                    } else {
869
                        $this->_error_log('Missing Content-Type: text/xml header in response!!');
870
                        return false;
871
                    }
872
                } else {
873
                    // return status code ...
874
                    return $response['status']['status-code'];
875
                }
876
            }
877
 
878
        // response was not http
879
        $this->_error_log('Ups in method ls: error in response from server');
880
        return false;
881
    }
882
 
883
 
884
    /**
885
     * Public method gpi
886
     *
887
     * Get's path information from webdav server for one element.
888
     *
889
     * @param string path
890
     * @return array dirinfo. false on error
891
     */
892
    function gpi($path) {
893
 
894
        // split path by last "/"
895
        $path = rtrim($path, "/");
896
        $item = basename($path);
897
        $dir  = dirname($path);
898
 
899
        $list = $this->ls($dir);
900
 
901
        // be sure it is an array
902
        if (is_array($list)) {
903
            foreach($list as $e) {
904
 
905
                $fullpath = urldecode($e['href']);
906
                $filename = basename($fullpath);
907
 
908
                if ($filename == $item && $filename != "" and $fullpath != $dir."/") {
909
                    return $e;
910
                }
911
            }
912
        }
913
        return false;
914
    }
915
 
916
    /**
917
     * Public method is_file
918
     *
919
     * Gathers whether a path points to a file or not.
920
     *
921
     * @param string path
922
     * @return bool true or false
923
     */
924
    function is_file($path) {
925
 
926
        $item = $this->gpi($path);
927
 
928
        if ($item === false) {
929
            return false;
930
        } else {
931
            return ($item['resourcetype'] != 'collection');
932
        }
933
    }
934
 
935
    /**
936
     * Public method is_dir
937
     *
938
     * Gather whether a path points to a directory
939
     * @param string path
940
     * return bool true or false
941
     */
942
    function is_dir($path) {
943
 
944
        // be sure path is utf-8
945
        $item = $this->gpi($path);
946
 
947
        if ($item === false) {
948
            return false;
949
        } else {
950
            return ($item['resourcetype'] == 'collection');
951
        }
952
    }
953
 
954
 
955
    /**
956
     * Public method mput
957
     *
958
     * Puts multiple files and/or directories onto a webdav server.
959
     *
960
     * Filenames should be allready UTF-8 encoded.
961
     * Param fileList must be in format array("localpath" => "destpath").
962
     *
963
     * @param array filelist
964
     * @return bool true on success. otherwise int status code on error
965
     */
966
    function mput($filelist) {
967
 
968
        $result = true;
969
 
970
        foreach ($filelist as $localpath => $destpath) {
971
 
972
            $localpath = rtrim($localpath, "/");
973
            $destpath  = rtrim($destpath, "/");
974
 
975
            // attempt to create target path
976
            if (is_dir($localpath)) {
977
                $pathparts = explode("/", $destpath."/ "); // add one level, last level will be created as dir
978
            } else {
979
                $pathparts = explode("/", $destpath);
980
            }
981
            $checkpath = "";
982
            for ($i=1; $i<sizeof($pathparts)-1; $i++) {
983
                $checkpath .= "/" . $pathparts[$i];
984
                if (!($this->is_dir($checkpath))) {
985
 
986
                    $result &= ($this->mkcol($checkpath) == 201 );
987
                }
988
            }
989
 
990
            if ($result) {
991
                // recurse directories
992
                if (is_dir($localpath)) {
993
                    if (!$dp = opendir($localpath)) {
994
                        $this->_error_log("Could not open localpath for reading");
995
                        return false;
996
                    }
997
                    $fl = array();
998
                    while($filename = readdir($dp)) {
999
                        if ((is_file($localpath."/".$filename) || is_dir($localpath."/".$filename)) && $filename!="." && $filename != "..") {
1000
                            $fl[$localpath."/".$filename] = $destpath."/".$filename;
1001
                        }
1002
                    }
1003
                    $result &= $this->mput($fl);
1004
                } else {
1005
                    $result &= ($this->put_file($destpath, $localpath) == 201);
1006
                }
1007
            }
1008
        }
1009
        return $result;
1010
    }
1011
 
1012
    /**
1013
     * Public method mget
1014
     *
1015
     * Gets multiple files and directories.
1016
     *
1017
     * FileList must be in format array("remotepath" => "localpath").
1018
     * Filenames are UTF-8 encoded.
1019
     *
1020
     * @param array filelist
1021
     * @return bool true on succes, other int status code on error
1022
     */
1023
    function mget($filelist) {
1024
 
1025
        $result = true;
1026
 
1027
        foreach ($filelist as $remotepath => $localpath) {
1028
 
1029
            $localpath   = rtrim($localpath, "/");
1030
            $remotepath  = rtrim($remotepath, "/");
1031
 
1032
            // attempt to create local path
1033
            if ($this->is_dir($remotepath)) {
1034
                $pathparts = explode("/", $localpath."/ "); // add one level, last level will be created as dir
1035
            } else {
1036
                $pathparts = explode("/", $localpath);
1037
            }
1038
            $checkpath = "";
1039
            for ($i=1; $i<sizeof($pathparts)-1; $i++) {
1040
                $checkpath .= "/" . $pathparts[$i];
1041
                if (!is_dir($checkpath)) {
1042
 
1043
                    $result &= mkdir($checkpath);
1044
                }
1045
            }
1046
 
1047
            if ($result) {
1048
                // recurse directories
1049
                if ($this->is_dir($remotepath)) {
1050
                    $list = $this->ls($remotepath);
1051
 
1052
                    $fl = array();
1053
                    foreach($list as $e) {
1054
                        $fullpath = urldecode($e['href']);
1055
                        $filename = basename($fullpath);
1056
                        if ($filename != '' and $fullpath != $remotepath . '/') {
1057
                            $fl[$remotepath."/".$filename] = $localpath."/".$filename;
1058
                        }
1059
                    }
1060
                    $result &= $this->mget($fl);
1061
                } else {
1062
                    $result &= ($this->get_file($remotepath, $localpath));
1063
                }
1064
            }
1065
        }
1066
        return $result;
1067
    }
1068
 
1069
    // --------------------------------------------------------------------------
1070
    // private xml callback and helper functions starting here
1071
    // --------------------------------------------------------------------------
1072
 
1073
 
1074
    /**
1075
     * Private method _endelement
1076
     *
1077
     * a generic endElement method  (used for all xml callbacks).
1078
     *
1079
     * @param resource parser, string name
1080
     * @access private
1081
     */
1082
 
1083
    private function _endElement($parser, $name) {
1084
        // end tag was found...
1085
        $parserid = $this->get_parser_id($parser);
1086
        $this->_xmltree[$parserid] = substr($this->_xmltree[$parserid],0, strlen($this->_xmltree[$parserid]) - (strlen($name) + 1));
1087
    }
1088
 
1089
    /**
1090
     * Private method _propfind_startElement
1091
     *
1092
     * Is needed by public method ls.
1093
     *
1094
     * Generic method will called by php xml_parse when a xml start element tag has been detected.
1095
     * The xml tree will translated into a flat php array for easier access.
1096
     * @param resource parser, string name, string attrs
1097
     * @access private
1098
     */
1099
    private function _propfind_startElement($parser, $name, $attrs) {
1100
        // lower XML Names... maybe break a RFC, don't know ...
1101
        $parserid = $this->get_parser_id($parser);
1102
 
1103
        $propname = strtolower($name);
1104
        if (!empty($this->_xmltree[$parserid])) {
1105
            $this->_xmltree[$parserid] .= $propname . '_';
1106
        } else {
1107
            $this->_xmltree[$parserid] = $propname . '_';
1108
        }
1109
 
1110
        // translate xml tree to a flat array ...
1111
        switch($this->_xmltree[$parserid]) {
1112
        case 'dav::multistatus_dav::response_':
1113
            // new element in mu
1114
            $this->_ls_ref =& $this->_ls[$parserid][];
1115
            break;
1116
        case 'dav::multistatus_dav::response_dav::href_':
1117
            $this->_ls_ref_cdata = &$this->_ls_ref['href'];
1118
            break;
1119
        case 'dav::multistatus_dav::response_dav::propstat_dav::prop_dav::creationdate_':
1120
            $this->_ls_ref_cdata = &$this->_ls_ref['creationdate'];
1121
            break;
1122
        case 'dav::multistatus_dav::response_dav::propstat_dav::prop_dav::getlastmodified_':
1123
            $this->_ls_ref_cdata = &$this->_ls_ref['lastmodified'];
1124
            break;
1125
        case 'dav::multistatus_dav::response_dav::propstat_dav::prop_dav::getcontenttype_':
1126
            $this->_ls_ref_cdata = &$this->_ls_ref['getcontenttype'];
1127
            break;
1128
        case 'dav::multistatus_dav::response_dav::propstat_dav::prop_dav::getcontentlength_':
1129
            $this->_ls_ref_cdata = &$this->_ls_ref['getcontentlength'];
1130
            break;
1131
        case 'dav::multistatus_dav::response_dav::propstat_dav::prop_dav::lockdiscovery_dav::activelock_dav::depth_':
1132
            $this->_ls_ref_cdata = &$this->_ls_ref['activelock_depth'];
1133
            break;
1134
        case 'dav::multistatus_dav::response_dav::propstat_dav::prop_dav::lockdiscovery_dav::activelock_dav::owner_dav::href_':
1135
            $this->_ls_ref_cdata = &$this->_ls_ref['activelock_owner'];
1136
            break;
1137
        case 'dav::multistatus_dav::response_dav::propstat_dav::prop_dav::lockdiscovery_dav::activelock_dav::owner_':
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::timeout_':
1141
            $this->_ls_ref_cdata = &$this->_ls_ref['activelock_timeout'];
1142
            break;
1143
        case 'dav::multistatus_dav::response_dav::propstat_dav::prop_dav::lockdiscovery_dav::activelock_dav::locktoken_dav::href_':
1144
            $this->_ls_ref_cdata = &$this->_ls_ref['activelock_token'];
1145
            break;
1146
        case 'dav::multistatus_dav::response_dav::propstat_dav::prop_dav::lockdiscovery_dav::activelock_dav::locktype_dav::write_':
1147
            $this->_ls_ref_cdata = &$this->_ls_ref['activelock_type'];
1148
            $this->_ls_ref_cdata = 'write';
1149
            $this->_ls_ref_cdata = &$this->_null;
1150
            break;
1151
        case 'dav::multistatus_dav::response_dav::propstat_dav::prop_dav::resourcetype_dav::collection_':
1152
            $this->_ls_ref_cdata = &$this->_ls_ref['resourcetype'];
1153
            $this->_ls_ref_cdata = 'collection';
1154
            $this->_ls_ref_cdata = &$this->_null;
1155
            break;
1156
        case 'dav::multistatus_dav::response_dav::propstat_dav::status_':
1157
            $this->_ls_ref_cdata = &$this->_ls_ref['status'];
1158
            break;
1159
 
1160
        default:
1161
            // handle unknown xml elements...
1162
            $this->_ls_ref_cdata = &$this->_ls_ref[$this->_xmltree[$parserid]];
1163
        }
1164
    }
1165
 
1166
    /**
1167
     * Private method _propfind_cData
1168
     *
1169
     * Is needed by public method ls.
1170
     *
1171
     * Will be called by php xml_set_character_data_handler() when xml data has to be handled.
1172
     * Stores data found into class var _ls_ref_cdata
1173
     * @param resource parser, string cdata
1174
     * @access private
1175
     */
1176
    private function _propfind_cData($parser, $cdata) {
1177
        if (trim($cdata) <> '') {
1178
            // cdata must be appended, because sometimes the php xml parser makes multiple calls
1179
            // to _propfind_cData before the xml end tag was reached...
1180
            $this->_ls_ref_cdata .= $cdata;
1181
        } else {
1182
            // do nothing
1183
        }
1184
    }
1185
 
1186
    /**
1187
     * Private method _delete_startElement
1188
     *
1189
     * Is used by public method delete.
1190
     *
1191
     * Will be called by php xml_parse.
1192
     * @param resource parser, string name, string attrs)
1193
     * @access private
1194
     */
1195
    private function _delete_startElement($parser, $name, $attrs) {
1196
        // lower XML Names... maybe break a RFC, don't know ...
1197
        $parserid = $this->get_parser_id($parser);
1198
        $propname = strtolower($name);
1199
        $this->_xmltree[$parserid] .= $propname . '_';
1200
 
1201
        // translate xml tree to a flat array ...
1202
        switch($this->_xmltree[$parserid]) {
1203
        case 'dav::multistatus_dav::response_':
1204
            // new element in mu
1205
            $this->_delete_ref =& $this->_delete[$parserid][];
1206
            break;
1207
        case 'dav::multistatus_dav::response_dav::href_':
1208
            $this->_delete_ref_cdata = &$this->_ls_ref['href'];
1209
            break;
1210
 
1211
        default:
1212
            // handle unknown xml elements...
1213
            $this->_delete_cdata = &$this->_delete_ref[$this->_xmltree[$parserid]];
1214
        }
1215
    }
1216
 
1217
 
1218
    /**
1219
     * Private method _delete_cData
1220
     *
1221
     * Is used by public method delete.
1222
     *
1223
     * Will be called by php xml_set_character_data_handler() when xml data has to be handled.
1224
     * Stores data found into class var _delete_ref_cdata
1225
     * @param resource parser, string cdata
1226
     * @access private
1227
     */
1228
    private function _delete_cData($parser, $cdata) {
1229
        if (trim($cdata) <> '') {
1230
            $this->_delete_ref_cdata .= $cdata;
1231
        } else {
1232
            // do nothing
1233
        }
1234
    }
1235
 
1236
 
1237
    /**
1238
     * Private method _lock_startElement
1239
     *
1240
     * Is needed by public method lock.
1241
     *
1242
     * Mmethod will called by php xml_parse when a xml start element tag has been detected.
1243
     * The xml tree will translated into a flat php array for easier access.
1244
     * @param resource parser, string name, string attrs
1245
     * @access private
1246
     */
1247
    private function _lock_startElement($parser, $name, $attrs) {
1248
        // lower XML Names... maybe break a RFC, don't know ...
1249
        $parserid = $this->get_parser_id($parser);
1250
        $propname = strtolower($name);
1251
        $this->_xmltree[$parserid] .= $propname . '_';
1252
 
1253
        // translate xml tree to a flat array ...
1254
        /*
1255
        dav::prop_dav::lockdiscovery_dav::activelock_dav::depth_=
1256
        dav::prop_dav::lockdiscovery_dav::activelock_dav::owner_dav::href_=
1257
        dav::prop_dav::lockdiscovery_dav::activelock_dav::timeout_=
1258
        dav::prop_dav::lockdiscovery_dav::activelock_dav::locktoken_dav::href_=
1259
         */
1260
        switch($this->_xmltree[$parserid]) {
1261
        case 'dav::prop_dav::lockdiscovery_dav::activelock_':
1262
            // new element
1263
            $this->_lock_ref =& $this->_lock[$parserid][];
1264
            break;
1265
        case 'dav::prop_dav::lockdiscovery_dav::activelock_dav::locktype_dav::write_':
1266
            $this->_lock_ref_cdata = &$this->_lock_ref['locktype'];
1267
            $this->_lock_cdata = 'write';
1268
            $this->_lock_cdata = &$this->_null;
1269
            break;
1270
        case 'dav::prop_dav::lockdiscovery_dav::activelock_dav::lockscope_dav::exclusive_':
1271
            $this->_lock_ref_cdata = &$this->_lock_ref['lockscope'];
1272
            $this->_lock_ref_cdata = 'exclusive';
1273
            $this->_lock_ref_cdata = &$this->_null;
1274
            break;
1275
        case 'dav::prop_dav::lockdiscovery_dav::activelock_dav::depth_':
1276
            $this->_lock_ref_cdata = &$this->_lock_ref['depth'];
1277
            break;
1278
        case 'dav::prop_dav::lockdiscovery_dav::activelock_dav::owner_dav::href_':
1279
            $this->_lock_ref_cdata = &$this->_lock_ref['owner'];
1280
            break;
1281
        case 'dav::prop_dav::lockdiscovery_dav::activelock_dav::timeout_':
1282
            $this->_lock_ref_cdata = &$this->_lock_ref['timeout'];
1283
            break;
1284
        case 'dav::prop_dav::lockdiscovery_dav::activelock_dav::locktoken_dav::href_':
1285
            $this->_lock_ref_cdata = &$this->_lock_ref['locktoken'];
1286
            break;
1287
        default:
1288
            // handle unknown xml elements...
1289
            $this->_lock_cdata = &$this->_lock_ref[$this->_xmltree[$parserid]];
1290
 
1291
        }
1292
    }
1293
 
1294
    /**
1295
     * Private method _lock_cData
1296
     *
1297
     * Is used by public method lock.
1298
     *
1299
     * Will be called by php xml_set_character_data_handler() when xml data has to be handled.
1300
     * Stores data found into class var _lock_ref_cdata
1301
     * @param resource parser, string cdata
1302
     * @access private
1303
     */
1304
    private function _lock_cData($parser, $cdata) {
1305
        $parserid = $this->get_parser_id($parser);
1306
        if (trim($cdata) <> '') {
1307
            // $this->_error_log(($this->_xmltree[$parserid]) . '='. htmlentities($cdata));
1308
            $this->_lock_ref_cdata .= $cdata;
1309
        } else {
1310
            // do nothing
1311
        }
1312
    }
1313
 
1314
 
1315
    /**
1316
     * Private method header_add
1317
     *
1318
     * extends class var array _req
1319
     * @param string string
1320
     * @access private
1321
     */
1322
    private function header_add($string) {
1323
        $this->_req[] = $string;
1324
    }
1325
 
1326
    /**
1327
     * Private method header_unset
1328
     *
1329
     * unsets class var array _req
1330
     * @access private
1331
     */
1332
 
1333
    private function header_unset() {
1334
        unset($this->_req);
1335
    }
1336
 
1337
    /**
1338
     * Private method create_basic_request
1339
     *
1340
     * creates by using private method header_add an general request header.
1341
     * @param string method
1342
     * @access private
1343
     */
1344
    private function create_basic_request($method) {
1345
        $this->header_add(sprintf('%s %s %s', $method, $this->_path, $this->_protocol));
1346
        $this->header_add(sprintf('Host: %s:%s', $this->_server, $this->_port));
1347
        //$request .= sprintf('Connection: Keep-Alive');
1348
        $this->header_add(sprintf('User-Agent: %s', $this->_user_agent));
1349
        $this->header_add('Connection: TE');
1350
        $this->header_add('TE: Trailers');
1351
        if ($this->_auth == 'basic') {
1352
            $this->header_add(sprintf('Authorization: Basic %s', base64_encode("$this->_user:$this->_pass")));
1353
        } else if ($this->_auth == 'digest') {
1354
            if ($signature = $this->digest_signature($method)){
1355
                $this->header_add($signature);
1356
            }
1357
        } else if ($this->_auth == 'bearer') {
1358
            $this->header_add(sprintf('Authorization: Bearer %s', $this->oauthtoken));
1359
        }
1360
    }
1361
 
1362
    /**
1363
     * Reads the header, stores the challenge information
1364
     *
1365
     * @return void
1366
     */
1367
    private function digest_auth() {
1368
 
1369
        $headers = array();
1370
        $headers[] = sprintf('%s %s %s', 'HEAD', $this->_path, $this->_protocol);
1371
        $headers[] = sprintf('Host: %s:%s', $this->_server, $this->_port);
1372
        $headers[] = sprintf('User-Agent: %s', $this->_user_agent);
1373
        $headers = implode("\r\n", $headers);
1374
        $headers .= "\r\n\r\n";
1375
        fputs($this->sock, $headers);
1376
 
1377
        // Reads the headers.
1378
        $i = 0;
1379
        $header = '';
1380
        do {
1381
            $header .= fread($this->sock, 1);
1382
            $i++;
1383
        } while (!preg_match('/\\r\\n\\r\\n$/', $header, $matches) && $i < $this->_maxheaderlenth);
1384
 
1385
        // Analyse the headers.
1386
        $digest = array();
1387
        $splitheaders = explode("\r\n", $header);
1388
        foreach ($splitheaders as $line) {
1389
            if (!preg_match('/^WWW-Authenticate: Digest/', $line)) {
1390
                continue;
1391
            }
1392
            $line = substr($line, strlen('WWW-Authenticate: Digest '));
1393
            $params = explode(',', $line);
1394
            foreach ($params as $param) {
1395
                list($key, $value) = explode('=', trim($param), 2);
1396
                $digest[$key] = trim($value, '"');
1397
            }
1398
            break;
1399
        }
1400
 
1401
        $this->_digestchallenge = $digest;
1402
    }
1403
 
1404
    /**
1405
     * Generates the digest signature
1406
     *
1407
     * @return string signature to add to the headers
1408
     * @access private
1409
     */
1410
    private function digest_signature($method) {
1411
        if (!$this->_digestchallenge) {
1412
            $this->digest_auth();
1413
        }
1414
 
1415
        $signature = array();
1416
        $signature['username'] = '"' . $this->_user . '"';
1417
        $signature['realm'] = '"' . $this->_digestchallenge['realm'] . '"';
1418
        $signature['nonce'] = '"' . $this->_digestchallenge['nonce'] . '"';
1419
        $signature['uri'] = '"' . $this->_path . '"';
1420
 
1421
        if (isset($this->_digestchallenge['algorithm']) && $this->_digestchallenge['algorithm'] != 'MD5') {
1422
            $this->_error_log('Algorithm other than MD5 are not supported');
1423
            return false;
1424
        }
1425
 
1426
        $a1 = $this->_user . ':' . $this->_digestchallenge['realm'] . ':' . $this->_pass;
1427
        $a2 = $method . ':' . $this->_path;
1428
 
1429
        if (!isset($this->_digestchallenge['qop'])) {
1430
            $signature['response'] = '"' . md5(md5($a1) . ':' . $this->_digestchallenge['nonce'] . ':' . md5($a2)) . '"';
1431
        } else {
1432
            // Assume QOP is auth
1433
            if (empty($this->_cnonce)) {
1434
                $this->_cnonce = random_string();
1435
                $this->_nc = 0;
1436
            }
1437
            $this->_nc++;
1438
            $nc = sprintf('%08d', $this->_nc);
1439
            $signature['cnonce'] = '"' . $this->_cnonce . '"';
1440
            $signature['nc'] = '"' . $nc . '"';
1441
            $signature['qop'] = '"' . $this->_digestchallenge['qop'] . '"';
1442
            $signature['response'] = '"' . md5(md5($a1) . ':' . $this->_digestchallenge['nonce'] . ':' .
1443
                    $nc . ':' . $this->_cnonce . ':' . $this->_digestchallenge['qop'] . ':' . md5($a2)) . '"';
1444
        }
1445
 
1446
        $response = array();
1447
        foreach ($signature as $key => $value) {
1448
            $response[] = "$key=$value";
1449
        }
1450
        return 'Authorization: Digest ' . implode(', ', $response);
1451
    }
1452
 
1453
    /**
1454
     * Private method send_request
1455
     *
1456
     * Sends a ready formed http/webdav request to webdav server.
1457
     *
1458
     * @access private
1459
     */
1460
    private function send_request() {
1461
        // check if stream is declared to be open
1462
        // only logical check we are not sure if socket is really still open ...
1463
        if ($this->_connection_closed) {
1464
            // reopen it
1465
            // be sure to close the open socket.
1466
            $this->close();
1467
            $this->reopen();
1468
        }
1469
 
1470
        // convert array to string
1471
        $buffer = implode("\r\n", $this->_req);
1472
        $buffer .= "\r\n\r\n";
1473
        $this->_error_log($buffer);
1474
        fputs($this->sock, $buffer);
1475
    }
1476
 
1477
    /**
1478
     * Private method get_respond
1479
     *
1480
     * Reads the response from the webdav server.
1481
     *
1482
     * Stores data into class vars _header for the header data and
1483
     * _body for the rest of the response.
1484
     * This routine is the weakest part of this class, because it very depends how php does handle a socket stream.
1485
     * If the stream is blocked for some reason php is blocked as well.
1486
     * @access private
1487
     * @param resource $fp optional the file handle to write the body content to (stored internally in the '_body' if not set)
1488
     */
1489
    private function get_respond($fp = null) {
1490
        $this->_error_log('get_respond()');
1491
        // init vars (good coding style ;-)
1492
        $buffer = '';
1493
        $header = '';
1494
        // attention: do not make max_chunk_size to big....
1495
        $max_chunk_size = 8192;
1496
        // be sure we got a open ressource
1497
        if (! $this->sock) {
1498
            $this->_error_log('socket is not open. Can not process response');
1499
            return false;
1500
        }
1501
 
1502
        // following code maybe helps to improve socket behaviour ... more testing needed
1503
        // disabled at the moment ...
1504
        // socket_set_timeout($this->sock,1 );
1505
        // $socket_state = socket_get_status($this->sock);
1506
 
1507
        // read stream one byte by another until http header ends
1508
        $i = 0;
1509
        $matches = array();
1510
        do {
1511
            $header.=fread($this->sock, 1);
1512
            $i++;
1513
        } while (!preg_match('/\\r\\n\\r\\n$/',$header, $matches) && $i < $this->_maxheaderlenth);
1514
 
1515
        $this->_error_log($header);
1516
 
1517
        if (preg_match('/Connection: close\\r\\n/', $header)) {
1518
            // This says that the server will close connection at the end of this stream.
1519
            // Therefore we need to reopen the socket, before are sending the next request...
1520
            $this->_error_log('Connection: close found');
1521
            $this->_connection_closed = true;
1522
        } else if (preg_match('@^HTTP/1\.(1|0) 401 @', $header)) {
1523
            $this->_error_log('The server requires an authentication');
1524
        }
1525
 
1526
        // check how to get the data on socket stream
1527
        // chunked or content-length (HTTP/1.1) or
1528
        // one block until feof is received (HTTP/1.0)
1529
        switch(true) {
1530
        case (preg_match('/Transfer\\-Encoding:\\s+chunked\\r\\n/',$header)):
1531
            $this->_error_log('Getting HTTP/1.1 chunked data...');
1532
            do {
1533
                $byte = '';
1534
                $chunk_size='';
1535
                do {
1536
                    $chunk_size.=$byte;
1537
                    $byte=fread($this->sock,1);
1538
                    // check what happens while reading, because I do not really understand how php reads the socketstream...
1539
                    // but so far - it seems to work here - tested with php v4.3.1 on apache 1.3.27 and Debian Linux 3.0 ...
1540
                    if (strlen($byte) == 0) {
1541
                        $this->_error_log('get_respond: warning --> read zero bytes');
1542
                    }
1543
                } while ($byte!="\r" and strlen($byte)>0);      // till we match the Carriage Return
1544
                fread($this->sock, 1);                           // also drop off the Line Feed
1545
                $chunk_size=hexdec($chunk_size);                // convert to a number in decimal system
1546
                if ($chunk_size > 0) {
1547
                    $read = 0;
1548
                    // Reading the chunk in one bite is not secure, we read it byte by byte.
1549
                    while ($read < $chunk_size) {
1550
                        $chunk = fread($this->sock, 1);
1551
                        self::update_file_or_buffer($chunk, $fp, $buffer);
1552
                        $read++;
1553
                    }
1554
                }
1555
                fread($this->sock, 2);                            // ditch the CRLF that trails the chunk
1556
            } while ($chunk_size);                            // till we reach the 0 length chunk (end marker)
1557
            break;
1558
 
1559
            // check for a specified content-length
1560
        case preg_match('/Content\\-Length:\\s+([0-9]*)\\r\\n/',$header,$matches):
1561
            $this->_error_log('Getting data using Content-Length '. $matches[1]);
1562
 
1563
            // check if we the content data size is small enough to get it as one block
1564
            if ($matches[1] <= $max_chunk_size ) {
1565
                // only read something if Content-Length is bigger than 0
1566
                if ($matches[1] > 0 ) {
1567
                    $chunk = fread($this->sock, $matches[1]);
1568
                    $loadsize = strlen($chunk);
1569
                    //did we realy get the full length?
1570
                    if ($loadsize < $matches[1]) {
1571
                        $max_chunk_size = $loadsize;
1572
                        do {
1573
                            $mod = $max_chunk_size % ($matches[1] - strlen($chunk));
1574
                            $chunk_size = ($mod == $max_chunk_size ? $max_chunk_size : $matches[1] - strlen($chunk));
1575
                            $chunk .= fread($this->sock, $chunk_size);
1576
                            $this->_error_log('mod: ' . $mod . ' chunk: ' . $chunk_size . ' total: ' . strlen($chunk));
1577
                        } while (strlen($chunk) < $matches[1]);
1578
                    }
1579
                    self::update_file_or_buffer($chunk, $fp, $buffer);
1580
                    break;
1581
                } else {
1582
                    $buffer = '';
1583
                    break;
1584
                }
1585
            }
1586
 
1587
            // data is to big to handle it as one. Get it chunk per chunk...
1588
            //trying to get the full length of max_chunk_size
1589
            $chunk = fread($this->sock, $max_chunk_size);
1590
            $loadsize = strlen($chunk);
1591
            self::update_file_or_buffer($chunk, $fp, $buffer);
1592
            if ($loadsize < $max_chunk_size) {
1593
                $max_chunk_size = $loadsize;
1594
            }
1595
            do {
1596
                $mod = $max_chunk_size % ($matches[1] - $loadsize);
1597
                $chunk_size = ($mod == $max_chunk_size ? $max_chunk_size : $matches[1] - $loadsize);
1598
                $chunk = fread($this->sock, $chunk_size);
1599
                self::update_file_or_buffer($chunk, $fp, $buffer);
1600
                $loadsize += strlen($chunk);
1601
                $this->_error_log('mod: ' . $mod . ' chunk: ' . $chunk_size . ' total: ' . $loadsize);
1602
            } while ($matches[1] > $loadsize);
1603
            break;
1604
 
1605
            // check for 204 No Content
1606
            // 204 responds have no body.
1607
            // Therefore we do not need to read any data from socket stream.
1608
        case preg_match('/HTTP\/1\.1\ 204/',$header):
1609
            // nothing to do, just proceed
1610
            $this->_error_log('204 No Content found. No further data to read..');
1611
            break;
1612
        default:
1613
            // just get the data until foef appears...
1614
            $this->_error_log('reading until feof...' . $header);
1615
            socket_set_timeout($this->sock, 0, 0);
1616
            while (!feof($this->sock)) {
1617
                $chunk = fread($this->sock, 4096);
1618
                self::update_file_or_buffer($chunk, $fp, $buffer);
1619
            }
1620
            // renew the socket timeout...does it do something ???? Is it needed. More debugging needed...
1621
            socket_set_timeout($this->sock, $this->_socket_timeout, 0);
1622
        }
1623
 
1624
        $this->_header = $header;
1625
        $this->_body = $buffer;
1626
        // $this->_buffer = $header . "\r\n\r\n" . $buffer;
1627
        $this->_error_log($this->_header);
1628
        $this->_error_log($this->_body);
1629
 
1630
    }
1631
 
1632
    /**
1633
     * Write the chunk to the file if $fp is set, otherwise append the data to the buffer
1634
     * @param string $chunk the data to add
1635
     * @param resource $fp the file handle to write to (or null)
1636
     * @param string &$buffer the buffer to append to (if $fp is null)
1637
     */
1638
    private static function update_file_or_buffer($chunk, $fp, &$buffer) {
1639
        if ($fp) {
1640
            fwrite($fp, $chunk);
1641
        } else {
1642
            $buffer .= $chunk;
1643
        }
1644
    }
1645
 
1646
    /**
1647
     * Private method process_respond
1648
     *
1649
     * Processes the webdav server respond and detects its components (header, body).
1650
     * and returns data array structure.
1651
     * @return array ret_struct
1652
     * @access private
1653
     */
1654
    private function process_respond() {
1655
        $lines = explode("\r\n", $this->_header);
1656
        $header_done = false;
1657
        // $this->_error_log($this->_buffer);
1658
        // First line should be a HTTP status line (see http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6)
1659
        // Format is: HTTP-Version SP Status-Code SP Reason-Phrase CRLF
1660
        list($ret_struct['status']['http-version'],
1661
            $ret_struct['status']['status-code'],
1662
            $ret_struct['status']['reason-phrase']) = explode(' ', $lines[0],3);
1663
 
1664
        // print "HTTP Version: '$http_version' Status-Code: '$status_code' Reason Phrase: '$reason_phrase'<br>";
1665
        // get the response header fields
1666
        // See http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6
1667
        for($i=1; $i<count($lines); $i++) {
1668
            if (rtrim($lines[$i]) == '' && !$header_done) {
1669
                $header_done = true;
1670
                // print "--- response header end ---<br>";
1671
 
1672
            }
1673
            if (!$header_done ) {
1674
                // store all found headers in array ...
1675
                list($fieldname, $fieldvalue) = explode(':', $lines[$i]);
1676
                // check if this header was allready set (apache 2.0 webdav module does this....).
1677
                // If so we add the the value to the end the fieldvalue, separated by comma...
1678
                if (empty($ret_struct['header'])) {
1679
                    $ret_struct['header'] = array();
1680
                }
1681
                if (empty($ret_struct['header'][$fieldname])) {
1682
                    $ret_struct['header'][$fieldname] = trim($fieldvalue);
1683
                } else {
1684
                    $ret_struct['header'][$fieldname] .= ',' . trim($fieldvalue);
1685
                }
1686
            }
1687
        }
1688
        // print 'string len of response_body:'. strlen($response_body);
1689
        // print '[' . htmlentities($response_body) . ']';
1690
        $ret_struct['body'] = $this->_body;
1691
        $this->_error_log('process_respond: ' . var_export($ret_struct,true));
1692
        return $ret_struct;
1693
 
1694
    }
1695
 
1696
    /**
1697
     * Private method reopen
1698
     *
1699
     * Reopens a socket, if 'connection: closed'-header was received from server.
1700
     *
1701
     * Uses public method open.
1702
     * @access private
1703
     */
1704
    private function reopen() {
1705
        // let's try to reopen a socket
1706
        $this->_error_log('reopen a socket connection');
1707
        return $this->open();
1708
    }
1709
 
1710
 
1711
    /**
1712
     * Private method translate_uri
1713
     *
1714
     * translates an uri to raw url encoded string.
1715
     * Removes any html entity in uri
1716
     * @param string uri
1717
     * @return string translated_uri
1718
     * @access private
1719
     */
1720
    private function translate_uri($uri) {
1721
        // remove all html entities...
1722
        $native_path = html_entity_decode($uri, ENT_COMPAT);
1723
        $parts = explode('/', $native_path);
1724
        for ($i = 0; $i < count($parts); $i++) {
1725
            // check if part is allready utf8
1726
            if (iconv('UTF-8', 'UTF-8', $parts[$i]) == $parts[$i]) {
1727
                $parts[$i] = rawurlencode($parts[$i]);
1728
            } else {
1729
                $parts[$i] = rawurlencode(\core_text::convert($parts[$i], 'ISO-8859-1', 'UTF-8'));
1730
            }
1731
        }
1732
        return implode('/', $parts);
1733
    }
1734
 
1735
    /**
1736
     * Private method utf_decode_path
1737
     *
1738
     * decodes a UTF-8 encoded string
1739
     * @return string decodedstring
1740
     * @access private
1741
     */
1742
    private function utf_decode_path($path) {
1743
        $fullpath = $path;
1744
        if (iconv('UTF-8', 'UTF-8', $fullpath) == $fullpath) {
1745
            $this->_error_log("filename is utf-8. Needs conversion...");
1746
            $fullpath = \core_text::convert($fullpath, 'UTF-8', 'ISO-8859-1');
1747
        }
1748
        return $fullpath;
1749
    }
1750
 
1751
    /**
1752
     * Private method _error_log
1753
     *
1754
     * a simple php error_log wrapper.
1755
     * @param string err_string
1756
     * @access private
1757
     */
1758
    private function _error_log($err_string) {
1759
        if ($this->_debug) {
1760
            error_log($err_string);
1761
        }
1762
    }
1763
 
1764
    /**
1765
     * Helper method to get the parser id for both PHP 7 and 8.
1766
     *
1767
     * @param resource|object $parser
1768
     * @return int
1769
     */
1770
    private function get_parser_id($parser): int {
1771
        if (is_object($parser)) {
1772
            return spl_object_id($parser);
1773
        } else {
1774
            return (int) $parser;
1775
        }
1776
    }
1777
}