diff --git a/admin/cron.php b/admin/cron.php
index 21e6ca9..3a9a527 100644
--- a/admin/cron.php
+++ b/admin/cron.php
@@ -345,6 +345,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/environment.xml b/admin/environment.xml
index 6216b1d..9e3d067 100644
--- a/admin/environment.xml
+++ b/admin/environment.xml
@@ -167,6 +167,11 @@
           <ON_CHECK message="tokenizerrecommended" />
         </FEEDBACK>
       </PHP_EXTENSION>
+      <PHP_EXTENSION name="xml" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="xmlrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
     </PHP_EXTENSIONS>
     <CUSTOM_CHECKS>
       <CUSTOM_CHECK file="question/upgrade.php" function="question_check_no_rqp_questions" level="optional">
@@ -234,6 +239,11 @@
           <ON_CHECK message="tokenizerrecommended" />
         </FEEDBACK>
       </PHP_EXTENSION>
+      <PHP_EXTENSION name="xml" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="xmlrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
     </PHP_EXTENSIONS>
     <CUSTOM_CHECKS>
       <CUSTOM_CHECK file="question/upgrade.php" function="question_check_no_rqp_questions" level="optional">
diff --git a/admin/settings/server.php b/admin/settings/server.php
index 029385d..af6a357 100644
--- a/admin/settings/server.php
+++ b/admin/settings/server.php
@@ -97,6 +97,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 aa19741..aebf1c1 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,23 @@
 
         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 (true || check_browser_version('MSIE')) {
+                       $msg = '<p>'.get_string('webdavconnectmsie')
+                       . " <code><a href=\"{$webdavurl}{$course->id}/\" folder=\"{$webdavurl}{$course->id}/\">{$webdavurl}{$course->id}/</a></code></p>";
+                }
+                $msg .= '<p>'.get_string('webdavcanmanage')
+                       . " <code><a href=\"$webdavurl\" folder=\"$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 28450b7..2ea7c18 100644
--- a/lang/en_utf8/admin.php
+++ b/lang/en_utf8/admin.php
@@ -424,6 +424,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';
@@ -720,6 +721,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['xmlstrictheaders'] = 'XML strict headers';
diff --git a/lang/en_utf8/moodle.php b/lang/en_utf8/moodle.php
index df3373e..91b5a3a 100644
--- a/lang/en_utf8/moodle.php
+++ b/lang/en_utf8/moodle.php
@@ -1582,6 +1582,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 ad296ca..fc0af4b 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 22b34e4..78db153 100644
--- a/lib/db/access.php
+++ b/lib/db/access.php
@@ -1203,8 +1203,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 a28985e..c02bf88 100644
--- a/lib/moodlelib.php
+++ b/lib/moodlelib.php
@@ -857,6 +857,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/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:
+ */

