Microsoft WebDav opens document as Read-Only when using RailsDav

I had been working on a project in which we wanted to utilize WebDAV (namely for editing Word & Excel Documents that were saved in our application). In order to do this we decided to use a plugin from liverail.net that can be found here. It was pretty easy to hook up after a little direction from a guy over at Benryan Inc [apologies I cannot find a link for them], but there was a major issue. When opening a document through the ActiveX controller for editing it was opening in Read-Only mode.

After a few starts and stops, many hours of reading through the webdav documentation, and browsing through the http traffic using Fiddler - it was determined that locking was the issue.

What I found in the RailsDav plugin is that the implementation of locking is as follows (lines 92-100 of railsdav/act_as_railsdav.rb):

def webdav_lock()
    #TODO implementation for now return a 200 OK
    render :nothing => true, :status => 200 and return
end

def webdav_unlock()
    #TODO implementation for now return a 200 OK
    render :nothing => true, :status => 200 and return
end

However, Microsoft actually won't let you edit a document with such lax locking. So I rewrote the lock method so that it returned a valid lock.

def webdav_lock()
     @lock = get_lock(@path_info)
     if @lock
         response.headers["Lock-Token"] = "<#{@lock.token}>"
         response.headers["Content-Type"] = 'text/xml; charset="utf-8"'
         render :inline => self.class.lock_xml, :layout => false, :type => :rxml, :status => 200 and return
       else
         render :nothing => true, :status => WebDavErrors::ForbiddenError and return
       end
end

There were a few other pieces of code that were needed to get this working though. Namely the .lock_xml method, and get_lock. Let's tackle them each in turn.

In railsdav/propxml.rb you can find the XML structures for the PROPFIND and the PROPPATCH methods for WebDAV. However both locking and unlocking require their own structures. So I added one for locking. It is as follows:

def lock_xml
<&lt;EOPROPFIND_XML
         xml.D(:multistatus, {"xmlns:D" => "DAV:"}) do
           xml.D :lockdiscovery do
             xml.D :activelock do
               xml.D :locktype do
                 xml.D @lock.type.to_sym
               end
               xml.D :lockscope do
                 xml.D @lock.scope.to_sym
               end
               xml.D :depth, @lock.depth
               xml.D :timeout, @lock.timeout_full
               xml.D :locktoken do
                 xml.D :href, @lock.token
               end
               xml.D :lockroot do
                 xml.D :href, @lock.href
               end
             end
           end
         end
EOPROPFIND_XML
end

Then there is the get_lock method. Writing get_lock was a two step process. If you look at (lines 269-287 of railsdav/act_as_railsdav.rb) you can see several methods that are very similar in function to their respective purposes. So I began by simply adding the following in this section:

def get_lock(path)
      raise WebDavErrors::ForbiddenError
end

Then in my application's controller that overrides all of the acts_as_railsdav methods I did just that:

def get_lock(path)
    return false unless get_resource_for_path(path).locked?
    return ActiveRecordLock.new({:type => 'write', :scope => 'exclusive', :timeout => 60*60*5, :href => path, :id =>4})
end

So now you'll notice two new things that need coding here. The .locked? method for the resource and ActiveRecordLock. We decided to not worry about multiple users editing the same document at the same time on this project. Because of this and the time constraints .locked? simply returns true. If one were to deal with unlocking at some point this would have to be come a check to the database or something to make sure that the document is indeed unlocked. The same goes for ActiveRecordLock. It really just deals with the flat information needed by the Lock XML and not saving it to check if it is locked later on. Maybe someday, somebody will undertake that task I suppose. Anyway, here is what I had for the portion of the ActiveRecordLock model that I needed.

class ActiveRecordLock
   attr_accessor :type, :scope, :timeout, :depth, :timeout_units, :href, :token

   def initialize(args)
      @type = args[:type]
      @scope = args[:scope]
      @timeout = args[:timeout]
      @href = args[:href]
      @depth = 'Infinity'
      @timeout_units = 'Second'
      @token = build_token(args[:id])
   end

   def timeout_full
     "#{self.timeout_units}-#{self.timeout}"
   end

   protected
     def build_token(text)
       require 'digest/md5'
       md5 = Digest::MD5.hexdigest(text.to_s).to_s
       'opaquelocktoken:'+md5[0,7]+'-'+md5[8,11]+'-'+md5[12,15]+'-'+md5[16,19]+'-'+md5[20,31]
     end
end

There is still plenty that needs to be done to get the full lock/unlock, but hopefully this will get someone started and save a few headaches.