The WebDAV for Drupal module aims to add WebDAV access to Drupal content management. It is basically a WebDAV server pumping its data from a specialized WebDAV API. It has been developed for three years now, used by many people on an everyday basis, improved to increase speed, compliance and security.

Managing Drupal content

From a user point of view, only the features added by WebDAV module are interesting. So the WebDAV module comes with some sub-modules specialized in handling a specific kind of content. For each module you activate, you'll add more virtual folders to the WebDAV tree.

WebDAV for nodes and attachments

By activating those two modules, you'll be able to browse nodes by adding a nodes folder at webdav root. Structure is like this :

    /webdav/
        /nodes/
            /story/
              /A node title/
                content.html
                /attachements/
                  a binary attached file.zip
                  a binary attached file.jpg
                  etc...
              /A second node title/
              etc...
            /book/
            /page/
            /project/
            etc..
  

Number of visible nodes depends of the setting found in /admin/settings/webdav and is 10 by default. This is for performance reasons. Recent nodes appear first. If you set this parameter to 0, you'll have the all nodes in their own "node type" folder.

Using the "WebDAV for Nodes" module you can :

  • Create a new node by creating a folder under any "node type" folder.
  • Delete a node by deleting its folder.
  • Change the node title by renaming its folder.
  • Open the node body in a text editor by opening the content file inside its folder. Content file mime type and extension depends node current "input format" and settings found under /admin/settings/webdav/nodes. If it is a newly created node, default text can be changed it webdav nodes settings.
  • Save the node body from text editor. In /admin/settings/webdav/nodes you can also enable an option to add a revision each time you save the document. You can also set a default logging message for this revision.

Using the "WebDAV for Attachments" module you can :

  • Browse attachments for a node in its "attachments" folder.
  • Add a new attachment using your file manager's drag & drop feature.
  • Remove an attachment by just deleting it.
  • Rename an attachment by renaming the file.
  • Move an attachment from one node to another.
  • Copy an attachment from one node to another.
    • WebDAV for taxonomy

      This module adds a new "taxonomy" folder under the webdav root. The directory structure is :

    /webdav/
        /taxonomy/
          /no vocabulary/
            a node with no tags
            
          /vocabulary/
            /term/
                /term/
                    /term/
                    a node title attached to this term
                    
                /term/
            /term
          /vocabulary/
  

Using this module, which will be available in RC7, you can :

  • Add a new vocabulary by creating a folder under /taxonomy/
  • Add a new term to a vocabulary by creating a folder under /selected_vocabulary/
  • Delete a term by deleting its folder
  • Delete a vocabulary by deleting its folder
  • Add a term to some node by copying in this term folder
  • Remove a term from some node by deleting it from the term folder
  • Change a term from some node by moving it from one term to another

Nodes that don't have any term attached will end up in /no vocabulary/ folder to enable managing them.

WebDAV for Modules

The structure added by this module (which will be available in RC8) will be :

      /webdav/
          /modules/
              /available/
                not installed and not enabled modules
              /installed/
                installed modules
              /enabled/
                enabled modules
  

Using this module you can :

  • Enable a module by dragging & dropping it to /enabled/
  • Disable a module by dragging & dropping it from /enabled/ to /available/
  • Uninstall a module by dragging & dropping it from /installed/ or /enabled/ to /available/

WebDAV limitations

Displayname vs. filename

With a real file system, not every character is allowed. You can't, for example, use / in a *nix file name. The same when speaking about URL, you can't directly use #, & ou ; (these are examples). WebDAV is a web file system protocol where filenames are URLs with the same char-set limitation.

In the WebDAV protocol each item (file or folder) can have an optional property called displayname. When this property is used, the real file name with its char-set limitation is visually replaced by its value. And this value has no limitation.

For a Drupal Virtual file system, you are manipulating nodes as if there were folders which name is the node title itself. So you can imagine that you can have many of those forbidden character in the folder name. That's why the displayname property is so useful in our case. You can have for example a folder name that is just the node ID (nid) and a display name that is the real node title.

