HTTP_WebDAV_Server   F
last analyzed

Complexity

Total Complexity 490

Size/Duplication

Total Lines 2863
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 1127
dl 0
loc 2863
rs 1.092
c 2
b 0
f 0
wmc 490

41 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 1
A http_OPTIONS() 0 26 3
F http_PROPFIND() 0 113 26
F ServeRequest() 0 122 22
A _unslashify() 0 8 2
A http_COPY() 0 5 1
B lockdiscovery() 0 56 7
A _new_locktoken() 0 3 1
F _hierarchical_prop_encode() 0 67 26
A http_UNLOCK() 0 18 2
B http_HEAD() 0 34 9
D http_PROPPATCH() 0 45 19
A _check_uri_condition() 0 13 2
B _copymove() 0 54 9
A _get_ranges() 0 17 5
A mkprop() 0 17 3
A bytes() 0 9 4
F http_POST() 0 168 36
A http_MOVE() 0 7 2
A _prop_encode() 0 15 5
A _new_uuid() 0 22 2
A _urldecode() 0 3 1
A _mergePaths() 0 8 2
C _if_header_parser() 0 73 14
B _check_if_header_conditions() 0 37 9
B http_ACL() 0 31 9
A _check_auth() 0 15 3
B _check_lock_status() 0 17 8
A _multipart_byterange_header() 0 24 4
F multistatus_responses() 0 378 118
A http_status() 0 13 2
A _allow() 0 28 6
F http_GET() 0 124 36
A _urlencode() 0 5 1
A _slashify() 0 8 2
A use_compression() 0 13 3
F http_LOCK() 0 104 32
F http_PUT() 0 188 40
A http_MKCOL() 0 9 1
A http_DELETE() 0 29 5
B _if_header_lexer() 0 45 7

How to fix   Complexity   

Complex Class

Complex classes like HTTP_WebDAV_Server often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use HTTP_WebDAV_Server, and based on these observations, apply Extract Interface, too.

1
<?php // $Id$
2
/*
3
   +----------------------------------------------------------------------+
4
   | Copyright (c) 2002-2007 Christian Stocker, Hartmut Holzgraefe        |
5
   | All rights reserved                                                  |
6
   |                                                                      |
7
   | Redistribution and use in source and binary forms, with or without   |
8
   | modification, are permitted provided that the following conditions   |
9
   | are met:                                                             |
10
   |                                                                      |
11
   | 1. Redistributions of source code must retain the above copyright    |
12
   |    notice, this list of conditions and the following disclaimer.     |
13
   | 2. Redistributions in binary form must reproduce the above copyright |
14
   |    notice, this list of conditions and the following disclaimer in   |
15
   |    the documentation and/or other materials provided with the        |
16
   |    distribution.                                                     |
17
   | 3. The names of the authors may not be used to endorse or promote    |
18
   |    products derived from this software without specific prior        |
19
   |    written permission.                                               |
20
   |                                                                      |
21
   | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS  |
22
   | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT    |
23
   | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS    |
24
   | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE       |
25
   | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,  |
26
   | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, |
27
   | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;     |
28
   | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER     |
29
   | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT   |
30
   | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN    |
31
   | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE      |
32
   | POSSIBILITY OF SUCH DAMAGE.                                          |
33
   +----------------------------------------------------------------------+
34
*/
35
36
require_once __DIR__."/Tools/_parse_propfind.php";
37
require_once __DIR__."/Tools/_parse_proppatch.php";
38
require_once __DIR__."/Tools/_parse_lockinfo.php";
39
40
/**
41
 * Virtual base class for implementing WebDAV servers
42
 *
43
 * WebDAV server base class, needs to be extended to do useful work
44
 *
45
 * @package HTTP_WebDAV_Server
46
 * @author  Hartmut Holzgraefe <[email protected]>
47
 * @version @package_version@
48
 */
