Commit | Line | Data |
---|---|---|
6855525e JL |
1 | <?php |
2 | // | |
3 | // +----------------------------------------------------------------------+ | |
4 | // | PHP Version 4 | | |
5 | // +----------------------------------------------------------------------+ | |
6 | // | Copyright (c) 1997-2003 The PHP Group | | |
7 | // +----------------------------------------------------------------------+ | |
8 | // | This source file is subject to version 2.02 of the PHP license, | | |
9 | // | that is bundled with this package in the file LICENSE, and is | | |
10 | // | available at through the world-wide-web at | | |
11 | // | http://www.php.net/license/2_02.txt. | | |
12 | // | If you did not receive a copy of the PHP license and are unable to | | |
13 | // | obtain it through the world-wide-web, please send a note to | | |
14 | // | license@php.net so we can mail you a copy immediately. | | |
15 | // +----------------------------------------------------------------------+ | |
16 | // | Authors: Hartmut Holzgraefe <hholzgra@php.net> | | |
17 | // | Christian Stocker <chregu@bitflux.ch> | | |
18 | // +----------------------------------------------------------------------+ | |
19 | // | |
20 | // $Id: Server.php,v 1.21 2004/04/14 21:44:26 hholzgra Exp $ | |
21 | // | |
22 | require_once "HTTP/WebDAV/Tools/_parse_propfind.php"; | |
23 | require_once "HTTP/WebDAV/Tools/_parse_proppatch.php"; | |
24 | require_once "HTTP/WebDAV/Tools/_parse_lockinfo.php"; | |
25 | ||
26 | ||
27 | ||
28 | /** | |
29 | * Virtual base class for implementing WebDAV servers | |
30 | * | |
31 | * WebDAV server base class, needs to be extended to do useful work | |
32 | * | |
33 | * @package HTTP_WebDAV_Server | |
34 | * @author Hartmut Holzgraefe <hholzgra@php.net> | |
35 | * @version 0.99.1dev | |
36 | */ | |
37 | class HTTP_WebDAV_Server | |
38 | { | |
39 | // {{{ Member Variables | |
40 | ||
41 | /** | |
42 | * URI path for this request | |
43 | * | |
44 | * @var string | |
45 | */ | |
46 | var $path; | |
47 | ||
48 | /** | |
49 | * Realm string to be used in authentification popups | |
50 | * | |
51 | * @var string | |
52 | */ | |
53 | var $http_auth_realm = "PHP WebDAV"; | |
54 | ||
55 | /** | |
56 | * String to be used in "X-Dav-Powered-By" header | |
57 | * | |
58 | * @var string | |
59 | */ | |
60 | var $dav_powered_by = ""; | |
61 | ||
62 | /** | |
63 | * Remember parsed If: (RFC2518/9.4) header conditions | |
64 | * | |
65 | * @var array | |
66 | */ | |
67 | var $_if_header_uris = array(); | |
68 | ||
69 | /** | |
70 | * HTTP response status/message | |
71 | * | |
72 | * @var string | |
73 | */ | |
74 | var $_http_status = "200 OK"; | |
75 | ||
76 | /** | |
77 | * encoding of property values passed in | |
78 | * | |
79 | * @var string | |
80 | */ | |
81 | var $_prop_encoding = "utf-8"; | |
82 | ||
83 | // }}} | |
84 | ||
85 | // {{{ Constructor | |
86 | ||
87 | /** | |
88 | * Constructor | |
89 | * | |
90 | * @param void | |
91 | */ | |
92 | function HTTP_WebDAV_Server() | |
93 | { | |
94 | // PHP messages destroy XML output -> switch them off | |
95 | ini_set("display_errors", 0); | |
96 | } | |
97 | ||
98 | // }}} | |
99 | ||
100 | // {{{ ServeRequest() | |
101 | /** | |
102 | * Serve WebDAV HTTP request | |
103 | * | |
104 | * dispatch WebDAV HTTP request to the apropriate method handler | |
105 | * | |
106 | * @param void | |
107 | * @return void | |
108 | */ | |
109 | function ServeRequest() | |
110 | { | |
111 | // identify ourselves | |
112 | if (empty($this->dav_powered_by)) { | |
113 | header("X-Dav-Powered-By: PHP class: ".get_class($this)); | |
114 | } else { | |
115 | header("X-Dav-Powered-By: ".$this->dav_powered_by ); | |
116 | } | |
117 | ||
118 | // check authentication | |
119 | if (!$this->_check_auth()) { | |
120 | $this->http_status('401 Unauthorized'); | |
121 | ||
122 | // RFC2518 says we must use Digest instead of Basic | |
123 | // but Microsoft Clients do not support Digest | |
124 | // and we don't support NTLM and Kerberos | |
125 | // so we are stuck with Basic here | |
126 | header('WWW-Authenticate: Basic realm="'.($this->http_auth_realm).'"'); | |
127 | ||
128 | return; | |
129 | } | |
130 | ||
131 | // check | |
132 | if(! $this->_check_if_header_conditions()) { | |
133 | $this->http_status("412 Precondition failed"); | |
134 | return; | |
135 | } | |
136 | ||
137 | // set path | |
138 | $this->path = $this->_urldecode(!empty($_SERVER["PATH_INFO"]) ? $_SERVER["PATH_INFO"] : "/"); | |
139 | if(ini_get("magic_quotes_gpc")) { | |
140 | $this->path = stripslashes($this->path); | |
141 | } | |
142 | ||
143 | ||
144 | // detect requested method names | |
145 | $method = strtolower($_SERVER["REQUEST_METHOD"]); | |
146 | $wrapper = "http_".$method; | |
147 | ||
148 | // activate HEAD emulation by GET if no HEAD method found | |
149 | if ($method == "head" && !method_exists($this, "head")) { | |
150 | $method = "get"; | |
151 | } | |
152 | ||
153 | if (method_exists($this, $wrapper) && ($method == "options" || method_exists($this, $method))) { | |
154 | $this->$wrapper(); // call method by name | |
155 | } else { // method not found/implemented | |
156 | if ($_SERVER["REQUEST_METHOD"] == "LOCK") { | |
157 | $this->http_status("412 Precondition failed"); | |
158 | } else { | |
159 | $this->http_status("405 Method not allowed"); | |
160 | header("Allow: ".join(", ", $this->_allow())); // tell client what's allowed | |
161 | } | |
162 | } | |
163 | } | |
164 | ||
165 | // }}} | |
166 | ||
167 | // {{{ abstract WebDAV methods | |
168 | ||
169 | // {{{ GET() | |
170 | /** | |
171 | * GET implementation | |
172 | * | |
173 | * overload this method to retrieve resources from your server | |
174 | * <br> | |
175 | * | |
176 | * | |
177 | * @abstract | |
178 | * @param array &$params Array of input and output parameters | |
179 | * <br><b>input</b><ul> | |
180 | * <li> path - | |
181 | * </ul> | |
182 | * <br><b>output</b><ul> | |
183 | * <li> size - | |
184 | * </ul> | |
185 | * @returns int HTTP-Statuscode | |
186 | */ | |
187 | ||
188 | /* abstract | |
189 | function GET(&$params) | |
190 | { | |
191 | // dummy entry for PHPDoc | |
192 | } | |
193 | */ | |
194 | ||
195 | // }}} | |
196 | ||
197 | // {{{ PUT() | |
198 | /** | |
199 | * PUT implementation | |
200 | * | |
201 | * PUT implementation | |
202 | * | |
203 | * @abstract | |
204 | * @param array &$params | |
205 | * @returns int HTTP-Statuscode | |
206 | */ | |
207 | ||
208 | /* abstract | |
209 | function PUT() | |
210 | { | |
211 | // dummy entry for PHPDoc | |
212 | } | |
213 | */ | |
214 | ||
215 | // }}} | |
216 | ||
217 | // {{{ COPY() | |
218 | ||
219 | /** | |
220 | * COPY implementation | |
221 | * | |
222 | * COPY implementation | |
223 | * | |
224 | * @abstract | |
225 | * @param array &$params | |
226 | * @returns int HTTP-Statuscode | |
227 | */ | |
228 | ||
229 | /* abstract | |
230 | function COPY() | |
231 | { | |
232 | // dummy entry for PHPDoc | |
233 | } | |
234 | */ | |
235 | ||
236 | // }}} | |
237 | ||
238 | // {{{ MOVE() | |
239 | ||
240 | /** | |
241 | * MOVE implementation | |
242 | * | |
243 | * MOVE implementation | |
244 | * | |
245 | * @abstract | |
246 | * @param array &$params | |
247 | * @returns int HTTP-Statuscode | |
248 | */ | |
249 | ||
250 | /* abstract | |
251 | function MOVE() | |
252 | { | |
253 | // dummy entry for PHPDoc | |
254 | } | |
255 | */ | |
256 | ||
257 | // }}} | |
258 | ||
259 | // {{{ DELETE() | |
260 | ||
261 | /** | |
262 | * DELETE implementation | |
263 | * | |
264 | * DELETE implementation | |
265 | * | |
266 | * @abstract | |
267 | * @param array &$params | |
268 | * @returns int HTTP-Statuscode | |
269 | */ | |
270 | ||
271 | /* abstract | |
272 | function DELETE() | |
273 | { | |
274 | // dummy entry for PHPDoc | |
275 | } | |
276 | */ | |
277 | // }}} | |
278 | ||
279 | // {{{ PROPFIND() | |
280 | ||
281 | /** | |
282 | * PROPFIND implementation | |
283 | * | |
284 | * PROPFIND implementation | |
285 | * | |
286 | * @abstract | |
287 | * @param array &$params | |
288 | * @returns int HTTP-Statuscode | |
289 | */ | |
290 | ||
291 | /* abstract | |
292 | function PROPFIND() | |
293 | { | |
294 | // dummy entry for PHPDoc | |
295 | } | |
296 | */ | |
297 | ||
298 | // }}} | |
299 | ||
300 | // {{{ PROPPATCH() | |
301 | ||
302 | /** | |
303 | * PROPPATCH implementation | |
304 | * | |
305 | * PROPPATCH implementation | |
306 | * | |
307 | * @abstract | |
308 | * @param array &$params | |
309 | * @returns int HTTP-Statuscode | |
310 | */ | |
311 | ||
312 | /* abstract | |
313 | function PROPPATCH() | |
314 | { | |
315 | // dummy entry for PHPDoc | |
316 | } | |
317 | */ | |
318 | // }}} | |
319 | ||
320 | // {{{ LOCK() | |
321 | ||
322 | /** | |
323 | * LOCK implementation | |
324 | * | |
325 | * LOCK implementation | |
326 | * | |
327 | * @abstract | |
328 | * @param array &$params | |
329 | * @returns int HTTP-Statuscode | |
330 | */ | |
331 | ||
332 | /* abstract | |
333 | function LOCK() | |
334 | { | |
335 | // dummy entry for PHPDoc | |
336 | } | |
337 | */ | |
338 | // }}} | |
339 | ||
340 | // {{{ UNLOCK() | |
341 | ||
342 | /** | |
343 | * UNLOCK implementation | |
344 | * | |
345 | * UNLOCK implementation | |
346 | * | |
347 | * @abstract | |
348 | * @param array &$params | |
349 | * @returns int HTTP-Statuscode | |
350 | */ | |
351 | ||
352 | /* abstract | |
353 | function UNLOCK() | |
354 | { | |
355 | // dummy entry for PHPDoc | |
356 | } | |
357 | */ | |
358 | // }}} | |
359 | ||
360 | // }}} | |
361 | ||
362 | // {{{ other abstract methods | |
363 | ||
364 | // {{{ check_auth() | |
365 | ||
366 | /** | |
367 | * check authentication | |
368 | * | |
369 | * overload this method to retrieve and confirm authentication information | |
370 | * | |
371 | * @abstract | |
372 | * @param string type Authentication type, e.g. "basic" or "digest" | |
373 | * @param string username Transmitted username | |
374 | * @param string passwort Transmitted password | |
375 | * @returns bool Authentication status | |
376 | */ | |
377 | ||
378 | /* abstract | |
379 | function checkAuth($type, $username, $password) | |
380 | { | |
381 | // dummy entry for PHPDoc | |
382 | } | |
383 | */ | |
384 | ||
385 | // }}} | |
386 | ||
387 | // {{{ checklock() | |
388 | ||
389 | /** | |
390 | * check lock status for a resource | |
391 | * | |
392 | * overload this method to return shared and exclusive locks | |
393 | * active for this resource | |
394 | * | |
395 | * @abstract | |
396 | * @param string resource Resource path to check | |
397 | * @returns array An array of lock entries each consisting | |
398 | * of 'type' ('shared'/'exclusive'), 'token' and 'timeout' | |
399 | */ | |
400 | ||
401 | /* abstract | |
402 | function checklock($resource) | |
403 | { | |
404 | // dummy entry for PHPDoc | |
405 | } | |
406 | */ | |
407 | ||
408 | // }}} | |
409 | ||
410 | // }}} | |
411 | ||
412 | // {{{ WebDAV HTTP method wrappers | |
413 | ||
414 | // {{{ http_OPTIONS() | |
415 | ||
416 | /** | |
417 | * OPTIONS method handler | |
418 | * | |
419 | * The OPTIONS method handler creates a valid OPTIONS reply | |
420 | * including Dav: and Allowed: heaers | |
421 | * based on the implemented methods found in the actual instance | |
422 | * | |
423 | * @param void | |
424 | * @return void | |
425 | */ | |
426 | function http_OPTIONS() | |
427 | { | |
428 | // Microsoft clients default to the Frontpage protocol | |
429 | // unless we tell them to use WebDAV | |
430 | header("MS-Author-Via: DAV"); | |
431 | ||
432 | // get allowed methods | |
433 | $allow = $this->_allow(); | |
434 | ||
435 | // dav header | |
436 | $dav = array(1); // assume we are always dav class 1 compliant | |
437 | if (isset($allow['LOCK'])) { | |
438 | $dav[] = 2; // dav class 2 requires that locking is supported | |
439 | } | |
440 | ||
441 | // tell clients what we found | |
442 | $this->http_status("200 OK"); | |
443 | header("DAV: " .join("," , $dav)); | |
444 | header("Allow: ".join(", ", $allow)); | |
445 | } | |
446 | ||
447 | // }}} | |
448 | ||
449 | ||
450 | // {{{ http_PROPFIND() | |
451 | ||
452 | /** | |
453 | * PROPFIND method handler | |
454 | * | |
455 | * @param void | |
456 | * @return void | |
457 | */ | |
458 | function http_PROPFIND() | |
459 | { | |
460 | $options = Array(); | |
461 | $options["path"] = $this->path; | |
462 | ||
463 | // search depth from header (default is "infinity) | |
464 | if (isset($_SERVER['HTTP_DEPTH'])) { | |
465 | $options["depth"] = $_SERVER["HTTP_DEPTH"]; | |
466 | } else { | |
467 | $options["depth"] = "infinity"; | |
468 | } | |
469 | ||
470 | // analyze request payload | |
471 | $propinfo = new _parse_propfind("php://input"); | |
472 | if (!$propinfo->success) { | |
473 | $this->http_status("400 Error"); | |
474 | return; | |
475 | } | |
476 | $options['props'] = $propinfo->props; | |
477 | ||
478 | // call user handler | |
479 | if (!$this->propfind($options, $files)) { | |
480 | $this->http_status("404 Not Found"); | |
481 | return; | |
482 | } | |
483 | ||
484 | // collect namespaces here | |
485 | $ns_hash = array(); | |
486 | ||
487 | // Microsoft Clients need this special namespace for date and time values | |
488 | $ns_defs = "xmlns:ns0=\"urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/\""; | |
489 | ||
490 | // now we loop over all returned file entries | |
491 | foreach($files["files"] as $filekey => $file) { | |
492 | ||
493 | // nothing to do if no properties were returend for a file | |
494 | if (!isset($file["props"]) || !is_array($file["props"])) { | |
495 | continue; | |
496 | } | |
497 | ||
498 | // now loop over all returned properties | |
499 | foreach($file["props"] as $key => $prop) { | |
500 | // as a convenience feature we do not require that user handlers | |
501 | // restrict returned properties to the requested ones | |
502 | // here we strip all unrequested entries out of the response | |
503 | ||
504 | switch($options['props']) { | |
505 | case "all": | |
506 | // nothing to remove | |
507 | break; | |
508 | ||
509 | case "names": | |
510 | // only the names of all existing properties were requested | |
511 | // so we remove all values | |
512 | unset($files["files"][$filekey]["props"][$key]["val"]); | |
513 | break; | |
514 | ||
515 | default: | |
516 | $found = false; | |
517 | ||
518 | // search property name in requested properties | |
519 | foreach((array)$options["props"] as $reqprop) { | |
520 | if ( $reqprop["name"] == $prop["name"] | |
521 | && $reqprop["xmlns"] == $prop["ns"]) { | |
522 | $found = true; | |
523 | break; | |
524 | } | |
525 | } | |
526 | ||
527 | // unset property and continue with next one if not found/requested | |
528 | if (!$found) { | |
529 | $files["files"][$filekey]["props"][$key]=""; | |
530 | continue(2); | |
531 | } | |
532 | break; | |
533 | } | |
534 | ||
535 | // namespace handling | |
536 | if (empty($prop["ns"])) continue; // no namespace | |
537 | $ns = $prop["ns"]; | |
538 | if ($ns == "DAV:") continue; // default namespace | |
539 | if (isset($ns_hash[$ns])) continue; // already known | |
540 | ||
541 | // register namespace | |
542 | $ns_name = "ns".(count($ns_hash) + 1); | |
543 | $ns_hash[$ns] = $ns_name; | |
544 | $ns_defs .= " xmlns:$ns_name=\"$ns\""; | |
545 | } | |
546 | ||
547 | // we also need to add empty entries for properties that were requested | |
548 | // but for which no values where returned by the user handler | |
549 | if (is_array($options['props'])) { | |
550 | foreach($options["props"] as $reqprop) { | |
551 | if($reqprop['name']=="") continue; // skip empty entries | |
552 | ||
553 | $found = false; | |
554 | ||
555 | // check if property exists in result | |
556 | foreach($file["props"] as $prop) { | |
557 | if ( $reqprop["name"] == $prop["name"] | |
558 | && $reqprop["xmlns"] == $prop["ns"]) { | |
559 | $found = true; | |
560 | break; | |
561 | } | |
562 | } | |
563 | ||
564 | if (!$found) { | |
565 | if($reqprop["xmlns"]==="DAV:" && $reqprop["name"]==="lockdiscovery") { | |
566 | // lockdiscovery is handled by the base class | |
567 | $files["files"][$filekey]["props"][] | |
568 | = $this->mkprop("DAV:", | |
569 | "lockdiscovery" , | |
570 | $this->lockdiscovery($files["files"][$filekey]['path'])); | |
571 | } else { | |
572 | // add empty value for this property | |
573 | $files["files"][$filekey]["noprops"][] = | |
574 | $this->mkprop($reqprop["xmlns"], $reqprop["name"], ""); | |
575 | ||
576 | // register property namespace if not known yet | |
577 | if ($reqprop["xmlns"] != "DAV:" && !isset($ns_hash[$reqprop["xmlns"]])) { | |
578 | $ns_name = "ns".(count($ns_hash) + 1); | |
579 | $ns_hash[$reqprop["xmlns"]] = $ns_name; | |
580 | $ns_defs .= " xmlns:$ns_name=\"$reqprop[xmlns]\""; | |
581 | } | |
582 | } | |
583 | } | |
584 | } | |
585 | } | |
586 | } | |
587 | ||
588 | // now we generate the reply header ... | |
589 | $this->http_status("207 Multi-Status"); | |
590 | header('Content-Type: text/xml; charset="utf-8"'); | |
591 | ||
592 | // ... and payload | |
593 | echo "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"; | |
594 | echo "<D:multistatus xmlns:D=\"DAV:\">\n"; | |
595 | ||
596 | foreach($files["files"] as $file) { | |
597 | // ignore empty or incomplete entries | |
598 | if(!is_array($file) || empty($file) || !isset($file["path"])) continue; | |
599 | $path = $file['path']; | |
600 | if(!is_string($path) || $path==="") continue; | |
601 | ||
602 | echo " <D:response $ns_defs>\n"; | |
603 | ||
604 | $href = (@$_SERVER["HTTPS"] === "on" ? "https:" : "http:"); | |
605 | $href.= "//".$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME']; | |
606 | $href.= $path; | |
607 | //TODO make sure collection resource pathes end in a trailing slash | |
608 | ||
609 | echo " <D:href>$href</D:href>\n"; | |
610 | ||
611 | // report all found properties and their values (if any) | |
612 | if (isset($file["props"]) && is_array($file["props"])) { | |
613 | echo " <D:propstat>\n"; | |
614 | echo " <D:prop>\n"; | |
615 | ||
616 | foreach($file["props"] as $key => $prop) { | |
617 | ||
618 | if (!is_array($prop)) continue; | |
619 | if (!isset($prop["name"])) continue; | |
620 | ||
621 | if (!isset($prop["val"]) || $prop["val"] === "" || $prop["val"] === false) { | |
622 | // empty properties (cannot use empty() for check as "0" is a legal value here) | |
623 | if($prop["ns"]=="DAV:") { | |
624 | echo " <D:$prop[name]/>\n"; | |
625 | } else if(!empty($prop["ns"])) { | |
626 | echo " <".$ns_hash[$prop["ns"]].":$prop[name]/>\n"; | |
627 | } else { | |
628 | echo " <$prop[name] xmlns=\"\"/>"; | |
629 | } | |
630 | } else if ($prop["ns"] == "DAV:") { | |
631 | // some WebDAV properties need special treatment | |
632 | switch ($prop["name"]) { | |
633 | case "creationdate": | |
634 | echo " <D:creationdate ns0:dt=\"dateTime.tz\">" | |
635 | . gmdate("Y-m-d\\TH:i:s\\Z",$prop['val']) | |
636 | . "</D:creationdate>\n"; | |
637 | break; | |
638 | case "getlastmodified": | |
639 | echo " <D:getlastmodified ns0:dt=\"dateTime.rfc1123\">" | |
640 | . gmdate("D, d M Y H:i:s ", $prop['val']) | |
641 | . "GMT</D:getlastmodified>\n"; | |
642 | break; | |
643 | case "resourcetype": | |
644 | echo " <D:resourcetype><D:$prop[val]/></D:resourcetype>\n"; | |
645 | break; | |
646 | case "supportedlock": | |
647 | echo " <D:supportedlock>$prop[val]</D:supportedlock>\n"; | |
648 | break; | |
649 | case "lockdiscovery": | |
650 | echo " <D:lockdiscovery>\n"; | |
651 | echo $prop["val"]; | |
652 | echo " </D:lockdiscovery>\n"; | |
653 | break; | |
654 | default: | |
655 | echo " <D:$prop[name]>" | |
656 | . $this->_prop_encode(htmlspecialchars($prop['val'])) | |
657 | . "</D:$prop[name]>\n"; | |
658 | break; | |
659 | } | |
660 | } else { | |
661 | // properties from namespaces != "DAV:" or without any namespace | |
662 | if ($prop["ns"]) { | |
663 | echo " <" . $ns_hash[$prop["ns"]] . ":$prop[name]>" | |
664 | . $this->_prop_encode(htmlspecialchars($prop['val'])) | |
665 | . "</" . $ns_hash[$prop["ns"]] . ":$prop[name]>\n"; | |
666 | } else { | |
667 | echo " <$prop[name] xmlns=\"\">" | |
668 | . $this->_prop_encode(htmlspecialchars($prop['val'])) | |
669 | . "</$prop[name]>\n"; | |
670 | } | |
671 | } | |
672 | } | |
673 | ||
674 | echo " </D:prop>\n"; | |
675 | echo " <D:status>HTTP/1.1 200 OK</D:status>\n"; | |
676 | echo " </D:propstat>\n"; | |
677 | } | |
678 | ||
679 | // now report all properties requested bot not found | |
680 | if (isset($file["noprops"])) { | |
681 | echo " <D:propstat>\n"; | |
682 | echo " <D:prop>\n"; | |
683 | ||
684 | foreach($file["noprops"] as $key => $prop) { | |
685 | if ($prop["ns"] == "DAV:") { | |
686 | echo " <D:$prop[name]/>\n"; | |
687 | } else if ($prop["ns"] == "") { | |
688 | echo " <$prop[name] xmlns=\"\"/>\n"; | |
689 | } else { | |
690 | echo " <" . $ns_hash[$prop["ns"]] . ":$prop[name]/>\n"; | |
691 | } | |
692 | } | |
693 | ||
694 | echo " </D:prop>\n"; | |
695 | echo " <D:status>HTTP/1.1 404 Not Found</D:status>\n"; | |
696 | echo " </D:propstat>\n"; | |
697 | } | |
698 | ||
699 | echo " </D:response>\n"; | |
700 | } | |
701 | ||
702 | echo "</D:multistatus>\n"; | |
703 | } | |
704 | ||
705 | ||
706 | // }}} | |
707 | ||
708 | // {{{ http_PROPPATCH() | |
709 | ||
710 | /** | |
711 | * PROPPATCH method handler | |
712 | * | |
713 | * @param void | |
714 | * @return void | |
715 | */ | |
716 | function http_PROPPATCH() | |
717 | { | |
718 | if($this->_check_lock_status($this->path)) { | |
719 | $options = Array(); | |
720 | $options["path"] = $this->path; | |
721 | ||
722 | $propinfo = new _parse_proppatch("php://input"); | |
723 | ||
724 | if (!$propinfo->success) { | |
725 | $this->http_status("400 Error"); | |
726 | return; | |
727 | } | |
728 | ||
729 | $options['props'] = $propinfo->props; | |
730 | ||
731 | $responsedescr = $this->proppatch($options); | |
732 | ||
733 | $this->http_status("207 Multi-Status"); | |
734 | header('Content-Type: text/xml; charset="utf-8"'); | |
735 | ||
736 | echo "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"; | |
737 | ||
738 | echo "<D:multistatus xmlns:D=\"DAV:\">\n"; | |
739 | echo " <D:response>\n"; | |
740 | echo " <D:href>".$this->_urlencode($_SERVER["SCRIPT_NAME"].$this->path)."</D:href>\n"; | |
741 | ||
742 | foreach($options["props"] as $prop) { | |
743 | echo " <D:propstat>\n"; | |
744 | echo " <D:prop><$prop[name] xmlns=\"$prop[ns]\"/></D:prop>\n"; | |
745 | echo " <D:status>HTTP/1.1 $prop[status]</D:status>\n"; | |
746 | echo " </D:propstat>\n"; | |
747 | } | |
748 | ||
749 | if ($responsedescr) { | |
750 | echo " <D:responsedescription>". | |
751 | $this->_prop_encode(htmlspecialchars($responsedescr)). | |
752 | "</D:responsedescription>\n"; | |
753 | } | |
754 | ||
755 | echo " </D:response>\n"; | |
756 | echo "</D:multistatus>\n"; | |
757 | } else { | |
758 | $this->http_status("423 Locked"); | |
759 | } | |
760 | } | |
761 | ||
762 | // }}} | |
763 | ||
764 | ||
765 | // {{{ http_MKCOL() | |
766 | ||
767 | /** | |
768 | * MKCOL method handler | |
769 | * | |
770 | * @param void | |
771 | * @return void | |
772 | */ | |
773 | function http_MKCOL() | |
774 | { | |
775 | $options = Array(); | |
776 | $options["path"] = $this->path; | |
777 | ||
778 | $stat = $this->mkcol($options); | |
779 | ||
780 | $this->http_status($stat); | |
781 | } | |
782 | ||
783 | // }}} | |
784 | ||
785 | ||
786 | // {{{ http_GET() | |
787 | ||
788 | /** | |
789 | * GET method handler | |
790 | * | |
791 | * @param void | |
792 | * @returns void | |
793 | */ | |
794 | function http_GET() | |
795 | { | |
796 | // TODO check for invalid stream | |
797 | $options = Array(); | |
798 | $options["path"] = $this->path; | |
799 | ||
800 | $this->_get_ranges($options); | |
801 | ||
802 | if (true === ($status = $this->get($options))) { | |
803 | if (!headers_sent()) { | |
804 | $status = "200 OK"; | |
805 | ||
806 | if (!isset($options['mimetype'])) { | |
807 | $options['mimetype'] = "application/octet-stream"; | |
808 | } | |
809 | header("Content-type: $options[mimetype]"); | |
810 | ||
811 | if (isset($options['mtime'])) { | |
812 | header("Last-modified:".gmdate("D, d M Y H:i:s ", $options['mtime'])."GMT"); | |
813 | } | |
814 | ||
815 | if (isset($options['stream'])) { | |
816 | // GET handler returned a stream | |
817 | if (!empty($options['ranges']) && (0===fseek($options['stream'], 0, SEEK_SET))) { | |
818 | // partial request and stream is seekable | |
819 | ||
820 | if (count($options['ranges']) === 1) { | |
821 | $range = $options['ranges'][0]; | |
822 | ||
823 | if (isset($range['start'])) { | |
824 | fseek($options['stream'], $range['start'], SEEK_SET); | |
825 | if (feof($options['stream'])) { | |
826 | http_status("416 Requested range not satisfiable"); | |
827 | exit; | |
828 | } | |
829 | ||
830 | if (isset($range['end'])) { | |
831 | $size = $range['end']-$range['start']+1; | |
832 | http_status("206 partial"); | |
833 | header("Content-length: $size"); | |
834 | header("Content-range: $range[start]-$range[end]/" | |
835 | . (isset($options['size']) ? $options['size'] : "*")); | |
836 | while ($size && !feof($options['stream'])) { | |
837 | $buffer = fread($options['stream'], 4096); | |
838 | $size -= strlen($buffer); | |
839 | echo $buffer; | |
840 | } | |
841 | } else { | |
842 | http_status("206 partial"); | |
843 | if (isset($options['size'])) { | |
844 | header("Content-length: ".($options['size'] - $range['start'])); | |
845 | header("Content-range: $start-$end/" | |
846 | . (isset($options['size']) ? $options['size'] : "*")); | |
847 | } | |
848 | fpassthru($options['stream']); | |
849 | } | |
850 | } else { | |
851 | header("Content-length: ".$range['last']); | |
852 | fseek($options['stream'], -$range['last'], SEEK_END); | |
853 | fpassthru($options['stream']); | |
854 | } | |
855 | } else { | |
856 | $this->_multipart_byterange_header(); // init multipart | |
857 | foreach ($options['ranges'] as $range) { | |
858 | // TODO what if size unknown? 500? | |
859 | if (isset($range['start'])) { | |
860 | $from = $range['start']; | |
861 | $to = !empty($range['end']) ? $range['end'] : $options['size']-1; | |
862 | } else { | |
863 | $from = $options['size'] - $range['last']-1; | |
864 | $to = $options['size'] -1; | |
865 | } | |
866 | $total = isset($options['size']) ? $options['size'] : "*"; | |
867 | $size = $to - $from + 1; | |
868 | $this->_multipart_byterange_header($options['mimetype'], $from, $to, $total); | |
869 | ||
870 | ||
871 | fseek($options['stream'], $start, SEEK_SET); | |
872 | while ($size && !feof($options['stream'])) { | |
873 | $buffer = fread($options['stream'], 4096); | |
874 | $size -= strlen($buffer); | |
875 | echo $buffer; | |
876 | } | |
877 | } | |
878 | $this->_multipart_byterange_header(); // end multipart | |
879 | } | |
880 | } else { | |
881 | // normal request or stream isn't seekable, return full content | |
882 | if (isset($options['size'])) { | |
883 | header("Content-length: ".$options['size']); | |
884 | } | |
885 | fpassthru($options['stream']); | |
886 | return; // no more headers | |
887 | } | |
888 | } elseif (isset($options['data'])) { | |
889 | if (is_array($options['data'])) { | |
890 | // reply to partial request | |
891 | } else { | |
892 | header("Content-length: ".strlen($options['data'])); | |
893 | echo $options['data']; | |
894 | } | |
895 | } | |
896 | } | |
897 | } | |
898 | ||
899 | if (false === $status) { | |
900 | $this->http_status("404 not found"); | |
901 | } | |
902 | ||
903 | $this->http_status("$status"); | |
904 | } | |
905 | ||
906 | ||
907 | /** | |
908 | * parse HTTP Range: header | |
909 | * | |
910 | * @param array options array to store result in | |
911 | * @return void | |
912 | */ | |
913 | function _get_ranges(&$options) | |
914 | { | |
915 | // process Range: header if present | |
916 | if (isset($_SERVER['HTTP_RANGE'])) { | |
917 | ||
918 | // we only support standard "bytes" range specifications for now | |
919 | if (ereg("bytes[[:space:]]*=[[:space:]]*(.+)", $_SERVER['HTTP_RANGE'], $matches)) { | |
920 | $options["ranges"] = array(); | |
921 | ||
922 | // ranges are comma separated | |
923 | foreach (explode(",", $matches[1]) as $range) { | |
924 | // ranges are either from-to pairs or just end positions | |
925 | list($start, $end) = explode("-", $range); | |
926 | $options["ranges"][] = ($start==="") | |
927 | ? array("last"=>$end) | |
928 | : array("start"=>$start, "end"=>$end); | |
929 | } | |
930 | } | |
931 | } | |
932 | } | |
933 | ||
934 | /** | |
935 | * generate separator headers for multipart response | |
936 | * | |
937 | * first and last call happen without parameters to generate | |
938 | * the initial header and closing sequence, all calls inbetween | |
939 | * require content mimetype, start and end byte position and | |
940 | * optionaly the total byte length of the requested resource | |
941 | * | |
942 | * @param string mimetype | |
943 | * @param int start byte position | |
944 | * @param int end byte position | |
945 | * @param int total resource byte size | |
946 | */ | |
947 | function _multipart_byterange_header($mimetype = false, $from = false, $to=false, $total=false) | |
948 | { | |
949 | if ($mimetype === false) { | |
950 | if (!isset($this->multipart_separator)) { | |
951 | // initial | |
952 | ||
953 | // a little naive, this sequence *might* be part of the content | |
954 | // but it's really not likely and rather expensive to check | |
955 | $this->multipart_separator = "SEPARATOR_".md5(microtime()); | |
956 | ||
957 | // generate HTTP header | |
958 | header("Content-type: multipart/byteranges; boundary=".$this->multipart_separator); | |
959 | } else { | |
960 | // final | |
961 | ||
962 | // generate closing multipart sequence | |
963 | echo "\n--{$this->multipart_separator}--"; | |
964 | } | |
965 | } else { | |
966 | // generate separator and header for next part | |
967 | echo "\n--{$this->multipart_separator}\n"; | |
968 | echo "Content-type: $mimetype\n"; | |
969 | echo "Content-range: $from-$to/". ($total === false ? "*" : $total); | |
970 | echo "\n\n"; | |
971 | } | |
972 | } | |
973 | ||
974 | ||
975 | ||
976 | // }}} | |
977 | ||
978 | // {{{ http_HEAD() | |
979 | ||
980 | /** | |
981 | * HEAD method handler | |
982 | * | |
983 | * @param void | |
984 | * @return void | |
985 | */ | |
986 | function http_HEAD() | |
987 | { | |
988 | $status = false; | |
989 | $options = Array(); | |
990 | $options["path"] = $this->path; | |
991 | ||
992 | if (method_exists($this, "HEAD")) { | |
993 | $status = $this->head($options); | |
994 | } else if (method_exists($this, "GET")) { | |
995 | ob_start(); | |
996 | $status = $this->GET($options); | |
997 | ob_end_clean(); | |
998 | } | |
999 | ||
1000 | if($status===true) $status = "200 OK"; | |
1001 | if($status===false) $status = "404 Not found"; | |
1002 | ||
1003 | $this->http_status($status); | |
1004 | } | |
1005 | ||
1006 | // }}} | |
1007 | ||
1008 | // {{{ http_PUT() | |
1009 | ||
1010 | /** | |
1011 | * PUT method handler | |
1012 | * | |
1013 | * @param void | |
1014 | * @return void | |
1015 | */ | |
1016 | function http_PUT() | |
1017 | { | |
1018 | if ($this->_check_lock_status($this->path)) { | |
1019 | $options = Array(); | |
1020 | $options["path"] = $this->path; | |
1021 | $options["content_length"] = $_SERVER["CONTENT_LENGTH"]; | |
1022 | ||
1023 | // get the Content-type | |
1024 | if (isset($_SERVER["CONTENT_TYPE"])) { | |
1025 | // for now we do not support any sort of multipart requests | |
1026 | if (!strncmp($_SERVER["CONTENT_TYPE"], "multipart/", 10)) { | |
1027 | $this->http_status("501 not implemented"); | |
1028 | echo "The service does not support mulipart PUT requests"; | |
1029 | return; | |
1030 | } | |
1031 | $options["content_type"] = $_SERVER["CONTENT_TYPE"]; | |
1032 | } else { | |
1033 | // default content type if none given | |
1034 | $options["content_type"] = "application/octet-stream"; | |
1035 | } | |
1036 | ||
1037 | /* RFC 2616 2.6 says: "The recipient of the entity MUST NOT | |
1038 | ignore any Content-* (e.g. Content-Range) headers that it | |
1039 | does not understand or implement and MUST return a 501 | |
1040 | (Not Implemented) response in such cases." | |
1041 | */ | |
1042 | foreach ($_SERVER as $key => $val) { | |
1043 | if (strncmp($key, "HTTP_CONTENT", 11)) continue; | |
1044 | switch ($key) { | |
1045 | case 'HTTP_CONTENT_ENCODING': // RFC 2616 14.11 | |
1046 | // TODO support this if ext/zlib filters are available | |
1047 | $this->http_status("501 not implemented"); | |
1048 | echo "The service does not support '$val' content encoding"; | |
1049 | return; | |
1050 | ||
1051 | case 'HTTP_CONTENT_LANGUAGE': // RFC 2616 14.12 | |
1052 | // we assume it is not critical if this one is ignored | |
1053 | // in the actual PUT implementation ... | |
1054 | $options["content_language"] = $value; | |
1055 | break; | |
1056 | ||
1057 | case 'HTTP_CONTENT_LOCATION': // RFC 2616 14.14 | |
1058 | /* The meaning of the Content-Location header in PUT | |
1059 | or POST requests is undefined; servers are free | |
1060 | to ignore it in those cases. */ | |
1061 | break; | |
1062 | ||
1063 | case 'HTTP_CONTENT_RANGE': // RFC 2616 14.16 | |
1064 | // single byte range requests are supported | |
1065 | // the header format is also specified in RFC 2616 14.16 | |
1066 | // TODO we have to ensure that implementations support this or send 501 instead | |
1067 | if (!preg_match('@bytes\s+(\d+)-(\d+)/((\d+)|\*)@', $value, $matches)) { | |
1068 | $this->http_status("400 bad request"); | |
1069 | echo "The service does only support single byte ranges"; | |
1070 | return; | |
1071 | } | |
1072 | ||
1073 | $range = array("start"=>$matches[1], "end"=>$matches[2]); | |
1074 | if (is_numeric($matches[3])) { | |
1075 | $range["total_length"] = $matches[3]; | |
1076 | } | |
1077 | $option["ranges"][] = $range; | |
1078 | ||
1079 | // TODO make sure the implementation supports partial PUT | |
1080 | // this has to be done in advance to avoid data being overwritten | |
1081 | // on implementations that do not support this ... | |
1082 | break; | |
1083 | ||
1084 | case 'HTTP_CONTENT_MD5': // RFC 2616 14.15 | |
1085 | // TODO: maybe we can just pretend here? | |
1086 | $this->http_status("501 not implemented"); | |
1087 | echo "The service does not support content MD5 checksum verification"; | |
1088 | return; | |
1089 | ||
1090 | default: | |
1091 | // any other unknown Content-* headers | |
1092 | $this->http_status("501 not implemented"); | |
1093 | echo "The service does not support '$key'"; | |
1094 | return; | |
1095 | } | |
1096 | } | |
1097 | ||
1098 | $options["stream"] = fopen("php://input", "r"); | |
1099 | ||
1100 | $stat = $this->PUT($options); | |
1101 | ||
1102 | if (is_resource($stat) && get_resource_type($stat) == "stream") { | |
1103 | $stream = $stat; | |
1104 | if (!empty($options["ranges"])) { | |
1105 | // TODO multipart support is missing (see also above) | |
1106 | // TODO error checking | |
1107 | $stat = fseek($stream, $range[0]["start"], SEEK_SET); | |
1108 | fwrite($stream, fread($options["stream"], $range[0]["end"]-$range[0]["start"]+1)); | |
1109 | } else { | |
1110 | while (!feof($options["stream"])) { | |
1111 | fwrite($stream, fread($options["stream"], 4096)); | |
1112 | } | |
1113 | } | |
1114 | fclose($stream); | |
1115 | ||
1116 | $stat = $options["new"] ? "201 Created" : "204 No Content"; | |
1117 | } | |
1118 | ||
1119 | $this->http_status($stat); | |
1120 | } else { | |
1121 | $this->http_status("423 Locked"); | |
1122 | } | |
1123 | } | |
1124 | ||
1125 | // }}} | |
1126 | ||
1127 | ||
1128 | // {{{ http_DELETE() | |
1129 | ||
1130 | /** | |
1131 | * DELETE method handler | |
1132 | * | |
1133 | * @param void | |
1134 | * @return void | |
1135 | */ | |
1136 | function http_DELETE() | |
1137 | { | |
1138 | // check RFC 2518 Section 9.2, last paragraph | |
1139 | if (isset($_SERVER["HTTP_DEPTH"])) { | |
1140 | if ($_SERVER["HTTP_DEPTH"] != "infinity") { | |
1141 | $this->http_status("400 Bad Request"); | |
1142 | return; | |
1143 | } | |
1144 | } | |
1145 | ||
1146 | // check lock status | |
1147 | if ($this->_check_lock_status($this->path)) { | |
1148 | // ok, proceed | |
1149 | $options = Array(); | |
1150 | $options["path"] = $this->path; | |
1151 | ||
1152 | $stat = $this->delete($options); | |
1153 | ||
1154 | $this->http_status($stat); | |
1155 | } else { | |
1156 | // sorry, its locked | |
1157 | $this->http_status("423 Locked"); | |
1158 | } | |
1159 | } | |
1160 | ||
1161 | // }}} | |
1162 | ||
1163 | // {{{ http_COPY() | |
1164 | ||
1165 | /** | |
1166 | * COPY method handler | |
1167 | * | |
1168 | * @param void | |
1169 | * @return void | |
1170 | */ | |
1171 | function http_COPY() | |
1172 | { | |
1173 | // no need to check source lock status here | |
1174 | // destination lock status is always checked by the helper method | |
1175 | $this->_copymove("copy"); | |
1176 | } | |
1177 | ||
1178 | // }}} | |
1179 | ||
1180 | // {{{ http_MOVE() | |
1181 | ||
1182 | /** | |
1183 | * MOVE method handler | |
1184 | * | |
1185 | * @param void | |
1186 | * @return void | |
1187 | */ | |
1188 | function http_MOVE() | |
1189 | { | |
1190 | if ($this->_check_lock_status($this->path)) { | |
1191 | // destination lock status is always checked by the helper method | |
1192 | $this->_copymove("move"); | |
1193 | } else { | |
1194 | $this->http_status("423 Locked"); | |
1195 | } | |
1196 | } | |
1197 | ||
1198 | // }}} | |
1199 | ||
1200 | ||
1201 | // {{{ http_LOCK() | |
1202 | ||
1203 | /** | |
1204 | * LOCK method handler | |
1205 | * | |
1206 | * @param void | |
1207 | * @return void | |
1208 | */ | |
1209 | function http_LOCK() | |
1210 | { | |
1211 | $options = Array(); | |
1212 | $options["path"] = $this->path; | |
1213 | ||
1214 | if (isset($_SERVER['HTTP_DEPTH'])) { | |
1215 | $options["depth"] = $_SERVER["HTTP_DEPTH"]; | |
1216 | } else { | |
1217 | $options["depth"] = "infinity"; | |
1218 | } | |
1219 | ||
1220 | if (isset($_SERVER["HTTP_TIMEOUT"])) { | |
1221 | $options["timeout"] = explode(",", $_SERVER["HTTP_TIMEOUT"]); | |
1222 | } | |
1223 | ||
1224 | if(empty($_SERVER['CONTENT_LENGTH']) && !empty($_SERVER['HTTP_IF'])) { | |
1225 | // check if locking is possible | |
1226 | if(!$this->_check_lock_status($this->path)) { | |
1227 | $this->http_status("423 Locked"); | |
1228 | return; | |
1229 | } | |
1230 | ||
1231 | // refresh lock | |
1232 | $options["update"] = substr($_SERVER['HTTP_IF'], 2, -2); | |
1233 | $stat = $this->lock($options); | |
1234 | } else { | |
1235 | // extract lock request information from request XML payload | |
1236 | $lockinfo = new _parse_lockinfo("php://input"); | |
1237 | if (!$lockinfo->success) { | |
1238 | $this->http_status("400 bad request"); | |
1239 | } | |
1240 | ||
1241 | // check if locking is possible | |
1242 | if(!$this->_check_lock_status($this->path, $lockinfo->lockscope === "shared")) { | |
1243 | $this->http_status("423 Locked"); | |
1244 | return; | |
1245 | } | |
1246 | ||
1247 | // new lock | |
1248 | $options["scope"] = $lockinfo->lockscope; | |
1249 | $options["type"] = $lockinfo->locktype; | |
1250 | $options["owner"] = $lockinfo->owner; | |
1251 | ||
1252 | $options["locktoken"] = $this->_new_locktoken(); | |
1253 | ||
1254 | $stat = $this->lock($options); | |
1255 | } | |
1256 | ||
1257 | if(is_bool($stat)) { | |
1258 | $http_stat = $stat ? "200 OK" : "423 Locked"; | |
1259 | } else { | |
1260 | $http_stat = $stat; | |
1261 | } | |
1262 | ||
1263 | $this->http_status($http_stat); | |
1264 | ||
1265 | if ($http_stat{0} == 2) { // 2xx states are ok | |
1266 | if($options["timeout"]) { | |
1267 | // more than a million is considered an absolute timestamp | |
1268 | // less is more likely a relative value | |
1269 | if($options["timeout"]>1000000) { | |
1270 | $timeout = "Second-".($options['timeout']-time()); | |
1271 | } else { | |
1272 | $timeout = "Second-$options[timeout]"; | |
1273 | } | |
1274 | } else { | |
1275 | $timeout = "Infinite"; | |
1276 | } | |
1277 | ||
1278 | header('Content-Type: text/xml; charset="utf-8"'); | |
1279 | header("Lock-Token: <$options[locktoken]>"); | |
1280 | echo "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"; | |
1281 | echo "<D:prop xmlns:D=\"DAV:\">\n"; | |
1282 | echo " <D:lockdiscovery>\n"; | |
1283 | echo " <D:activelock>\n"; | |
1284 | echo " <D:lockscope><D:$options[scope]/></D:lockscope>\n"; | |
1285 | echo " <D:locktype><D:$options[type]/></D:locktype>\n"; | |
1286 | echo " <D:depth>$options[depth]</D:depth>\n"; | |
1287 | echo " <D:owner>$options[owner]</D:owner>\n"; | |
1288 | echo " <D:timeout>$timeout</D:timeout>\n"; | |
1289 | echo " <D:locktoken><D:href>$options[locktoken]</D:href></D:locktoken>\n"; | |
1290 | echo " </D:activelock>\n"; | |
1291 | echo " </D:lockdiscovery>\n"; | |
1292 | echo "</D:prop>\n\n"; | |
1293 | } | |
1294 | } | |
1295 | ||
1296 | ||
1297 | // }}} | |
1298 | ||
1299 | // {{{ http_UNLOCK() | |
1300 | ||
1301 | /** | |
1302 | * UNLOCK method handler | |
1303 | * | |
1304 | * @param void | |
1305 | * @return void | |
1306 | */ | |
1307 | function http_UNLOCK() | |
1308 | { | |
1309 | $options = Array(); | |
1310 | $options["path"] = $this->path; | |
1311 | ||
1312 | if (isset($_SERVER['HTTP_DEPTH'])) { | |
1313 | $options["depth"] = $_SERVER["HTTP_DEPTH"]; | |
1314 | } else { | |
1315 | $options["depth"] = "infinity"; | |
1316 | } | |
1317 | ||
1318 | // strip surrounding <> | |
1319 | $options["token"] = substr(trim($_SERVER["HTTP_LOCK_TOKEN"]), 1, -1); | |
1320 | ||
1321 | // call user method | |
1322 | $stat = $this->unlock($options); | |
1323 | ||
1324 | $this->http_status($stat); | |
1325 | } | |
1326 | ||
1327 | // }}} | |
1328 | ||
1329 | // }}} | |
1330 | ||
1331 | // {{{ _copymove() | |
1332 | ||
1333 | function _copymove($what) | |
1334 | { | |
1335 | $options = Array(); | |
1336 | $options["path"] = $this->path; | |
1337 | ||
1338 | if (isset($_SERVER["HTTP_DEPTH"])) { | |
1339 | $options["depth"] = $_SERVER["HTTP_DEPTH"]; | |
1340 | } else { | |
1341 | $options["depth"] = "infinity"; | |
1342 | } | |
1343 | ||
1344 | extract(parse_url($_SERVER["HTTP_DESTINATION"])); | |
1345 | $http_host = $host; | |
1346 | if (isset($port) && $port != 80) | |
1347 | $http_host.= ":$port"; | |
1348 | ||
1349 | list($http_header_host,$http_header_port) = explode(":",$_SERVER["HTTP_HOST"]); | |
1350 | if (isset($http_header_port) && $http_header_port != 80) { | |
1351 | $http_header_host .= ":".$http_header_port; | |
1352 | } | |
1353 | ||
1354 | if ($http_host == $http_header_host && | |
1355 | !strncmp($_SERVER["SCRIPT_NAME"], $path, | |
1356 | strlen($_SERVER["SCRIPT_NAME"]))) { | |
1357 | $options["dest"] = substr($path, strlen($_SERVER["SCRIPT_NAME"])); | |
1358 | if (!$this->_check_lock_status($options["dest"])) { | |
1359 | $this->http_status("423 Locked"); | |
1360 | return; | |
1361 | } | |
1362 | ||
1363 | } else { | |
1364 | $options["dest_url"] = $_SERVER["HTTP_DESTINATION"]; | |
1365 | } | |
1366 | ||
1367 | // see RFC 2518 Sections 9.6, 8.8.4 and 8.9.3 | |
1368 | if (isset($_SERVER["HTTP_OVERWRITE"])) { | |
1369 | $options["overwrite"] = $_SERVER["HTTP_OVERWRITE"] == "T"; | |
1370 | } else { | |
1371 | $options["overwrite"] = true; | |
1372 | } | |
1373 | ||
1374 | $stat = $this->$what($options); | |
1375 | $this->http_status($stat); | |
1376 | } | |
1377 | ||
1378 | // }}} | |
1379 | ||
1380 | // {{{ _allow() | |
1381 | ||
1382 | /** | |
1383 | * check for implemented HTTP methods | |
1384 | * | |
1385 | * @param void | |
1386 | * @return array something | |
1387 | */ | |
1388 | function _allow() | |
1389 | { | |
1390 | // OPTIONS is always there | |
1391 | $allow = array("OPTIONS" =>"OPTIONS"); | |
1392 | ||
1393 | // all other METHODS need both a http_method() wrapper | |
1394 | // and a method() implementation | |
1395 | // the base class supplies wrappers only | |
1396 | foreach(get_class_methods($this) as $method) { | |
1397 | if (!strncmp("http_", $method, 5)) { | |
1398 | $method = strtoupper(substr($method, 5)); | |
1399 | if (method_exists($this, $method)) { | |
1400 | $allow[$method] = $method; | |
1401 | } | |
1402 | } | |
1403 | } | |
1404 | ||
1405 | // we can emulate a missing HEAD implemetation using GET | |
1406 | if (isset($allow["GET"])) | |
1407 | $allow["HEAD"] = "HEAD"; | |
1408 | ||
1409 | // no LOCK without checklok() | |
1410 | if (!method_exists($this, "checklock")) { | |
1411 | unset($allow["LOCK"]); | |
1412 | unset($allow["UNLOCK"]); | |
1413 | } | |
1414 | ||
1415 | return $allow; | |
1416 | } | |
1417 | ||
1418 | // }}} | |
1419 | ||
1420 | /** | |
1421 | * helper for property element creation | |
1422 | * | |
1423 | * @param string XML namespace (optional) | |
1424 | * @param string property name | |
1425 | * @param string property value | |
1426 | * @return array property array | |
1427 | */ | |
1428 | function mkprop() | |
1429 | { | |
1430 | $args = func_get_args(); | |
1431 | if (count($args) == 3) { | |
1432 | return array("ns" => $args[0], | |
1433 | "name" => $args[1], | |
1434 | "val" => $args[2]); | |
1435 | } else { | |
1436 | return array("ns" => "DAV:", | |
1437 | "name" => $args[0], | |
1438 | "val" => $args[1]); | |
1439 | } | |
1440 | } | |
1441 | ||
1442 | // {{{ _check_auth | |
1443 | ||
1444 | /** | |
1445 | * check authentication if check is implemented | |
1446 | * | |
1447 | * @param void | |
1448 | * @return bool true if authentication succeded or not necessary | |
1449 | */ | |
1450 | function _check_auth() | |
1451 | { | |
1452 | if (method_exists($this, "checkAuth")) { | |
1453 | // PEAR style method name | |
1454 | return $this->checkAuth(@$_SERVER["AUTH_TYPE"], | |
1455 | @$_SERVER["PHP_AUTH_USER"], | |
1456 | @$_SERVER["PHP_AUTH_PW"]); | |
1457 | } else if (method_exists($this, "check_auth")) { | |
1458 | // old (pre 1.0) method name | |
1459 | return $this->check_auth(@$_SERVER["AUTH_TYPE"], | |
1460 | @$_SERVER["PHP_AUTH_USER"], | |
1461 | @$_SERVER["PHP_AUTH_PW"]); | |
1462 | } else { | |
1463 | // no method found -> no authentication required | |
1464 | return true; | |
1465 | } | |
1466 | } | |
1467 | ||
1468 | // }}} | |
1469 | ||
1470 | // {{{ UUID stuff | |
1471 | ||
1472 | /** | |
1473 | * generate Unique Universal IDentifier for lock token | |
1474 | * | |
1475 | * @param void | |
1476 | * @return string a new UUID | |
1477 | */ | |
1478 | function _new_uuid() | |
1479 | { | |
1480 | // use uuid extension from PECL if available | |
1481 | if (function_exists("uuid_create")) { | |
1482 | return uuid_create(); | |
1483 | } | |
1484 | ||
1485 | // fallback | |
1486 | $uuid = md5(microtime().getmypid()); // this should be random enough for now | |
1487 | ||
1488 | // set variant and version fields for 'true' random uuid | |
1489 | $uuid{12} = "4"; | |
1490 | $n = 8 + (ord($uuid{16}) & 3); | |
1491 | $hex = "0123456789abcdef"; | |
1492 | $uuid{16} = $hex{$n}; | |
1493 | ||
1494 | // return formated uuid | |
1495 | return substr($uuid, 0, 8)."-" | |
1496 | . substr($uuid, 8, 4)."-" | |
1497 | . substr($uuid, 12, 4)."-" | |
1498 | . substr($uuid, 16, 4)."-" | |
1499 | . substr($uuid, 20); | |
1500 | } | |
1501 | ||
1502 | /** | |
1503 | * create a new opaque lock token as defined in RFC2518 | |
1504 | * | |
1505 | * @param void | |
1506 | * @return string new RFC2518 opaque lock token | |
1507 | */ | |
1508 | function _new_locktoken() | |
1509 | { | |
1510 | return "opaquelocktoken:".$this->_new_uuid(); | |
1511 | } | |
1512 | ||
1513 | // }}} | |
1514 | ||
1515 | // {{{ WebDAV If: header parsing | |
1516 | ||
1517 | /** | |
1518 | * | |
1519 | * | |
1520 | * @param string header string to parse | |
1521 | * @param int current parsing position | |
1522 | * @return array next token (type and value) | |
1523 | */ | |
1524 | function _if_header_lexer($string, &$pos) | |
1525 | { | |
1526 | // skip whitespace | |
1527 | while (ctype_space($string{$pos})) { | |
1528 | ++$pos; | |
1529 | } | |
1530 | ||
1531 | // already at end of string? | |
1532 | if (strlen($string) <= $pos) { | |
1533 | return false; | |
1534 | } | |
1535 | ||
1536 | // get next character | |
1537 | $c = $string{$pos++}; | |
1538 | ||
1539 | // now it depends on what we found | |
1540 | switch ($c) { | |
1541 | case "<": | |
1542 | // URIs are enclosed in <...> | |
1543 | $pos2 = strpos($string, ">", $pos); | |
1544 | $uri = substr($string, $pos, $pos2 - $pos); | |
1545 | $pos = $pos2 + 1; | |
1546 | return array("URI", $uri); | |
1547 | ||
1548 | case "[": | |
1549 | //Etags are enclosed in [...] | |
1550 | if ($string{$pos} == "W") { | |
1551 | $type = "ETAG_WEAK"; | |
1552 | $pos += 2; | |
1553 | } else { | |
1554 | $type = "ETAG_STRONG"; | |
1555 | } | |
1556 | $pos2 = strpos($string, "]", $pos); | |
1557 | $etag = substr($string, $pos + 1, $pos2 - $pos - 2); | |
1558 | $pos = $pos2 + 1; | |
1559 | return array($type, $etag); | |
1560 | ||
1561 | case "N": | |
1562 | // "N" indicates negation | |
1563 | $pos += 2; | |
1564 | return array("NOT", "Not"); | |
1565 | ||
1566 | default: | |
1567 | // anything else is passed verbatim char by char | |
1568 | return array("CHAR", $c); | |
1569 | } | |
1570 | } | |
1571 | ||
1572 | /** | |
1573 | * parse If: header | |
1574 | * | |
1575 | * @param string header string | |
1576 | * @return array URIs and their conditions | |
1577 | */ | |
1578 | function _if_header_parser($str) | |
1579 | { | |
1580 | $pos = 0; | |
1581 | $len = strlen($str); | |
1582 | ||
1583 | $uris = array(); | |
1584 | ||
1585 | // parser loop | |
1586 | while ($pos < $len) { | |
1587 | // get next token | |
1588 | $token = $this->_if_header_lexer($str, $pos); | |
1589 | ||
1590 | // check for URI | |
1591 | if ($token[0] == "URI") { | |
1592 | $uri = $token[1]; // remember URI | |
1593 | $token = $this->_if_header_lexer($str, $pos); // get next token | |
1594 | } else { | |
1595 | $uri = ""; | |
1596 | } | |
1597 | ||
1598 | // sanity check | |
1599 | if ($token[0] != "CHAR" || $token[1] != "(") { | |
1600 | return false; | |
1601 | } | |
1602 | ||
1603 | $list = array(); | |
1604 | $level = 1; | |
1605 | $not = ""; | |
1606 | while ($level) { | |
1607 | $token = $this->_if_header_lexer($str, $pos); | |
1608 | if ($token[0] == "NOT") { | |
1609 | $not = "!"; | |
1610 | continue; | |
1611 | } | |
1612 | switch ($token[0]) { | |
1613 | case "CHAR": | |
1614 | switch ($token[1]) { | |
1615 | case "(": | |
1616 | $level++; | |
1617 | break; | |
1618 | case ")": | |
1619 | $level--; | |
1620 | break; | |
1621 | default: | |
1622 | return false; | |
1623 | } | |
1624 | break; | |
1625 | ||
1626 | case "URI": | |
1627 | $list[] = $not."<$token[1]>"; | |
1628 | break; | |
1629 | ||
1630 | case "ETAG_WEAK": | |
1631 | $list[] = $not."[W/'$token[1]']>"; | |
1632 | break; | |
1633 | ||
1634 | case "ETAG_STRONG": | |
1635 | $list[] = $not."['$token[1]']>"; | |
1636 | break; | |
1637 | ||
1638 | default: | |
1639 | return false; | |
1640 | } | |
1641 | $not = ""; | |
1642 | } | |
1643 | ||
1644 | if (@is_array($uris[$uri])) { | |
1645 | $uris[$uri] = array_merge($uris[$uri],$list); | |
1646 | } else { | |
1647 | $uris[$uri] = $list; | |
1648 | } | |
1649 | } | |
1650 | ||
1651 | return $uris; | |
1652 | } | |
1653 | ||
1654 | /** | |
1655 | * check if conditions from "If:" headers are meat | |
1656 | * | |
1657 | * the "If:" header is an extension to HTTP/1.1 | |
1658 | * defined in RFC 2518 section 9.4 | |
1659 | * | |
1660 | * @param void | |
1661 | * @return void | |
1662 | */ | |
1663 | function _check_if_header_conditions() | |
1664 | { | |
1665 | if (isset($_SERVER["HTTP_IF"])) { | |
1666 | $this->_if_header_uris = | |
1667 | $this->_if_header_parser($_SERVER["HTTP_IF"]); | |
1668 | ||
1669 | foreach($this->_if_header_uris as $uri => $conditions) { | |
1670 | if ($uri == "") { | |
1671 | // default uri is the complete request uri | |
1672 | $uri = (@$_SERVER["HTTPS"] === "on" ? "https:" : "http:"); | |
1673 | $uri.= "//$_SERVER[HTTP_HOST]$_SERVER[SCRIPT_NAME]$_SERVER[PATH_INFO]"; | |
1674 | } | |
1675 | // all must match | |
1676 | $state = true; | |
1677 | foreach($conditions as $condition) { | |
1678 | // lock tokens may be free form (RFC2518 6.3) | |
1679 | // but if opaquelocktokens are used (RFC2518 6.4) | |
1680 | // we have to check the format (litmus tests this) | |
1681 | if (!strncmp($condition, "<opaquelocktoken:", strlen("<opaquelocktoken"))) { | |
1682 | if (!ereg("^<opaquelocktoken:[[:xdigit:]]{8}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{12}>$", $condition)) { | |
1683 | return false; | |
1684 | } | |
1685 | } | |
1686 | if (!$this->_check_uri_condition($uri, $condition)) { | |
1687 | $state = false; | |
1688 | break; | |
1689 | } | |
1690 | } | |
1691 | ||
1692 | // any match is ok | |
1693 | if ($state == true) { | |
1694 | return true; | |
1695 | } | |
1696 | } | |
1697 | return false; | |
1698 | } | |
1699 | return true; | |
1700 | } | |
1701 | ||
1702 | /** | |
1703 | * Check a single URI condition parsed from an if-header | |
1704 | * | |
1705 | * Check a single URI condition parsed from an if-header | |
1706 | * | |
1707 | * @abstract | |
1708 | * @param string $uri URI to check | |
1709 | * @param string $condition Condition to check for this URI | |
1710 | * @returns bool Condition check result | |
1711 | */ | |
1712 | function _check_uri_condition($uri, $condition) | |
1713 | { | |
1714 | // not really implemented here, | |
1715 | // implementations must override | |
1716 | return true; | |
1717 | } | |
1718 | ||
1719 | ||
1720 | /** | |
1721 | * | |
1722 | * | |
1723 | * @param string path of resource to check | |
1724 | * @param bool exclusive lock? | |
1725 | */ | |
1726 | function _check_lock_status($path, $exclusive_only = false) | |
1727 | { | |
1728 | // FIXME depth -> ignored for now | |
1729 | if (method_exists($this, "checkLock")) { | |
1730 | // is locked? | |
1731 | $lock = $this->checkLock($path); | |
1732 | ||
1733 | // ... and lock is not owned? | |
1734 | if (is_array($lock) && count($lock)) { | |
1735 | // FIXME doesn't check uri restrictions yet | |
1736 | if (!strstr($_SERVER["HTTP_IF"], $lock["token"])) { | |
1737 | if (!$exclusive_only || ($lock["scope"] !== "shared")) | |
1738 | return false; | |
1739 | } | |
1740 | } | |
1741 | } | |
1742 | return true; | |
1743 | } | |
1744 | ||
1745 | ||
1746 | // }}} | |
1747 | ||
1748 | ||
1749 | /** | |
1750 | * Generate lockdiscovery reply from checklock() result | |
1751 | * | |
1752 | * @param string resource path to check | |
1753 | * @return string lockdiscovery response | |
1754 | */ | |
1755 | function lockdiscovery($path) | |
1756 | { | |
1757 | // no lock support without checklock() method | |
1758 | if (!method_exists($this, "checklock")) { | |
1759 | return ""; | |
1760 | } | |
1761 | ||
1762 | // collect response here | |
1763 | $activelocks = ""; | |
1764 | ||
1765 | // get checklock() reply | |
1766 | $lock = $this->checklock($path); | |
1767 | ||
1768 | // generate <activelock> block for returned data | |
1769 | if (is_array($lock) && count($lock)) { | |
1770 | // check for 'timeout' or 'expires' | |
1771 | if (!empty($lock["expires"])) { | |
1772 | $timeout = "Second-".($lock["expires"] - time()); | |
1773 | } else if (!empty($lock["timeout"])) { | |
1774 | $timeout = "Second-$lock[timeout]"; | |
1775 | } else { | |
1776 | $timeout = "Infinite"; | |
1777 | } | |
1778 | ||
1779 | // genreate response block | |
1780 | $activelocks.= " | |
1781 | <D:activelock> | |
1782 | <D:lockscope><D:$lock[scope]/></D:lockscope> | |
1783 | <D:locktype><D:$lock[type]/></D:locktype> | |
1784 | <D:depth>$lock[depth]</D:depth> | |
1785 | <D:owner>$lock[owner]</D:owner> | |
1786 | <D:timeout>$timeout</D:timeout> | |
1787 | <D:locktoken><D:href>$lock[token]</D:href></D:locktoken> | |
1788 | </D:activelock> | |
1789 | "; | |
1790 | } | |
1791 | ||
1792 | // return generated response | |
1793 | return $activelocks; | |
1794 | } | |
1795 | ||
1796 | /** | |
1797 | * set HTTP return status and mirror it in a private header | |
1798 | * | |
1799 | * @param string status code and message | |
1800 | * @return void | |
1801 | */ | |
1802 | function http_status($status) | |
1803 | { | |
1804 | // simplified success case | |
1805 | if($status === true) { | |
1806 | $status = "200 OK"; | |
1807 | } | |
1808 | ||
1809 | // remember status | |
1810 | $this->_http_status = $status; | |
1811 | ||
1812 | // generate HTTP status response | |
1813 | header("HTTP/1.1 $status"); | |
1814 | header("X-WebDAV-Status: $status", true); | |
1815 | } | |
1816 | ||
1817 | /** | |
1818 | * private minimalistic version of PHP urlencode() | |
1819 | * | |
1820 | * only blanks and XML special chars must be encoded here | |
1821 | * full urlencode() encoding confuses some clients ... | |
1822 | * | |
1823 | * @param string URL to encode | |
1824 | * @return string encoded URL | |
1825 | */ | |
1826 | function _urlencode($url) | |
1827 | { | |
1828 | return strtr($url, array(" "=>"%20", | |
1829 | "&"=>"%26", | |
1830 | "<"=>"%3C", | |
1831 | ">"=>"%3E", | |
1832 | )); | |
1833 | } | |
1834 | ||
1835 | /** | |
1836 | * private version of PHP urldecode | |
1837 | * | |
1838 | * not really needed but added for completenes | |
1839 | * | |
1840 | * @param string URL to decode | |
1841 | * @return string decoded URL | |
1842 | */ | |
1843 | function _urldecode($path) | |
1844 | { | |
1845 | return urldecode($path); | |
1846 | } | |
1847 | ||
1848 | /** | |
1849 | * UTF-8 encode property values if not already done so | |
1850 | * | |
1851 | * @param string text to encode | |
1852 | * @return string utf-8 encoded text | |
1853 | */ | |
1854 | function _prop_encode($text) | |
1855 | { | |
1856 | switch (strtolower($this->_prop_encoding)) { | |
1857 | case "utf-8": | |
1858 | return $text; | |
1859 | case "iso-8859-1": | |
1860 | case "iso-8859-15": | |
1861 | case "latin-1": | |
1862 | default: | |
1863 | return utf8_encode($text); | |
1864 | } | |
1865 | } | |
1866 | } | |
1867 | ||
1868 | /* | |
1869 | * Local variables: | |
1870 | * tab-width: 4 | |
1871 | * c-basic-offset: 4 | |
1872 | * End: | |
1873 | */ | |
1874 | ?> |