Unfortunately not all WebDAV clients are equal in how they treat WebDAV specifications, especially regarding display name support. Most of them handle it nicely, but some like MacOS or Cadaver, just don't. So for them the module has a special mode in which display name is not used and filename is encoded to accept as many characters as possible. This is working but has some limitations. Actually not every character can be encoded using %xx format and in this case, the module has to replace them with some ugly {SL} (for slash) or {NB} (for #).

So if you can't access some folders. When creating an issue, please take note of the original name of the item (node, attachment file) and the mode (auto, on or off) you used at this time.

File Managers

With this module you'll be able to browse Drupal content as if it was constructed of simple files and folders. But there are no files and folders there -- everything is virtual. And sometime this "virtuality" can lead the WebDAV client, specifically when it is a file manager like Nautilus or Windows Explorer, to do things which are incompatible with the Drupal reality.

For example when your in the nodes/story folder, you have a list of folders representing nodes of "story" type. Each folder contains a file which is the content itself, and a sub-folder called "attachments" where you can find all binary files you attached to your node using upload module.

This taken, imagine that you want to delete the node. To do so you just want to supcodess the node folder. The file manager will so try to delete attached files, after the attachment folder, and the the file recodesenting the node content. The problem is this file is just a recodesentation of the node body, so It can't be deleted. In order to allow a file manager to delete a node, the module have to simulate the content deletion, in order to let it finaly delete the node folder itself. So if you delete a "content.html" file, don't be surprise if the system don't do it, event if there is no error, it's not a bug, it's a feature ;-)

In the same idea, MacOS just don't understand anything about remote file systems. It wants to create all it's ._DS* stuff on any folder it found. The system will try to explain to it that it is forbidden but for just on file saving, MacOS generate something like 10 calls to create such files. So don't be surprised if MacOS WebDav access is codetty slow, it's not a server problem.

How to connect ?

Using embedded file browser

With a simple WEB browser by using the address http://my_server/webdav/. Using this mode you are inside drupal and WebDav files are just a drupal view. After authentication (if needed) you should be able to browse your content.

Using Gnome

For Gnome/Nautilus and any GIO/GVFS compliant applications (Gnome 2.24 and later), you can directly enter the url like this dav://my_server/webdav (davs if it is secured).

Using KDE

For KDE/Dolphin and any KIO compliant application, you can directly enter the url like this webdav://my_server/webdav (webdavs if it is secured).

Using command line

With Cadaver you just have to entre cadaver http://my_server/webdav.

With DavFS and DavFS2 you have to mount like this mount -t davfs http://my_server/webdav/ /mnt/disk/ -o username=gaston. You can also use display names by setting use_displayname 1 in /etc/davfs2/davfs2.conf.

Using MacOS

For MacOS, you can connect to a webdav folder using GO/Connect to server. Now you can add your server url : http://my_server/webdav (https if it is secured).

Using Windows

For Windows, you can connect to a webdav folder by opening Networks and clicking on Add a favorite network. Now you can add your server url : http://my_server/webdav (https if it is secured).

How to test ?

This module is not stable and should be test in order to be sure everything, specially security, is fine.

The best option for testing is cadaver as it is plain and simple. The only problem with cadaver is it don't support display names; So for those tests, to use NID and FID.

First be sure you set debug level to "verbose" in WebDAV server settings. Be careful with your data, never do this on a production server as the module has not been fully tested.

Now you can follow my testing path :

# We first create test data
$ mkdir test
$ cd test
$ cat > "new_content"
This is a new content
^D
$ cp ~/Desktop/test.pdf new_attachement.pdf

# we start cadaver
$ cadaver http://my_server/webdav/

# first "cd" should trigger authentication
dav:/webdav/> cd nodes
Authentication required for Drupal WebDav Access for 'Artisan Numérique on server `artisan.karma-lab.net':
Username: gaston
Password: 