49
class HTTP_WebDAV_Server
50
{
51
    // {{{ Member Variables
52
53
    /**
54
     * complete URI for this request
55
     *
56
     * @var string
57
     */
58
    var $uri;
59
60
    /**
61
     * base URI for this request
62
     *
63
     * @var string
64
     */
65
    var $base_uri;
66
67
    /**
68
     * Set if client requires <D:href> to be a url (true) or a path (false).
69
     * RFC 4918 allows both: http://www.webdav.org/specs/rfc4918.html#ELEMENT_href
70
     * But some clients can NOT deal with one or the other!
71
     *
72
     * @var boolean
73
     */
74
    var $client_require_href_as_url;
75
76
     /**
77
     * Set if client requires or does not allow namespace redundacy.
78
     * The XML Namespace specification does allow both
79
     * But some clients can NOT deal with one or the other!
80
     *
81
     * $this->crrnd === false:
82
     * <D:multistatus xmlns:D="DAV:">
83
     *  <D:response xmlns:ns0="urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/">
84
     *   <D:href>/egroupware/webdav.php/home/ralf/</D:href>
85
     *    <D:propstat>
86
     *     <D:prop>
87
     *      <D:resourcetype><D:collection /></D:resourcetype>
88
     *     </D:prop>
89
     *    <D:status>HTTP/1.1 200 OK</D:status>
90
     *   </D:propstat>
91
     *  </D:response>
92
     * </D:multistatus>
93
     *
94
     * $this->crrnd === true:
95
     * <multistatus xmlns="DAV:">
96
     *  <response xmlns:ns0="urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/">
97
     *   <href>/egroupware/webdav.php/home/ralf/</href>
98
     *    <propstat>
99
     *     <prop>
100
     *      <resourcetype><collection /></resourcetype>
101
     *     </prop>
102
     *    <status>HTTP/1.1 200 OK</status>
103
     *   </propstat>
104
     *  </response>
105
     * </multistatus>
106
     *
107
     * @var boolean (client_refuses_redundand_namespace_declarations)
108
     */
109
    var $crrnd = false;
110
111
    /**
112
113
114
    /**
115
     * URI path for this request
116
     *
117
     * @var string
118
     */
119
    var $path;
120
121
    /**
122
     * Realm string to be used in authentification popups
123
     *
124
     * @var string
125
     */
126
    var $http_auth_realm = "PHP WebDAV";
127
128
    /**
129
     * String to be used in "X-Dav-Powered-By" header
130
     *
131
     * @var string
132
     */
133
    var $dav_powered_by = "";
134
135
    /**
136
     * Remember parsed If: (RFC2518/9.4) header conditions
137
     *
138
     * @var array
139
     */
140
    var $_if_header_uris = array();
141
142
    /**
143
     * HTTP response status/message
144
     *
145
     * @var string
146
     */
147
    var $_http_status = "200 OK";
148
149
    /**
150
     * encoding of property values passed in
151
     *
152
     * @var string
153
     */
154
    var $_prop_encoding = "utf-8";
155
156
    /**
157
     * Copy of $_SERVER superglobal array
158
     *
159
     * Derived classes may extend the constructor to
160
     * modify its contents
161
     *
162
     * @var array
163
     */
164
    var $_SERVER;
165
166
    // }}}
167
168
    // {{{ Constructor
169
170
    /**
171
     * Constructor
172
     *
173
     * @param void
174
     */
175
    function __construct()
176
    {
177
        // PHP messages destroy XML output -> switch them off
178
        ini_set("display_errors", 0);
179
180
        // copy $_SERVER variables to local _SERVER array
181
        // so that derived classes can simply modify these
182
        $this->_SERVER = $_SERVER;
183
    }
184
185
    // }}}
186
187
    // {{{ ServeRequest()
188
    /**
189
     * Serve WebDAV HTTP request
190
     *
191
     * dispatch WebDAV HTTP request to the apropriate method handler
192
     *
193
     * @param  $prefix =null prefix filesystem path with given path, eg. "/webdav" for owncloud 4.5 remote.php
0 ignored issues
show
Documentation Bug introduced by
The doc comment =null at position 0 could not be parsed: Unknown type name '=null' at position 0 in =null.
Loading history...
194
     * @return void
195
     */
196
    function ServeRequest($prefix=null)
197
    {
198
        // prevent warning in litmus check 'delete_fragment'
199
        if (strstr($this->_SERVER["REQUEST_URI"], '#')) {
200
            $this->http_status("400 Bad Request");
201
            return;
202
        }
203
204
        // default is currently to use just the path, extending class can set $this->client_require_href_as_url depending on user-agent
205
        if ($this->client_require_href_as_url)
206
        {
207
	        // default uri is the complete request uri
208
	        $uri = (@$this->_SERVER["HTTPS"] === "on" ? "https:" : "http:") . '//'.$this->_SERVER['HTTP_HOST'];
209
        }
210
        $uri .= $this->_SERVER["SCRIPT_NAME"];
211
212
        // WebDAV has no concept of a query string and clients (including cadaver)
213
        // seem to pass '?' unencoded, so we need to extract the path info out
214
        // of the request URI ourselves
215
        // if request URI contains a full url, remove schema and domain
216
		$matches = null;
217
        if (preg_match('|^https?://[^/]+(/.*)$|', $path_info=$this->_SERVER["REQUEST_URI"], $matches))
218
        {
219
        	$path_info = $matches[1];
220
        }
221
        $path_info_raw = substr($path_info, strlen($this->_SERVER["SCRIPT_NAME"]));
222
223
        // just in case the path came in empty ...
224
        if (empty($path_info_raw)) {
225
            $path_info_raw = "/";
226
        }
227
228
        $path_info = self::_urldecode($path_info_raw);
229
230
        if ($prefix && strpos($path_info, $prefix) === 0)
231
        {
232
        	$uri .= $prefix;
233
        	list(,$path_info) = explode($prefix, $path_info, 2);
234
        }
235
236
        $this->base_uri = $uri;
237
        $this->uri      = $uri . $path_info;
238
239
        // set path
240
        // Vfs stores %, # and ? urlencoded, we do the encoding here on a central place
241
        $this->path = strtr($path_info,array(
242
        	'%' => '%25',
243
        	'#' => '%23',
244
        	'?' => '%3F',
245
        ));
246
        if (!strlen($this->path)) {
247
            if ($this->_SERVER["REQUEST_METHOD"] == "GET") {
248
                // redirect clients that try to GET a collection
249
                // WebDAV clients should never try this while
250
                // regular HTTP clients might ...
251
                header("Location: ".$this->base_uri."/");
252
                return;
253
            } else {
254
                // if a WebDAV client didn't give a path we just assume '/'
255
                $this->path = "/";
256
            }
257
        }
258
259
        if (ini_get("magic_quotes_gpc")) {
260
            $this->path = stripslashes($this->path);
261
        }
262
263
264
        // identify ourselves
265
        if (empty($this->dav_powered_by)) {
266
            header("X-Dav-Powered-By: PHP class: ".get_class($this));
267
        } else {
268
            header("X-Dav-Powered-By: ".$this->dav_powered_by);
269
        }
270
271
        // check authentication
272
        // for the motivation for not checking OPTIONS requests on / see
273
        // http://pear.php.net/bugs/bug.php?id=5363
274
        if ( (   !(($this->_SERVER['REQUEST_METHOD'] == 'OPTIONS') && ($this->path == "/")))
275
             && (!$this->_check_auth())) {
276
            // RFC2518 says we must use Digest instead of Basic
277
            // but Microsoft Clients do not support Digest
278
            // and we don't support NTLM and Kerberos
279
            // so we are stuck with Basic here
280
            header('WWW-Authenticate: Basic realm="'.($this->http_auth_realm).'"');
281
282
            // Windows seems to require this being the last header sent
283
            // (changed according to PECL bug #3138)
284
            $this->http_status('401 Unauthorized');
285
286
            return;
287
        }
288
289
        // check
290
        if (! $this->_check_if_header_conditions()) {
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->_check_if_header_conditions() targeting HTTP_WebDAV_Server::_check_if_header_conditions() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
291
            return;
292
        }
293
294
        // detect requested method names
295
        $method  = strtolower($this->_SERVER["REQUEST_METHOD"]);
296
        $wrapper = "http_".$method;
297
298
        // activate HEAD emulation by GET if no HEAD method found
299
        if ($method == "head" && !method_exists($this, "head")) {
300
            $method = "get";
301
        }
302
303
        if (method_exists($this, $wrapper) && ($method == "options" || method_exists($this, $method))) {
304
            $this->$wrapper();  // call method by name
305
        } else { // method not found/implemented
306
            if ($this->_SERVER["REQUEST_METHOD"] == "LOCK") {
307
            	$error = '412 Precondition failed';
308
            } else {
309
                $error = '405 Method not allowed';
310
                header("Allow: ".join(", ", $this->_allow()));  // tell client what's allowed
311
            }
312
            $this->http_status($error);
313
            echo "<html><head><title>Error $error</title></head>\n";
314
            echo "<body><h1>$error</h1>\n";
315
            echo "The requested could not by handled by this server.\n";
316
            echo '(URI ' . $this->_SERVER['REQUEST_URI'] . ")<br>\n<br>\n";
317
            echo "</body></html>\n";
318
        }
319
    }
320
321
    // }}}
322
323
    // {{{ abstract WebDAV methods
324
325
    // {{{ GET()
326
    /**
327
     * GET implementation
328
     *
329
     * overload this method to retrieve resources from your server
330
     * <br>
331
     *
332
     *
333
     * @abstract
334
     * @param array &$params Array of input and output parameters
335
     * <br><b>input</b><ul>
336
     * <li> path -
337
     * </ul>
338
     * <br><b>output</b><ul>
339
     * <li> size -
340
     * </ul>
341
     * @returns int HTTP-Statuscode
342
     */
343
344
    /* abstract
345
     function GET(&$params)
346
     {
347
     // dummy entry for PHPDoc
348
     }
349
    */
350
351
    // }}}
352
353
    // {{{ PUT()
354
    /**
355
     * PUT implementation
356
     *
357
     * PUT implementation
358
     *
359
     * @abstract
360
     * @param array &$params
361
     * @returns int HTTP-Statuscode
362
     */
363
364
    /* abstract
365
     function PUT()
366
     {
367
     // dummy entry for PHPDoc
368
     }
369
    */
370
371
    // }}}
372
373
    // {{{ COPY()
374
375
    /**
376
     * COPY implementation
377
     *
378
     * COPY implementation
379
     *
380
     * @abstract
381
     * @param array &$params
382
     * @returns int HTTP-Statuscode
383
     */
384
385
    /* abstract
386
     function COPY()
387
     {
388
     // dummy entry for PHPDoc
389
     }
390
    */
391
392
    // }}}
393
394
    // {{{ MOVE()
395
396
    /**
397
     * MOVE implementation
398
     *
399
     * MOVE implementation
400
     *
401
     * @abstract
402
     * @param array &$params
403
     * @returns int HTTP-Statuscode
404
     */
405
406
    /* abstract
407
     function MOVE()
408
     {
409
     // dummy entry for PHPDoc
410
     }
411
    */
412
413
    // }}}
414
415
    // {{{ DELETE()
416
417
    /**
418
     * DELETE implementation
419
     *
420
     * DELETE implementation
421
     *
422
     * @abstract
423
     * @param array &$params
424
     * @returns int HTTP-Statuscode
425
     */
426
427
    /* abstract
428
     function DELETE()
429
     {
430
     // dummy entry for PHPDoc
431
     }
432
    */
433
    // }}}
434
435
    // {{{ PROPFIND()
436
437
    /**
438
     * PROPFIND implementation
439
     *
440
     * PROPFIND implementation
441
     *
442
     * @abstract
443
     * @param array &$params
444
     * @returns int HTTP-Statuscode
445
     */
446
447
    /* abstract
448
     function PROPFIND()
449
     {
450
     // dummy entry for PHPDoc
451
     }
452
    */
453
454
    // }}}
455
456
    // {{{ PROPPATCH()
457
458
    /**
459
     * PROPPATCH implementation
460
     *
461
     * PROPPATCH implementation
462
     *
463
     * @abstract
464
     * @param array &$params
465
     * @returns int HTTP-Statuscode
466
     */
467
468
    /* abstract
469
     function PROPPATCH()
470
     {
471
     // dummy entry for PHPDoc
472
     }
473
    */
474
    // }}}
475
476
    // {{{ LOCK()
477
478
    /**
479
     * LOCK implementation
480
     *
481
     * LOCK implementation
482
     *
483
     * @abstract
484
     * @param array &$params
485
     * @returns int HTTP-Statuscode
486
     */
487
488
    /* abstract
489
     function LOCK()
490
     {
491
     // dummy entry for PHPDoc
492
     }
493
    */
494
    // }}}
495
496
    // {{{ UNLOCK()
497
498
    /**
499
     * UNLOCK implementation
500
     *
501
     * UNLOCK implementation
502
     *
503
     * @abstract
504
     * @param array &$params
505
     * @returns int HTTP-Statuscode
506
     */
507
508
    /* abstract
509
     function UNLOCK()
510
     {
511
     // dummy entry for PHPDoc
512
     }
513
    */
514
    // }}}
515
516
    // {{{ ACL()
517
518
    /**
519
     * ACL implementation
520
     *
521
     * ACL implementation
522
     *
523
     * @abstract
524
     * @param array &$params
525
     * @returns int HTTP-Statuscode
526
     */
527
528
    /* abstract
529
     function ACL()
530
     {
531
     // dummy entry for PHPDoc
532
     }
533
    */
534
    // }}}
535
536
    // }}}
537
538
    // {{{ other abstract methods
539
540
    // {{{ check_auth()
541
542
    /**
543
     * check authentication
544
     *
545
     * overload this method to retrieve and confirm authentication information
546
     *
547
     * @abstract
548
     * @param string type Authentication type, e.g. "basic" or "digest"
549
     * @param string username Transmitted username
550
     * @param string passwort Transmitted password
551
     * @returns bool Authentication status
552
     */
553
554
    /* abstract
555
     function checkAuth($type, $username, $password)
556
     {
557
     // dummy entry for PHPDoc
558
     }
559
    */
560
561
    // }}}
562
563
    // {{{ checklock()
564
565
    /**
566
     * check lock status for a resource
567
     *
568
     * overload this method to return shared and exclusive locks
569
     * active for this resource
570
     *
571
     * @abstract
572
     * @param string resource Resource path to check
573
     * @returns array An array of lock entries each consisting
574
     *                of 'type' ('shared'/'exclusive'), 'token' and 'timeout'
575
     */
576
577
    /* abstract
578
     function checklock($resource)
579
     {
580
     // dummy entry for PHPDoc
581
     }
582
    */
583
584
    // }}}
585
586
    // }}}
587
588
    // {{{ WebDAV HTTP method wrappers
589
590
    // {{{ http_OPTIONS()
591
592
    /**
593
     * OPTIONS method handler
594
     *
595
     * The OPTIONS method handler creates a valid OPTIONS reply
596
     * including Dav: and Allowed: headers
597
     * based on the implemented methods found in the actual instance
598
     *
599
     * @param  void
600
     * @return void
601
     */
602
    function http_OPTIONS()
603
    {
604
        // Microsoft clients default to the Frontpage protocol
605
        // unless we tell them to use WebDAV
606
        header("MS-Author-Via: DAV");
607
608
        // get allowed methods
609
        $allow = $this->_allow();
610
611
        // dav header
612
        $dav = array(1);        // assume we are always dav class 1 compliant
613
        if (isset($allow['LOCK'])) {
614
            $dav[] = 2;         // dav class 2 requires that locking is supported
615
        }
616
617
        // allow extending class to modify DAV and Allow headers
618
		if (method_exists($this,'OPTIONS')) {
619
			$this->OPTIONS($this->path,$dav,$allow);
620
		}
621
622
        // tell clients what we found
623
        $this->http_status("200 OK");
624
        header("DAV: "  .join(", ", $dav));
625
        header("Allow: ".join(", ", $allow));
626
627
        header("Content-length: 0");
628
    }
629
630
    // }}}
631
632
633
    // {{{ http_PROPFIND()
634
635
    /**
636
     * Should the whole PROPFIND request (xml) be stored
637
     *
638
     * @var boolean
639
     */
640
    var $store_request = false;
641
    /**
642
     * Content of (last) PROPFIND request
643
     *
644
     * @var string
645
     */
646
    var $request;
647
648
    /**
649
     * PROPFIND method handler
650
     *
651
     * @param  string $handler ='PROPFIND' allows to use method eg. for CalDAV REPORT
652
     * @return void
653
     */
654
    function http_PROPFIND($handler='PROPFIND')
655
    {
656
        $options = Array();
657
        $files   = Array();
658
659
        $options["path"] = $this->path;
660
661
        // search depth from header (default is "infinity)
662
        if (isset($this->_SERVER['HTTP_DEPTH'])) {
663
            $options["depth"] = $this->_SERVER["HTTP_DEPTH"];
664
        } else {
665
            $options["depth"] = "infinity";
666
        }
667
668
        // analyze request payload
669
        $propinfo = new _parse_propfind("php://input", $this->store_request, $handler);
670
        if ($this->store_request) $this->request = $propinfo->request;
671
        if ($propinfo->error) {
672
            $this->http_status("400 Bad Request");
673
			if (method_exists($this, 'log')) $this->log('Error parsing propfind: '.$propinfo->error);
674
            return;
675
        }
676
		$options['root'] = $propinfo->root;
677
		$options['props'] = $propinfo->props;
678
		if ($propinfo->filters)
0 ignored issues
show
Bug Best Practice introduced by
The expression $propinfo->filters of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
679
			$options['filters'] = $propinfo->filters;
680
		if ($propinfo->other)
0 ignored issues
show
Bug Best Practice introduced by
The expression $propinfo->other of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
681
			$options['other'] = $propinfo->other;
682
683
        // call user handler
684
        if (!($retval =$this->$handler($options, $files))) {
685
            $files = array("files" => array());
686
            if (method_exists($this, "checkLock")) {
687
                // is locked?
688
                $lock = $this->checkLock($this->path);
689
690
                if (is_array($lock) && count($lock)) {
691
                    $created          = isset($lock['created'])  ? $lock['created']  : time();
692
                    $modified         = isset($lock['modified']) ? $lock['modified'] : time();
693
                    $files['files'][] = array("path"  => self::_slashify($this->path),
694
                                              "props" => array($this->mkprop("displayname",      $this->path),
695
                                                               $this->mkprop("creationdate",     $created),
696
                                                               $this->mkprop("getlastmodified",  $modified),
697
                                                               $this->mkprop("resourcetype",     ""),
698
                                                               $this->mkprop("getcontenttype",   ""),
699
                                                               $this->mkprop("getcontentlength", 0))
700
                                              );
701
                }
702
            }
703
704
            if (empty($files['files'])) {
705
                $this->http_status("404 Not Found");
706
                return;
707
            }
708
        }
709
710
        // now we generate the reply header ...
711
		if ($retval === true)
712
		{
713
			$this->http_status('207 Multi-Status');
714
		}
715
		elseif (is_string($retval))
716
		{
717
			$this->http_status($retval);
718
			header('Content-Type: text/html');
719
			echo "<html><head><title>Error $retval</title></head>\n";
720
			echo "<body><h1>$retval</h1>\n";
721
			switch (substr($retval, 0 ,3))
722
			{
723
				case '501': // Not Implemented
724
					echo "The requested feature is not (yet) supported by this server.\n";
725
					break;
726
				default:
727
					echo "The request could not be handled by this server.\n";
728
			}
729
			echo '(URI ' . $this->_SERVER['REQUEST_URI'] . ")<br>\n<br>\n";
730
			echo "</body></html>\n";
731
			return;
732
		}
733
		// dav header
734
        $dav = array(1);        // assume we are always dav class 1 compliant
735
        $allow = false;
736
737
        // allow extending class to modify DAV
738
		if (method_exists($this,'OPTIONS')) {
739
			$this->OPTIONS($this->path,$dav,$allow);
740
		}
741
        header("DAV: "  .join(", ", $dav));
742
        header('Content-Type: text/xml; charset="utf-8"');
743
744
        // add Vary and Preference-Applied header for Prefer: return=minimal
745
        if (isset($this->_SERVER['HTTP_PREFER']) && in_array('return=minimal', preg_split('/, ?/', $this->_SERVER['HTTP_PREFER'])))
746
        {
747
        	header("Preference-Applied: return=minimal");
748
        	header("Vary: Prefer");
749
        }
750
751
        // ... and payload
752
        echo "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
753
        echo $this->crrnd ? "<multistatus xmlns=\"DAV:\">\n" : "<D:multistatus xmlns:D=\"DAV:\">\n";
754
755
        $this->multistatus_responses($files['files'], $options['props']);
756
757
        // WebDAV sync report sync-token, can be either the sync-token or a callback (called with params in $files['sync-token-params'])
758
        if (isset($files['sync-token']))
759
        {
760
            echo ($this->crrnd ? " <" : " <D:")."sync-token>".
761
            	htmlspecialchars(!is_callable($files['sync-token']) ? $files['sync-token'] :
762
            		call_user_func_array($files['sync-token'], (array)$files['sync-token-params'])).
763
            	($this->crrnd ? "</" : "</D:")."sync-token>\n";
764
        }
765
766
        echo '</'.($this->crrnd?'':'D:')."multistatus>\n";
767
    }
768
769
    /**
770
     * Render (echo) XML for given multistatus responses
771
     *
772
     * @param array|Iterator $files
773
     * @param array|string $props
774
     */
775
    function multistatus_responses(&$files, $props, $initial_ns_hash=null, $initial_ns_defs=null)
776
    {
777
    	if (!isset($initial_ns_hash)) $initial_ns_hash = array('DAV:' => 'D');
778
    	if (!isset($initial_ns_defs)) $initial_ns_defs = 'xmlns:ns0="urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/"';
779
780
    	// using an ArrayIterator to prevent foreach from copying the array,
781
        // as we cant loop by reference, when an iterator is given in $files
782
        if (is_array($files))
783
        {
784
        	$files = new ArrayIterator($files);
785
        }
786
        // support for "Prefer: depth-noroot" header on PROPFIND
787
        $skip_root = $this->_SERVER['REQUEST_METHOD'] == 'PROPFIND' &&
788
        	!isset($initial_ns_hash) &&	// multistatus_response calls itself, do NOT apply skip in that case
789
        	isset($this->_SERVER['HTTP_PREFER']) && in_array('depth-noroot', preg_split('/, ?/', $this->_SERVER['HTTP_PREFER']));
790
791
        // now we loop over all returned file entries
792
        foreach ($files as $file) {
793
794
        	// skip first element (root), if requested by Prefer: depth-noroot
795
        	if ($skip_root) {
796
        		$skip_root = false;
797
        		continue;
798
        	}
799
800
	        // collect namespaces here
801
	        $ns_hash = $initial_ns_hash;
802
803
	        // Microsoft Clients need this special namespace for date and time values
804
	        $ns_defs = $initial_ns_defs;
805
806
            // nothing to do if no properties were returend for a file
807
			if (isset($file["props"]) && is_array($file["props"])) {
808
809
	            // now loop over all returned properties
810
	            foreach ($file["props"] as &$prop) {
811
	                // as a convenience feature we do not require that user handlers
812
	                // restrict returned properties to the requested ones
813
	                // here we strip all unrequested entries out of the response
814
815
	            	// this can happen if we have allprop and prop in one propfind:
816
	            	// <allprop /><prop><blah /></prop>, eg. blah is not automatic returned by allprop
817
	                switch(is_array($props) ? $props[0] : $props) {
818
	                case "all":
819
	                    // nothing to remove
820
	                    break;
821
822
	                case "names":
823
	                    // only the names of all existing properties were requested
824
	                    // so we remove all values
825
	                    unset($prop["val"]);
826
	                    break;
827
828
	                default:
829
	                    $found = false;
830
831
	                    // search property name in requested properties
832
	                    foreach ((array)$props as $reqprop) {
833
	                        if (   $reqprop["name"]  == $prop["name"]
834
	                               && @$reqprop["xmlns"] == $prop["ns"]) {
835
	                            $found = true;
836
	                            break;
837
	                        }
838
	                    }
839
840
	                    // unset property and continue with next one if not found/requested
841
	                    if (!$found) {
842
	                        $prop="";
843
	                        continue(2);
844
	                    }
845
	                    break;
846
	                }
847
848
	                // namespace handling
849
	                if (empty($prop["ns"])) continue; // no namespace
850
	                $ns = $prop["ns"];
851
	                //if ($ns == "DAV:") continue; // default namespace
852
	                if (isset($ns_hash[$ns])) continue; // already known
853
854
	                // register namespace
855
	                $ns_name = "ns".(count($ns_hash) + 1);
856
	                $ns_hash[$ns] = $ns_name;
857
	                $ns_defs .= " xmlns:$ns_name=\"$ns\"";
858
	            }
859
860
	            // we also need to add empty entries for properties that were requested
861
	            // but for which no values where returned by the user handler
862
	            if (is_array($props)) {
863
	                foreach ($props as $reqprop) {
864
	                    if (!is_array($reqprop) || $reqprop['name']=="") continue; // skip empty entries, or 'all' if <allprop /> used together with <prop>
865
866
	                    $found = false;
867
868
	                    // check if property exists in result
869
	                    foreach ($file["props"] as &$prop) {
870
	                        if (is_array($prop) && $reqprop["name"] == $prop["name"]
871
	                               && @$reqprop["xmlns"] == $prop["ns"]) {
872
	                            $found = true;
873
	                            break;
874
	                        }
875
	                    }
876
877
	                    if (!$found) {
878
	                        if ($reqprop["xmlns"]==="DAV:" && $reqprop["name"]==="lockdiscovery") {
879
	                            // lockdiscovery is handled by the base class
880
	                            $file["props"][]
881
	                                = $this->mkprop("DAV:",
882
	                                                "lockdiscovery",
883
	                                                $this->lockdiscovery($file['path']));
884
	                        // only collect $file['noprops'] if we have NO Brief: t and NO Prefer: return=minimal HTTP Header
885
	                        } elseif ((!isset($this->_SERVER['HTTP_BRIEF']) || $this->_SERVER['HTTP_BRIEF'] != 't') &&
886
	                        	(!isset($this->_SERVER['HTTP_PREFER']) || !in_array('return=minimal', preg_split('/, ?/', $this->_SERVER['HTTP_PREFER'])))) {
887
	                            // add empty value for this property
888
	                            $file["noprops"][] =
889
	                                $this->mkprop($reqprop["xmlns"], $reqprop["name"], "");
890
891
	                            // register property namespace if not known yet
892
	                            if ($reqprop["xmlns"] != "DAV:" && !isset($ns_hash[$reqprop["xmlns"]])) {
893
	                                $ns_name = "ns".(count($ns_hash) + 1);
894
	                                $ns_hash[$reqprop["xmlns"]] = $ns_name;
895
	                                $ns_defs .= " xmlns:$ns_name=\"$reqprop[xmlns]\"";
896
	                            }
897
	                        }
898
	                    }
899
	                }
900
	            }
901
	        }
902
            // ignore empty or incomplete entries
903
            if (!is_array($file) || empty($file) || !isset($file["path"])) continue;
904
            $path = $file['path'];
905
            if (!is_string($path) || $path==="") continue;
906
907
            if ($this->crrnd)
908
            {
909
            	echo " <response $ns_defs>\n";
910
            }
911
            else
912
            {
913
            	echo " <D:response $ns_defs>\n";
914
            }
915
916
            /* TODO right now the user implementation has to make sure
917
             collections end in a slash, this should be done in here
918
             by checking the resource attribute */
919
            $href_raw = $this->_mergePaths($this->base_uri, $path);
920
921
            /* minimal urlencoding is needed for the resource path */
922
            $href = $this->_urlencode($href_raw);
923
924
            if ($this->crrnd)
925
            {
926
            	echo "  <href>$href</href>\n";
927
            }
928
            else
929
            {
930
            	echo "  <D:href>$href</D:href>\n";
931
            }
932
933
            // report all found properties and their values (if any)
934
            if (isset($file["props"]) && is_array($file["props"])) {
935
                echo '   <'.($this->crrnd?'':'D:')."propstat>\n";
936
                echo '    <'.($this->crrnd?'':'D:')."prop>\n";
937
938
                foreach ($file["props"] as &$prop) {
939
940
                    if (!is_array($prop)) continue;
941
                    if (!isset($prop["name"])) continue;
942
943
                    if (!isset($prop["val"]) || $prop["val"] === "" || $prop["val"] === false) {
944
                        // empty properties (cannot use empty() for check as "0" is a legal value here)
945
                        if ($prop["ns"]=="DAV:") {
946
                            echo '     <'.($this->crrnd?'':'D:')."$prop[name]/>\n";
947
                        } else if (!empty($prop["ns"])) {
948
                            echo "     <".$ns_hash[$prop["ns"]].":$prop[name]/>\n";
949
                        } else {
950
                            echo "     <$prop[name] xmlns=\"\"/>";
951
                        }
952
                    }
953
                    // multiple level of responses required for expand-property reports
954
                    elseif(isset($prop['props']) && is_array($prop['val']))
955
                    {
956
                        if ($prop['ns'] && !isset($ns_hash[$prop['ns']])) {
957
                            $ns_name = "ns".(count($ns_hash) + 1);
958
                            $ns_hash[$prop['ns']] = $ns_name;
959
                        }
960
                    	echo '     <'.$ns_hash[$prop['ns']].":$prop[name]>\n";
961
                        $this->multistatus_responses($prop['val'], $prop['props'], $ns_hash, '');
962
                    	echo '     </'.$ns_hash[$prop['ns']].":$prop[name]>\n";
963
                    } else if ($prop["ns"] == "DAV:") {
964
                        // some WebDAV properties need special treatment
965
                        switch ($prop["name"]) {
966
                        case "creationdate":
967
                            echo '     <'.($this->crrnd?'':'D:')."creationdate ns0:dt=\"dateTime.tz\">"
968
                                . gmdate("Y-m-d\\TH:i:s\\Z", $prop['val'])
969
                                . '</'.($this->crrnd?'':'D:')."creationdate>\n";
970
                            break;
971
                        case "getlastmodified":
972
                            echo '     <'.($this->crrnd?'':'D:')."getlastmodified ns0:dt=\"dateTime.rfc1123\">"
973
                                . gmdate("D, d M Y H:i:s ", $prop['val'])
974
                                . "GMT</".($this->crrnd?'':'D:')."getlastmodified>\n";
975
                            break;
976
                        case "supportedlock":
977
                            echo '     <'.($this->crrnd?'':'D:')."supportedlock>$prop[val]</".($this->crrnd?'':'D:')."supportedlock>\n";
978
                            break;
979
                        case "lockdiscovery":
980
                            echo '     <'.($this->crrnd?'':'D:')."lockdiscovery>\n";
981
                            echo $prop["val"];
982
                            echo '     </'.($this->crrnd?'':'D:')."lockdiscovery>\n";
983
                            break;
984
                        // the following are non-standard Microsoft extensions to the DAV namespace
985
                        case "lastaccessed":
986
                            echo '     <'.($this->crrnd?'':'D:')."lastaccessed ns0:dt=\"dateTime.rfc1123\">"
987
                                . gmdate("D, d M Y H:i:s ", $prop['val'])
988
                                . 'GMT</'.($this->crrnd?'':'D:')."lastaccessed>\n";
989
                            break;
990
                        case "ishidden":
991
                            echo '     <'.($this->crrnd?'':'D:')."ishidden>"
992
                                . is_string($prop['val']) ? $prop['val'] : ($prop['val'] ? 'true' : 'false')
993
                                . '</'.($this->crrnd?'':'D:')."</D:ishidden>\n";
994
                            break;
995
                        default:
996
                        	$ns_defs = '';
997
                            if (is_array($prop['val']))
998
                            {
999
                            	$hns_hash = $ns_hash;
1000
                            	$val = $this->_hierarchical_prop_encode($prop['val'], 'DAV:', $ns_defs, $hns_hash);
1001
                            } elseif (isset($prop['raw'])) {
1002
                            	$val = $this->_prop_encode('<![CDATA['.$prop['val'].']]>');
1003
                            } else {
1004
	                    		$val = $this->_prop_encode(htmlspecialchars($prop['val'], ENT_NOQUOTES|ENT_XML1|ENT_DISALLOWED, 'utf-8'));
1005
                            }
1006
	                        echo '     <'.($this->crrnd?'':'D:')."$prop[name]$ns_defs>$val".
1007
	                        	'</'.($this->crrnd?'':'D:')."$prop[name]>\n";
1008
                            break;
1009
                        }
1010
                    } else {
1011
                        // allow multiple values and attributes, required eg. for caldav:supported-calendar-component-set
1012
                        if ($prop['ns'] && is_array($prop['val'])) {
1013
                    		if (!isset($ns_hash[$prop['ns']])) {
1014
                                $ns_name = "ns".(count($ns_hash) + 1);
1015
                                $ns_hash[$prop['ns']] = $ns_name;
1016
                    		}
1017
                  			$vals = $extra_ns = '';
1018
                    		foreach($prop['val'] as $subprop)
1019
                    		{
1020
                    			if ($subprop['ns'] && $subprop['ns'] != 'DAV:') {
1021
		                    		// register property namespace if not known yet
1022
		                    		if (!isset($ns_hash[$subprop['ns']])) {
1023
			                    		$ns_name = "ns".(count($ns_hash) + 1);
1024
			                    		$ns_hash[$subprop['ns']] = $ns_name;
1025
		                    		} else {
1026
			                    		$ns_name = $ns_hash[$subprop['ns']];
1027
		                    		}
1028
		                    		if (strchr($extra_ns,$extra=' xmlns:'.$ns_name.'="'.$subprop['ns'].'"') === false) {
1029
			                    		$extra_ns .= $extra;
1030
		                    		}
1031
		                    		$ns_name .= ':';
1032
	                    		} elseif ($subprop['ns'] == 'DAV:') {
1033
		                    		$ns_name = 'D:';
1034
	                    		} else {
1035
		                    		$ns_name = '';
1036
	                    		}
1037
	                    		$vals .= "<$ns_name$subprop[name]";
1038
	                    		if (is_array($subprop['val']))
1039
	                    		{
1040
	                    			if (isset($subprop['val'][0]))
1041
	                    			{
1042
		                    			$vals .= '>';
1043
		                    			$vals .= $this->_hierarchical_prop_encode($subprop['val'], $subprop['ns'], $ns_defs, $ns_hash);
1044
			                    		$vals .= "</$ns_name$subprop[name]>";
1045
	                    			}
1046
	                    			else	// val contains only attributes, no value
1047
	                    			{
1048
			                    		foreach($subprop['val'] as $attr => $val)
1049
										{
1050
				                    		$vals .= ' '.$attr.'="'.htmlspecialchars($val, ENT_NOQUOTES|ENT_XML1|ENT_DISALLOWED, 'utf-8').'"';
1051
										}
1052
			                    		$vals .= '/>';
1053
	                    			}
1054
	                    		}
1055
	                    		else
1056
	                    		{
1057
	                    			$vals .= '>';
1058
	                    			if (isset($subprop['raw'])) {
1059
	                    				$vals .= '<![CDATA['.$subprop['val'].']]>';
1060
	                    			} else {
1061
										// do NOT urlencode mailto href, as no clients understands them
1062
										if ($subprop['name'] == 'href' && strpos($subprop['val'], 'mailto:') !== 0)
1063
										{
1064
											$subprop['val'] = $this->_urlencode($subprop['val']);
1065
										}
1066
		                    			$vals .= htmlspecialchars($subprop['val'], ENT_NOQUOTES|ENT_XML1|ENT_DISALLOWED, 'utf-8');
1067
	                    			}
1068
	                    			$vals .= "</$ns_name$subprop[name]>";
1069
	                    		}
1070
                    		}
1071
                    		echo '     <'.$ns_hash[$prop['ns']].":$prop[name]$extra_ns>$vals</".$ns_hash[$prop['ns']].":$prop[name]>\n";
1072
                        } else {
1073
                        	if ($prop['raw'])
1074
                        	{
1075
                        		$val = '<![CDATA['.$prop['val'].']]>';
1076
                        	} else {
1077
                        		$val = htmlspecialchars($prop['val'], ENT_NOQUOTES|ENT_XML1|ENT_DISALLOWED, 'utf-8');
1078
                        	}
1079
                        	$val = $this->_prop_encode($val);
1080
	                        // properties from namespaces != "DAV:" or without any namespace
1081
	                        if ($prop['ns']) {
1082
		                        if ($this->crrnd) {
1083
			                        echo "     <$prop[name] xmlns=".'"'.$prop["ns"].'">'
1084
									. $val . "</$prop[name]>\n";
1085
		                        } else {
1086
			                        echo "     <" . $ns_hash[$prop["ns"]] . ":$prop[name]>"
1087
									. $val . '</'.$ns_hash[$prop['ns']].":$prop[name]>\n";
1088
		                        }
1089
	                        } else {
1090
		                        echo "     <$prop[name] xmlns=\"\">$val</$prop[name]>\n";
1091
	                        }
1092
                        }
1093
                    }
1094
                }
1095
1096
                if ($this->crrnd)
1097
                {
1098
	                echo "    </prop>\n";
1099
	                echo "   <status>HTTP/1.1 200 OK</status>\n";
1100
	                echo "  </propstat>\n";
1101
                }
1102
                else
1103
                {
1104
	                echo "    </D:prop>\n";
1105
	                echo "   <D:status>HTTP/1.1 200 OK</D:status>\n";
1106
	                echo "  </D:propstat>\n";
1107
                }
1108
            }
1109
1110
            // now report all properties requested but not found
1111
            if (isset($file["noprops"])) {
1112
                echo '   <'.($this->crrnd?'':'D:')."propstat>\n";
1113
                echo '    <'.($this->crrnd?'':'D:')."prop>\n";
1114
1115
                foreach ($file["noprops"] as &$prop) {
1116
                    if ($prop["ns"] == "DAV:") {
1117
                        echo '     <'.($this->crrnd?'':'D:')."$prop[name]/>\n";
1118
                    } else if ($prop["ns"] == "") {
1119
                        echo "     <$prop[name] xmlns=\"\"/>\n";
1120
                    } else {
1121
                        echo "     <" . $ns_hash[$prop["ns"]] . ":$prop[name]/>\n";
1122
                    }
1123
                }
1124
1125
                if ($this->crrnd)
1126
                {
1127
	                echo "   </prop>\n";
1128
	                echo "   <status>HTTP/1.1 404 Not Found</status>\n";
1129
	                echo "  </propstat>\n";
1130
                }
1131
                else
1132
                {
1133
	                echo "   </D:prop>\n";
1134
	                echo "   <D:status>HTTP/1.1 404 Not Found</D:status>\n";
1135
	                echo "  </D:propstat>\n";
1136
                }
1137
            }
1138
1139
            // 404 Not Found status element for WebDAV sync report
1140
            if (!isset($file['props']) && !isset($file['noprops']))
1141
            {
1142
                if ($this->crrnd)
1143
                {
1144
	                echo "  <status>HTTP/1.1 404 Not Found</status>\n";
1145
                }
1146
                else
1147
                {
1148
	                echo "  <D:status>HTTP/1.1 404 Not Found</D:status>\n";
1149
                }
1150
            }
1151
1152
            echo ' </'.($this->crrnd?'':'D:')."response>\n";
1153
        }
1154
    }
1155
1156
1157
    // }}}
1158
1159
    // {{{ http_PROPPATCH()
1160
1161
    /**
1162
     * PROPPATCH method handler
1163
     *
1164
     * @param  void
1165
     * @return void
1166
     */
1167
    function http_PROPPATCH()
1168
    {
1169
        if ($this->_check_lock_status($this->path)) {
1170
            $options = Array();
1171
1172
            $options["path"] = $this->path;
1173
1174
            $propinfo = new _parse_proppatch("php://input", $this->store_request);
1175
            if ($this->store_request) $this->request = $propinfo->request;
1176
1177
            if (!$propinfo->success) {
1178
                $this->http_status("400 Error");
1179
                return;
1180
            }
1181
1182
            $options['props'] = $propinfo->props;
1183
1184
            $responsedescr = $this->PROPPATCH($options);
0 ignored issues
show
Bug introduced by
The method PROPPATCH() does not exist on HTTP_WebDAV_Server. Did you maybe mean http_PROPPATCH()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1184
            /** @scrutinizer ignore-call */ 
1185
            $responsedescr = $this->PROPPATCH($options);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
1185
1186
            $this->http_status("207 Multi-Status");
1187
            header('Content-Type: text/xml; charset="utf-8"');
1188
1189
            echo "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
1190
1191
            echo "<D:multistatus xmlns:D=\"DAV:\">\n";
1192
            echo ' <'.($this->crrnd?'':'D:')."response>\n";
1193
            echo '  <'.($this->crrnd?'':'D:')."href>".$this->_urlencode($this->_mergePaths($this->_SERVER["SCRIPT_NAME"], $this->path)).'</'.($this->crrnd?'':'D:')."href>\n";
1194
1195
            foreach ($options["props"] as $prop) {
1196
                echo '   <'.($this->crrnd?'':'D:')."propstat>\n";
1197
                echo '    <'.($this->crrnd?'':'D:')."prop><$prop[name] xmlns=\"$prop[ns]\"/></".($this->crrnd?'':'D:')."prop>\n";
1198
                echo '    <'.($this->crrnd?'':'D:')."status>HTTP/1.1 $prop[status]</".($this->crrnd?'':'D:')."status>\n";
1199
                echo '   </'.($this->crrnd?'':'D:')."propstat>\n";
1200
            }
1201
1202
            if ($responsedescr) {
1203
                echo '  <'.($this->crrnd?'':'D:')."responsedescription>".
1204
                    $this->_prop_encode(htmlspecialchars($responsedescr, ENT_NOQUOTES|ENT_XML1|ENT_DISALLOWED, 'utf-8')).
1205
                    '</'.($this->crrnd?'':'D:')."responsedescription>\n";
1206
            }
1207
1208
            echo ' </'.($this->crrnd?'':'D:')."response>\n";
1209
            echo '</'.($this->crrnd?'':'D:')."multistatus>\n";
1210
        } else {
1211
            $this->http_status("423 Locked");
1212
        }
1213
    }
1214
1215
    // }}}
1216
1217
1218
    // {{{ http_MKCOL()
1219
1220
    /**
1221
     * MKCOL method handler
1222
     *
1223
     * @param  void
1224
     * @return void
1225
     */
1226
    function http_MKCOL()
1227
    {
1228
        $options = Array();
1229
1230
        $options["path"] = $this->path;
1231
1232
        $stat = $this->MKCOL($options);
0 ignored issues
show
introduced by
The method MKCOL() does not exist on HTTP_WebDAV_Server. Maybe you want to declare this class abstract? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1232
        /** @scrutinizer ignore-call */ 
1233
        $stat = $this->MKCOL($options);
Loading history...
1233
1234
        $this->http_status($stat);
1235
    }
1236
1237
    // }}}
1238
1239
	/**
1240
	 * Check or set if we want ot use compression as transfer encoding
1241
	 *
1242
	 * If we use compression via zlib.output_compression as transfer encoding,
1243
	 * we can NOT send Content-Length headers, as the have to reflect size
1244
	 * AFTER applying compression/transfer encoding.
1245
	 *
1246
	 * @param boolean $set =null
1247
	 * @return boolean true if we use compression, false otherwise
1248
	 */
1249
	public static function use_compression($set=null)
1250
	{
1251
		static $compression = null;
1252
		if (isset($set))
1253
		{
1254
			ini_set('zlib.output_compression', $compression=(boolean)$set);
1255
		}
1256
		elseif (!isset($compression))
1257
		{
1258
			$compression = (boolean)ini_get('zlib.output_compression');
1259
		}
1260
		//error_log(__METHOD__."(".array2string($set).") returning ".array2string($compression));
1261
		return $compression;
1262
	}
1263
1264
    // {{{ http_GET()
1265
1266
    /**
1267
     * GET method handler
1268
     *
1269
     * @param void
1270
     * @return void
1271
     */
1272
    function http_GET()
1273
    {
1274
        // TODO check for invalid stream
1275
        $options         = Array();
1276
        $options["path"] = $this->path;
1277
1278
        $this->_get_ranges($options);
1279
1280
        if (true === ($status = $this->GET($options))) {
0 ignored issues
show
introduced by
The method GET() does not exist on HTTP_WebDAV_Server. Maybe you want to declare this class abstract? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1280
        if (true === ($status = $this->/** @scrutinizer ignore-call */ GET($options))) {
Loading history...
1281
            if (!headers_sent()) {
1282
                $status = "200 OK";
1283
1284
                if (!isset($options['mimetype'])) {
1285
                    $options['mimetype'] = "application/octet-stream";
1286
                }
1287
                // switching off zlib.output_compression for everything but text files,
1288
                // as the double compression of zip files makes problems eg. with lighttpd
1289
                // and anyway little sense with with other content like pictures
1290
                if (substr($options['mimetype'],0,5) != 'text/')
1291
                {
1292
					self::use_compression(false);
1293
                }
1294
                header("Content-type: $options[mimetype]");
1295
1296
                if (isset($options['mtime'])) {
1297
                    header("Last-modified:".gmdate("D, d M Y H:i:s ", $options['mtime'])."GMT");
1298
                }
1299
                // fix for IE and https, thanks to [email protected]
1300
                // see http://us3.php.net/manual/en/function.header.php#83219
1301
                // and http://support.microsoft.com/kb/812935
1302
				header("Cache-Control: maxage=1"); //In seconds
1303
				header("Pragma: public");
1304
1305
                if (isset($options['stream'])) {
1306
                    // GET handler returned a stream
1307
                    if (!empty($options['ranges']) && (0===fseek($options['stream'], 0, SEEK_SET))) {
1308
                        // partial request and stream is seekable
1309
1310
                        if (count($options['ranges']) === 1) {
1311
                            $range = $options['ranges'][0];
1312
1313
                            if (isset($range['start'])) {
1314
                                fseek($options['stream'], $range['start'], SEEK_SET);
1315
                                if (feof($options['stream'])) {
1316
                                    $this->http_status($status = "416 Requested range not satisfiable");
1317
                                    return;
1318
                                }
1319
1320
                                if (!empty($range['end'])) {
1321
                                    $size = $range['end']-$range['start']+1;
1322
                                    $this->http_status($status = "206 Partial content");
1323
                                    if (!self::use_compression()) header("Content-Length: $size");
1324
                                    header("Content-Range: bytes $range[start]-$range[end]/"
1325
                                           . (isset($options['size']) ? $options['size'] : "*"));
1326
                                    while ($size > 0 && !feof($options['stream'])) {
1327
                                        $buffer = fread($options['stream'], $size < 8192 ? $size : 8192);
1328
                                        $size  -= self::bytes($buffer);
1329
                                        echo $buffer;
1330
                                    }
1331
                                } else {
1332
                                    $this->http_status($status = "206 Partial content");
1333
                                    if (isset($options['size'])) {
1334
                                        if (!self::use_compression()) header("Content-Length: ".($options['size'] - $range['start']));
1335
                                        header("Content-Range: bytes ".$range['start']."-".
1336
											(isset($options['size']) ? $options['size']-1 : "")."/"
1337
										   . (isset($options['size']) ? $options['size'] : "*"));
1338
                                    }
1339
                                    fpassthru($options['stream']);
1340
                                }
1341
                            } else {
1342
                                if (!self::use_compression()) header("Content-length: ".$range['last']);
1343
                                fseek($options['stream'], -$range['last'], SEEK_END);
1344
                                fpassthru($options['stream']);
1345
                            }
1346
                        } else {
1347
                            $this->_multipart_byterange_header(); // init multipart
1348
                            foreach ($options['ranges'] as $range) {
1349
                                // TODO what if size unknown? 500?
1350
                                if (isset($range['start'])) {
1351
                                    $from = $range['start'];
1352
                                    $to   = !empty($range['end']) ? $range['end'] : $options['size']-1;
1353
                                } else {
1354
                                    $from = $options['size'] - $range['last']-1;
1355
                                    $to   = $options['size'] -1;
1356
                                }
1357
                                $total = isset($options['size']) ? $options['size'] : "*";
1358
                                $size  = $to - $from + 1;
1359
                                $this->_multipart_byterange_header($options['mimetype'], $from, $to, $total);
1360
1361
1362
                                fseek($options['stream'], $from, SEEK_SET);
1363
                                while ($size && !feof($options['stream'])) {
1364
                                    $buffer = fread($options['stream'], 4096);
1365
                                    $size  -= self::bytes($buffer);
1366
                                    echo $buffer;
1367
                                }
1368
                            }
1369
                            $this->_multipart_byterange_header(); // end multipart
1370
                        }
1371
                    } else {
1372
                        // normal request or stream isn't seekable, return full content
1373
                        if (isset($options['size']) && !self::use_compression()) {
1374
                            header("Content-Length: ".$options['size']);
1375
                        }
1376
                        fpassthru($options['stream']);
1377
                        return; // no more headers
1378
                    }
1379
                } elseif (isset($options['data'])) {
1380
                    if (is_array($options['data'])) {
1381
                        // reply to partial request
1382
                    } else {
1383
                        if (!self::use_compression()) header("Content-Length: ".self::bytes($options['data']));
1384
                        echo $options['data'];
1385
                    }
1386
                }
1387
            }
1388
        }
1389
1390
        if (!headers_sent()) {
1391
            if (false === $status) {
1392
                $this->http_status("404 not found");
1393
            } else {
1394
                // TODO: check setting of headers in various code paths above
1395
                $this->http_status("$status");
1396
            }
1397
        }
1398
    }
1399
1400
1401
    /**
1402
     * parse HTTP Range: header
1403
     *
1404
     * @param  array options array to store result in
0 ignored issues
show
Bug introduced by
The type options was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
1405
     * @return void
1406
     */
1407
    function _get_ranges(&$options)
1408
    {
1409
        // process Range: header if present
1410
        if (isset($this->_SERVER['HTTP_RANGE'])) {
1411
1412
            // we only support standard "bytes" range specifications for now
1413
			$matches = null;
1414
            if (preg_match('/bytes\s*=\s*(.+)/', $this->_SERVER['HTTP_RANGE'], $matches)) {
1415
                $options["ranges"] = array();
1416
1417
                // ranges are comma separated
1418
                foreach (explode(",", $matches[1]) as $range) {
1419
                    // ranges are either from-to pairs or just end positions
1420
                    list($start, $end) = explode("-", $range);
1421
                    $options["ranges"][] = ($start==="")
1422
                        ? array("last"=>$end)
1423
                        : array("start"=>$start, "end"=>$end);
1424
                }
1425
            }
1426
        }
1427
    }
1428
1429
    /**
1430
     * generate separator headers for multipart response
1431
     *
1432
     * first and last call happen without parameters to generate
1433
     * the initial header and closing sequence, all calls inbetween
1434
     * require content mimetype, start and end byte position and
1435
     * optionaly the total byte length of the requested resource
1436
     *
1437
     * @param  string  mimetype
0 ignored issues
show
Bug introduced by
The type mimetype was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
1438
     * @param  int     start byte position
1439
     * @param  int     end   byte position
1440
     * @param  int     total resource byte size
0 ignored issues
show
Bug introduced by
The type total was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
1441
     */
1442
    function _multipart_byterange_header($mimetype = false, $from = false, $to=false, $total=false)
1443
    {
1444
        if ($mimetype === false) {
1445
            if (!isset($this->multipart_separator)) {
1446
                // initial
1447
1448
                // a little naive, this sequence *might* be part of the content
1449
                // but it's really not likely and rather expensive to check
1450
                $this->multipart_separator = "SEPARATOR_".md5(microtime());
0 ignored issues
show
Bug Best Practice introduced by
The property multipart_separator does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
1451
1452
                // generate HTTP header
1453
                header("Content-type: multipart/byteranges; boundary=".$this->multipart_separator);
1454
            } else {
1455
                // final
1456
1457
                // generate closing multipart sequence
1458
                echo "\n--{$this->multipart_separator}--";
1459
            }
1460
        } else {
1461
            // generate separator and header for next part
1462
            echo "\n--{$this->multipart_separator}\n";
1463
            echo "Content-type: $mimetype\n";
1464
            echo "Content-range: $from-$to/". ($total === false ? "*" : $total);
1465
            echo "\n\n";
1466
        }
1467
    }
1468
1469
1470
1471
    // }}}
1472
1473
    // {{{ http_HEAD()
1474
1475
    /**
1476
     * HEAD method handler
1477
     *
1478
     * @param  void
1479
     * @return void
1480
     */
1481
    function http_HEAD()
1482
    {
1483
        $status          = false;
1484
        $options         = Array();
1485
        $options["path"] = $this->path;
1486
1487
        if (method_exists($this, "HEAD")) {
1488
            $status = $this->head($options);
1489
        } else if (method_exists($this, "GET")) {
1490
            ob_start();
1491
            $status = $this->GET($options);
1492
            if (!isset($options['size'])) {
1493
                $options['size'] = ob_get_length();
1494
            }
1495
            ob_end_clean();
1496
        }
1497
1498
        if (!isset($options['mimetype'])) {
1499
            $options['mimetype'] = "application/octet-stream";
1500
        }
1501
        header("Content-type: $options[mimetype]");
1502
1503
        if (isset($options['mtime'])) {
1504
            header("Last-modified:".gmdate("D, d M Y H:i:s ", $options['mtime'])."GMT");
1505
        }
1506
1507
        if (isset($options['size'])) {
1508
            header("Content-Length: ".$options['size']);
1509
        }
1510
1511
        if ($status === true)  $status = "200 OK";
1512
        if ($status === false) $status = "404 Not found";
1513
1514
        $this->http_status($status);
1515
    }
1516
1517
    // }}}
1518
1519
    // {{{ http_POST()
1520
1521
    /**
1522
     * POST method handler
1523
     *
1524
     * @param  void
1525
     * @return void
1526
     */
1527
    function http_POST()
1528
    {
1529
        $status          = '405 Method not allowed';
1530
        $options         = Array();
1531
        $options['path'] = $this->path;
1532
1533
        if (isset($this->_SERVER['CONTENT_LENGTH']))
1534
        {
1535
	        $options['content_length'] = $this->_SERVER['CONTENT_LENGTH'];
1536
        }
1537
        elseif (isset($this->_SERVER['X-Expected-Entity-Length']))
1538
		{
1539
	        // MacOS gives us that hint
1540
	        $options['content_length'] = $this->_SERVER['X-Expected-Entity-Length'];
1541
		}
1542
1543
        // get the Content-type
1544
        if (isset($this->_SERVER["CONTENT_TYPE"])) {
1545
	        // for now we do not support any sort of multipart requests
1546
	        if (!strncmp($this->_SERVER["CONTENT_TYPE"], 'multipart/', 10)) {
1547
		        $this->http_status('501 not implemented');
1548
		        echo 'The service does not support mulipart POST requests';
1549
		        return;
1550
	        }
1551
	        $options['content_type'] = $this->_SERVER['CONTENT_TYPE'];
1552
        } else {
1553
	        // default content type if none given
1554
	        $options['content_type'] = 'application/octet-stream';
1555
        }
1556
1557
        $options['stream'] = fopen('php://input', 'r');
1558
    	switch($this->_SERVER['HTTP_CONTENT_ENCODING'])
1559
    	{
1560
    		case 'gzip':
1561
    		case 'deflate':	//zlib
1562
    			if (extension_loaded('zlib'))
1563
     			{
1564
      				stream_filter_append($options['stream'], 'zlib.inflate', STREAM_FILTER_READ);
1565
       			}
1566
    	}
1567
		// store request in $this->request, if requested via $this->store_request
1568
		if ($this->store_request)
1569
		{
1570
			$options['content'] = '';
1571
			while(!feof($options['stream']))
1572
			{
1573
				$options['content'] .= fread($options['stream'],8192);
1574
			}
1575
			$this->request =& $options['content'];
1576
			unset($options['stream']);
1577
		}
1578
1579
        /* RFC 2616 2.6 says: "The recipient of the entity MUST NOT
1580
         ignore any Content-* (e.g. Content-Range) headers that it
1581
         does not understand or implement and MUST return a 501
1582
         (Not Implemented) response in such cases."
1583
         */
1584
        foreach ($this->_SERVER as $key => $val) {
1585
	        if (strncmp($key, 'HTTP_CONTENT', 11)) continue;
1586
	        switch ($key) {
1587
		        case 'HTTP_CONTENT_ENCODING': // RFC 2616 14.11
1588
		        	switch($this->_SERVER['HTTP_CONTENT_ENCODING'])
1589
		        	{
1590
		        		case 'gzip':
1591
		        		case 'deflate':	//zlib
1592
		        			if (extension_loaded('zlib')) break;
1593
		        			// fall through for no zlib support
1594
		        		default:
1595
					        $this->http_status('415 Unsupported Media Type');
1596
					        echo "The service does not support '$val' content encoding";
1597
					        return;
1598
		        	}
1599
		        	break;
1600
1601
		        case 'HTTP_CONTENT_LANGUAGE': // RFC 2616 14.12
1602
			        // we assume it is not critical if this one is ignored
1603
			        // in the actual POST implementation ...
1604
			        $options['content_language'] = $val;
1605
			        break;
1606
1607
		        case 'HTTP_CONTENT_LENGTH':
1608
			        // defined on IIS and has the same value as CONTENT_LENGTH
1609
			        break;
1610
1611
		        case 'HTTP_CONTENT_LOCATION': // RFC 2616 14.14
1612
			        /* The meaning of the Content-Location header in PUT
1613
			         or POST requests is undefined; servers are free
1614
			         to ignore it in those cases. */
1615
			        break;
1616
1617
		        case 'HTTP_CONTENT_RANGE':    // RFC 2616 14.16
1618
			        // single byte range requests are supported
1619
			        // the header format is also specified in RFC 2616 14.16
1620
			        // TODO we have to ensure that implementations support this or send 501 instead
1621
					$matches = null;
1622
			        if (!preg_match('@bytes\s+(\d+)-(\d+)/((\d+)|\*)@', $val, $matches)) {
1623
				        $this->http_status('400 bad request');
1624
				        echo 'The service does only support single byte ranges';
1625
				        return;
1626
			        }
1627
1628
			        $range = array('start'=>$matches[1], 'end'=>$matches[2]);
1629
			        if (is_numeric($matches[3])) {
1630
				        $range['total_length'] = $matches[3];
1631
			        }
1632
			        $options['ranges'][] = $range;
1633
1634
			        // TODO make sure the implementation supports partial POST
1635
			        // this has to be done in advance to avoid data being overwritten
1636
			        // on implementations that do not support this ...
1637
			        break;
1638
1639
		        case 'HTTP_CONTENT_TYPE':
1640
			        // defined on IIS and has the same value as CONTENT_TYPE
1641
			        break;
1642
1643
		        case 'HTTP_CONTENT_MD5':      // RFC 2616 14.15
1644
			        // TODO: maybe we can just pretend here?
1645
			        $this->http_status('501 not implemented');
1646
			        echo 'The service does not support content MD5 checksum verification';
1647
			        return;
1648
1649
		        case 'HTTP_CONTENT_DISPOSITION':
1650
		        	// do NOT care about Content-Disposition in POST requests required by CalDAV managed attachments
1651
		        	break;
1652
1653
		        default:
1654
			        // any other unknown Content-* headers
1655
			        $this->http_status('501 not implemented');
1656
		        echo "The service does not support '$key'";
1657
		        return;
1658
	        }
1659
        }
1660
1661
        if (method_exists($this, 'POST')) {
1662
	        $status = $this->POST($options);
1663
1664
	        if ($status === false) {
1665
		        $status = '400 Something went wrong';
1666
	        } else if ($status === true) {
1667
	        	$status = '200 OK';
1668
	        } else if (is_resource($status) && get_resource_type($status) == 'stream') {
1669
		        $stream = $status;
1670
1671
		        $status = empty($options['new']) ? '200 OK' : '201 Created';
1672
1673
		        if (!empty($options['ranges'])) {
1674
			        // TODO multipart support is missing (see also above)
1675
			        if (0 == fseek($stream, $range[0]['start'], SEEK_SET)) {
1676
				        $length = $range[0]['end']-$range[0]['start']+1;
1677
				        if (!fwrite($stream, fread($options['stream'], $length))) {
1678
					        $status = '403 Forbidden';
1679
				        }
1680
			        } else {
1681
				        $status = '403 Forbidden';
1682
			        }
1683
		        } else {
1684
			        while (!feof($options['stream'])) {
1685
				        if (false === fwrite($stream, fread($options['stream'], 4096))) {
1686
					        $status = '403 Forbidden';
1687
					        break;
1688
				        }
1689
			        }
1690
		        }
1691
		        fclose($stream);
1692
	        }
1693
        }
1694
        $this->http_status($status);
1695
    }
1696
1697
    // }}}
1698
1699
    // {{{ http_PUT()
1700
1701
    /**
1702
     * PUT method handler
1703
     *
1704
     * @param  void
1705
     * @return void
1706
     */
1707
    function http_PUT()
1708
    {
1709
        if ($this->_check_lock_status($this->path)) {
1710
            $options                   = Array();
1711
            $options["path"]           = $this->path;
1712
1713
            if (isset($this->_SERVER['CONTENT_LENGTH']))
1714
            {
1715
            	$options['content_length'] = $this->_SERVER['CONTENT_LENGTH'];
1716
            }
1717
            elseif (isset($this->_SERVER['X-Expected-Entity-Length']))
1718
            {
1719
            	// MacOS gives us that hint
1720
            	$options['content_length'] = $this->_SERVER['X-Expected-Entity-Length'];
1721
            }
1722
1723
            // get the Content-type
1724
            if (isset($this->_SERVER["CONTENT_TYPE"])) {
1725
                // for now we do not support any sort of multipart requests
1726
                if (!strncmp($this->_SERVER["CONTENT_TYPE"], "multipart/", 10)) {
1727
                    $this->http_status("501 not implemented");
1728
                    echo "The service does not support multipart PUT requests";
1729
                    return;
1730
                }
1731
                $options["content_type"] = $this->_SERVER["CONTENT_TYPE"];
1732
            } else {
1733
                // default content type if none given
1734
                $options["content_type"] = "application/octet-stream";
1735
            }
1736
1737
            $options["stream"] = fopen("php://input", "r");
1738
	    	switch($this->_SERVER['HTTP_CONTENT_ENCODING'])
1739
	    	{
1740
	    		case 'gzip':
1741
	    		case 'deflate':	//zlib
1742
	    			if (extension_loaded('zlib'))
1743
	     			{
1744
	      				stream_filter_append($options['stream'], 'zlib.inflate', STREAM_FILTER_READ);
1745
	       			}
1746
	    	}
1747
			// store request in $this->request, if requested via $this->store_request
1748
			if ($this->store_request)
1749
			{
1750
				$options['content'] = '';
1751
				while(!feof($options['stream']))
1752
				{
1753
					$options['content'] .= fread($options['stream'],8192);
1754
				}
1755
				$this->request =& $options['content'];
1756
				unset($options['stream']);
1757
			}
1758
1759
            /* RFC 2616 2.6 says: "The recipient of the entity MUST NOT
1760
             ignore any Content-* (e.g. Content-Range) headers that it
1761
             does not understand or implement and MUST return a 501
1762
             (Not Implemented) response in such cases."
1763
            */
1764
            foreach ($this->_SERVER as $key => $val) {
1765
                if (strncmp($key, "HTTP_CONTENT", 11)) continue;
1766
                switch ($key) {
1767
                case 'HTTP_CONTENT_ENCODING': // RFC 2616 14.11
1768
		        	switch($this->_SERVER['HTTP_CONTENT_ENCODING'])
1769
		        	{
1770
		        		case 'gzip':
1771
		        		case 'deflate':	//zlib
1772
		        			if (extension_loaded('zlib')) break;
1773
		        			// fall through for no zlib support
1774
		        		default:
1775
					        $this->http_status('415 Unsupported Media Type');
1776
					        echo "The service does not support '$val' content encoding";
1777
					        return;
1778
		        	}
1779
		        	break;
1780
1781
                case 'HTTP_CONTENT_LANGUAGE': // RFC 2616 14.12
1782
                    // we assume it is not critical if this one is ignored
1783
                    // in the actual PUT implementation ...
1784
                    $options["content_language"] = $val;
1785
                    break;
1786
1787
                case 'HTTP_CONTENT_LENGTH':
1788
                    // defined on IIS and has the same value as CONTENT_LENGTH
1789
                    break;
1790
1791
                case 'HTTP_CONTENT_LOCATION': // RFC 2616 14.14
1792
                    /* The meaning of the Content-Location header in PUT
1793
                     or POST requests is undefined; servers are free
1794
                     to ignore it in those cases. */
1795
                    break;
1796
1797
                case 'HTTP_CONTENT_RANGE':    // RFC 2616 14.16
1798
                    // single byte range requests are supported
1799
                    // the header format is also specified in RFC 2616 14.16
1800
                    // TODO we have to ensure that implementations support this or send 501 instead
1801
					$matches = null;
1802
                    if (!preg_match('@bytes\s+(\d+)-(\d+)/((\d+)|\*)@', $val, $matches)) {
1803
                        $this->http_status("400 bad request");
1804
                        echo "The service does only support single byte ranges";
1805
                        return;
1806
                    }
1807
1808
                    $range = array("start" => $matches[1], "end" => $matches[2]);
1809
                    if (is_numeric($matches[3])) {
1810
                        $range["total_length"] = $matches[3];
1811
                    }
1812
1813
                    if (!isset($options['ranges'])) {
1814
                        $options['ranges'] = array();
1815
                    }
1816
1817
                    $options["ranges"][] = $range;
1818
1819
                    // TODO make sure the implementation supports partial PUT
1820
                    // this has to be done in advance to avoid data being overwritten
1821
                    // on implementations that do not support this ...
1822
                    break;
1823
1824
                case 'HTTP_CONTENT_TYPE':
1825
                    // defined on IIS and has the same value as CONTENT_TYPE
1826
                    break;
1827
1828
                case 'HTTP_CONTENT_MD5':      // RFC 2616 14.15
1829
                    // TODO: maybe we can just pretend here?
1830
                    $this->http_status("501 not implemented");
1831
                    echo "The service does not support content MD5 checksum verification";
1832
                    return;
1833
1834
                default:
1835
                    // any other unknown Content-* headers
1836
                    $this->http_status("501 not implemented");
1837
                    echo "The service does not support '$key'";
1838
                    return;
1839
                }
1840
            }
1841
1842
            $stat = $this->PUT($options);
0 ignored issues
show
introduced by
The method PUT() does not exist on HTTP_WebDAV_Server. Maybe you want to declare this class abstract? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1842
            /** @scrutinizer ignore-call */ 
1843
            $stat = $this->PUT($options);
Loading history...
1843
1844
            if ($stat === false) {
1845
                $stat = "403 Forbidden";
1846
            } else if (is_resource($stat) && get_resource_type($stat) == "stream") {
1847
                $stream = $stat;
1848
1849
                $stat = $options["new"] ? "201 Created" : "204 No Content";
1850
1851
                if (!empty($options["ranges"])) {
1852
                    // TODO multipart support is missing (see also above)
1853
                    if (0 == fseek($stream, $options['ranges'][0]["start"], SEEK_SET)) {
1854
                        $length = $options['ranges'][0]["end"] - $options['ranges'][0]["start"]+1;
1855
1856
                        while (!feof($options['stream'])) {
1857
                            if ($length <= 0) {
1858
                               break;
1859
                            }
1860
1861
                            if ($length <= 8192) {
1862
                                $data = fread($options['stream'], $length);
1863
                            } else {
1864
                                $data = fread($options['stream'], 8192);
1865
                            }
1866
1867
                            if ($data === false) {
1868
                                $stat = "400 Bad request";
1869
                            } elseif (strlen($data)) {
1870
                                if (false === fwrite($stream, $data)) {
1871
                                    $stat = "403 Forbidden";
1872
                                    break;
1873
                                }
1874
                                $length -= strlen($data);
1875
                            }
1876
                        }
1877
                    } else {
1878
                        $stat = "403 Forbidden";
1879
                    }
1880
                } else {
1881
                    while (!feof($options["stream"])) {
1882
                        if (false === fwrite($stream, fread($options["stream"], 8192))) {
1883
                            $stat = "403 Forbidden";
1884
                            break;
1885
                        }
1886
                    }
1887
                }
1888
1889
                fclose($stream);
1890
            }
1891
1892
            $this->http_status($stat);
1893
        } else {
1894
            $this->http_status("423 Locked");
1895
        }
1896
    }
1897
1898
    // }}}
1899
1900
1901
    // {{{ http_DELETE()
1902
1903
    /**
1904
     * DELETE method handler
1905
     *
1906
     * @param  void
1907
     * @return void
1908
     */
1909
    function http_DELETE()
1910
    {
1911
        // check RFC 2518 Section 9.2, last paragraph
1912
        if (isset($this->_SERVER["HTTP_DEPTH"])) {
1913
            if ($this->_SERVER["HTTP_DEPTH"] != "infinity") {
1914
				if (stripos($_SERVER['HTTP_USER_AGENT'],'webdrive') !== false)
1915
				{
1916
					// pretend we didnt see it, as webdrive does not handle the depth parameter correctly while deleting collections
1917
				}
1918
				else
1919
				{
1920
                	$this->http_status("400 Bad Request");
1921
                	return;
1922
				}
1923
            }
1924
        }
1925
1926
        // check lock status
1927
        if ($this->_check_lock_status($this->path)) {
1928
            // ok, proceed
1929
            $options         = Array();
1930
            $options["path"] = $this->path;
1931
1932
            $stat = $this->DELETE($options);
0 ignored issues
show
Bug introduced by
The method DELETE() does not exist on HTTP_WebDAV_Server. Did you maybe mean http_DELETE()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1932
            /** @scrutinizer ignore-call */ 
1933
            $stat = $this->DELETE($options);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
1933
1934
            $this->http_status($stat);
1935
        } else {
1936
            // sorry, its locked
1937
            $this->http_status("423 Locked");
1938
        }
1939
    }
1940
1941
    // }}}
1942
1943
    // {{{ http_COPY()
1944
1945
    /**
1946
     * COPY method handler
1947
     *
1948
     * @param  void
1949
     * @return void
1950
     */
1951
    function http_COPY()
1952
    {
1953
        // no need to check source lock status here
1954
        // destination lock status is always checked by the helper method
1955
        $this->_copymove("copy");
1956
    }
1957
1958
    // }}}
1959
1960
    // {{{ http_MOVE()
1961
1962
    /**
1963
     * MOVE method handler
1964
     *
1965
     * @param  void
1966
     * @return void
1967
     */
1968
    function http_MOVE()
1969
    {
1970
        if ($this->_check_lock_status($this->path)) {
1971
            // destination lock status is always checked by the helper method
1972
            $this->_copymove("move");
1973
        } else {
1974
            $this->http_status("423 Locked");
1975
        }
1976
    }
1977
1978
    // }}}
1979
1980
1981
    // {{{ http_LOCK()
1982
1983
    /**
1984
     * LOCK method handler
1985
     *
1986
     * @param  void
1987
     * @return void
1988
     */
1989
    function http_LOCK()
1990
    {
1991
        $options         = Array();
1992
        $options["path"] = $this->path;
1993
1994
        if (isset($this->_SERVER['HTTP_DEPTH'])) {
1995
            $options["depth"] = $this->_SERVER["HTTP_DEPTH"];
1996
        } else {
1997
            $options["depth"] = "infinity";
1998
        }
1999
2000
        if (isset($this->_SERVER["HTTP_TIMEOUT"])) {
2001
            $options["timeout"] = explode(",", $this->_SERVER["HTTP_TIMEOUT"]);
2002
        }
2003
2004
        if (empty($this->_SERVER['CONTENT_LENGTH']) && !empty($this->_SERVER['HTTP_IF'])) {
2005
            // check if locking is possible
2006
            if (!$this->_check_lock_status($this->path)) {
2007
                $this->http_status("423 Locked");
2008
                return;
2009
            }
2010
2011
            // refresh lock
2012
            $options["locktoken"] = substr($this->_SERVER['HTTP_IF'], 2, -2);
2013
            $options["update"]    = $options["locktoken"];
2014
2015
            // setting defaults for required fields, LOCK() SHOULD overwrite these
2016
            $options['owner']     = "unknown";
2017
            $options['scope']     = "exclusive";
2018
            $options['type']      = "write";
2019
2020
2021
            $stat = $this->LOCK($options);
0 ignored issues
show
introduced by
The method LOCK() does not exist on HTTP_WebDAV_Server. Maybe you want to declare this class abstract? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

2021
            /** @scrutinizer ignore-call */ 
2022
            $stat = $this->LOCK($options);
Loading history...
2022
        } else {
2023
            // extract lock request information from request XML payload
2024
            $lockinfo = new _parse_lockinfo("php://input");
2025
            if (!$lockinfo->success) {
2026
                $this->http_status("400 bad request");
2027
            }
2028
2029
            // check if locking is possible
2030
            if (!$this->_check_lock_status($this->path, $lockinfo->lockscope === "shared")) {
2031
                $this->http_status("423 Locked");
2032
                return;
2033
            }
2034
2035
            // new lock
2036
            $options["scope"]     = $lockinfo->lockscope;
2037
            $options["type"]      = $lockinfo->locktype;
2038
            // Todo: lockinfo::owner still contains D:href opening and closing tags, maybe they should be removed here with strip_tags
2039
            $options["owner"]     = $lockinfo->owner;
2040
            $options["locktoken"] = $this->_new_locktoken();
2041
2042
            $stat = $this->LOCK($options);
2043
        }
2044
2045
        if (is_bool($stat)) {
2046
            $http_stat = $stat ? "200 OK" : "423 Locked";
2047
        } else {
2048
            $http_stat = (string)$stat;
2049
        }
2050
        $this->http_status($http_stat);
2051
2052
        if ($http_stat{0} == 2) { // 2xx states are ok
2053
            if ($options["timeout"]) {
2054
                // if multiple timeout values were given we take the first only
2055
                if (is_array($options["timeout"])) {
2056
                    reset($options["timeout"]);
2057
                    $options["timeout"] = current($options["timeout"]);
2058
                }
2059
                // if the timeout is numeric only we need to reformat it
2060
                if (is_numeric($options["timeout"])) {
2061
                    // more than a million is considered an absolute timestamp
2062
                    // less is more likely a relative value
2063
                    if ($options["timeout"]>1000000) {
2064
                        $timeout = "Second-".($options['timeout']-time());
2065
                    } else {
2066
                        $timeout = "Second-$options[timeout]";
2067
                    }
2068
                } else {
2069
                    // non-numeric values are passed on verbatim,
2070
                    // no error checking is performed here in this case
2071
                    // TODO: send "Infinite" on invalid timeout strings?
2072
                    $timeout = $options["timeout"];
2073
                }
2074
            } else {
2075
                $timeout = "Infinite";
2076
            }
2077
2078
            header('Content-Type: text/xml; charset="utf-8"');
2079
            header("Lock-Token: <$options[locktoken]>");
2080
            echo "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
2081
            echo "<D:prop xmlns:D=\"DAV:\">\n";
2082
            echo ' <'.($this->crrnd?'':'D:')."lockdiscovery>\n";
2083
            echo '  <'.($this->crrnd?'':'D:')."activelock>\n";
2084
            echo '   <'.($this->crrnd?'':'D:')."lockscope><D:$options[scope]/></".($this->crrnd?'':'D:')."lockscope>\n";
2085
            echo '   <'.($this->crrnd?'':'D:')."locktype><D:$options[type]/></".($this->crrnd?'':'D:')."locktype>\n";
2086
            echo '   <'.($this->crrnd?'':'D:')."depth>$options[depth]</".($this->crrnd?'':'D:')."depth>\n";
2087
            echo '   <'.($this->crrnd?'':'D:')."owner>$options[owner]</".($this->crrnd?'':'D:')."owner>\n";
2088
            echo '   <'.($this->crrnd?'':'D:')."timeout>$timeout</".($this->crrnd?'':'D:')."timeout>\n";
2089
            echo '   <'.($this->crrnd?'':'D:')."locktoken><D:href>$options[locktoken]</D:href></".($this->crrnd?'':'D:')."locktoken>\n";
2090
            echo '  </'.($this->crrnd?'':'D:')."activelock>\n";
2091
            echo ' </'.($this->crrnd?'':'D:')."lockdiscovery>\n";
2092
            echo '</'.($this->crrnd?'':'D:')."prop>\n\n";
2093
        }
2094
    }
2095
2096
2097
    // }}}
2098
2099
    // {{{ http_UNLOCK()
2100
2101
    /**
2102
     * UNLOCK method handler
2103
     *
2104
     * @param  void
2105
     * @return void
2106
     */
2107
    function http_UNLOCK()
2108
    {
2109
        $options         = Array();
2110
        $options["path"] = $this->path;
2111
2112
        if (isset($this->_SERVER['HTTP_DEPTH'])) {
2113
            $options["depth"] = $this->_SERVER["HTTP_DEPTH"];
2114
        } else {
2115
            $options["depth"] = "infinity";
2116
        }
2117
2118
        // strip surrounding <>
2119
        $options["token"] = substr(trim($this->_SERVER["HTTP_LOCK_TOKEN"]), 1, -1);
2120
2121
        // call user method
2122
        $stat = $this->UNLOCK($options);
0 ignored issues
show
Bug introduced by
The method UNLOCK() does not exist on HTTP_WebDAV_Server. Did you maybe mean http_UNLOCK()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

2122
        /** @scrutinizer ignore-call */ 
2123
        $stat = $this->UNLOCK($options);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
2123
2124
        $this->http_status($stat);
2125
    }
2126
2127
    // }}}
2128
2129
    // {{{ http_ACL()
2130
2131
	/**
2132
     * ACL method handler
2133
     *
2134
     * @param  void
2135
     * @return void
2136
     */
2137
    function http_ACL()
2138
    {
2139
        $options         = Array();
2140
        $options['path'] = $this->path;
2141
        $options['errors'] = array();
2142
2143
        if (isset($this->_SERVER['HTTP_DEPTH'])) {
2144
            $options['depth'] = $this->_SERVER['HTTP_DEPTH'];
2145
        } else {
2146
            $options['depth'] = 'infinity';
2147
        }
2148
2149
        // call user method
2150
        $status = $this->ACL($options);
0 ignored issues
show
Bug introduced by
The method ACL() does not exist on HTTP_WebDAV_Server. It seems like you code against a sub-type of HTTP_WebDAV_Server such as EGroupware\Api\CalDAV. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

2150
        /** @scrutinizer ignore-call */ 
2151
        $status = $this->ACL($options);
Loading history...
2151
2152
		// now we generate the reply header ...
2153
		$this->http_status($status);
2154
		$content = '';
2155
2156
        if (is_array($options['errors']) && count($options['errors'])) {
2157
	        header('Content-Type: text/xml; charset="utf-8"');
2158
	        // ... and payload
2159
	        $content .= "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
2160
	        $content .= "<D:error xmlns:D=\"DAV:\"> \n";
2161
	        foreach ($options['errors'] as $violation) {
2162
	        	$content .= '<'.($this->crrnd?'':'D:')."$violation/>\n";
2163
	        }
2164
	        $content .=  '</'.($this->crrnd?'':'D:')."error>\n";
2165
        }
2166
        if (!self::use_compression()) header("Content-Length: ".self::bytes($content));
2167
        if ($content) echo $options['content'];
2168
    }
2169
2170
    // }}}
2171
2172
    // }}}
2173
2174
    // {{{ _copymove()
2175
2176
    function _copymove($what)
2177
    {
2178
        $options         = Array();
2179
        $options["path"] = $this->path;
2180
2181
        if (isset($this->_SERVER["HTTP_DEPTH"])) {
2182
            $options["depth"] = $this->_SERVER["HTTP_DEPTH"];
2183
        } else {
2184
            $options["depth"] = "infinity";
2185
        }
2186
2187
        $http_header_host = preg_replace("/:80$/", "", $this->_SERVER["HTTP_HOST"]);
2188
2189
        $url  = parse_url($this->_SERVER["HTTP_DESTINATION"]);
2190
        // Vfs stores %, # and ? urlencoded, we do the encoding here on a central place
2191
        $path = strtr(self::_urldecode($url["path"]), array(
2192
            '%' => '%25',
2193
            '#' => '%23',
2194
            '?' => '%3F',
2195
        ));
2196
		//error_log(__METHOD__."(".array2string($what).") parse_url(HTTP_DESTINATION=".array2string($this->_SERVER["HTTP_DESTINATION"]).")=".array2string($url)." --> ".array2string($path));
2197
2198
        if (isset($url["host"])) {
2199
            // TODO check url scheme, too
2200
            $http_host = $url["host"];
2201
            if (isset($url["port"]) && $url["port"] != 80)
2202
                $http_host.= ":".$url["port"];
2203
        } else {
2204
            // only path given, set host to self
2205
            $http_host = $http_header_host;
2206
        }
2207
2208
        if ($http_host == $http_header_host &&
2209
            !strncmp($this->_SERVER["SCRIPT_NAME"], $path,
2210
                     strlen($this->_SERVER["SCRIPT_NAME"]))) {
2211
            $options["dest"] = substr($path, strlen($this->_SERVER["SCRIPT_NAME"]));
2212
            if (!$this->_check_lock_status($options["dest"])) {
2213
                $this->http_status("423 Locked");
2214
                return;
2215
            }
2216
2217
        } else {
2218
            $options["dest_url"] = $this->_SERVER["HTTP_DESTINATION"];
2219
        }
2220
2221
        // see RFC 2518 Sections 9.6, 8.8.4 and 8.9.3
2222
        if (isset($this->_SERVER["HTTP_OVERWRITE"])) {
2223
            $options["overwrite"] = $this->_SERVER["HTTP_OVERWRITE"] == "T";
2224
        } else {
2225
            $options["overwrite"] = true;
2226
        }
2227
2228
        $stat = $this->$what($options);
2229
        $this->http_status($stat);
2230
    }
2231
2232
    // }}}
2233
2234
    // {{{ _allow()
2235
2236
    /**
2237
     * check for implemented HTTP methods
2238
     *
2239
     * @param  void
2240
     * @return array something
2241
     */
2242
    function _allow()
2243
    {
2244
        // OPTIONS is always there
2245
        $allow = array("OPTIONS" =>"OPTIONS");
2246
2247
        // all other METHODS need both a http_method() wrapper
2248
        // and a method() implementation
2249
        // the base class supplies wrappers only
2250
        foreach (get_class_methods($this) as $method) {
2251
            if (!strncmp("http_", $method, 5)) {
2252
                $method = strtoupper(substr($method, 5));
2253
                if (method_exists($this, $method)) {
2254
                    $allow[$method] = $method;
2255
                }
2256
            }
2257
        }
2258
2259
        // we can emulate a missing HEAD implemetation using GET
2260
        if (isset($allow["GET"]))
2261
            $allow["HEAD"] = "HEAD";
2262
2263
        // no LOCK without checklok()
2264
        if (!method_exists($this, "checklock")) {
2265
            unset($allow["LOCK"]);
2266
            unset($allow["UNLOCK"]);
2267
        }
2268
2269
        return $allow;
2270
    }
2271
2272
    // }}}
2273
2274
    /**
2275
     * helper for property element creation
2276
     *
2277
     * @param  string  XML namespace (optional)
2278
     * @param  string  property name
0 ignored issues
show
Bug introduced by
The type property was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
2279
     * @param  string  property value
2280
     * @praram boolen  property raw-flag
2281
     * @return array   property array
2282
     */
2283
    public static function mkprop()
2284
    {
2285
	    $args = func_get_args();
2286
	    switch (count($args)) {
2287
		    case 4:
2288
			    return array('ns'   => $args[0],
2289
				    'name' => $args[1],
2290
					'val'  => $args[2],
2291
					'raw'	=> true);
2292
		    case 3:
2293
			    return array('ns'   => $args[0],
2294
				    'name' => $args[1],
2295
					'val'  => $args[2]);
2296
		    default:
2297
			    return array('ns'   => 'DAV:',
2298
				    'name' => $args[0],
2299
					'val'  => $args[1]);
2300
	    }
2301
    }
2302
2303
    // {{{ _check_auth
2304
2305
    /**
2306
     * check authentication if check is implemented
2307
     *
2308
     * @param  void
2309
     * @return bool  true if authentication succeded or not necessary
2310
     */
2311
    function _check_auth()
2312
    {
2313
        if (method_exists($this, "checkAuth")) {
2314
            // PEAR style method name
2315
            return $this->checkAuth(@$this->_SERVER["AUTH_TYPE"],
2316
                                    @$this->_SERVER["PHP_AUTH_USER"],
2317
                                    @$this->_SERVER["PHP_AUTH_PW"]);
2318
        } else if (method_exists($this, "check_auth")) {
2319
            // old (pre 1.0) method name
2320
            return $this->check_auth(@$this->_SERVER["AUTH_TYPE"],
2321
                                     @$this->_SERVER["PHP_AUTH_USER"],
2322
                                     @$this->_SERVER["PHP_AUTH_PW"]);
2323
        } else {
2324
            // no method found -> no authentication required
2325
            return true;
2326
        }
2327
    }
2328
2329
    // }}}
2330
2331
    // {{{ UUID stuff
2332
2333
    /**
2334
     * generate Unique Universal IDentifier for lock token
2335
     *
2336
     * @param  void
2337
     * @return string  a new UUID
2338
     */
2339
    public static function _new_uuid()
2340
    {
2341
        // use uuid extension from PECL if available
2342
        if (function_exists("uuid_create")) {
2343
            return uuid_create();
2344
        }
2345
2346
        // fallback
2347
        $uuid = md5(microtime().getmypid());    // this should be random enough for now
2348
2349
        // set variant and version fields for 'true' random uuid
2350
        $uuid{12} = "4";
2351
        $n = 8 + (ord($uuid{16}) & 3);
2352
        $hex = "0123456789abcdef";
2353
        $uuid{16} = $hex{$n};
2354
2355
        // return formated uuid
2356
        return substr($uuid,  0, 8)."-"
2357
            .  substr($uuid,  8, 4)."-"
2358
            .  substr($uuid, 12, 4)."-"
2359
            .  substr($uuid, 16, 4)."-"
2360
            .  substr($uuid, 20);
2361
    }
2362
2363
    /**
2364
     * create a new opaque lock token as defined in RFC2518
2365
     *
2366
     * @param  void
2367
     * @return string  new RFC2518 opaque lock token
2368
     */
2369
    public static function _new_locktoken()
2370
    {
2371
        return "opaquelocktoken:".self::_new_uuid();
2372
    }
2373
2374
    // }}}
2375
2376
    // {{{ WebDAV If: header parsing
2377
2378
    /**
2379
     *
2380
     *
2381
     * @param  string  header string to parse
0 ignored issues
show
Bug introduced by
The type header was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
2382
     * @param  int     current parsing position
0 ignored issues
show
Bug introduced by
The type current was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
2383
     * @return array   next token (type and value)
2384
     */
2385
    function _if_header_lexer($string, &$pos)
2386
    {
2387
        // skip whitespace
2388
        while (ctype_space($string{$pos})) {
2389
            ++$pos;
2390
        }
2391
2392
        // already at end of string?
2393
        if (strlen($string) <= $pos) {
2394
            return false;
2395
        }
2396
2397
        // get next character
2398
        $c = $string{$pos++};
2399
2400
        // now it depends on what we found
2401
        switch ($c) {
2402
        case "<":
2403
            // URIs are enclosed in <...>
2404
            $pos2 = strpos($string, ">", $pos);
2405
            $uri  = substr($string, $pos, $pos2 - $pos);
2406
            $pos  = $pos2 + 1;
2407
            return array("URI", $uri);
2408
2409
        case "[":
2410
            //Etags are enclosed in [...]
2411
            if ($string{$pos} == "W") {
2412
                $type = "ETAG_WEAK";
2413
                $pos += 2;
2414
            } else {
2415
                $type = "ETAG_STRONG";
2416
            }
2417
            $pos2 = strpos($string, "]", $pos);
2418
            $etag = substr($string, $pos + 1, $pos2 - $pos - 2);
2419
            $pos  = $pos2 + 1;
2420
            return array($type, $etag);
2421
2422
        case "N":
2423
            // "N" indicates negation
2424
            $pos += 2;
2425
            return array("NOT", "Not");
2426
2427
        default:
2428
            // anything else is passed verbatim char by char
2429
            return array("CHAR", $c);
2430
        }
2431
    }
2432
2433
    /**
2434
     * parse If: header
2435
     *
2436
     * @param  string  header string
2437
     * @return array   URIs and their conditions
2438
     */
2439
    function _if_header_parser($str)
2440
    {
2441
        $pos  = 0;
2442
        $len  = strlen($str);
2443
        $uris = array();
2444
2445
        // parser loop
2446
        while ($pos < $len) {
2447
            // get next token
2448
            $token = $this->_if_header_lexer($str, $pos);
2449
2450
            // check for URI
2451
            if ($token[0] == "URI") {
2452
                $uri   = $token[1]; // remember URI
2453
                $token = $this->_if_header_lexer($str, $pos); // get next token
2454
            } else {
2455
                $uri = "";
2456
            }
2457
2458
            // sanity check
2459
            if ($token[0] != "CHAR" || $token[1] != "(") {
2460
                return false;
2461
            }
2462
2463
            $list  = array();
2464
            $level = 1;
2465
            $not   = "";
2466
            while ($level) {
2467
                $token = $this->_if_header_lexer($str, $pos);
2468
                if ($token[0] == "NOT") {
2469
                    $not = "!";
2470
                    continue;
2471
                }
2472
                switch ($token[0]) {
2473
                case "CHAR":
2474
                    switch ($token[1]) {
2475
                    case "(":
2476
                        $level++;
2477
                        break;
2478
                    case ")":
2479
                        $level--;
2480
                        break;
2481
                    default:
2482
                        return false;
2483
                    }
2484
                    break;
2485
2486
                case "URI":
2487
                    $list[] = $not."<$token[1]>";
2488
                    break;
2489
2490
                case "ETAG_WEAK":
2491
                    $list[] = $not."[W/'$token[1]']>";
2492
                    break;
2493
2494
                case "ETAG_STRONG":
2495
                    $list[] = $not."['$token[1]']>";
2496
                    break;
2497
2498
                default:
2499
                    return false;
2500
                }
2501
                $not = "";
2502
            }
2503
2504
            if (@is_array($uris[$uri])) {
2505
                $uris[$uri] = array_merge($uris[$uri], $list);
2506
            } else {
2507
                $uris[$uri] = $list;
2508
            }
2509
        }
2510
2511
        return $uris;
2512
    }
2513
2514
    /**
2515
     * check if conditions from "If:" headers are meat
2516
     *
2517
     * the "If:" header is an extension to HTTP/1.1
2518
     * defined in RFC 2518 section 9.4
2519
     *
2520
     * @param  void
2521
     * @return void
2522
     */
2523
    function _check_if_header_conditions()
2524
    {
2525
        if (isset($this->_SERVER["HTTP_IF"])) {
2526
            $this->_if_header_uris =
2527
                $this->_if_header_parser($this->_SERVER["HTTP_IF"]);
2528
2529
            foreach ($this->_if_header_uris as $uri => $conditions) {
2530
                if ($uri == "") {
2531
                    $uri = $this->uri;
2532
                }
2533
                // all must match
2534
                $state = true;
2535
                foreach ($conditions as $condition) {
2536
                    // lock tokens may be free form (RFC2518 6.3)
2537
                    // but if opaquelocktokens are used (RFC2518 6.4)
2538
                    // we have to check the format (litmus tests this)
2539
                    if (!strncmp($condition, "<opaquelocktoken:", strlen("<opaquelocktoken"))) {
2540
                        if (!preg_match('/^<opaquelocktoken:[[:xdigit:]]{8}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{12}>$/', $condition)) {
2541
                            $this->http_status("423 Locked");
2542
                            return false;
2543
                        }
2544
                    }
2545
                    if (!$this->_check_uri_condition($uri, $condition)) {
2546
                        $this->http_status("412 Precondition failed");
2547
                        $state = false;
2548
                        break;
2549
                    }
2550
                }
2551
2552
                // any match is ok
2553
                if ($state == true) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
2554
                    return true;
2555
                }
2556
            }
2557
            return false;
2558
        }
2559
        return true;
2560
    }
2561
2562
    /**
2563
     * Check a single URI condition parsed from an if-header
2564
     *
2565
     * Check a single URI condition parsed from an if-header
2566
     *
2567
     * @abstract
2568
     * @param string $uri URI to check
2569
     * @param string $condition Condition to check for this URI
2570
     * @returns bool Condition check result
2571
     */
2572
    function _check_uri_condition($uri, $condition)
2573
    {
2574
		unset($uri);	// not used, but required by function signature
2575
        // not really implemented here,
2576
        // implementations must override
2577
2578
        // a lock token can never be from the DAV: scheme
2579
        // litmus uses DAV:no-lock in some tests
2580
        if (!strncmp("<DAV:", $condition, 5)) {
2581
            return false;
2582
        }
2583
2584
        return true;
2585
    }
2586
2587
2588
    /**
2589
     *
2590
     *
2591
     * @param  string  path of resource to check
0 ignored issues
show
Bug introduced by
The type path was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
2592
     * @param  bool    exclusive lock?
0 ignored issues
show
Bug introduced by
The type exclusive was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
2593
     */
2594
    function _check_lock_status($path, $exclusive_only = false)
2595
    {
2596
        // FIXME depth -> ignored for now
2597
        if (method_exists($this, "checkLock")) {
2598
            // is locked?
2599
            $lock = $this->checkLock($path);
2600
2601
            // ... and lock is not owned?
2602
            if (is_array($lock) && count($lock)) {
2603
                // FIXME doesn't check uri restrictions yet
2604
                if (!isset($this->_SERVER["HTTP_IF"]) || !strstr($this->_SERVER["HTTP_IF"], $lock["token"])) {
2605
                    if (!$exclusive_only || ($lock["scope"] !== "shared"))
2606
                        return false;
2607
                }
2608
            }
2609
        }
2610
        return true;
2611
    }
2612
2613
2614
    // }}}
2615
2616
2617
    /**
2618
     * Generate lockdiscovery reply from checklock() result
2619
     *
2620
     * @param   string  resource path to check
2621
     * @return  string  lockdiscovery response
2622
     */
2623
    function lockdiscovery($path)
2624
    {
2625
        // no lock support without checklock() method
2626
        if (!method_exists($this, "checklock")) {
2627
            return "";
2628
        }
2629
2630
        // collect response here
2631
        $activelocks = "";
2632
2633
        // get checklock() reply
2634
        $lock = $this->checklock($path);
2635
2636
        // generate <activelock> block for returned data
2637
        if (is_array($lock) && count($lock)) {
2638
            // check for 'timeout' or 'expires'
2639
            if (!empty($lock["expires"])) {
2640
                $timeout = "Second-".($lock["expires"] - time());
2641
            } else if (!empty($lock["timeout"])) {
2642
                $timeout = "Second-$lock[timeout]";
2643
            } else {
2644
                $timeout = "Infinite";
2645
            }
2646
2647
            // genreate response block
2648
            if ($this->crrnd)
2649
            {
2650
	            $activelocks.= "
2651
		            <activelock>
2652
		            <lockscope><$lock[scope]/></lockscope>
2653
		            <locktype><$lock[type]/></locktype>
2654
		            <depth>$lock[depth]</depth>
2655
		            <owner>$lock[owner]</owner>
2656
		            <timeout>$timeout</timeout>
2657
		            <locktoken><href>$lock[token]</href></locktoken>
2658
		            </activelock>
2659
		            ";
2660
            }
2661
            else
2662
            {
2663
	            $activelocks.= "
2664
		            <D:activelock>
2665
		            <D:lockscope><D:$lock[scope]/></D:lockscope>
2666
		            <D:locktype><D:$lock[type]/></D:locktype>
2667
		            <D:depth>$lock[depth]</D:depth>
2668
		            <D:owner>$lock[owner]</D:owner>
2669
		            <D:timeout>$timeout</D:timeout>
2670
		            <D:locktoken><D:href>$lock[token]</D:href></D:locktoken>
2671
		            </D:activelock>
2672
		            ";
2673
            }
2674
        }
2675
2676
        // return generated response
2677
        //error_log(__METHOD__."\n".print_r($activelocks,true));
2678
		return $activelocks;
2679
    }
2680
2681
    /**
2682
     * set HTTP return status and mirror it in a private header
2683
     *
2684
     * @param  string  status code and message
2685
     * @return void
2686
     */
2687
    function http_status($status)
2688
    {
2689
        // simplified success case
2690
        if ($status === true) {
2691
            $status = "200 OK";
2692
        }
2693
2694
        // remember status
2695
        $this->_http_status = $status;
2696
2697
        // generate HTTP status response
2698
        header("HTTP/1.1 $status");
2699
        header("X-WebDAV-Status: $status", true);
2700
    }
2701
2702
    /**
2703
     * private URL encoding
2704
	 *
2705
	 * We use now full url-encoding as required by WebDAV RFC and many clients.
2706
	 * Formerly HTTP_WebDAV_Server used to encode only: " %&<>+"
2707
     *
2708
     * @param  string  URL to encode
0 ignored issues
show
Bug introduced by
The type URL was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
2709
     * @return string  encoded URL
2710
     */
2711
    public static function _urlencode($url)
2712
    {
2713
		return strtr(rawurlencode($url),array(
2714
			'%2F' => '/',
2715
			'%3A' => ':',
2716
		));
2717
    }
2718
2719
    /**
2720
     * private version of PHP urldecode
2721
     *
2722
     * not really needed but added for completenes
2723
     *
2724
     * @param  string  URL to decode
2725
     * @return string  decoded URL
2726
     */
2727
    public static function _urldecode($path)
2728
    {
2729
        return rawurldecode($path);
2730
    }
2731
2732
    /**
2733
     * Encode a hierarchical properties like:
2734
     *
2735
 	 * <D:supported-report-set>
2736
	 *    <supported-report>
2737
	 *       <report>
2738
	 *          <addressbook-query xmlns='urn:ietf:params:xml:ns:carddav'/>
2739
	 *       </report>
2740
	 *    </supported-report>
2741
	 *    <supported-report>
2742
	 *       <report>
2743
	 *          <addressbook-multiget xmlns='urn:ietf:params:xml:ns:carddav'/>
2744
	 *       </report>
2745
	 *    </supported-report>
2746
	 * </D:supported-report-set>
2747
     *
2748
     * @param array $props
2749
     * @param string $ns
2750
     * @param strin $ns_defs
2751
     * @param array $ns_hash
2752
     * @return string
2753
     */
2754
	function _hierarchical_prop_encode(array $props, $ns, &$ns_defs, array &$ns_hash)
2755
    {
2756
    	$ret = '';
2757
2758
    	//error_log(__METHOD__.'('.array2string($props).')');
2759
    	if (isset($props['name'])) $props = array($props);
2760
2761
    	foreach($props as $prop)
2762
		{
2763
	    	if (!isset($ns_hash[$prop['ns']])) // unknown namespace
2764
	    	{
2765
		    	// register namespace
2766
		    	$ns_name = 'ns'.(count($ns_hash) + 1);
2767
		    	$ns_hash[$prop['ns']] = $ns_name;
2768
		    	$ns_defs .= ' xmlns:'.$ns_name.'="'.$prop['ns'].'"';
2769
	    	}
2770
	    	if (is_array($prop['val']))
2771
	    	{
2772
	    		$subprop = $prop['val'];
2773
		    	if (isset($subprop['ns']) || isset($subprop[0]['ns']))
2774
		    	{
2775
			    	$ret .= '<'.($prop['ns'] == $ns ? ($this->crrnd ? '' : $ns_hash[$ns].':') : $ns_hash[$prop['ns']].':').$prop['name'].
2776
						(empty($prop['val']) ? '/>' : '>'.$this->_hierarchical_prop_encode($prop['val'], $prop['ns'], $ns_defs, $ns_hash).
2777
						'</'.($prop['ns'] == $ns ? ($this->crrnd ? '' : $ns_hash[$ns].':') : ($this->crrnd ? '' : $ns_hash[$prop['ns']].':')).$prop['name'].'>');
2778
		    	}
2779
		    	else // val contains only attributes, no value
2780
		    	{
2781
			    	$vals = '';
2782
2783
			    	foreach($subprop as $attr => $val)
2784
					{
2785
				    	$vals .= ' '.$attr.'="'.htmlspecialchars($val, ENT_NOQUOTES|ENT_XML1|ENT_DISALLOWED, 'utf-8').'"';
2786
					}
2787
2788
		             $ret .= '<'.($prop['ns'] == $ns ? ($this->crrnd ? '' : $ns_hash[$ns].':') : $ns_hash[$prop['ns']].':').$prop['name'].
2789
				    	$vals .'/>';
2790
		    	}
2791
	    	}
2792
	    	else
2793
	    	{
2794
		    	if (empty($prop['val']))
2795
		    	{
2796
			    	$val = '';
2797
		    	}
2798
		    	else
2799
		    	{
2800
			    	if(isset($prop['raw']))
2801
					{
2802
						$val = $this->_prop_encode('<![CDATA['.$prop['val'].']]>');
2803
					} else {
2804
						$val = $this->_prop_encode(htmlspecialchars($prop['val'], ENT_NOQUOTES, 'utf-8'));
2805
						// do NOT urlencode mailto href, as no clients understands them
2806
						if ($prop['name'] == 'href' && stripos($val, 'mailto:') !== 0)
2807
						{
2808
							$val = $this->_urlencode($val);
2809
						}
2810
					}
2811
		    	}
2812
2813
		    	$ret .= '<'.($prop['ns'] == $ns ? ($this->crrnd ? '' : $ns_hash[$ns].':') : $ns_hash[$prop['ns']].':').$prop['name'].
2814
			    	(empty($prop['val']) ? ' />' : '>'.$val.'</'.
2815
			    	($prop['ns'] == $ns ? ($this->crrnd ? '' : $ns_hash[$ns].':') : ($this->crrnd ? '' : $ns_hash[$prop['ns']].':')).$prop['name'].'>');
2816
	    	}
2817
		}
2818
2819
    	//error_log(__METHOD__.'('.array2string($props).") crrnd=$this->crrnd returning ".array2string($ret));
2820
    	return $ret;
2821
    }
2822
2823
    /**
2824
     * UTF-8 encode property values if not already done so
2825
     *
2826
     * @param  string  text to encode
2827
     * @return string  utf-8 encoded text
2828
     */
2829
    function _prop_encode($text)
2830
    {
2831
		//error_log( __METHOD__."\n" .print_r($text,true));
2832
		//error_log("prop-encode:" . print_r($this->_prop_encoding,true));
2833
2834
		switch (strtolower($this->_prop_encoding)) {
2835
			case "utf-8":
2836
       			//error_log( __METHOD__."allready encoded\n" .print_r($text,true));
2837
				return $text;
2838
			case "iso-8859-1":
2839
			case "iso-8859-15":
2840
			case "latin-1":
2841
			default:
2842
				error_log( __METHOD__."utf8 encode\n" .print_r(utf8_encode($text),true));
2843
				return utf8_encode($text);
2844
        }
2845
    }
2846
2847
    /**
2848
     * Slashify - make sure path ends in a slash
2849
     *
2850
     * @param   string directory path
2851
     * @returns string directory path wiht trailing slash
2852
     */
2853
    public static function _slashify($path)
2854
    {
2855
		//error_log(__METHOD__." called with $path");
2856
		if ($path[self::bytes($path)-1] != '/') {
2857
			//error_log(__METHOD__." added slash at the end of path");
2858
			$path = $path."/";
2859
		}
2860
		return $path;
2861
    }
2862
2863
    /**
2864
     * Unslashify - make sure path doesn't in a slash
2865
     *
2866
     * @param   string directory path
2867
     * @returns string directory path wihtout trailing slash
2868
     */
2869
    public static function _unslashify($path)
2870
    {
2871
        //error_log(__METHOD__." called with $path");
2872
        if ($path[self::bytes($path)-1] == '/') {
2873
            $path = substr($path, 0, -1);
2874
			//error_log(__METHOD__." removed slash at the end of path");
2875
        }
2876
        return $path;
2877
    }
2878
2879
    /**
2880
     * Merge two paths, make sure there is exactly one slash between them
2881
     *
2882
     * @param  string  parent path
2883
     * @param  string  child path
2884
     * @return string  merged path
2885
     */
2886
    public static function _mergePaths($parent, $child)
2887
    {
2888
        //error_log("merge called :\n$parent \n$child\n" . function_backtrace());
2889
        //error_log("merge :\n".print_r($this->_mergePaths($this->_SERVER["SCRIPT_NAME"], $this->path)true));
2890
        if ($child{0} == '/') {
2891
            return self::_unslashify($parent).$child;
2892
        } else {
2893
            return self::_slashify($parent).$child;
2894
        }
2895
    }
2896
2897
    /**
2898
     * mbstring.func_overload save strlen version: counting the bytes not the chars
2899
     *
2900
     * @param string $str
2901
     * @return int
2902
     */
2903
    public static function bytes($str)
2904
    {
2905
    	static $func_overload=null;
2906
2907
    	if (is_null($func_overload))
2908
    	{
2909
    		$func_overload = @extension_loaded('mbstring') ? ini_get('mbstring.func_overload') : 0;
2910
    	}
2911
    	return $func_overload & 2 ? mb_strlen($str,'ascii') : strlen($str);
2912
    }
2913
}
2914
2915
/*
2916
 * Local variables:
2917
 * tab-width: 4
2918
 * c-basic-offset: 4
2919
 * End:
2920
 */
2921