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
<<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.