# we go to story content type
dav:/webdav/nodes/> cd story

# we create a new node
dav:/webdav/nodes/story/> mkdir "This is a test"
Creating `This is a test': succeeded.

# Now as Cadaver don't understand display names, we should have a look at drupal admin/content) in order to grap
# the new NID for this node, here 1648
dav:/webdav/nodes/story/> cd 1648

# we get its content
dav:/webdav/nodes/story/1648/> cat content.html 
Displaying `/webdav/nodes/story/1648/content.html':
<!-- ----------------------------------
     - Created by Drupal DAV module
     ----------------------------------
-->

Enter your data here...

# we replace with a new content
dav:/webdav/nodes/story/1648/> put new_content content.html
Uploading new_content to `/webdav/nodes/story/1648/content.html':
Progress: [=============================>] 100,0% of 22 bytes succeeded.

# test again to see if it changed
dav:/webdav/nodes/story/1648/> cat content.html
Displaying `/webdav/nodes/story/1648/content.html':
This is a new content

# go to attachments and see there's no rabbit inside
dav:/webdav/nodes/story/1648/> cd attachements
dav:/webdav/nodes/story/1648/attachements/> ls
Listing collection `/webdav/nodes/story/1648/attachements/': collection is empty.

# we put a new attachment in the folder
dav:/webdav/nodes/story/1648/attachements/> put new_attachement.pdf .
Uploading new_attachement.pdf to `/webdav/nodes/story/1648/attachements/':
Progress: [=============================>] 100,0% of 1460698 bytes succeeded.

# we see if the rabbit is there
dav:/webdav/nodes/story/1648/attachements/> ls
Listing collection `/webdav/nodes/story/1647/attachements/': succeeded.
1386 1460698 sept. 22 14:54

# we get it so we can verify binary integrity using evince
dav:/webdav/nodes/story/1647/attachements/> get 1386 getted.pdf
Downloading `/webdav/nodes/story/1647/attachements/1386' to getted.pdf:
Progress: [=============================>] 100,0% of 1460698 bytes succeeded.

# we delete the attachement
dav:/webdav/nodes/story/1647/attachements/> rm 1386
Deleting `1386': succeeded.

# no one there now
dav:/webdav/nodes/story/1647/attachements/> ls
Listing collection `/webdav/nodes/story/1647/attachements/': collection is empty.

# we go up two times to delete the node
dav:/webdav/nodes/story/1647/attachements/> cd ..
dav:/webdav/nodes/story/1647/> cd ..

