diff --git a/admin/cron.php b/admin/cron.php
index dd238b2..5bd0a8e 100644
--- a/admin/cron.php
+++ b/admin/cron.php
@@ -375,6 +375,8 @@
         mtrace ('Cleaned up contexts');
         gc_cache_flags();
         mtrace ('Cleaned cache flags');
+        gc_webdav_locks();
+        mtrace ('Cleaned stale webdav locks');
         // If you suspect that the context paths are somehow corrupt
         // replace the line below with: build_context_path(true); 
         build_context_path();
diff --git a/admin/settings/server.php b/admin/settings/server.php
index 67e0c53..dce21e6 100644
--- a/admin/settings/server.php
+++ b/admin/settings/server.php
@@ -98,6 +98,15 @@ $temp = new admin_settingpage('rss', get_string('rss'));
 $temp->add(new admin_setting_configcheckbox('enablerssfeeds', get_string('enablerssfeeds', 'admin'), get_string('configenablerssfeeds', 'admin'), 0));
 $ADMIN->add('server', $temp);
 
+// "webdav" settingpage
+$temp = new admin_settingpage('webdav', get_string('webdav', 'admin'));
+$temp->add(new admin_setting_configcheckbox('webdavenable', get_string('webdavenable', 'admin'), get_string('configwebdavenable', 'admin'), 0));
+$temp->add(new admin_setting_configcheckbox('webdavallowfilemanagers', get_string('webdavallowfilemanagers', 'admin'), get_string('configwebdavallowfilemanagers', 'admin'), 0));
+$temp->add(new admin_setting_configtext('webdavroot', get_string('webdavroot', 'admin'), get_string('configwebdavroot', 'admin'), '', PARAM_URL));
+$temp->add(new admin_setting_configtext('webdavsubnet', get_string('webdavsubnet', 'admin'), get_string('configwebdavsubnet', 'admin'), '', PARAM_RAW));
+$temp->add(new admin_setting_configtext('webdavsessttl', get_string('webdavsessttl', 'admin'), get_string('configwebdavsessttl', 'admin'), 60, PARAM_INT));
+
+$ADMIN->add('server', $temp);
 
 // "debugging" settingpage
 $temp = new admin_settingpage('debugging', get_string('debugging', 'admin'));
diff --git a/files/index.php b/files/index.php
index 96d710c..4fa72f3 100644
--- a/files/index.php
+++ b/files/index.php
@@ -149,7 +149,6 @@
             }
         }
 
-
         echo "<table border=\"0\" style=\"margin-left:auto;margin-right:auto\" cellspacing=\"3\" cellpadding=\"3\" width=\"640\">";
         echo "<tr>";
         echo "<td colspan=\"2\">";
@@ -625,6 +624,26 @@
 
         default:
             html_header($course, $wdir);
+            if (empty($CFG->webdavroot)) {
+                $webdavurl = $CFG->wwwroot.'/webdav/moodledata-server.php/';
+            } else {
+                $webdavurl = $CFG->webdavroot.'/webdav/moodledata-server.php/';
+            }
+            if ($wdir==='/' &&
+                (!empty($CFG->webdavallowfilemanagers)) || has_capability('moodle/site:webdav', get_context_instance(CONTEXT_SYSTEM))) {
+                $msg = '';
+                if (check_browser_version('MSIE')) {
+                       $msg = '<p>'.get_string('webdavconnectmsie')
+                       . " <code><a href=\"{$webdavurl}{$course->id}/\" "
+                       . " folder=\"{$webdavurl}{$course->id}/\""
+                       . " style=\"behavior:url(#default#AnchorClick)\""
+                       . ">{$webdavurl}{$course->id}/</a></code></p>";
+                }
+                $msg .= '<p>'.get_string('webdavcanmanage')
+                       . " <code><a href=\"$webdavurl\" >$webdavurl</a></code></p>"
+                       . '<p>'.get_string('webdavmoreinfo').'</p>';
+                print_box($msg);
+            }							
             displaydir($wdir);
             html_footer();
             break;
diff --git a/lang/en_utf8/admin.php b/lang/en_utf8/admin.php
index 77a016a..8058406 100644
--- a/lang/en_utf8/admin.php
+++ b/lang/en_utf8/admin.php
@@ -434,6 +434,7 @@ $string['langupdatecomplete'] = 'Language pack update completed';
 $string['latexpreamble'] = 'LaTeX preamble';
 $string['latexsettings'] = 'LaTeX renderer Settings';
 $string['latinexcelexport'] = 'Excel encoding';
+$string['xmlrecommended'] = 'Installing the optional PHP XML extension is recommended -- it enables WebDAV support.';
 $string['localetext'] = 'Sitewide locale';
 $string['localstringcustomization'] = 'Local string customization';
 $string['location'] = 'Location';
@@ -747,6 +748,17 @@ $string['uuupdatemissing'] = 'Fill in missing from file and defaults';
 $string['uuupdatetype'] = 'Existing user details';
 $string['validateerror'] = 'This value was not valid:';
 $string['warningcurrentsetting'] = 'Invalid current value: $a';
+$string['webdav'] = 'WebDAV';
+$string['webdavenable'] = 'Enable WebDAV access';
+$string['webdavallowfilemanagers'] = 'Allow WebDAV to file managers';
+$string['webdavroot'] = 'WebDAV root URL';
+$string['webdavsessttl'] = 'WebDAV session TTL';
+$string['webdavsubnet'] = 'WebDAV subnet';
+$string['configwebdavenable'] = 'Enables access to Moodledata using the WebDAV protocol. Only users with \'moodle/site:webdav\' capabilities will be allowed to use it (only admins by default).';
+$string['configwebdavallowfilemanagers'] = 'Allows WebDAV access to users that have are allowed file management (\'moodle/course:managefiles\') <em>in any course</em>. \'Editing teacher\' roles normally have this, so this is a handy way to allow editing teachers.';
+$string['configwebdavroot'] = 'Override wwwroot for WebDAV connections -- normally not needed. Useful to bypass load balancers.';
+$string['configwebdavsessttl'] = 'In minutes. Controls how long the WebDAV cached credentials last. Moodle caches access credentials used over WebDAV to avoid innecesary traffic to the authentication backends.';
+$string['configwebdavsubnet'] = 'Only allow WebDAV connections from this subnet. Can be used to only permit WebDAV access from the local LAN. If empty, connections are accepted from any machine.';
 $string['webproxy'] = 'Web proxy';
 $string['webproxyinfo'] = 'Fill in following options if your Moodle server can not access internet directly. Internet access is required for download of environment data, language packs, RSS feeds, timezones, etc.<br /><em>PHP cURL extension is highly recommended.</em>';
 $string['xmlrpcrecommended'] = 'Installing the optional xmlrpc extension is useful for Moodle Networking functionality.';
diff --git a/lang/en_utf8/moodle.php b/lang/en_utf8/moodle.php
index 353ee8a..347c64b 100644
--- a/lang/en_utf8/moodle.php
+++ b/lang/en_utf8/moodle.php
@@ -1586,6 +1586,9 @@ $string['virusplaceholder'] = 'This file that has been uploaded was found to con
 $string['visible'] = 'Visible';
 $string['visibletostudents'] = 'Visible to $a';
 $string['warningdeleteresource'] = 'Warning: $a is referred in a resource. Would you like to update the resource?';
+$string['webdavcanmanage'] = 'To manage files for all the courses you have access to using WebDAV, use this address:';
+$string['webdavconnectmsie'] = 'You can connect to this directory via Web Folders (will open in Windows File Explorer) following this link:';
+$string['webdavmoreinfo'] = 'For more information on how to connect to WebDAV, see the <a href=\"http://docs.moodle.org/en/WebDAV_Connect\">WebDAV Connect</a> page.';
 $string['webpage'] = 'Web page';
 $string['week'] = 'Week';
 $string['weekhide'] = 'Hide this week from $a';
diff --git a/lang/en_utf8/role.php b/lang/en_utf8/role.php
index 7f2e9c4..5571349 100644
--- a/lang/en_utf8/role.php
+++ b/lang/en_utf8/role.php
@@ -150,6 +150,7 @@ $string['site:uploadusers'] = 'Upload new users from file';
 $string['site:viewfullnames'] = 'Always see full names of users';
 $string['site:viewparticipants'] = 'View participants';
 $string['site:viewreports'] = 'View reports';
+$string['site:webdav'] = 'Connect via WebDAV';
 $string['tag:manage'] = 'Manage all tags';
 $string['tag:create'] = 'Create new tags';
 $string['tag:edit'] = 'Edit existing tags';
diff --git a/lib/db/access.php b/lib/db/access.php
index 3bb9f05..f7c233e 100644
--- a/lib/db/access.php
+++ b/lib/db/access.php
@@ -1178,8 +1178,16 @@ $moodle_capabilities = array(
             'editingteacher' => CAP_ALLOW,
             'coursecreator' => CAP_ALLOW
         )
