17d4d7abeae35a1e1d29e65e3e45dafb53e2be23
[atutor.git] / mods / wiki / plugins / lib / minidav.php
1 <?php
2 /*
3    The MiniDav class is a complete rewrite of PEAR::WebDAV_Server and
4    strives to be easier to use. There are still enough similarities,
5    but this version removes many features and comes without PHP license
6    preamble (this is Public Domain).
7     + adds Content-Encoding support for XML command in/out and GET,PUT
8     + more senseful xml namespace handling (->xmlns[] keeps a prefix list)
9     + PROPFIND uses simple property lists, with path and status inside
10     + LOCK, PROPATCH, MOVE, COPY, DELETE are unimplemented as of yet
11     + all M$-DAV compatibility workarounds have been disabled
12     + the GET() interface will NOT work with file handles and big files
13     
14    This class automatically adds and removes the base URL from incoming
15    and sent pathnames. It also translates XML namespace qualifiers, if
16    they get registered correctly beforehand. Most request parameters are
17    available as object properties AND as method parameters.
18
19    File lists for PROPFIND and PROPPATCH simply contain a "path" (again
20    without the base URL) and multiple DAV: properties (without any xmlns
21    monikers). "status" is of course optional, and custom (from other XML
22    namespaces) entries may be there. While it is possible to treat some
23    of them specially, most should be simple name-value pairs.
24    
25    Simply derive a class from "MiniDav" and implement at least GET and
26    PROPFIND as described by the out-commented examples herein. Then make
27    an object instance "$dav = new YourDavClass();" and call the main
28    "$dav->ServerRequest();" method. Place an exit; after that into your
29    interface script.
30    
31    This class does not handle authentication (one of the many parts of
32    the WebDAV spec everybody ignores). Implement an ->auth() method,
33    which is already called for all RW-methods (PUT, DELELTE, MKCOL).
34 */
35
36 define("MINIDAV_IGNORE_MIME", 0);   // there are a few bogus clients
37
38
39 #-- WebDAV base class
40 class MiniDav {
41
42    #-- request
43    var $path = "";
44    var $depth = 9;
45    var $dest = "";
46    var $version = NULL;   // no SVN proto support
47    var $props = NULL;
48
49    #-- both must be lowercase here
50    var $int_charset = "iso-8859-1";
51    var $ext_charset = "iso-8859-1";
52
53    #-- namespace mapping (we don't want to deal with ugly URIs throughout the code)
54    var $xmlns = array();  // gets fed directly into xml parsers
55    
56    #-- pre-defined values (constants)
57    var $base_url = "";    // will get prefixed to PROPFIND response URLs
58                           // and stripped from incoming path names
59    
60    #-- special handling
61    var $prop_as_tag = array(
62       "resourcetype" => 1,
63    );
64    var $prop_as_date = array(
65       "creationdate" => "%G-%m-%dT%TZ",
66       "getlastmodified" => "%a, %d %b %G %T %Z",
67    );
68    
69
70    #-- constructor
71    function MiniDav() {
72
73       #-- you have to set all desired namespace prefixes beforehand
74         //@FIX: list not yet feeded into XML parser
75       $this->xmlns[""] = "DAV:";   // <default xmlns=...>
76       $this->xmlns["wiki1"] = "http://purl.org/rss/1.0/modules/wiki/";
77       #<eee># $this->xmlns["ms-time-compat"] = "urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/";
78    }
79
80
81    #-- main function
82    function ServeRequest($path="") {
83
84       #-- pathname
85       if (!$path) {
86          $path = $_SERVER["PATH_INFO"];
87       }
88       $this->path = $path;
89       
90       #-- base url
91       if (!$this->base_url) {
92          $port = $_SERVER["SERVER_PORT"];
93          $s = ($_SERVER["HTTPS"] && $_SERVER["SSL_PROTOCOL"] || ($port == 443) ? "s" : "");
94          $port = ($port != ($s?443:80) ? ":$port" : "");
95          $this->base_url = "http" . $s . "://" . $_SERVER["SERVER_NAME"] . $port . $_SERVER["SCRIPT_NAME"];
96       }
97
98       #-- depth
99       if (isset($_SERVER["HTTP_DEPTH"])) {
100          $d = trim(strtolower($_SERVER["HTTP_DEPTH"]));
101          if ($d == "infinity") {
102             $d = 16;
103          }
104          $this->depth = (int)$d;
105       }
106       
107       #-- destination path
108       if ($this->dest = trim($_SERVER["HTTP_DESTINATION"])) {
109          if (!strncmp($this->dest, $this->base_url, strlen($this->base_url))) {
110             $this->dest = substr($this->dest, strlen($this->base_url));
111          }
112          else {
113             $this->dest = "";
114          }
115       }
116       
117       #-- overwrite?
118       $this->overwrite = (($_SERVER["HTTP_OVERWRITE"] != "F") ? 1 : 0);
119
120       #-- ??? more params
121       // ...
122
123
124 # $this->request_debug();
125       #-- call subroutine
126       $method = strtoupper($_SERVER["REQUEST_METHOD"]);
127       $handler = $method."_request";
128       if (method_exists($this, $handler) and method_exists($this, $method)) {
129          call_user_method("$handler", $this);
130       }
131       else {
132          $this->http_error("405 Unimplemented Request Method");  // or 501 ?
133          $this->OPTIONS_request();
134       }
135
136       #-- stop here
137       // exit;
138       # (this is handled by the caller typically)
139    }
140
141
142
143    #------------------------------------------------------- request methods ---
144
145
146
147                                /////// GET ///////
148
149    #-- retrieve a single document
150    function GET_request() {
151       $ok = $this->GET( $this->path );
152       $this->http_error($ok);
153    }
154    
155    /*
156    function GET($path) {
157       - just call the ->GET_response() handler with some file $data
158    }
159    */
160
161    #-- finish
162    function GET_response($data, $mtime=0, $ctype="") {
163
164       #-- fix params
165       if (!$mtime) {
166          $mtime = time();
167       }
168       if (!$ctype) {
169          $ctype = "application/octet-stream";
170       }
171       if (is_resource($data)) {
172          $data = fread($data, 1<<22);
173       }
174
175       #-- send
176       header("Content-Type: $ctype");
177       header("Last-Modified: ".gmstrftime($this->prop_as_date["getlastmodified"], $mtime));
178       $this->cut_content($data);
179       $this->content_encode($data);
180       print($data);
181
182       #-- done
183       return("200 OK");
184    }
185
186
187
188                                ////// HEAD ///////
189
190    #-- we handle this by calling ->GET()
191    function HEAD_request() {
192       $this->HEAD( $this->path );
193    }
194
195    #-- HEAD() gets automatically emulated
196    function HEAD($path) {
197       ob_start();
198       $this->GET();
199       ob_end_clean();
200    }
201
202
203
204                                /////// PUT ///////
205    
206    #-- store resources under given filename
207    function PUT_request() {
208       $this->auth();
209
210       #-- prepare params
211       list($content_type, $charset) = $this->get_ctype();
212       if (!$content_type) {
213          $this->http_error("415 Use a MIME compliant client!");
214       }
215       else {
216          $ok = $this->PUT( $this->path, $content_type, $charset );
217          $this->http_error($ok);
218       }
219    }
220
221    /*
222    function PUT($path, $ctype, $charset) {
223       - simply calls $this->get_body() to get the decompressed
224         input stream
225       - checks $overwrite and $ctype, $charset
226       - complains at will
227    }
228    */
229
230
231
232                                ///// PROPFIND /////
233
234    #-- sort of directory listing
235    function PROPFIND_request() {
236
237       #-- request type
238       if (($ctype = $this->get_ctype()) and ($this->is_xml($ctype[0]))
239       and ($body = $this->get_body()))
240       {
241          $p = & new MiniDav_PropFind($body, $ctype[1], $this->int_charset, $this->xmlns);
242          $p->parse();
243 #print_r($p);
244          if (isset($p->prop)) {
245             $this->props = array_keys($p->prop);
246          }
247          elseif (isset($p->allprop)) {
248             $this->props = 1;
249          }
250          elseif (isset($p->propname)) {
251             $this->props = 0;
252          }
253          else {
254             $this->http_error("400 Couldn't determine <propfind> request type");
255             exit;
256          }
257       }
258       else {
259          $this->props = 1;   // ALL
260 #print_r($_SERVER);
261 #echo "NON($ctype[0],$body)";
262       }
263 #print_r($this);
264
265       #-- get
266       $files = $this->PROPFIND($this->path, $this->props);
267       
268       #-- send transformed list
269       $this->PROPFIND_response($files);
270    }
271
272    /*
273    function PROPFIND($path, $wanted_props) {
274       - searches resources for given $path up to ->depth subdirs
275       - generates a result list in a $files[] array with $wanted_props:
276         $files[] = array(
277                      "path"=>"/fn/...",    // ALWAYS!, without URL prefix
278                      "status"=>"200 OK",   // optional
279                      "getcontenttype"=>"...",  // in DAV: default namespace
280                      "myXmlNs:p1"=>"...",      // other XML namespace
281                   );
282       - $wanted_props is either 1 or 0 or an array (1 = ALL fields, 0 = only NAMES
283         of fields, or ARRAY = the respective list of requested property field names)
284       - send the collected file info list to ->PROPFIND_response() finally
285    }
286    */
287
288    #-- send back
289    function PROPFIND_response($files) {
290
291       #-- start
292       header("Content-Type: application/xml");
293       echo "<?xml version=\"1.0\" encoding=\"{$this->int_charset}\" ?>\n";
294       echo "<multistatus" . $this->xmlns_out() . ">\n";
295
296       #-- required fields
297       $add_req = array();
298       if (is_array($this->props)) {
299          foreach ($this->props as $id) {
300             $add_req[$id] = false;  // become <empty/> nodes
301          }
302       }
303 #print_r($add_req);
304 #print_r($this);
305
306       #-- files
307       foreach ($files as $row) {
308       
309          #-- transform/add fields
310          $href = $this->xmlentities($this->base_url . $row["path"]);
311          if (!$href) { continue; }
312          unset($row["path"]);
313          $status = $row["status"] ? $row["status"] : "HTTP/1.0 200 OK";
314          unset($row["status"]);
315
316          #-- add _required_ fields (but empty then)
317          if ($add_req) {
318             $row = $row + $add_req;
319          }
320          
321          #-- throw entry + properties
322          echo "  <response>\n";
323          echo "    <href>$href</href>\n";
324          echo "    <propstat>\n";
325          echo "      <prop>\n";
326
327          #-- output fields
328          foreach ($row as $id=>$value) {
329
330             #-- names only?
331             if (!$this->props || ($value===false)) {
332                echo "\t<$id/>\n";
333             }
334             else {
335                #-- skip filtered
336                if (is_array($this->props) && !in_array($id, $this->props)) {
337                   continue;
338                }
339                #-- xml encode
340                if ($this->prop_as_tag[$id]) {
341                   if (!strlen($value)) {
342                      $value = "<!--empty-->";
343                   }
344                   else {
345                      $value = "<$value/>";
346                   }
347                }
348                elseif ($sft = $this->prop_as_date[$id]) {
349                   $value = gmstrftime($sft, $value);
350                }
351                else {
352                   $value = $this->xmlentities($value);
353                }
354                echo "\t<$id>$value</$id>\n";
355             }
356          }
357          echo "      </prop>\n";
358          echo "      <status>$status</status>\n";
359          echo "    </propstat>\n";
360          echo "  </response>\n";
361       }
362       
363       #-- fin
364       echo "</multistatus>\n";
365    }
366
367
368
369                                //// PROPPATCH ////
370
371    function PROPPATCH_request() {
372    }
373    
374    /*
375    function PROPPATCH($path, $set, $remove) {
376       - $set and $remove are associate arrays, with property names
377         and values ($set only) inside
378       - $remove only contains false as values, and tells to unset
379         the according property from all matched files
380       - $path is either a file or a directory again (should honor
381         $this->depth then)
382       - returns a $files[] list, similar to the PROPFIND() method,
383         which contains property names associated to status values:
384           array( "getcontenttype" => "500 bad request", )
385         (1 is allowed as status value)
386    }
387    */
388
389
390
391                                ////// MKCOL //////
392
393    #-- create a collection (sub directory)
394    function MKCOL_request() {
395       $this->auth();
396       $r = $this->MKCOL($this->path);
397       $this->http_error($r);
398    }
399    
400    /*
401    function MKCOL($dir) {
402    }
403    */
404
405
406
407                                ////// COPY ///////
408
409    #-- removes an entry
410    function COPY_request() {
411       $this->auth();
412       $this->check_src_dest();
413       $r = $this->COPY($this->path, $this->dest, $this->overwrite, $this->depth);
414       $this->http_error($r);
415    }
416    
417    /*
418    function COPY($src, $dest, $overwrite, $depth) {
419       - duplicates a $src file or a directory tree ($depth) to the
420         give $dest-ination
421       - if $overwrite is 1, then existing files should be ->DELETEd
422         automatically (simply overwrite)
423       - return success
424    }
425    */
426    
427    #-- simple pre-conditions for COPY and MOVE
428    function check_src_dest() {
429       if (!$this->dest) {
430          $this->http_error("400 No valid Destination: given");
431          exit;
432       }
433       elseif (trim($this->dest, "/") == trim($this->path, "/")) {
434          $this->http_error("409 Source and Destination are the same");
435          exit;
436       }
437    }
438
439
440
441                                ////// MOVE ///////
442
443    #-- removes an entry
444    function MOVE_request() {
445       $this->auth();
446       $this->check_src_dest();
447       $r = $this->MOVE($this->path, $this->dest, $this->overwrite, $this->depth);
448       $this->http_error($r);
449    }
450    
451    /*
452    function MOVE($src, $dest, $overwrite, $depth) {
453       - works like COPY, but that all source files must be deleted
454         after a successful move
455    }
456    */
457
458
459
460                                ///// DELETE //////
461
462    #-- removes an entry
463    function DELETE_request() {
464       $this->auth();
465       $this->DELETE($this->path);
466    }
467    
468    /*
469    function DELETE($path) {
470       - purge the given file if it wants to
471       - watch out for directories and $this->depth
472    }
473    */
474
475
476
477                                ///// OPTIONS /////
478
479    #-- yields some informational headers
480    function OPTIONS_request() {
481       header("Allow: " . implode(", ", $this->get_options()));
482       header("DAV: 1");  // version 1 means without locking support
483    }
484
485    function OPTIONS() {
486       // nothing to do
487    }
488    
489    #-- list of defined request method handler pairs
490    function get_options() {
491       $class = get_class($this);
492       $r = array();
493       foreach (get_class_methods($class) as $fn) {
494          #-- for every "METHOD()", there must be a "METHOD_request()"
495          if (!strpos($fn, "_") && method_exists($this, "{$fn}_request")) {
496             $r[] = strtoupper($fn);
497          }
498       }
499
500       $r[] = "TRACE";   // Apache must handle this
501       return($r);
502    }
503
504
505
506                                ///////////////////
507
508
509                                ////// LOCK ///////
510
511
512                                ///// UNLOCK //////
513
514
515
516                                ///////////////////
517
518
519
520
521    #------------------------------------------------------ utility code ---
522
523
524    #-- is called for all writing methods
525    function auth() {
526       // Makes it easy to bring up authentication.
527       // If you want to "protect" the read-only WebDAV method as
528       // well, then just wrap this into your interface script.
529       
530       # include("ewiki/plugins/../fragments/funcs/auth.php");
531    }
532
533
534    #-- encode string values
535    function xmlentities($s) {
536       return xmlentities($s);
537    }
538
539
540    #-- serialize all valid $this->xmlns[]
541    function xmlns_out() {
542       $s = "";
543       foreach ($this->xmlns as $prefix=>$uri) {
544          if (strpos($uri, ":")) {
545             $s .= " xmlns" . ($prefix ? ":$prefix" : "") ."=\"" . $uri . "\"";
546          }
547       }
548       return($s);
549    }
550
551
552
553
554    #------------------------------------------------------- http in/out ---
555
556
557    #-- compress page content (prior sending)
558    function content_encode(&$body) {
559       $ae = strtolower($_SERVER["ACCEPT_ENCODING"]);
560       $alg = array("gzip"=>"gzencode", "deflate"=>"gzdeflate", "compress"=>"gzcompress", "x-bzip2"=>"bzcompress");
561       if ($ae) {
562          foreach (explode(",", $ae) as $ae) {
563             $ae = trim(strtok($ae, ";"));
564             if ($pf = $alg[$ae]) {
565                $body = $pf($body);
566                header("Content-Encoding: $ae");
567                break;
568             }
569          }
570       }
571 //      unset($_SERVER["ACCEPT_ENCODING"]); //@HACK: prevent accidential double encoding - ewiki plugins could do this automatically
572    }
573
574
575    #-- or decompress received body
576    function content_decode(&$body) {
577       $de = strtolower(trim($_SERVER["HTTP_CONTENT_ENCODING"]));
578       if (!strlen($de)) { /* nop */ }
579       elseif ($de == "gzip") { $body = function_exists("gzdecode") ? gzdecode($body) : gzinflate(substr($body, 10, strlen($body) - 18)); }
580       elseif ($de == "deflate") { $body = gzinflate($body); }
581       elseif ($de == "compress") { $body = gzuncompress($body); }
582       elseif ($de == "x-bzip2") { $body = bzuncompress($body); }
583 //      unset($_SERVER["HTTP_CONTENT_ENCODING"]); //@HACK: prevent accidential double decoding, wrong here
584    }
585
586
587    #-- convinience function for sending HTTP status responses
588    function http_error($w=false, $def_success="200 OkiDoki") {
589       if ($w == false) {
590          $w = "500 Internal Error";
591       }
592       elseif (($w === true) || ($w === 1)) {
593          $w = $def_success;
594       }
595       if (!headers_sent() && !isset($this->no_status)) {
596          if (ini_get("cgi.rfc2616_headers")) {
597             header("HTTP/1.1 $w");
598          }
599          header("Status: $w");   // always ok
600       }
601    }
602
603
604    #-- check content type and charset
605    function get_ctype() {
606       $ct = trim(strtolower(strtok($_SERVER["CONTENT_TYPE"], ";,(")));
607       $rest = strtok(",(");
608       if (preg_match("#charset=[\"\']?([-\d\w]+)#", $rest, $uu)) {
609          $charset = $uu[1];
610       }
611       elseif ($ct == "text/xml") {
612          $charset = "iso-8859-1";
613       }
614       return(array($ct, $charset));
615    }
616    
617
618    #-- check content-type for being */*xml*
619    function is_xml($ct) {
620       if (preg_match("#(^x\.?ml/...|...[/+]xml$)#", $ct) || MINIDAV_IGNORE_MIME) {
621          return(true);
622       }
623    }
624
625    
626    #-- get request body, decoded
627    function get_body() {
628
629       #-- fetch
630       $f = fopen("php://input", "rb");
631       $body = fread($f, 1<<22);
632       fclose($f);
633
634       #-- uncompress
635       $this->content_decode($body);
636      
637       return($body);
638    }
639
640
641    #-- partial responses (only contiguous ranges)
642    function cut_content(&$data) {
643
644       if (($h = $_SERVER["HTTP_RANGE"])
645       and preg_match("/^bytes=(\d*)-(\d+)$/", trim($h), $uu))
646       { 
647          list($uu, $start, $end) = $uu;
648
649          #-- correct positions
650          $len = strlen($data);
651          if (!strlen($start)) {
652             if ($end > $len) {
653                $start = 0;
654             }
655             else {
656                $start = $len - $end;
657             }
658             $end = $len - 1;
659          }
660          if ($start > $end) {
661             $this->http_error("416 Unsatisfiable Range:");
662             return;
663          }
664
665          #-- cut
666          $data = substr($data, $start, $end - $start + 1);
667          
668          #-- send headers
669          header("Content-Range: bytes $start-$end/$len");
670          $this->http_error("206 Partial Content");
671          $this->no_status = 1;
672       }
673    }
674
675
676    #---------------------------------------------------------- old code ---
677
678    /*
679    #-- output raw page data
680    function out_content(&$data) {
681       $c = & $data["content"];
682       $this->content_encode($c);
683       header("Content-Length: ".strlen($c));
684       print $c;
685    }
686    */
687
688
689    #-- debugging
690    function request_debug() {
691       ob_start();
692       print_r($_SERVER);
693       echo $this->get_body();
694       print_r($this);
695       $d = ob_get_contents();
696       ob_end_clean();
697       file_put_contents("/tmp/minidav.".time().".".rand(0, 99), $d);
698    }
699
700
701
702 }// end of class
703
704
705
706
707 #------------------------------------------------------------------ xml ---
708
709
710 #-- <propfind> request bodies
711 class MiniDav_PropFind extends easy_xml {
712
713    function MiniDav_PropFind($xml, $ctype="", $uu=NULL, $addns=array()) {
714       parent::easy_xml($xml, $ctype);
715       $this->xmlns2["dav"] = "";
716       $this->xmlns += array_flip($addns);
717    }
718    
719    function start($xp, $tag, $attr) {
720       parent::start($xp, $tag, $attr);
721       if ($this->parent) {
722          $this->{$this->parent}[$tag] = true;
723       }
724    }
725 }
726
727
728
729 ?>