dav:/webdav/nodes/story/> rmcol 1647
Deleting collection `1647': succeeded.

WebDAV API

Vocabulary

path
The path in our case is the REQUEST URI with first /webdav removed. So for /webdav/nodes/story/, path is /nodes/story/. For /webdav/nodes/story/1234/Content.html, path is /story/1234/Content.html.
route
A route is a convenient way of naming a path split into pieces. So the codevious path as array("root", "nodes", "story", "1234", "Content.html") as route.
collection
A collection is the WebDAV term meaning "folder". A collection's URL always ends with /.
resource
A resource is a collection or a file.
member
A member is a collection item. Its a resource.
route handler
This is an internal thing. Every module can give (see hook_webdav_definitions) an association with a path and a callback function. Two modules can give different members for the same path. Si a route handler is :
          array(
            'route'=&gt;array('aaa','%bbb',...),       // route of the handler
            'module'=&gt;'source_module_1',            // path owner
            'definitions' =&gt; array(
              array(
                'module'=&gt;source_module_1_name,
                'members callback'=&gt;...
                'arguments'=&gt;....
              ),
              array(
                'module'=&gt;source_module_2_name,
                'members callback'=&gt;...
                'arguments'=&gt;....
              ),
              ...
            )
          )
        



So route handlers can be seen as an aggregation of hook_webdav_definitions invocation result.

resolved route handler
The same as route handler but with route replaced with the real route and with a new parameters member containing association key/values.

hook_webdav_definitions

Description

This hook enables modules to register webdav resource by their path.

  function hook_webdav_definitions() {
    $items=array(
      '/node/%node_type/%node'=&gt;array(
        'arguments'=&gt;array(1,2),
        'members callback'=&gt;'webdav_node_get_node_members',
        'put callback'=&gt;'webdav_node_content_put',
        'access' =&gt; array (WEBDAV_NODE_PERMS_ACCESS),
      ),
    );
    return $items;
  }
  

Path definition

A path can contain variable parts starting with %part_name. In this case, the system will search for a loader called $module_name.'_'.$part_name.'_load'. module_name is the name of the module which contains the hook. If loader is found, the loader result is used as argument value for part_name, else, the original matching path part is used as a string value.

if you use %%part_name, this will match the rest of the path from this point. As an example, when you have /my_collection/%%full_path matching /my_collection/aaa/bbb/ccc, you'll have full_path=>'aaa/bbb/ccc'. %% MUST BE UNIQUE AND AT THE END of the path. If the matching path don't have any "full path" (ex. /my_collection/), the result will be a / value.

As MENU API, every path should have a definition entry to be recognized. This is the case for collections but also for files. In most cases files are handled by a path like /aaa/bbb/ccc/%file_name.

WebDAV operation

Many operation can be done on a resource : populate members, get, put, delete, move, copy, etc... the definition block will contains callback function for each single operation that can be done on the define resource. So, there is no "hooks" for deleting, copying, getting, putting, etc. a webdav resource. Everything is done by callback as it is with Menu API. So, hook_webdav_definitions is actually the only real hook in this API.

webdav definition

Now let's go back to hook_webdav_definitions. As you know now, it returns an associative array of path and definition. A definition can have following properties :

  • "module" : This is the module owner of this path. Every path got a owner in order to resolve conflicts. If not specified, the module owner is the module in which the path is declared. So if you want to attach resources to some path your module don't own, you just have to specify the module owner of it. In the future, system will detect conflicts over path when two modules claim to be owner of the same path.
  • "{operation} callback" : Required if this is a collection resource. The function to call to populate members for this path.
  • "arguments" : An array of arguments to pass to the any call back function. Each element is the identifier given with % or %%.
  • "access" : An array of perms.

Webdav operation callbacks

A callback is linked to its definition, so every callback will have the same first parameters following arguments definition attribute. So if we have this definition :

      '/node/%node_type/%node/%content_type'=&gt;array(
        'arguments'=&gt;array('node'),
        'members callback'=&gt;'webdav_node_get_node_members',
        'get callback'=&gt;'webdav_node_content_get',
        'access' =&gt; array (WEBDAV_NODE_PERMS_ACCESS),
      ),   
   

The webdav_node_content_get callback will have $node as first parameter. This said, some callbacks can have more parameters. For example, for put, you'll have the $file_name containing the data to put.

get operation callback

This callback has only parameters defined in its webdav definition. The function can return a plain string or a file stream handler. If an error occurs, you have to user webdav_session_set_status with any HTTP error (ex. WEBDAV_HTTP_STATUS_FORBIDDEN).

function webdav_node_node_content_get($node) {
  return $node-&gt;body;
}
   

put operation callback

This callback have parameters defined in its webdav definition and a $file_name pointing to the file to put. If an error occurs, you have to user webdav_session_set_status with any HTTP error (ex. WEBDAV_HTTP_STATUS_FORBIDDEN).

function webdav_node_node_content_put($node, $file_name) {
  // as node_save don't do any check, we should validate permissions here
  if (!node_access("update", $node)) {
    error_log("==".$node-&gt;nid);
    webdav_session_set_status(WEBDAV_STATUS_FORBIDDEN);
    return;
  }
  $content = file_get_contents($file_name);
  $node-&gt;body = $content;
  _webdav_node_save($node);
}
   

create operation callback

This callback have parameters defined in its webdav definition and a $name for the new resource name to create. If an error occurs, you have to user webdav_session_set_status with any HTTP error (ex. WEBDAV_HTTP_STATUS_FORBIDDEN).

function webdav_node_node_create ($node_type, $name) {
  // Create the new node
  global $user;
  $node=(object)array();
  $node-&gt;title=$name;
  $node-&gt;body=webdav_node_create_node_default_body();
  $node-&gt;log=webdav_node_create_node_default_log_message();
  $node-&gt;type=$node_type;
  $node-&gt;uid=$user-&gt;uid;
  $node-&gt;status=0;

  if (!node_access( "create", $node)) {
    webdav_session_set_status(WEBDAV_STATUS_FORBIDDEN);
    return;
  }
  node_save($node);
}

   

delete operation callback

This callback has only the parameters defined in its webdav definition. If an error occurs, you have to user webdav_session_set_status with any HTTP error (ex. WEBDAV_HTTP_STATUS_FORBIDDEN).

function webdav_node_node_delete($node) {
  if (!node_access( "delete", $node)) {
    webdav_session_set_status(WEBDAV_STATUS_FORBIDDEN);
  } else {
    node_delete($node-&gt;nid);
  }
}
   

members operation callback

This callback has only the parameters defined in its webdav definition. It is used to get all members of a specified route. If an error occurs, you have to user webdav_session_set_status with any HTTP error (ex. WEBDAV_HTTP_STATUS_FORBIDDEN).

function webdav_node_node_members($node) {
  $format=$node-&gt;format;
  $extension = webdav_node_extension_for_input_format($format);
  $mime_type = webdav_node_mime_type_for_input_format($format);

  $items[] = array (
    'id' =&gt; "content.".$extension,
    'name' =&gt; $node-&gt;title.".".$extension,
    'created' =&gt; $node-&gt;created,
    'changed' =&gt; $node-&gt;changed,
    'size' =&gt; strlen($node-&gt;body),
    'type' =&gt; $mime_type,
    'encoding' =&gt; 'UTF8',
  );
  return $items;
}
   

copy operation callback

This callback is a bit more tricky. As it involves a source and a target, and each two of them go a path definition with arguments, the callback go first the target definition parameters as arguments, and after the source definition arguments.

Second tricky part, the callback should be attached somewhere. So it will be to the target path definition.

If an error occurs, you have to user webdav_session_set_status with any HTTP error (ex. WEBDAV_HTTP_STATUS_FORBIDDEN).

function webdav_node_webdav_copy(
  /* target path definition arguments */ $target_node_type, 
  /* source path definition arguments */ $source_node) {
  if ($target_node_type!=$source_node-&gt;type) {
    webdav_session_set_status(WEBDAV_STATUS_FORBIDDEN);
    return;
  }

  // Create the new node
  global $user;
  $node=(object)array();
  $node-&gt;title=$name;
  $node-&gt;body=$source_node-&gt;body;
  $node-&gt;log=webdav_node_create_node_default_log_message();
  $node-&gt;type=$source_node-&gt;type;
  $node-&gt;uid=$user-&gt;uid;
  $node-&gt;status=0;

  // Check right
  if (!node_access( "create", $node)) {
    webdav_session_set_status(WEBDAV_STATUS_FORBIDDEN);
    return;
  }
  node_save($node);
}
   

copy operation callback

It's working quite the same as copy but with a $new_name between the two arguments definition.

  function webdav_node_move($target_node_type, $new_name, $source_node) {
  if ($target_node_type!=$source_node-&gt;type) {
    webdav_session_set_status(WEBDAV_STATUS_FORBIDDEN);
    return;
  }

  // as node_save don't do any check, we should validate permissions here
  if (!node_access( "update", $source_node)) {
    webdav_session_set_status(WEBDAV_STATUS_FORBIDDEN);
    return;
  }
  $source_node-&gt;title = $name;
  _webdav_node_save($source-&gt;parameters['node']);
}