Release diogenes-0.9.22
[diogenes.git] / include / diogenes.webdav.inc.php
1 <?php
2 /*
3 * Copyright (C) 2003-2004 Polytechnique.org
4 * http://opensource.polytechnique.org/
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19 */
20
21
22 require_once 'HTTP/WebDAV/Server.php';
23 require_once 'diogenes/diogenes.misc.inc.php';
24 require_once 'diogenes.webdav.logger.inc.php';
25 require_once 'Barrel.php';
26 require_once 'Barrel/Page.php';
27
28 /**
29 * Filesystem access using WebDAV
30 *
31 */
32 class DiogenesWebDAV extends HTTP_WebDAV_Server
33 {
34 /** The Diogenes_Barrel representing the barrel */
35 var $barrel;
36
37 /** Debugging log file handle */
38 var $debug_handle;
39
40 /** Capture out to log ? */
41 var $debug_capture;
42
43 /**
44 * The constructor
45 */
46 function DiogenesWebDAV()
47 {
48 global $globals;
49
50 // call parent constructor
51 $this->HTTP_WebDAV_Server();
52
53 // construct new session each time, there is no practical way
54 // of tracking sessions with WebDAV clients
55 $_SESSION['session'] = new $globals->session;
56
57 $this->debug_capture = $globals->debugwebdav_capture;
58
59 // init debug log if necessary
60 if ($globals->debugwebdav)
61 {
62 $this->debugInit($globals->debugwebdav);
63 register_shutdown_function(array(&$this, 'debugClose'));
64 }
65
66 }
67
68
69 /**
70 * Hack to support barrels on virtualhosts.
71 *
72 * @param path
73 * @param for_html
74 */
75 function _urlencode($path, $for_html=false) {
76 if ($this->barrel->vhost) {
77 $path = preg_replace("/^(.*)\/site\/{$this->barrel->alias}\/(.*)/", "/\$2",$path);
78 }
79 return parent::_urlencode($path, $for_html);
80 }
81
82
83 /**
84 * Perform authentication
85 *
86 * @param type HTTP Authentication type (Basic, Digest, ...)
87 * @param user Username
88 * @param pass Password
89 */
90 function check_auth($type, $user, $pass)
91 {
92 global $globals;
93
94 // WebDAV access is only granted for users who log in
95 if (!$_SESSION['session']->doAuthWebDAV($user,$pass))
96 return false;
97
98 // we retrieve the user's permissions on the current barrel
99 $_SESSION['session']->setBarrelPerms($this->barrel->alias);
100
101 return true;
102 }
103
104
105
106 /**
107 * Handle a request to copy a file or directory.
108 *
109 * @param options
110 */
111 function copy($options)
112 {
113 // we do not allow copying files
114 return "403 Forbidden";
115 }
116
117
118
119
120 /**
121 * Open the debugging log file
122 */
123 function debugInit($debugfile)
124 {
125 if (empty($debugfile))
126 return;
127
128 // open the log file
129 if ($fp = fopen($debugfile, "a"))
130 {
131 $this->debug_handle = $fp;
132 }
133 }
134
135
136 /**
137 * Log a debugging message
138 */
139 function debug($func, $msg)
140 {
141 if (isset($this->debug_handle))
142 {
143 $out = sprintf("[%s] %s : %s\n",date("H:i:s"), $func, $msg);
144 fputs($this->debug_handle, $out);
145 }
146 }
147
148
149 /**
150 * Close the debugging log file
151 */
152 function debugClose()
153 {
154 if (isset($this->debug_handle))
155 {
156 fclose($this->debug_handle);
157 }
158 }
159
160
161 /**
162 * Handle a request to delete a file or directory.
163 *
164 * @param options
165 */
166 function delete($options)
167 {
168 global $globals;
169 $pathinfo = $this->parsePathInfo($options["path"]);
170
171 // get the page ID and write permissions for the current path
172 $pid = $this->barrel->getPID($pathinfo['dir']);
173 if (!$pid || !$bpage = Diogenes_Barrel_Page::fromDb($this->barrel, $pid))
174 {
175 $this->debug('delete', "Could not find directory {$pathinfo['dir']}");
176 return false;
177 }
178
179 // check permissions
180 if (!$_SESSION['session']->hasPerms($bpage->props['wperms']))
181 {
182 $this->debug('delete', "Insufficient privileges (needed : {$bpage->props['wperms']})");
183 return "403 Forbidden";
184 }
185
186 // create an RCS handle
187 $rcs = $this->getRcs();
188 $rcs->del($pid,$pathinfo['file']);
189 return "204 No content";
190 }
191
192
193 /**
194 * Return information about a file or directory.
195 *
196 * @param fspath path of the filesystem entry
197 * @param uri the address at which the item is visible
198 */
199 function fileinfo($fspath, $uri)
200 {
201 global $globals;
202
203 $file = array();
204 $file["path"]= $uri;
205
206 $file["props"][] = $this->mkprop("displayname", strtoupper($uri));
207
208 $file["props"][] = $this->mkprop("creationdate", filectime($fspath));
209 $file["props"][] = $this->mkprop("getlastmodified", filemtime($fspath));
210
211 if (is_dir($fspath)) {
212 $file["props"][] = $this->mkprop("getcontentlength", 0);
213 $file["props"][] = $this->mkprop("resourcetype", "collection");
214 $file["props"][] = $this->mkprop("getcontenttype", "httpd/unix-directory");
215 } else {
216 $file["props"][] = $this->mkprop("resourcetype", "");
217 $file["props"][] = $this->mkprop("getcontentlength", filesize($fspath));
218 if (is_readable($fspath)) {
219 $file["props"][] = $this->mkprop("getcontenttype", get_mime_type($fspath));
220 } else {
221 $file["props"][] = $this->mkprop("getcontenttype", "application/x-non-readable");
222 }
223 }
224 return $file;
225 }
226
227
228 /**
229 * Handle a GET request.
230 *
231 * @param options
232 */
233 function GET(&$options)
234 {
235 global $globals;
236 $pathinfo = $this->parsePathInfo($options["path"]);
237
238 // get the page ID and read permissions for the current path
239 $pid = $this->barrel->getPID($pathinfo['dir']);
240 if (!$pid || !$bpage = Diogenes_Barrel_Page::fromDb($this->barrel, $pid))
241 {
242 $this->debug('PROPFIND', "Could not find directory {$pathinfo['dir']}");
243 return false;
244 }
245
246 // check permissions
247 if (!$_SESSION['session']->hasPerms($bpage->props['perms']))
248 {
249 $this->debug('PROPFIND', "Insufficient privileges (needed : {$bpage->props['perms']})");
250 return "403 Forbidden";
251 }
252
253 // create stream
254 $fspath = $this->barrel->spool->spoolPath($pid,$pathinfo['file']);
255 if (file_exists($fspath)) {
256 $options['mimetype'] = get_mime_type($fspath);
257
258 // see rfc2518, section 13.7
259 // some clients seem to treat this as a reverse rule
260 // requiering a Last-Modified header if the getlastmodified header was set
261 $options['mtime'] = filemtime($fspath);
262 $options['size'] = filesize($fspath);
263
264 // TODO check permissions/result
265 $options['stream'] = fopen($fspath, "r");
266
267 return true;
268 } else {
269 return false;
270 }
271 }
272
273
274 /**
275 * Return an RCS handle.
276 */
277 function getRcs()
278 {
279 global $globals;
280 return new $globals->rcs($this,$this->barrel->alias,$_SESSION['session']->username);
281 }
282
283
284 /**
285 * PROPFIND method handler
286 *
287 * @return void
288 */
289 function http_PROPFIND()
290 {
291 $this->debug('http_PROPFIND', "called");
292 return parent::http_PROPFIND();
293 }
294
295
296 /** Report an information. Needed for RCS operations.
297 *
298 * @param msg
299 *
300 * @see Diogenes_VCS_RCS
301 */
302 function info($msg) {
303 // we do nothing
304 }
305
306
307 /**
308 * Die with a given error message. Needed for RCS operations.
309 *
310 * @param msg
311 *
312 * @see Diogenes_VCS_RCS
313 */
314 function kill($msg) {
315 $this->http_status("400 Error");
316 exit;
317 }
318
319
320 /**
321 * Record an action to the log. Needed for RCS operations.
322 *
323 * @param action
324 * @param data
325 *
326 * @see Diogenes_VCS_RCS
327 */
328 function log($action,$data) {
329 if (isset($_SESSION['log']) && is_object($_SESSION['log']))
330 $_SESSION['log']->log($action,$data);
331 }
332
333
334 /**
335 * Handle an MKCOL (directory creation) request.
336 *
337 * @param options
338 */
339 function MKCOL($options)
340 {
341 // we do not allow directory creations
342 return "403 Forbidden";
343 }
344
345
346 /**
347 * Handle a request to move/rename a file or directory.
348 *
349 * @param options
350 */
351 function move($options)
352 {
353 // we do not allow moving files
354 return "403 Forbidden";
355 }
356
357
358 /**
359 * Break down a PATH_INFO into site, page id and file for WebDAV
360 * We accept a missing trailing slash after the directory name.
361 *
362 * @param path the path to parse
363 */
364 function parsePathInfo($path) {
365 global $globals;
366
367 $this->debug('parsePathInfo', "path : $path");
368 if (empty($path) || !preg_match("/^\/([^\/]+)\/webdav(\/((.+)\/)?([^\/]*))?$/",$path,$asplit))
369 return false;
370
371 $split['alias'] = $asplit[1];
372 $split['dir'] = isset($asplit[4]) ? $asplit[4] : "";
373 $split['file'] = isset($asplit[5]) ? $asplit[5] : "";
374
375 // check that what we considered as a file is not in fact a directory
376 // with a missing trailing slash
377 /*
378 if ( empty($split['dir']) and
379 !empty($split['file']) and
380 (mysql_num_rows($globals->db->query("select location from {$split['alias']}_page where location='{$split['file']}'"))>0))
381 {
382 $split['dir'] = $split['file'];
383 $split['file'] = "";
384 }
385 */
386 return $split;
387 }
388
389 /**
390 * Handle a PROPFIND request.
391 *
392 * @param options
393 * @param files
394 */
395 function PROPFIND($options, &$files)
396 {
397 global $globals;
398 $pathinfo = $this->parsePathInfo($options["path"]);
399
400 // get the page ID and read permissions for the current path
401 $pid = $this->barrel->getPID($pathinfo['dir']);
402 if (!$pid || !$bpage = Diogenes_Barrel_Page::fromDb($this->barrel, $pid))
403 {
404 $this->debug('PROPFIND', "Could not find directory {$pathinfo['dir']}");
405 return false;
406 }
407
408 // check permissions
409 if (!$_SESSION['session']->hasPerms($bpage->props['perms']))
410 {
411 $this->debug('PROPFIND', "Insufficient privileges (needed : {$bpage->props['perms']})");
412 return "403 Forbidden";
413 }
414
415 // get absolute fs path to requested resource
416 $fspath = $this->barrel->spool->spoolPath($pid,$pathinfo['file']);
417
418 // sanity check
419 if (!file_exists($fspath)) {
420 return false;
421 }
422
423 // prepare property array
424 $files["files"] = array();
425
426 // store information for the requested path itself
427 $files["files"][] = $this->fileinfo($fspath, $options["path"]);
428
429 // information for contained resources requested?
430 if (!$pathinfo['file'] && !empty($options["depth"])) {
431
432 // make sure path ends with '/'
433 if (substr($options["path"],-1) != "/") {
434 $options["path"] .= "/";
435 }
436
437 // list the sub-directories
438 $res = $globals->db->query("select PID,location from {$this->barrel->table_page} where parent='$pid'");
439 while (list($dpid,$dloc) = mysql_fetch_row($res)) {
440 $dpath = $this->barrel->spool->spoolPath($dpid);
441 $duri = $options["path"].$dloc;
442 $files["files"][] = $this->fileinfo($dpath, $duri);
443 }
444 mysql_free_result($res);
445
446 // now list the files in the current directory
447 $handle = @opendir($fspath);
448 if ($handle) {
449 // ok, now get all its contents
450 while ($filename = readdir($handle)) {
451 if ($filename != "." && $filename != "..") {
452 $fpath = $this->barrel->spool->spoolPath($pid,$filename);
453 $furi = $options["path"].$filename;
454 if (!is_dir($fpath))
455 $files["files"][] = $this->fileinfo ($fpath, $furi);
456 }
457 }
458 }
459 }
460
461 // ok, all done
462 return true;
463 }
464
465 /**
466 * Handle a PROPPATCH request.
467 *
468 * @param options
469 */
470 function proppatch(&$options)
471 {
472 global $prefs, $tab;
473
474 $msg = "";
475
476 $path = $options["path"];
477
478 $dir = dirname($path)."/";
479 $base = basename($path);
480
481 foreach($options["props"] as $key => $prop) {
482 if($ns == "DAV:") {
483 $options["props"][$key][$status] = "403 Forbidden";
484 }
485 }
486
487 return "";
488 }
489
490 /**
491 * Handle a PUT request.
492 *
493 * @param options
494 */
495 function PUT(&$options)
496 {
497 global $globals;
498 $pathinfo = $this->parsePathInfo($options["path"]);
499
500 // we do not support multipart
501 if (!empty($options["ranges"]))
502 return "501";
503
504 // get the page ID and write permissions for the current path
505 $pid = $this->barrel->getPID($pathinfo['dir']);
506 if (!$pid || !$bpage = Diogenes_Barrel_Page::fromDb($this->barrel, $pid))
507 {
508 $this->debug('PROPFIND', "Could not find directory {$pathinfo['dir']}");
509 return false;
510 }
511
512 // check permissions
513 if (!$_SESSION['session']->hasPerms($bpage->props['wperms']))
514 {
515 $this->debug('PROPFIND', "Insufficient privileges (needed : {$bpage->props['wperms']})");
516 return "403 Forbidden";
517 }
518
519 // create an RCS handle
520 $rcs = $this->getRcs();
521 $options["new"] = !file_exists($rcs->rcsFile($pid,$pathinfo['file']));
522
523 // read content
524 $content = "";
525 while (!feof($options["stream"])) {
526 $content .= fread($options["stream"], 4096);
527 }
528
529 // if this is a barrel page, strip extraneous tags
530 if ($pathinfo['file'] == $globals->htmlfile)
531 $content = $rcs->importHtmlString($content);
532
533 // perform commit
534 if (!$rcs->commit($pid,$pathinfo['file'],$content,"WebDAV PUT of {$pathinfo['file']}"))
535 return "400 Error";
536
537 // if this is Word master document, do the HTML conversion
538 if ($globals->word_import && $pathinfo['file'] == $globals->wordfile) {
539 $myfile = $this->barrel->spool->spoolPath($pid,$globals->wordfile);
540 $rcs->importWordFile($pid, $globals->htmlfile, $myfile);
541 }
542
543 return $options["new"] ? "201 Created" : "204 No Content";
544 }
545
546
547 /**
548 * Serve a webdav request
549 */
550 function ServeRequest()
551 {
552 global $globals;
553
554 // break down path into site and location components
555 if (!($pathinfo = $this->parsePathInfo($_SERVER['PATH_INFO'])))
556 {
557 $this->http_status("404 not found");
558 exit;
559 }
560
561 // Retrieve site-wide info from database
562 $this->barrel = new Diogenes_Barrel($pathinfo['alias'], $this);
563 if (!$this->barrel->alias)
564 {
565 $this->debug("Could not find barrel '{$pathinfo['alias']}'");
566 $this->http_status("404 not found");
567 exit;
568 }
569 $this->debug('ServeRequest', "barrel : ".$this->barrel->alias);
570
571 // Debugging info
572 $props = array( 'REQUEST_METHOD', 'REQUEST_URI', 'SCRIPT_NAME', 'PATH_INFO');
573 foreach ($props as $prop) {
574 $this->debug('ServeRequest', "$prop : ". $_SERVER[$prop]);
575 }
576
577 // Here we perform some magic on the script name (on PHP 4.3.10)
578 // If the script is addressed by REQUEST_URI /site/foo/bar/ :
579 // - Apache 1.x returns /site as the SCRIPT_NAME
580 // - Apache 2.x returns /site.php as the SCRIPT_NAME
581 //
582 // We set SCRIPT_NAME to match the REQUEST_URI minus the PATH_INFO
583 //
584 $_SERVER['SCRIPT_NAME'] = substr($_SERVER['REQUEST_URI'], 0, - strlen($_SERVER['PATH_INFO']));
585 $this->debug('ServeRequest', "SCRIPT_NAME(mod) : ". $_SERVER['SCRIPT_NAME']);
586
587 // turn on output buffering
588 ob_start();
589
590 // let the base class do all the work
591 parent::ServeRequest();
592
593 // stop output buffering
594 $out = ob_get_contents();
595 ob_end_clean();
596
597 // to support barrels on virtualhosts, we rewrite the URLs
598 if ($this->barrel->vhost)
599 {
600 $ohref = (@$_SERVER["HTTPS"] === "on" ? "https:" : "http:");
601 $ohref.= "//".$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME'];
602 $ohref.= "/" . $this->barrel->alias . "/webdav/";
603 $this->debug('ServeRequest', "ohref : ".$ohref);
604
605 $vhref = (@$_SERVER['HTTPS'] === "on" ? "https:" : "http:");
606 $vhref.= "//".$this->barrel->vhost . "/webdav/";
607 $this->debug('ServeRequest', "vhref : ".$vhref);
608
609 $out = str_replace($ohref, $vhref, $out);
610 $out = str_replace('<D:displayname>/'.strtoupper($this->barrel->alias).'/WEBDAV/', '<D:displayname>/WEBDAV/', $out);
611 }
612
613 // send output
614 echo $out;
615
616 // if requested, log the output that is sent to the client
617 if ($this->debug_capture)
618 {
619 $this->debug('ServeRequest', "out\n--- begin --\n$out--- end ---\n");
620 }
621
622 }
623
624
625 }
626
627 ?>