+    ),
+
+    'moodle/site:webdav' => array(
+        'riskbitmask' => RISK_XSS,
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_SYSTEM,
+        'legacy' => array(
+            'coursecreator' => CAP_ALLOW
+        )
     )
 );
 
-
 ?>
diff --git a/lib/moodlelib.php b/lib/moodlelib.php
index 3d32558..40b9e43 100644
--- a/lib/moodlelib.php
+++ b/lib/moodlelib.php
@@ -860,6 +860,14 @@ function gc_cache_flags() {
 }
 
 /**
+ * Garbage-collect webdav locks
+ *
+ */
+function gc_webdav_locks() {
+    return delete_records_select('webdav_locks', 'expiry < ' . time());
+}
+
+/**
  * Refresh current $USER session global variable with all their current preferences.
  * @uses $USER
  */
diff --git a/lib/pear/Console/Getopt.php b/lib/pear/Console/Getopt.php
new file mode 100644
index 0000000..1580bca
--- /dev/null
+++ b/lib/pear/Console/Getopt.php
@@ -0,0 +1,290 @@
+<?php
+/* vim: set expandtab tabstop=4 shiftwidth=4: */
+// +----------------------------------------------------------------------+
+// | PHP Version 5                                                        |
+// +----------------------------------------------------------------------+
+// | Copyright (c) 1997-2004 The PHP Group                                |
+// +----------------------------------------------------------------------+
+// | This source file is subject to version 3.0 of the PHP license,       |
+// | that is bundled with this package in the file LICENSE, and is        |
+// | available through the world-wide-web at the following url:           |
+// | http://www.php.net/license/3_0.txt.                                  |
+// | If you did not receive a copy of the PHP license and are unable to   |
+// | obtain it through the world-wide-web, please send a note to          |
+// | license@php.net so we can mail you a copy immediately.               |
+// +----------------------------------------------------------------------+
+// | Author: Andrei Zmievski <andrei@php.net>                             |
+// +----------------------------------------------------------------------+
+//
+// $Id: Getopt.php,v 1.1 2008/01/07 01:54:29 mjollnir_ Exp $
+
+require_once 'PEAR.php';
+
+/**
+ * Command-line options parsing class.
+ *
+ * @author Andrei Zmievski <andrei@php.net>
+ *
+ */
+class Console_Getopt {
+    /**
+     * Parses the command-line options.
+     *
+     * The first parameter to this function should be the list of command-line
+     * arguments without the leading reference to the running program.
+     *
+     * The second parameter is a string of allowed short options. Each of the
+     * option letters can be followed by a colon ':' to specify that the option
+     * requires an argument, or a double colon '::' to specify that the option
+     * takes an optional argument.
+     *
+     * The third argument is an optional array of allowed long options. The
+     * leading '--' should not be included in the option name. Options that
+     * require an argument should be followed by '=', and options that take an
+     * option argument should be followed by '=='.
+     *
+     * The return value is an array of two elements: the list of parsed
+     * options and the list of non-option command-line arguments. Each entry in
+     * the list of parsed options is a pair of elements - the first one
+     * specifies the option, and the second one specifies the option argument,
+     * if there was one.
+     *
+     * Long and short options can be mixed.
+     *
+     * Most of the semantics of this function are based on GNU getopt_long().
+     *
+     * @param array  $args           an array of command-line arguments
+     * @param string $short_options  specifies the list of allowed short options
+     * @param array  $long_options   specifies the list of allowed long options
+     *
+     * @return array two-element array containing the list of parsed options and
+     * the non-option arguments
+     *
+     * @access public
+     *
+     */
+    function getopt2($args, $short_options, $long_options = null)
+    {
+        return Console_Getopt::doGetopt(2, $args, $short_options, $long_options);
+    }
+
+    /**
+     * This function expects $args to start with the script name (POSIX-style).
+     * Preserved for backwards compatibility.
+     * @see getopt2()
+     */    
+    function getopt($args, $short_options, $long_options = null)
+    {
+        return Console_Getopt::doGetopt(1, $args, $short_options, $long_options);
+    }
+
+    /**
+     * The actual implementation of the argument parsing code.
+     */
+    function doGetopt($version, $args, $short_options, $long_options = null)
+    {
+        // in case you pass directly readPHPArgv() as the first arg
+        if (PEAR::isError($args)) {
+            return $args;
+        }
+        if (empty($args)) {
+            return array(array(), array());
+        }
+        $opts     = array();
+        $non_opts = array();
+
+        settype($args, 'array');
+
+        if ($long_options) {
+            sort($long_options);
+        }
+
+        /*
+         * Preserve backwards compatibility with callers that relied on
+         * erroneous POSIX fix.
+         */
+        if ($version < 2) {
+            if (isset($args[0]{0}) && $args[0]{0} != '-') {
+                array_shift($args);
+            }
+        }
+
+        reset($args);
+        while (list($i, $arg) = each($args)) {
+
+            /* The special element '--' means explicit end of
+               options. Treat the rest of the arguments as non-options
+               and end the loop. */
+            if ($arg == '--') {
+                $non_opts = array_merge($non_opts, array_slice($args, $i + 1));
+                break;
+            }
+
+            if ($arg{0} != '-' || (strlen($arg) > 1 && $arg{1} == '-' && !$long_options)) {
+                $non_opts = array_merge($non_opts, array_slice($args, $i));
+                break;
+            } elseif (strlen($arg) > 1 && $arg{1} == '-') {
+                $error = Console_Getopt::_parseLongOption(substr($arg, 2), $long_options, $opts, $args);
+                if (PEAR::isError($error))
+                    return $error;
+            } elseif ($arg == '-') {
+                // - is stdin
+                $non_opts = array_merge($non_opts, array_slice($args, $i));
+                break;
+            } else {
+                $error = Console_Getopt::_parseShortOption(substr($arg, 1), $short_options, $opts, $args);
+                if (PEAR::isError($error))
+                    return $error;
+            }
+        }
+
+        return array($opts, $non_opts);
+    }
+
+    /**
+     * @access private
+     *
+     */
+    function _parseShortOption($arg, $short_options, &$opts, &$args)
+    {
+        for ($i = 0; $i < strlen($arg); $i++) {
+            $opt = $arg{$i};
+            $opt_arg = null;
+
+            /* Try to find the short option in the specifier string. */
+            if (($spec = strstr($short_options, $opt)) === false || $arg{$i} == ':')
+            {
+                return PEAR::raiseError("Console_Getopt: unrecognized option -- $opt");
+            }
+
+            if (strlen($spec) > 1 && $spec{1} == ':') {
+                if (strlen($spec) > 2 && $spec{2} == ':') {
+                    if ($i + 1 < strlen($arg)) {
+                        /* Option takes an optional argument. Use the remainder of
+                           the arg string if there is anything left. */
+                        $opts[] = array($opt, substr($arg, $i + 1));
+                        break;
+                    }
+                } else {
+                    /* Option requires an argument. Use the remainder of the arg
+                       string if there is anything left. */
+                    if ($i + 1 < strlen($arg)) {
+                        $opts[] = array($opt,  substr($arg, $i + 1));
+                        break;
+                    } else if (list(, $opt_arg) = each($args)) {
+                        /* Else use the next argument. */;
+                        if (Console_Getopt::_isShortOpt($opt_arg) || Console_Getopt::_isLongOpt($opt_arg)) {
+                            return PEAR::raiseError("Console_Getopt: option requires an argument -- $opt");
+                        }
+                    } else {
+                        return PEAR::raiseError("Console_Getopt: option requires an argument -- $opt");
+                    }
+                }
+            }
+
+            $opts[] = array($opt, $opt_arg);
+        }
+    }
+
+    /**
+     * @access private
+     *
+     */
+    function _isShortOpt($arg)
+    {
+        return strlen($arg) == 2 && $arg[0] == '-' && preg_match('/[a-zA-Z]/', $arg[1]);
+    }
+
+    /**
+     * @access private
+     *
+     */
+    function _isLongOpt($arg)
+    {
+        return strlen($arg) > 2 && $arg[0] == '-' && $arg[1] == '-' &&
+            preg_match('/[a-zA-Z]+$/', substr($arg, 2));
+    }
+
+    /**
+     * @access private
+     *
+     */
+    function _parseLongOption($arg, $long_options, &$opts, &$args)
+    {
+        @list($opt, $opt_arg) = explode('=', $arg, 2);
+        $opt_len = strlen($opt);
+
+        for ($i = 0; $i < count($long_options); $i++) {
+            $long_opt  = $long_options[$i];
+            $opt_start = substr($long_opt, 0, $opt_len);
+            $long_opt_name = str_replace('=', '', $long_opt);
+
+            /* Option doesn't match. Go on to the next one. */
+            if ($long_opt_name != $opt) {
+                continue;
+            }
+
+            $opt_rest  = substr($long_opt, $opt_len);
+
+            /* Check that the options uniquely matches one of the allowed
+               options. */
+            if ($i + 1 < count($long_options)) {
+                $next_option_rest = substr($long_options[$i + 1], $opt_len);
+            } else {
+                $next_option_rest = '';
+            }
+            if ($opt_rest != '' && $opt{0} != '=' &&
+                $i + 1 < count($long_options) &&
+                $opt == substr($long_options[$i+1], 0, $opt_len) &&
+                $next_option_rest != '' &&
+                $next_option_rest{0} != '=') {
+                return PEAR::raiseError("Console_Getopt: option --$opt is ambiguous");
+            }
+
+            if (substr($long_opt, -1) == '=') {
+                if (substr($long_opt, -2) != '==') {
+                    /* Long option requires an argument.
+                       Take the next argument if one wasn't specified. */;
+                    if (!strlen($opt_arg) && !(list(, $opt_arg) = each($args))) {
+                        return PEAR::raiseError("Console_Getopt: option --$opt requires an argument");
+                    }
+                    if (Console_Getopt::_isShortOpt($opt_arg) || Console_Getopt::_isLongOpt($opt_arg)) {
+                        return PEAR::raiseError("Console_Getopt: option requires an argument --$opt");
+                    }
+                }
+            } else if ($opt_arg) {
+                return PEAR::raiseError("Console_Getopt: option --$opt doesn't allow an argument");
+            }
+
+            $opts[] = array('--' . $opt, $opt_arg);
+            return;
+        }
+
+        return PEAR::raiseError("Console_Getopt: unrecognized option --$opt");
+    }
+
+    /**
+    * Safely read the $argv PHP array across different PHP configurations.
+    * Will take care on register_globals and register_argc_argv ini directives
+    *
+    * @access public
+    * @return mixed the $argv PHP array or PEAR error if not registered
+    */
+    function readPHPArgv()
+    {
+        global $argv;
+        if (!is_array($argv)) {
+            if (!@is_array($_SERVER['argv'])) {
+                if (!@is_array($GLOBALS['HTTP_SERVER_VARS']['argv'])) {
+                    return PEAR::raiseError("Console_Getopt: Could not read cmd args (register_argc_argv=Off?)");
+                }
+                return $GLOBALS['HTTP_SERVER_VARS']['argv'];
+            }
+            return $_SERVER['argv'];
+        }
+        return $argv;
+    }
+
+}
+
+?>
diff --git a/lib/pear/System.php b/lib/pear/System.php
new file mode 100644
index 0000000..dc3acee
--- /dev/null
+++ b/lib/pear/System.php
@@ -0,0 +1,540 @@
+<?php
+//
+// +----------------------------------------------------------------------+
+// | PHP Version 5                                                        |
+// +----------------------------------------------------------------------+
+// | Copyright (c) 1997-2004 The PHP Group                                |
+// +----------------------------------------------------------------------+
+// | This source file is subject to version 3.0 of the PHP license,       |
+// | that is bundled with this package in the file LICENSE, and is        |
+// | available through the world-wide-web at the following url:           |
+// | http://www.php.net/license/3_0.txt.                                  |
+// | If you did not receive a copy of the PHP license and are unable to   |
+// | obtain it through the world-wide-web, please send a note to          |
+// | license@php.net so we can mail you a copy immediately.               |
+// +----------------------------------------------------------------------+
+// | Authors: Tomas V.V.Cox <cox@idecnet.com>                             |
+// +----------------------------------------------------------------------+
+//
+// $Id: System.php,v 1.36 2004/06/15 16:33:46 pajoye Exp $
+//
+
+require_once 'PEAR.php';
+require_once 'Console/Getopt.php';
+
+$GLOBALS['_System_temp_files'] = array();
+
+/**
+* System offers cross plattform compatible system functions
+*
+* Static functions for different operations. Should work under
+* Unix and Windows. The names and usage has been taken from its respectively
+* GNU commands. The functions will return (bool) false on error and will
+* trigger the error with the PHP trigger_error() function (you can silence
+* the error by prefixing a '@' sign after the function call).
+*
+* Documentation on this class you can find in:
+* http://pear.php.net/manual/
+*
+* Example usage:
+* if (!@System::rm('-r file1 dir1')) {
+*    print "could not delete file1 or dir1";
+* }
+*
+* In case you need to to pass file names with spaces,
+* pass the params as an array:
+*
+* System::rm(array('-r', $file1, $dir1));
+*
+* @package  System
+* @author   Tomas V.V.Cox <cox@idecnet.com>
+* @version  $Revision: 1.36 $
+* @access   public
+* @see      http://pear.php.net/manual/
+*/
+class System
+{
+    /**
+    * returns the commandline arguments of a function
+    *
+    * @param    string  $argv           the commandline
+    * @param    string  $short_options  the allowed option short-tags
+    * @param    string  $long_options   the allowed option long-tags
+    * @return   array   the given options and there values
+    * @access private
+    */
+    function _parseArgs($argv, $short_options, $long_options = null)
+    {
+        if (!is_array($argv) && $argv !== null) {
+            $argv = preg_split('/\s+/', $argv);
+        }
+        return Console_Getopt::getopt2($argv, $short_options);
+    }
+
+    /**
+    * Output errors with PHP trigger_error(). You can silence the errors
+    * with prefixing a "@" sign to the function call: @System::mkdir(..);
+    *
+    * @param mixed $error a PEAR error or a string with the error message
+    * @return bool false
+    * @access private
+    */
+    function raiseError($error)
+    {
+        if (PEAR::isError($error)) {
+            $error = $error->getMessage();
+        }
+        trigger_error($error, E_USER_WARNING);
+        return false;
+    }
+
+    /**
+    * Creates a nested array representing the structure of a directory
+    *
+    * System::_dirToStruct('dir1', 0) =>
+    *   Array
+    *    (
+    *    [dirs] => Array
+    *        (
+    *            [0] => dir1
+    *        )
+    *
+    *    [files] => Array
+    *        (
+    *            [0] => dir1/file2
+    *            [1] => dir1/file3
+    *        )
+    *    )
+    * @param    string  $sPath      Name of the directory
+    * @param    integer $maxinst    max. deep of the lookup
+    * @param    integer $aktinst    starting deep of the lookup
+    * @return   array   the structure of the dir
+    * @access   private
+    */
+
+    function _dirToStruct($sPath, $maxinst, $aktinst = 0)
+    {
+        $struct = array('dirs' => array(), 'files' => array());
+        if (($dir = @opendir($sPath)) === false) {
+            System::raiseError("Could not open dir $sPath");
+            return $struct; // XXX could not open error
+        }
+        $struct['dirs'][] = $sPath; // XXX don't add if '.' or '..' ?
+        $list = array();
+        while ($file = readdir($dir)) {
+            if ($file != '.' && $file != '..') {
+                $list[] = $file;
+            }
+        }
+        closedir($dir);
+        sort($list);
+        if ($aktinst < $maxinst || $maxinst == 0) {
+            foreach($list as $val) {
+                $path = $sPath . DIRECTORY_SEPARATOR . $val;
+                if (is_dir($path)) {
+                    $tmp = System::_dirToStruct($path, $maxinst, $aktinst+1);
+                    $struct = array_merge_recursive($tmp, $struct);
+                } else {
+                    $struct['files'][] = $path;
+                }
+            }
+        }
+        return $struct;
+    }
+
+    /**
+    * Creates a nested array representing the structure of a directory and files
+    *
+    * @param    array $files Array listing files and dirs
+    * @return   array
+    * @see System::_dirToStruct()
+    */
+    function _multipleToStruct($files)
+    {
+        $struct = array('dirs' => array(), 'files' => array());
+        settype($files, 'array');
+        foreach ($files as $file) {
+            if (is_dir($file)) {
+                $tmp = System::_dirToStruct($file, 0);
+                $struct = array_merge_recursive($tmp, $struct);
+            } else {
+                $struct['files'][] = $file;
+            }
+        }
+        return $struct;
+    }
+
+    /**
+    * The rm command for removing files.
+    * Supports multiple files and dirs and also recursive deletes
+    *
+    * @param    string  $args   the arguments for rm
+    * @return   mixed   PEAR_Error or true for success
+    * @access   public
+    */
+    function rm($args)
+    {
+        $opts = System::_parseArgs($args, 'rf'); // "f" do nothing but like it :-)
+        if (PEAR::isError($opts)) {
+            return System::raiseError($opts);
+        }
+        foreach($opts[0] as $opt) {
+            if ($opt[0] == 'r') {
+                $do_recursive = true;
+            }
+        }
+        $ret = true;
+        if (isset($do_recursive)) {
+            $struct = System::_multipleToStruct($opts[1]);
+            foreach($struct['files'] as $file) {
+                if (!@unlink($file)) {
+                    $ret = false;
+                }
+            }
+            foreach($struct['dirs'] as $dir) {
+                if (!@rmdir($dir)) {
+                    $ret = false;
+                }
+            }
+        } else {
+            foreach ($opts[1] as $file) {
+                $delete = (is_dir($file)) ? 'rmdir' : 'unlink';
+                if (!@$delete($file)) {
+                    $ret = false;
+                }
+            }
+        }
+        return $ret;
+    }
+
+    /**
+    * Make directories. Note that we use call_user_func('mkdir') to avoid
+    * a problem with ZE2 calling System::mkDir instead of the native PHP func.
+    *
+    * @param    string  $args    the name of the director(y|ies) to create
+    * @return   bool    True for success
+    * @access   public
+    */
+    function mkDir($args)
+    {
+        $opts = System::_parseArgs($args, 'pm:');
+        if (PEAR::isError($opts)) {
+            return System::raiseError($opts);
+        }
+        $mode = 0777; // default mode
+        foreach($opts[0] as $opt) {
+            if ($opt[0] == 'p') {
+                $create_parents = true;
+            } elseif($opt[0] == 'm') {
+                // if the mode is clearly an octal number (starts with 0)
+                // convert it to decimal
+                if (strlen($opt[1]) && $opt[1]{0} == '0') {
+                    $opt[1] = octdec($opt[1]);
+                } else {
+                    // convert to int
+                    $opt[1] += 0;
+                }
+                $mode = $opt[1];
+            }
+        }
+        $ret = true;
+        if (isset($create_parents)) {
+            foreach($opts[1] as $dir) {
+                $dirstack = array();
+                while (!@is_dir($dir) && $dir != DIRECTORY_SEPARATOR) {
+                    array_unshift($dirstack, $dir);
+                    $dir = dirname($dir);
+                }
+                while ($newdir = array_shift($dirstack)) {
+                    if (!call_user_func('mkdir', $newdir, $mode)) {
+                        $ret = false;
+                    }
+                }
+            }
+        } else {
+            foreach($opts[1] as $dir) {
+                if (!@is_dir($dir) && !call_user_func('mkdir', $dir, $mode)) {
+                    $ret = false;
+                }
+            }
+        }
+        return $ret;
+    }
+
+    /**
+    * Concatenate files
+    *
+    * Usage:
+    * 1) $var = System::cat('sample.txt test.txt');
+    * 2) System::cat('sample.txt test.txt > final.txt');
+    * 3) System::cat('sample.txt test.txt >> final.txt');
+    *
+    * Note: as the class use fopen, urls should work also (test that)
+    *
+    * @param    string  $args   the arguments
+    * @return   boolean true on success
+    * @access   public
+    */
+    function &cat($args)
+    {
+        $ret = null;
+        $files = array();
+        if (!is_array($args)) {
+            $args = preg_split('/\s+/', $args);
+        }
+        for($i=0; $i < count($args); $i++) {
+            if ($args[$i] == '>') {
+                $mode = 'wb';
+                $outputfile = $args[$i+1];
+                break;
+            } elseif ($args[$i] == '>>') {
+                $mode = 'ab+';
+                $outputfile = $args[$i+1];
+                break;
+            } else {
+                $files[] = $args[$i];
+            }
+        }
+        if (isset($mode)) {
+            if (!$outputfd = fopen($outputfile, $mode)) {
+                $err = System::raiseError("Could not open $outputfile");
+                return $err;
+            }
+            $ret = true;
+        }
+        foreach ($files as $file) {
+            if (!$fd = fopen($file, 'r')) {
+                System::raiseError("Could not open $file");
+                continue;
+            }
+            while ($cont = fread($fd, 2048)) {
+                if (isset($outputfd)) {
+                    fwrite($outputfd, $cont);
+                } else {
+                    $ret .= $cont;
+                }
+            }
+            fclose($fd);
+        }
+        if (@is_resource($outputfd)) {
+            fclose($outputfd);
+        }
+        return $ret;
+    }
+
+    /**
+    * Creates temporary files or directories. This function will remove
+    * the created files when the scripts finish its execution.
+    *
+    * Usage:
+    *   1) $tempfile = System::mktemp("prefix");
+    *   2) $tempdir  = System::mktemp("-d prefix");
+    *   3) $tempfile = System::mktemp();
+    *   4) $tempfile = System::mktemp("-t /var/tmp prefix");
+    *
+    * prefix -> The string that will be prepended to the temp name
+    *           (defaults to "tmp").
+    * -d     -> A temporary dir will be created instead of a file.
+    * -t     -> The target dir where the temporary (file|dir) will be created. If
+    *           this param is missing by default the env vars TMP on Windows or
+    *           TMPDIR in Unix will be used. If these vars are also missing
+    *           c:\windows\temp or /tmp will be used.
+    *
+    * @param   string  $args  The arguments
+    * @return  mixed   the full path of the created (file|dir) or false
+    * @see System::tmpdir()
+    * @access  public
+    */
+    function mktemp($args = null)
+    {
+        static $first_time = true;
+        $opts = System::_parseArgs($args, 't:d');
+        if (PEAR::isError($opts)) {
+            return System::raiseError($opts);
+        }
+        foreach($opts[0] as $opt) {
+            if($opt[0] == 'd') {
+                $tmp_is_dir = true;
+            } elseif($opt[0] == 't') {
+                $tmpdir = $opt[1];
+            }
+        }
+        $prefix = (isset($opts[1][0])) ? $opts[1][0] : 'tmp';
+        if (!isset($tmpdir)) {
+            $tmpdir = System::tmpdir();
+        }
+        if (!System::mkDir("-p $tmpdir")) {
+            return false;
+        }
+        $tmp = tempnam($tmpdir, $prefix);
+        if (isset($tmp_is_dir)) {
+            unlink($tmp); // be careful possible race condition here
+            if (!call_user_func('mkdir', $tmp, 0700)) {
+                return System::raiseError("Unable to create temporary directory $tmpdir");
+            }
+        }
+        $GLOBALS['_System_temp_files'][] = $tmp;
+        if ($first_time) {
+            PEAR::registerShutdownFunc(array('System', '_removeTmpFiles'));
+            $first_time = false;
+        }
+        return $tmp;
+    }
+
+    /**
+    * Remove temporary files created my mkTemp. This function is executed
+    * at script shutdown time
+    *
+    * @access private
+    */
+    function _removeTmpFiles()
+    {
+        if (count($GLOBALS['_System_temp_files'])) {
+            $delete = $GLOBALS['_System_temp_files'];
+            array_unshift($delete, '-r');
+            System::rm($delete);
+        }
+    }
+
+    /**
+    * Get the path of the temporal directory set in the system
+    * by looking in its environments variables.
+    * Note: php.ini-recommended removes the "E" from the variables_order setting,
+    * making unavaible the $_ENV array, that s why we do tests with _ENV
+    *
+    * @return string The temporal directory on the system
+    */
+    function tmpdir()
+    {
+        if (OS_WINDOWS) {
+            if ($var = isset($_ENV['TEMP']) ? $_ENV['TEMP'] : getenv('TEMP')) {
+                return $var;
+            }
+            if ($var = isset($_ENV['TMP']) ? $_ENV['TMP'] : getenv('TMP')) {
+                return $var;
+            }
+            if ($var = isset($_ENV['windir']) ? $_ENV['windir'] : getenv('windir')) {
+                return $var;
+            }
+            return getenv('SystemRoot') . '\temp';
+        }
+        if ($var = isset($_ENV['TMPDIR']) ? $_ENV['TMPDIR'] : getenv('TMPDIR')) {
+            return $var;
+        }
+        return '/tmp';
+    }
+
+    /**
+    * The "which" command (show the full path of a command)
+    *
+    * @param string $program The command to search for
+    * @return mixed A string with the full path or false if not found
+    * @author Stig Bakken <ssb@php.net>
+    */
+    function which($program, $fallback = false)
+    {
+        // is_executable() is not available on windows
+        if (OS_WINDOWS) {
+            $pear_is_executable = 'is_file';
+        } else {
+            $pear_is_executable = 'is_executable';
+        }
+
+        // full path given
+        if (basename($program) != $program) {
+            return (@$pear_is_executable($program)) ? $program : $fallback;
+        }
+
+        // XXX FIXME honor safe mode
+        $path_delim = OS_WINDOWS ? ';' : ':';
+        $exe_suffixes = OS_WINDOWS ? array('.exe','.bat','.cmd','.com') : array('');
+        $path_elements = explode($path_delim, getenv('PATH'));
+        foreach ($exe_suffixes as $suff) {
+            foreach ($path_elements as $dir) {
+                $file = $dir . DIRECTORY_SEPARATOR . $program . $suff;
+                if (@is_file($file) && @$pear_is_executable($file)) {
+                    return $file;
+                }
+            }
+        }
+        return $fallback;
+    }
+
+    /**
+    * The "find" command
+    *
+    * Usage:
+    *
+    * System::find($dir);
+    * System::find("$dir -type d");
+    * System::find("$dir -type f");
+    * System::find("$dir -name *.php");
+    * System::find("$dir -name *.php -name *.htm*");
+    * System::find("$dir -maxdepth 1");
+    *
+    * Params implmented:
+    * $dir            -> Start the search at this directory
+    * -type d         -> return only directories
+    * -type f         -> return only files
+    * -maxdepth <n>   -> max depth of recursion
+    * -name <pattern> -> search pattern (bash style). Multiple -name param allowed
+    *
+    * @param  mixed Either array or string with the command line
+    * @return array Array of found files
+    *
+    */
+    function find($args)
+    {
+        if (!is_array($args)) {
+            $args = preg_split('/\s+/', $args, -1, PREG_SPLIT_NO_EMPTY);
+        }
+        $dir = array_shift($args);
+        $patterns = array();
+        $depth = 0;
+        $do_files = $do_dirs = true;
+        for ($i = 0; $i < count($args); $i++) {
+            switch ($args[$i]) {
+                case '-type':
+                    if (in_array($args[$i+1], array('d', 'f'))) {
+                        if ($args[$i+1] == 'd') {
+                             $do_files = false;
+                        } else {
+                            $do_dirs = false;
+                        }
+                    }
+                    $i++;
+                    break;
+                case '-name':
+                    $patterns[] = "(" . preg_replace(array('/\./', '/\*/'),
+                                                     array('\.', '.*'),
+                                                     $args[$i+1])
+                                      . ")";
+                    $i++;
+                    break;
+                case '-maxdepth':
+                    $depth = $args[$i+1];
+                    break;
+            }
+        }
+        $path = System::_dirToStruct($dir, $depth);
+        if ($do_files && $do_dirs) {
+            $files = array_merge($path['files'], $path['dirs']);
+        } elseif ($do_dirs) {
+            $files = $path['dirs'];
+        } else {
+            $files = $path['files'];
+        }
+        if (count($patterns)) {
+            $patterns = implode('|', $patterns);
+            $ret = array();
+            for ($i = 0; $i < count($files); $i++) {
+                if (preg_match("#^$patterns\$#", $files[$i])) {
+                    $ret[] = $files[$i];
+                }
+            }
+            return $ret;
+        }
+        return $files;
+    }
+}
+?>
diff --git a/webdav/moodledata-server.php b/webdav/moodledata-server.php
new file mode 100644
index 0000000..5c8dcfd
--- /dev/null
+++ b/webdav/moodledata-server.php
@@ -0,0 +1,48 @@
+<?php
+
+// Early setup of tight output
+ini_set('error_reporting', 34815);
+ini_set('log_errors', 1);
+
+// Prepare to load config.php
+// with a very limited lib/setup.php
+$nomoodlecookie=true;
+
+define('MOODLE_SANE_OUTPUT',true); // No random output in our XML, thanks
+define('MOODLE_SANE_INPUT',true);  // No magic_quotes, we know how to use $db->qstr();
+
+require_once(dirname(dirname(__FILE__)).'/config.php');
+
+if (empty($CFG->webdavenable)) {
+    header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden');
+    $msg = 'WebDAV request forbidden - WebDAV is disabled';
+    echo "<p>$msg</p>";
+    mdie($msg);
+}
+if (!empty($CFG->webdavsubnet)) {
+    // A local reverse proxy defeats the purpose of this :-/
+    if (!address_in_subnet($_SERVER['REMOTE_ADDR'],$CFG->webdavsubnet)) {
+        $msg = 'WebDAV request forbidden - WebDAV is restricted';
+        echo "<p>$msg</p>";
+        mdie($msg);
+    }
+}
+if (!function_exists('xml_parser_create_ns')) {
+    header($_SERVER['SERVER_PROTOCOL'] . ' 500 Server Error');
+    $msg = 'WebDAV requires the XML extension.';
+    echo "<p>$msg</p>";
+    mdie($msg);
+}
+
+// Adding our PEAR path - maybe this should be
+// in lib/setup.php
+ini_set("include_path", ini_get("include_path"). PATH_SEPARATOR . $CFG->libdir.'/pear');
+
+// Here we go!
+require_once($CFG->libdir . '/filelib.php');
+require_once (dirname(__FILE__)."/server/moodledata.class.php");
+$server = new HTTP_WebDAV_Server_Moodledata();
+$server->ServeRequest($CFG->dataroot);
+
+// The EOF should be flush with the PHP closing tag...
+?>
\ No newline at end of file
diff --git a/webdav/server/moodledata.class.php b/webdav/server/moodledata.class.php
new file mode 100644
index 0000000..71e01be
--- /dev/null
+++ b/webdav/server/moodledata.class.php
@@ -0,0 +1,990 @@
+<?php // $Id: Filesystem.php,v 1.46 2007/11/14 13:43:51 hholzgra Exp $
+/*
+   +----------------------------------------------------------------------+
+   | Copyright (c) 2002-2008 Christian Stocker, Hartmut Holzgraefe        |
+   |                         Godalming College (Joe McCarthy)
+   | All rights reserved                                                  |
+   |                                                                      |
+   | Redistribution and use in source and binary forms, with or without   |
+   | modification, are permitted provided that the following conditions   |
+   | are met:                                                             |
+   |                                                                      |
+   | 1. Redistributions of source code must retain the above copyright    |
+   |    notice, this list of conditions and the following disclaimer.     |
+   | 2. Redistributions in binary form must reproduce the above copyright |
+   |    notice, this list of conditions and the following disclaimer in   |
+   |    the documentation and/or other materials provided with the        |
+   |    distribution.                                                     |
+   | 3. The names of the authors may not be used to endorse or promote    |
+   |    products derived from this software without specific prior        |
+   |    written permission.                                               |
+   |                                                                      |
+   | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS  |
+   | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT    |
+   | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS    |
+   | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE       |
+   | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,  |
+   | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, |
+   | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;     |
+   | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER     |
+   | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT   |
+   | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN    |
+   | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE      |
+   | POSSIBILITY OF SUCH DAMAGE.                                          |
+   +----------------------------------------------------------------------+
+*/
+
+require_once "HTTP/WebDAV/Server.php";
+require_once "System.php";
+    
+/**
+ * Moodledata access using WebDAV
+ *
+ * @access  public
+ * @author  Hartmut Holzgraefe <hartmut@php.net>
+ * @author  Martin Langhoff <martin@catalyst.net.nz>
+ * @version @package-version@
+ */
+class HTTP_WebDAV_Server_Moodledata extends HTTP_WebDAV_Server 
+{
+    /**
+     * Root directory for WebDAV access
+     *
+     * Defaults to webserver document root (set by ServeRequest)
+     *
+     * @access private
+     * @var    string
+     */
+    var $base = '';
+
+    var $http_auth_realm = '';
+
+    // file/dir names that are forbidden
+    // in a course directory
+    var $coursedirforbidden = array('moddata');
+
+    /**
+     * Constructor ;-)
+     */
+    function HTTP_WebDAV_Server_Moodledata()
+    {
+        global $SITE, $CFG;
+        $this->http_auth_realm = $SITE->shortname . " Moodledata";
+        $this->HTTP_WebDAV_Server();
+        if (!empty($CFG->webdavroot)) {
+            $this->webdavroot = $CFG->webdavroot;
+        } else {
+            $this->webdavroot = $CFG->wwwroot;
+        }
+    }
+
+    /**
+     * Serve a webdav request
+     *
+     * @access public
+     * @param  string  
+     */
+    function ServeRequest($base = false) 
+    {
+        global $CFG;
+
+        // special treatment for litmus compliance test
+        // reply on its identifier header
+        // not needed for the test itself but eases debugging
+        if (isset($this->_SERVER['HTTP_X_LITMUS'])) {
+            error_log("Litmus test ".$this->_SERVER['HTTP_X_LITMUS']);
+            header("X-Litmus-reply: ".$this->_SERVER['HTTP_X_LITMUS']);
+        }
+
+        // set root directory, defaults to webserver document root if not set
+        if ($base) {
+            $this->base = realpath($base); // TODO throw if not a directory
+        } else if (!$this->base) {
+            $this->base = $this->_SERVER['DOCUMENT_ROOT'];
+        }
+
+        if (!empty($CFG->webdavsessttl)) {
+            $this->sessttl = $CFG->webdavsessttl;
+        } else {
+            $this->sessttl = 60 * 60; // 1hr
+        }
+                
+        // establish connection to property/locking db
+        //mysql_connect($this->db_host, $this->db_user, $this->db_passwd) or die(mysql_error());
+        //mysql_select_db($this->db_name) or die(mysql_error());
+        // TODO throw on connection problems
+
+        // let the base class do all the work
+        parent::ServeRequest();
+    }
+
+    /**
+     * No authentication is needed here
+     *
+     * @access private
+     * @param  string  HTTP Authentication type (Basic, Digest, ...)
+     * @param  string  Username
+     * @param  string  Password
+     * @return bool    true on successful authentication
+     */
+    function check_auth($type, $user, $pass) 
+    {
+        global $CFG;
+
+        // Note - we only get $type if apache configured to
+        // require Basic/Digest auth -- which is not the normal
+        // case for this code anyway. So the normal thing is that
+        // we see $type being empty.
+        if (($type==='Basic' || empty($type)) && !empty($user)) {
+
+            //
+            // Windows clients that are part of a NTLM domain
+            // will automagically prepend the username with the
+            // NTLM domain and a backslash. Even if the user
+            // doesn't want it there. This is a nasty workaround
+            // while the world migrates to a saner platform.
+            //
+            // There is a similar patch for apache's mod_dav :-(
+            //
+            $backslashpos = strpos($user, '\\');
+            if ($backslashpos!==false) { // there'a a \ somewhere there
+                // get rid of the Windows-style domain prefix
+                $user = substr($user, $backslashpos+1);
+            }
+
+            // username/password based sessid -      
+            // (the username is used as salt)
+            $sessid = sha1("$user:$pass");
+            $this->sessid = $sessid;
+
+            if ($sess = get_cache_flag('webdav/moodledata', $sessid)) {
+                $this->session = unserialize($sess);
+                return true;
+            }
+
+
+            // Attempt a Moodle login without bringing in
+            // all the power and glory of a real session setup
+            $u = authenticate_user_login(addslashes($user),$pass);
+            if ($u===false) {
+                return false;
+            }
+            
+            // Get access control data...
+            $canconnect = false;
+            $accessdata = get_user_access_sitewide($u->id);
+            $canconnect = has_capability_in_accessdata('moodle/site:webdav',
+                                                       get_context_instance(CONTEXT_SYSTEM),
+                                                       $accessdata, true);
+
+            if (!$canconnect && empty($CFG->webdavallowfilemanagers)) {
+                return false;
+            }
+
+            $courses = get_user_courses_bycap($u->id, 'moodle/course:managefiles', $accessdata,
+                                              true, 'c.shortname', array('id', 'shortname'));
+            $sitecoursemanage = has_capability_in_accessdata('moodle/course:managefiles',
+                                                             get_context_instance(CONTEXT_COURSE, SITEID),
+                                                             $accessdata, true);
+            if (count($courses)===0 && $sitecoursemanage===false) {
+                return false;
+            }
+
+            $coursesbyid = array();
+            if ($sitecoursemanage) {
+                global $SITE;
+                $coursesbyid[SITEID] = $SITE;
+            }
+            foreach ($courses as $course) {
+                $coursesbyid[$course->id] = $course;
+            }
+
+            // Save as a WebDAV session - for now all it holds is courses
+            // TTL is 1 hour - as the hash is based on the pw, if you have
+            // a valid session your old password will be valid for an extra hr
+            // (perhaps we can tighten this checking user->timemodified but
+            //  I am not sure if user->timemodified is updated on password changes.
+            //  And with external auth sources, Moodle may never know of the pw change).
+            $this->session = array( 'coursesbyid' => $coursesbyid,
+                                    'userid' => $u->id);
+            set_cache_flag('webdav/moodledata', $sessid, 
+                           serialize($this->session), time()+$this->sessttl );
+            return true;
+        }
+        return false;
+    }
+
+
+    /**
+     * PROPFIND method handler
+     *
+     * @param  array  general parameter passing array
+     * @param  array  return array for file properties
+     * @return bool   true on success
+     */
+    function PROPFIND(&$options, &$files) 
+    {
+        // get absolute fs path to requested resource
+        $fspath = $this->base . $this->_cleanpath($options["path"]);
+            
+        // sanity check
+        if (!file_exists($fspath)) {
+            return false;
+        }
+
+        // prepare property array
+        $files["files"] = array();
+
+        // store information for the requested path itself
+        // unless depth says "noroot"
+        if (!isset($options['depth']) || !preg_match('/noroot$/',$options['depth'])) {
+            $files["files"][] = $this->fileinfo($options["path"]);
+        }
+
+        // information for contained resources requested?
+        // Depth can be  "0", "1", "1,noroot", "infinity","infinity,noroot")
+        if (!empty($options["depth"]) && is_dir($fspath) && is_readable($fspath)) {
+
+            // make sure path ends with '/'
+            $options["path"] = $this->_slashify($options["path"]);
+
+            // Similar to GetDir
+            $atroot = false;
+            $atcourse = false;
+            $rxbasedir = preg_quote($this->base,':');
+            if ($fspath === $this->base.'/') {
+                $atroot = true;
+            } elseif (preg_match(":^$rxbasedir/\d+/?$:",$fspath) ) {
+                $atcourse = true;
+            }
+
+            // try to open directory
+            $handle = opendir($fspath);
+
+            $coursesbyid = $this->session['coursesbyid'];
+
+            if ($handle) {
+                // ok, now get all its contents
+                while ($filename = readdir($handle)) {
+                    if ($filename === "." || $filename === "..") {
+                        continue;
+                    }
+
+                    // Only numbered directories at Moodledata root
+                    if ($atroot) {
+                        if (!preg_match('/^\d+$/',$filename)) {
+                            continue;
+                        }
+                        if (!isset($coursesbyid[$filename])) {
+                            // the user does not have rights to see
+                            // this course
+                            continue;
+                        }
+                    } elseif ($atcourse) {
+                        if (in_array($filename, $this->coursedirforbidden)) {
+                            continue;
+                        }
+                    }
+                    $files["files"][] = $this->fileinfo($options["path"].$filename,
+                                                        $atroot);
+                }
+                // TODO recursion needed if "Depth: infinite"
+            }
+        }
+
+        // ok, all done
+        return true;
+    }
+        
+    /**
+     * Get properties for a single file/resource
+     *
+     * @param  string  resource path
+     * @return array   resource properties
+     */
+    function fileinfo($path, $atroot=null) 
+    {
+        // map URI path to filesystem path
+        $fspath = $this->base . $path;
+
+        // create result array
+        $info = array();
+        // TODO remove slash append code when base clase is able to do it itself
+        $info["path"]  = is_dir($fspath) ? $this->_slashify($path) : $path; 
+        $info["props"] = array();
+            
+        if ($atroot) {
+            $courseid = basename($path); // chop the leading slash
+            $displayname = $this->session['coursesbyid'][$courseid]->shortname;
+            $displayname = preg_replace(':/:','-',$displayname);
+            $info["props"][] = $this->mkprop("displayname",
+                                             $displayname);
+        } else {
+            // no special beautified displayname here ...
+            $info["props"][] = $this->mkprop("displayname", basename($path));
+        }
+            
+        // creation and modification time
+        $info["props"][] = $this->mkprop("creationdate",    filectime($fspath));
+        $info["props"][] = $this->mkprop("getlastmodified", filemtime($fspath));
+
+        // Microsoft extensions: last access time and 'hidden' status
+        $info["props"][] = $this->mkprop("lastaccessed",    fileatime($fspath));
+        $info["props"][] = $this->mkprop("ishidden", ('.' === substr(basename($fspath), 0, 1)));
+
+        // type and size (caller already made sure that path exists)
+        if (is_dir($fspath)) {
+            // directory (WebDAV collection)
+            $info["props"][] = $this->mkprop("resourcetype", "collection");
+            $info["props"][] = $this->mkprop("getcontenttype", "httpd/unix-directory");             
+        } else {
+            // plain file (WebDAV resource)
+            $info["props"][] = $this->mkprop("resourcetype", "");
+            if (is_readable($fspath)) {
+                $info["props"][] = $this->mkprop("getcontenttype", mimeinfo('type', $fspath));
+            } else {
+                $info["props"][] = $this->mkprop("getcontenttype", "application/x-non-readable");
+            }               
+            $info["props"][] = $this->mkprop("getcontentlength", filesize($fspath));
+        }
+
+        return $info;
+    }
+
+
+    /**
+     * HEAD method handler
+     * 
+     * @param  array  parameter passing array
+     * @return bool   true on success
+     */
+    function HEAD(&$options)
+    {
+        // get absolute fs path to requested resource
+        $fspath = $this->base.$this->_cleanpath($options['path']);
+
+        if (!$this->_fspathallowed($fspath)) {
+            return false;
+        }
+
+        // sanity check
+        if (!file_exists($fspath)) return false;
+            
+        // detect resource type
+        $options['mimetype'] = mimeinfo('type', $fspath); 
+                
+        // detect modification time
+        // see rfc2518, section 13.7
+        // some clients seem to treat this as a reverse rule
+        // requiering a Last-Modified header if the getlastmodified header was set
+        $options['mtime'] = filemtime($fspath);
+            
+        // detect resource size
+        $options['size'] = filesize($fspath);
+            
+        return true;
+    }
+
+    /**
+     * GET method handler
+     * 
+     * @param  array  parameter passing array
+     * @return bool   true on success
+     */
+    function GET(&$options) 
+    {
+        // get absolute fs path to requested resource
+        $fspath = $this->base . $this->_cleanpath($options["path"]);
+
+        // Is this real path allowed?
+        if (!$this->_fspathallowed($fspath)) {
+            return false;
+        }
+
+        // is this a collection?
+        if (is_dir($fspath)) {
+            return $this->GetDir($fspath, $options);
+        }
+
+        // the header output is the same as for HEAD
+        if (!$this->HEAD($options)) {
+            return false;
+        }
+
+        // no need to check result here, it is handled by the base class
+        $options['stream'] = fopen($fspath, "r");
+            
+        return true;
+    }
+
+    // Note that fspath must be absolute on the FS
+    // return @bool
+    function _fspathallowed($fspath) {
+
+        if ($fspath===$this->base.'/' || $fspath===$this->base) {
+            return true;
+        }
+
+        $subpath = substr($fspath, strlen($this->base));
+        // trim any number of leading slashes
+        $subpath = preg_replace(":^/+:", '',$subpath);
+        $tokens = explode('/', $subpath);
+        $courseid = (int)$tokens[0];
+        if ($courseid < 1 || !isset($this->session['coursesbyid'][$courseid])) {
+            return false;
+        }
+        if (!isset($tokens[1])) {
+            return true;
+        }
+        // Subdir
+        if (in_array($tokens[1], $this->coursedirforbidden)) {
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * GET method handler for directories
+     *
+     * This is a very simple mod_index lookalike.
+     * See RFC 2518, Section 8.4 on GET/HEAD for collections
+     *
+     * @param  string  directory path
+     * @return void    function has to handle HTTP response itself
+     */
+    function GetDir($fspath, &$options) 
+    {
+        global $CFG;
+        $path = $this->_slashify($this->_cleanpath($options["path"]));
+        if ($path != $options["path"]) {
+            header("Location: ".$this->base_uri.$path);
+            exit;
+        }
+
+        // fixed width directory column format
+        $format = "%15s  %-19s  %-s\n";
+
+        if (!is_readable($fspath)) {
+            return false;
+        }
+        header('Content-Type: text/html; charset="utf-8"');
+        $handle = opendir($fspath);
+        if (!$handle) {
+            return false;
+        }
+
+        $atroot = false;
+        $atcourse = false;
+        $rxbasedir = preg_quote($this->base,':');
+        if ($fspath === $this->base.'/') {
+            $atroot = true;
+        } elseif (preg_match(":^$rxbasedir/\d+/?$:",$fspath) ) {
+            $atcourse = true;
+        }
+
+        // We could have a $this->_fulldisplaypath($fspath) method
+        // but this is not the main means of access, so it's probably
+        // not needed - 
+        $displaypath = $fspath;
+
+        echo "<html><head><title>Index of ".htmlspecialchars($displaypath)."</title></head>\n";
+            
+        echo "<h1>Index of ".htmlspecialchars($displaypath)."</h1>\n";
+
+        echo "<pre>";
+        printf($format, "Size", "Last modified", "Filename");
+        echo "<hr>";
+
+        $coursesbyid = $this->session['coursesbyid'];
+
+        while ($filename = readdir($handle)) {
+            $displayname = $filename;
+            // self and up dirs
+            if ($filename === "." || $filename === "..") {
+                continue;
+            }
+            // Only numbered directories at Moodledata root
+            if ($atroot) {
+                if (!preg_match('/^\d+$/',$filename)) {
+                    continue;
+                }
+                if (!isset($coursesbyid[$filename])) {
+                    // the user does not have rights to see
+                    // this course
+                    continue;
+                }
+                $displayname = $coursesbyid[$filename]->shortname;
+                $displayname = preg_replace(':/:','-',$displayname);
+            } elseif ($atcourse) {
+                if (in_array($filename, $this->coursedirforbidden)) {
+                    continue;
+                }
+            }
+
+            $fullpath     = $path.$filename;
+            $fullfspath   = $fspath .$filename;
+            $name     = htmlspecialchars($displayname, ENT_QUOTES, 'UTF-8');
+            if (is_dir($fullfspath)) {
+                $fullpath .= '/';
+                printf($format, 
+                       number_format(filesize($fullfspath)),
+                       strftime("%Y-%m-%d %H:%M:%S", filemtime($fullfspath)), 
+                       '<a href="'.$CFG->wwwroot.'/webdav/moodledata-server.php'.$fullpath.
+                       '" folder="'.$this->webdavroot.'/webdav/moodledata-server.php'.$fullpath.
+                       '" style="behavior:url(#default#AnchorClick)" >'.$name.'</a>');
+            } else {
+                printf($format, 
+                       number_format(filesize($fullfspath)),
+                       strftime("%Y-%m-%d %H:%M:%S", filemtime($fullfspath)), 
+                       '<a href="'.$CFG->wwwroot.'/webdav/moodledata-server.php'.$fullpath.
+                       '" >'.$name.'</a>');
+            }
+        }
+
+        echo "</pre>";
+
+        closedir($handle);
+
+        echo "</html>\n";
+
+        exit;
+    }
+
+    /**
+     * PUT method handler
+     * 
+     * @param  array  parameter passing array
+     * @return bool   true on success
+     */
+    function PUT(&$options) 
+    {
+        $fspath = $this->base . $this->_cleanpath($options["path"]);
+        $fspath = dirname($fspath) . '/' . clean_param(basename($fspath),PARAM_FILE);
+    
+        if (!$this->_fspathallowed($fspath)) {
+            return "403 Forbidden";
+        }
+
+        $dir = dirname($fspath);
+
+        if ($dir===$this->base) {
+            // No writing at the top of the tree!
+            return "403 Forbidden";
+        }
+
+        if (!file_exists($dir) || !is_dir($dir)) {
+            return "409 Conflict"; // TODO right status code for both?
+        }
+
+        $options["new"] = ! file_exists($fspath);
+
+        if ($options["new"] && !is_writeable($dir)) {
+            return "403 Forbidden";
+        }
+        if (!$options["new"] && !is_writeable($fspath)) {
+            return "403 Forbidden";
+        }
+        if (!$options["new"] && is_dir($fspath)) {
+            return "403 Forbidden";
+        }
+        $fp = fopen($fspath, "w");
+        return $fp;
+    }
+
+
+    /**
+     * MKCOL method handler
+     *
+     * @param  array  general parameter passing array
+     * @return bool   true on success
+     */
+    function MKCOL($options) 
+    {
+        $path   = $this->base .$this->_cleanpath($options["path"]);
+        $parent = dirname($path);
+        $name   = clean_param(basename($path), PARAM_FILE);
+
+        if (!$this->_fspathallowed($path)) {
+            return "403 Forbidden";
+        }
+        if ($parent===$this->base) {
+            // No writing at the top of the tree!
+            return "403 Forbidden";
+        }
+        
+
+        if (!file_exists($parent)) {
+            return "409 Conflict";
+        }
+
+        if (!is_dir($parent)) {
+            return "403 Forbidden";
+        }
+
+        if ( file_exists($parent."/".$name) ) {
+            return "405 Method not allowed";
+        }
+
+        if (!empty($this->_SERVER["CONTENT_LENGTH"])) { // no body parsing yet
+            return "415 Unsupported media type";
+        }
+            
+        $stat = mkdir($parent."/".$name, 0777);
+        if (!$stat) {
+            return "403 Forbidden";                 
+        }
+
+        return ("201 Created");
+    }
+        
+        
+    /**
+     * DELETE method handler
+     *
+     * @param  array  general parameter passing array
+     * @return bool   true on success
+     */
+    function DELETE($options) 
+    {
+        if (empty($options["path"]) || $options["path"]==='/') {
+            return '403 Forbidden';
+        }
+
+        $path = $this->base .$this->_cleanpath($options["path"]);
+
+        if (!$this->_fspathallowed($path)) {
+            return '403 Forbidden';
+        }
+
+        if (preg_match(":^/\d+/?$:",$options['path']) ) {
+            // course directory!
+            return '403 Forbidden';
+        }
+
+        if (!file_exists($path)) {
+            return "404 Not found";
+        }
+
+        if (is_dir($path)) {
+            System::rm(array("-rf", $path));
+        } else {
+            unlink($path);
+        }
+
+        return "204 No Content";
+    }
+
+
+    /**
+     * MOVE method handler
+     *
+     * @param  array  general parameter passing array
+     * @return bool   true on success
+     */
+    function MOVE($options) 
+    {
+        return $this->COPY($options, true);
+    }
+
+    /**
+     * COPY method handler
+     *
+     * @param  array  general parameter passing array
+     * @return bool   true on success
+     */
+    function COPY($options, $del=false) 
+    {
+        // TODO Property updates still broken (Litmus should detect this?)
+
+        if (!empty($this->_SERVER["CONTENT_LENGTH"])) { // no body parsing yet
+            return "415 Unsupported media type";
+        }
+
+        // no copying to different WebDAV Servers yet
+        if (isset($options["dest_url"])) {
+            return "502 bad gateway";
+        }
+
+        $source = $this->base . $this->_cleanpath($options["path"]);
+        if (!file_exists($source)) {
+            return "404 Not found";
+        }
+
+        if (is_dir($source)) { // resource is a collection
+            switch ($options["depth"]) {
+            case "infinity": // valid 
+                break;
+            case "0": // valid for COPY only
+                if ($del) { // MOVE?
+                    return "400 Bad request";
+                }
+                break;
+            case "1": // invalid for both COPY and MOVE
+            default: 
+                return "400 Bad request";
+            }
+        }
+
+        if ($del===true) { // MOVE - check we are not being asked to move a coursedir
+            $rxbasedir = preg_quote($this->base,':');
+            if (preg_match(":^$rxbasedir/\d+/?$:",$source) ) {
+                return "403 Forbidden";
+            }
+        }
+
+        $dest         = $this->base . $this->_cleanpath($options["dest"]);
+        $destdir      = dirname($dest);
+        
+        if ($destdir===$this->base
+            || (is_dir($source) && ($source===$this->base || $source===$this->base.'/'))) {
+            // No writing at the top of the tree, or copying the whole tree!
+            return "403 Forbidden";
+        }
+
+        if (!$this->_fspathallowed($dest) || !$this->_fspathallowed($source)) {
+            return "403 Forbidden";
+        }
+
+        if (!file_exists($destdir) || !is_dir($destdir)) {
+            return "409 Conflict";
+        }
+
+        $new          = !file_exists($dest);
+        $existing_col = false;
+
+        if (!$new) {
+            if ($del && is_dir($dest)) {
+                if (!$options["overwrite"]) {
+                    return "412 precondition failed";
+                }
+                $dest .= basename($source);
+                if (file_exists($dest)) {
+                    $options["dest"] .= basename($source);
+                } else {
+                    $new          = true;
+                    $existing_col = true;
+                }
+            }
+        }
+
+        if (!$new) {
+            if ($options["overwrite"]) {
+                $stat = $this->DELETE(array("path" => $options["dest"]));
+                if (($stat{0} != "2") && (substr($stat, 0, 3) != "404")) {
+                    return $stat; 
+                }
+            } else {
+                return "412 precondition failed";
+            }
+        }
+
+        if ($del) {
+            if (!rename($source, $dest)) {
+                return "500 Internal server error";
+            }
+            $destpath = $this->_unslashify($options["dest"]);
+        } else {
+            if (is_dir($source)) {
+                $files = System::find($source);
+                $files = array_reverse($files);
+            } else {
+                $files = array($source);
+            }
+
+            if (!is_array($files) || empty($files)) {
+                return "500 Internal server error";
+            }
+                    
+                
+            foreach ($files as $file) {
+                if (is_dir($file)) {
+                    $file = $this->_slashify($file);
+                }
+
+                $destfile = str_replace($source, $dest, $file);
+                    
+                if (is_dir($file)) {
+                    if (!file_exists($destfile)) {
+                        if (!is_writeable(dirname($destfile))) {
+                            return "403 Forbidden";
+                        }
+                        if (!mkdir($destfile)) {
+                            return "409 Conflict";
+                        }
+                    } else if (!is_dir($destfile)) {
+                        return "409 Conflict";
+                    }
+                } else {
+                    
+                    if (!copy($file, $destfile)) {
+                        return "409 Conflict";
+                    }
+                }
+            }
+
+        }
+
+        return ($new && !$existing_col) ? "201 Created" : "204 No Content";         
+    }
+
+    /**
+     * PROPPATCH method handler
+     *
+     * @param  array  general parameter passing array
+     * @return bool   true on success
+     */
+    function PROPPATCH(&$options) 
+    {
+        // No additional properties
+        return "403 Forbidden";
+    }
+
+
+    /**
+     * LOCK method handler
+     *
+     * @param  array  general parameter passing array
+     * @return bool   true on success
+     */
+    function LOCK(&$options) 
+    {
+        global $CFG, $db;
+
+        // get absolute fs path to requested resource
+        $fspath = $this->base . $this->_cleanpath($options["path"]);
+
+        // TODO recursive locks on directories not supported yet
+        // makes litmus test "32. lock_collection" fail
+        if (is_dir($fspath) && !empty($options["depth"])) {
+            return "409 Conflict";
+        }
+
+        $options["timeout"] = time()+300; // 5min. hardcoded
+
+        // Sanitise input values we'll use
+        $options['path'] = addslashes($options['path']);
+        if (isset($options['update'])) {
+            $options['update'] = addslashes($options['update']);
+        }
+        if (isset($options['locktoken'])) {
+            $options['locktoken'] = addslashes($options['locktoken']);
+        }
+
+        if (isset($options["update"])) { // Lock Update
+            $where = " path = '{$options[path]}' AND token = '{$options[update]}'";
+            if (get_records_where('webdav_locks', $where)) {
+                $query = "UPDATE {$CFG->prefix}webdav_locks 
+                                 SET expiry = '{$options[timeout]}', 
+                                     modified = ".time()."
+                          WHERE $where";
+                execute_sql($query,1);
+
+                // martinl's notes:
+                // these here are a no-op... there's something odd
+                // about them
+                $options['owner'] = $row['owner'];
+                $options['scope'] = $row["exclusivelock"] ? "exclusive" : "shared";
+                $options['type']  = $row["exclusivelock"] ? "write"     : "read";
+
+                return true;
+            } else {
+                return false;
+            }
+        }
+
+        $newlock = new StdClass;
+        $newlock->token= $options['locktoken'];
+        $newlock->path = $options['path'];
+        $newlock->created = time();
+        $newlock->modified = time();
+        $newlock->expiry = $options['timeout'];
+        $newlock->userid = $this->session['userid'];
+        $newlock->exclusivelock = ($options['scope'] === "exclusive" ? "1" : "0");
+        if (!empty($options['owner'])) {
+            $newlock->owner = addslashes($options['owner']);
+        }
+
+        if (insert_record('webdav_locks',$newlock)) {
+            return '200 OK';
+        } else {
+            return "409 Conflict";
+        }
+    }
+
+    /**
+     * UNLOCK method handler
+     *
+     * @param  array  general parameter passing array
+     * @return bool   true on success
+     */
+    function UNLOCK(&$options) 
+    {
+        global $db;
+
+        $res = delete_records('webdav_locks',
+                              'path', addslashes($options['path']),
+                              'token',addslashes($options['token']));
+        return $db->Affected_Rows() ? "204 No Content" : "409 Conflict";
+    }
+
+    /**
+     * checkLock() helper
+     *
+     * @param  string resource path to check for locks
+     * @return bool   true on success
+     */
+    function checkLock($path) 
+    {
+        global $CFG;
+
+        $result = false;
+            
+        $path = addslashes($path);
+        $sql = "SELECT l.exclusivelock, l.token, l.created,
+                       l.modified, l.expiry, l.owner,
+                       u.username
+                FROM {$CFG->prefix}webdav_locks l
+                LEFT OUTER JOIN {$CFG->prefix}user u
+                  ON l.userid=u.id
+                WHERE l.path='{$path}'";
+        $lock = get_record_sql($sql);
+        if ($lock) {
+            $result = array( "type"     => "write",
+                             "scope"    => $lock->exclusivelock ? "exclusive" : "shared",
+                             "depth"    => 0,
+                             "owner"    => empty($lock->owner) ? $lock->username : $lock->owner,
+                             "token"    => $lock->token,
+                             "created"  => $lock->created,
+                             "modified" => $lock->modified,
+                             // WebDAV protocol calls it 'expires'
+                             // but all expiries in Moodle are called 'expiry'
+                             "expires"  => $lock->expiry
+                             );
+        }
+        return $result;
+    }
+
+    /*
+     * Ensure that a client-provided path is consistent -
+     * it should have no doubled-up slashes. If it's a directory, it
+     * may still have a trailing slash or not.
+     *
+     * @param string path
+     * @return string path
+     */
+
+    function _cleanpath($path) {
+        $path = preg_replace(':/+:', '/', $path); // collapse doubled-up slashes
+        return ($path);
+    }
+
+}
+
+
+/*
+ * Local variables:
+ * tab-width: 4
+ * c-basic-offset: 4
+ * indent-tabs-mode:nil
+ * End:
+ */
