summaryrefslogtreecommitdiff
path: root/addons/document_webdav
diff options
context:
space:
mode:
authorP. Christeas <p_christ@hol.gr>2010-12-07 15:40:41 +0200
committerP. Christeas <p_christ@hol.gr>2010-12-07 15:40:41 +0200
commit6b2c832f5f9cc1667cd50230f8f0a0139d352201 (patch)
tree61e04e42c975e98e93e061bd6c7b2f50e6cfb2cd /addons/document_webdav
parent9a4c99980315d80e2da5a3af9a1d870bde4cf734 (diff)
doc webdav: Implement locking support
This replaces the pseydo-locking of python-webdav library with a real db-based locking. Locks are stored as DAV properties, which will effectively also be listed in the PROPFIND response of the nodes. With locking in place, Office suites can collaborate on documents online. bzr revid: p_christ@hol.gr-20101207134041-8negkvxrbscv7fs7
Diffstat (limited to 'addons/document_webdav')
-rw-r--r--addons/document_webdav/dav_fs.py100
-rw-r--r--addons/document_webdav/nodes.py197
-rw-r--r--addons/document_webdav/webdav.py33
-rw-r--r--addons/document_webdav/webdav_server.py136
4 files changed, 454 insertions, 12 deletions
diff --git a/addons/document_webdav/dav_fs.py b/addons/document_webdav/dav_fs.py
index 7f2447b6e6f..ab7b1dbf5db 100644
--- a/addons/document_webdav/dav_fs.py
+++ b/addons/document_webdav/dav_fs.py
@@ -35,6 +35,9 @@ import urllib
from DAV.davcmd import copyone, copytree, moveone, movetree, delone, deltree
from cache import memoize
from tools import misc
+
+from webdav import mk_lock_response
+
try:
from tools.dict_tools import dict_merge2
except ImportError:
@@ -209,18 +212,20 @@ class openerp_dav_handler(dav_interface):
self.parent.log_error("Cannot %s: %s", opname, err.strerror)
self.parent.log_message("Exc: %s",traceback.format_exc())
raise default_exc(err.strerror)
- except Exception,e:
+ except Exception, e:
import traceback
if cr: cr.close()
self.parent.log_error("Cannot %s: %s", opname, str(e))
self.parent.log_message("Exc: %s",traceback.format_exc())
raise default_exc("Operation failed")
- #def _get_dav_lockdiscovery(self, uri):
- # raise DAV_NotFound
+ def _get_dav_lockdiscovery(self, uri):
+ """ We raise that so that the node API is used """
+ raise DAV_NotFound
- #def A_get_dav_supportedlock(self, uri):
- # raise DAV_NotFound
+ def _get_dav_supportedlock(self, uri):
+ """ We raise that so that the node API is used """
+ raise DAV_NotFound
def match_prop(self, uri, match, ns, propname):
if self.M_NS.has_key(ns):
@@ -911,6 +916,91 @@ class openerp_dav_handler(dav_interface):
cr.close()
return result
+ def unlock(self, uri, token):
+ """ Unlock a resource from that token
+
+ @return True if unlocked, False if no lock existed, Exceptions
+ """
+ cr, uid, pool, dbname, uri2 = self.get_cr(uri)
+ if not dbname:
+ if cr: cr.close()
+ raise DAV_Error, 409
+
+ node = self.uri2object(cr, uid, pool, uri2)
+ try:
+ node_fn = node.dav_unlock
+ except AttributeError:
+ # perhaps the node doesn't support locks
+ cr.close()
+ raise DAV_Error(400, 'No locks for this resource')
+
+ res = self._try_function(node_fn, (cr, token), "unlock %s" % uri, cr=cr)
+ cr.commit()
+ cr.close()
+ return res
+
+ def lock(self, uri, lock_data):
+ """ Lock (may create) resource.
+ Data is a dict, may contain:
+ depth, token, refresh, lockscope, locktype, owner
+ """
+ cr, uid, pool, dbname, uri2 = self.get_cr(uri)
+ created = False
+ if not dbname:
+ if cr: cr.close()
+ raise DAV_Error, 409
+
+ try:
+ node = self.uri2object(cr, uid, pool, uri2[:])
+ except Exception:
+ node = False
+
+ objname = misc.ustr(uri2[-1])
+
+ if not node:
+ dir_node = self.uri2object(cr, uid, pool, uri2[:-1])
+ if not dir_node:
+ cr.close()
+ raise DAV_NotFound('Parent folder not found')
+
+ # We create a new node (file) but with empty data=None,
+ # as in RFC4918 p. 9.10.4
+ node = self._try_function(dir_node.create_child, (cr, objname, None),
+ "create %s" % objname, cr=cr)
+ if not node:
+ cr.commit()
+ cr.close()
+ raise DAV_Error(400, "Failed to create resource")
+
+ created = True
+
+ try:
+ node_fn = node.dav_lock
+ except AttributeError:
+ # perhaps the node doesn't support locks
+ cr.close()
+ raise DAV_Error(400, 'No locks for this resource')
+
+ # Obtain the lock on the node
+ lres, pid, token = self._try_function(node_fn, (cr, lock_data), "lock %s" % objname, cr=cr)
+
+ if not lres:
+ cr.commit()
+ cr.close()
+ raise DAV_Error(423, "Resource already locked")
+
+ assert isinstance(lres, list), 'lres: %s' % repr(lres)
+
+ try:
+ data = mk_lock_response(self, uri, lres)
+ cr.commit()
+ except Exception:
+ cr.close()
+ raise
+
+ cr.close()
+ return created, data, token
+
@memoize(CACHE_SIZE)
def is_collection(self, uri):
""" test if the given uri is a collection """
diff --git a/addons/document_webdav/nodes.py b/addons/document_webdav/nodes.py
index 8bea54ab7d3..fc48a47056a 100644
--- a/addons/document_webdav/nodes.py
+++ b/addons/document_webdav/nodes.py
@@ -22,14 +22,14 @@
from document import nodes
from tools.safe_eval import safe_eval as eval
+import time
+import urllib
+import uuid
try:
from tools.dict_tools import dict_filter
except ImportError:
from document.dict_tools import dict_filter
-import urllib
-
-
class node_acl_mixin(object):
def _get_dav_owner(self, cr):
return self.uuser
@@ -116,6 +116,153 @@ class node_acl_mixin(object):
return val
return None
+ def _dav_lock_hlpr(self, cr, lock_data, par_class, prop_model,
+ prop_ref_field, res_id):
+ """ Helper, which uses the dav properties table for placing locks
+
+ @param lock_data a dictionary of input to this function.
+ @return list of tuples, DAV:activelock _contents_ structure.
+ See webdav.py:class Prop2Xml() for semantics
+
+ Note: although the DAV response shall be an <activelock/>, this
+ function will only return the elements inside the activelock,
+ because the calling function needs to append the <lockroot/> in
+ it. See webdav.py:mk_lock_response()
+
+ In order to reuse code, this function can be called with
+ lock_data['unlock_mode']=True, in order to unlock.
+
+ @return bool in unlock mode, (davstruct, prop_id, token) in lock/refresh,
+ or (False, prop_id, token) if already locked,
+ or (False, False, False) if lock not found to refresh
+ """
+ assert prop_model
+ assert res_id
+ assert isinstance(lock_data, dict), '%r' % lock_data
+ propobj = self.context._dirobj.pool.get(prop_model)
+ uid = self.context.uid
+ ctx = self.context.context.copy()
+ ctx.update(self.dctx)
+ ctx.update({'uid': uid, 'dbname': self.context.dbname })
+ ctx['node_classname'] = "%s.%s" % (self.__class__.__module__, self.__class__.__name__)
+ dict_filter(self.context.extra_ctx, ['username', 'groupname', 'webdav_path'], ctx)
+ sdomain = [(prop_ref_field, '=', res_id), ('namespace', '=', 'DAV:'),
+ ('name','=', 'lockdiscovery')]
+ props_to_delete = []
+ lock_found = False
+ lock_val = None
+ tmout2 = int(lock_data.get('timeout', 3*3600))
+
+ prop_ids = propobj.search(cr, uid, sdomain, context=ctx)
+ if prop_ids:
+ for pbro in propobj.browse(cr, uid, prop_ids, context=ctx):
+ val = pbro.value
+ if pbro.do_subst:
+ if val.startswith("('") and val.endswith(")"):
+ glbls = { 'urlquote': urllib.quote, }
+ val = eval(val, glbls, ctx)
+ else:
+ # all locks should be at "subst" format
+ continue
+ if not (val and isinstance(val, tuple)
+ and val[0:2] == ( 'activelock','DAV:')):
+ # print "Value is not activelock:", val
+ continue
+
+ old_token = False
+ old_owner = False
+ try:
+ # discover the timeout. If anything goes wrong, delete
+ # the lock (cleanup)
+ tmout = False
+ for parm in val[2]:
+ if parm[1] != 'DAV:':
+ continue
+ if parm[0] == 'timeout':
+ if isinstance(parm[2], basestring) \
+ and parm[2].startswith('Second-'):
+ tmout = int(parm[2][7:])
+ elif parm[0] == 'locktoken':
+ if isinstance(parm[2], basestring):
+ old_token = parm[2]
+ elif isinstance(parm[2], tuple) and \
+ parm[2][0:2] == ('href','DAV:'):
+ old_token = parm[2][2]
+ else:
+ # print "Mangled token in DAV property: %r" % parm[2]
+ props_to_delete.append(pbro.id)
+ continue
+ elif parm[0] == 'owner':
+ old_owner = parm[2] # not used yet
+ if tmout:
+ mdate = pbro.write_date or pbro.create_date
+ mdate = time.mktime(time.strptime(mdate,'%Y-%m-%d %H:%M:%S'))
+ if mdate + tmout < time.time():
+ props_to_delete.append(pbro.id)
+ continue
+ else:
+ props_to_delete.append(pbro.id)
+ continue
+ except ValueError:
+ props_to_delete.append(pbro.id)
+ continue
+
+ # A valid lock is found here
+ if lock_data.get('refresh', False):
+ if old_token != lock_data.get('token'):
+ continue
+ # refresh mode. Just touch anything and the ORM will update
+ # the write uid+date, won't it?
+ # Note: we don't update the owner, because incoming refresh
+ # wouldn't have a body, anyway.
+ propobj.write(cr, uid, [pbro.id,], { 'name': 'lockdiscovery'})
+ elif lock_data.get('unlock_mode', False):
+ if old_token != lock_data.get('token'):
+ continue
+ props_to_delete.append(pbro.id)
+
+ lock_found = pbro.id
+ lock_val = val
+
+ if tmout2 > 3*3600: # 3 hours maximum
+ tmout2 = 3*3600
+ elif tmout2 < 300:
+ # 5 minutes minimum, but an unlock request can always
+ # break it at any time. Ensures no negative values, either.
+ tmout2 = 300
+
+ if props_to_delete:
+ # explicitly delete, as admin, any of the ids we have identified.
+ propobj.unlink(cr, 1, props_to_delete)
+
+ if lock_data.get('unlock_mode', False):
+ return lock_found and True
+ elif (not lock_found) and not (lock_data.get('refresh', False)):
+ # Create a new lock, attach and return it.
+ new_token = uuid.uuid4().urn
+ lock_val = ('activelock', 'DAV:',
+ [ ('locktype', 'DAV:', (lock_data.get('locktype',False) or 'write','DAV:')),
+ ('lockscope', 'DAV:', (lock_data.get('lockscope',False) or 'exclusive','DAV:')),
+ # ? ('depth', 'DAV:', lock_data.get('depth','0') ),
+ ('timeout','DAV:', 'Second-%d' % tmout2),
+ ('locktoken', 'DAV:', ('href', 'DAV:', new_token)),
+ # ('lockroot', 'DAV: ..., we don't store that, appended by caller
+ ])
+ new_owner = lock_data.get('lockowner',False) or ctx.get('username', False)
+ if new_owner:
+ lock_val[2].append( ('owner', 'DAV:', new_owner) )
+ prop_id = propobj.create(cr, uid, { prop_ref_field: res_id,
+ 'namespace': 'DAV:', 'name': 'lockdiscovery',
+ 'do_subst': True, 'value': repr(lock_val) })
+ return (lock_val[2], prop_id, new_token )
+ elif not lock_found: # and refresh
+ return (False, False, False)
+ elif lock_found and not lock_data.get('refresh', False):
+ # already locked
+ return (False, lock_found, old_token)
+ else:
+ return (lock_val[2], lock_found, old_token )
+
class node_dir(node_acl_mixin, nodes.node_dir):
""" override node_dir and add DAV functionality
"""
@@ -141,7 +288,8 @@ class node_dir(node_acl_mixin, nodes.node_dir):
class node_file(node_acl_mixin, nodes.node_file):
DAV_PROPS = { "DAV:": ('owner', 'group',
'supported-privilege-set',
- 'current-user-privilege-set'),
+ 'current-user-privilege-set',
+ ),
}
DAV_M_NS = { "DAV:" : '_get_dav',}
http_options = { 'DAV': ['access-control', ] }
@@ -152,10 +300,45 @@ class node_file(node_acl_mixin, nodes.node_file):
def get_dav_props(self, cr):
return self._get_dav_props_hlpr(cr, nodes.node_dir,
- None, 'file_id', self.file_id)
- #'document.webdav.dir.property', 'dir_id', self.dir_id)
+ 'document.webdav.file.property', 'file_id', self.file_id)
- #def get_dav_eprop(self, cr, ns, prop):
+ def dav_lock(self, cr, lock_data):
+ """ Locks or unlocks the node, using DAV semantics.
+
+ Unlocking will be done when lock_data['unlock_mode'] == True
+
+ See _dav_lock_hlpr() for calling details.
+
+ It is fundamentally OK to use this function from non-DAV endpoints,
+ but they will all have to emulate the tuple-in-list structure of
+ the DAV lock data. RFC if this translation should be done inside
+ the _dav_lock_hlpr (to ease other protocols).
+ """
+ return self._dav_lock_hlpr(cr, lock_data, nodes.node_file,
+ 'document.webdav.file.property', 'file_id', self.file_id)
+
+ def dav_unlock(self, cr, token):
+ """Releases the token lock held for the node
+
+ This is a utility complement of dav_lock()
+ """
+ lock_data = { 'token': token, 'unlock_mode': True }
+ return self._dav_lock_hlpr(cr, lock_data, nodes.node_file,
+ 'document.webdav.file.property', 'file_id', self.file_id)
+
+ def get_dav_eprop(self, cr, ns, prop):
+ if ns == 'DAV:' and prop == 'supportedlock':
+ return [ ('lockentry', 'DAV:',
+ [ ('lockscope','DAV:', ('shared', 'DAV:')),
+ ('locktype','DAV:', ('write', 'DAV:')),
+ ]),
+ ('lockentry', 'DAV:',
+ [ ('lockscope','DAV:', ('exclusive', 'DAV:')),
+ ('locktype','DAV:', ('write', 'DAV:')),
+ ] )
+ ]
+ return self._get_dav_eprop_hlpr(cr, ns, prop, nodes.node_file,
+ 'document.webdav.file.property', 'file_id', self.file_id)
class node_database(nodes.node_database):
def get_dav_resourcetype(self, cr):
diff --git a/addons/document_webdav/webdav.py b/addons/document_webdav/webdav.py
index 302d8c2a8a7..dd19aeef7d5 100644
--- a/addons/document_webdav/webdav.py
+++ b/addons/document_webdav/webdav.py
@@ -268,6 +268,39 @@ def mk_propname_response(self,uri,propnames,doc):
PROPFIND.mk_prop_response = mk_prop_response
PROPFIND.mk_propname_response = mk_propname_response
+def mk_lock_response(self, uri, props):
+ """ Prepare the data response to a DAV LOCK command
+
+ This function is here, merely to be in the same file as the
+ ones above, that have similar code.
+ """
+ doc = domimpl.createDocument('DAV:', "D:prop", None)
+ ms = doc.documentElement
+ ms.setAttribute("xmlns:D", "DAV:")
+ # ms.tagName = 'D:multistatus'
+ namespaces = []
+ nsnum = 0
+ propgen = Prop2xml(doc, namespaces, nsnum)
+ # write href information
+ uparts=urlparse.urlparse(uri)
+ fileloc=uparts[2]
+ if isinstance(fileloc, unicode):
+ fileloc = fileloc.encode('utf-8')
+ davpath = self.parent.get_davpath()
+ if uparts[0] and uparts[1]:
+ hurl = '%s://%s%s%s' % (uparts[0], uparts[1], davpath, urllib.quote(fileloc))
+ else:
+ # When the request has been relative, we don't have enough data to
+ # reply with absolute url here.
+ hurl = '%s%s' % (davpath, urllib.quote(fileloc))
+
+ props.append( ('lockroot', 'DAV:', ('href', 'DAV:', (hurl))))
+ pld = doc.createElement('D:lockdiscovery')
+ ms.appendChild(pld)
+ propgen._prop_child(pld, 'DAV:', 'activelock', props)
+
+ return doc.toxml(encoding="utf-8")
+
super_create_prop = REPORT.create_prop
def create_prop(self):
diff --git a/addons/document_webdav/webdav_server.py b/addons/document_webdav/webdav_server.py
index 0e5973c95b3..5d73ae292f2 100644
--- a/addons/document_webdav/webdav_server.py
+++ b/addons/document_webdav/webdav_server.py
@@ -38,7 +38,9 @@ import urllib
import re
from string import atoi
from DAV.errors import *
+from DAV.utils import IfParser, TagList
# from DAV.constants import DAV_VERSION_1, DAV_VERSION_2
+from xml.dom import minidom
khtml_re = re.compile(r' KHTML/([0-9\.]+) ')
@@ -103,6 +105,7 @@ class DAVHandler(HttpOptions, FixSendError, DAVRequestHandler):
self.headers['Destination'] = up.path[len(self.davpath):]
else:
raise DAV_Forbidden("Not allowed to copy/move outside webdav path")
+ # TODO: locks
DAVRequestHandler.copymove(self, CLASS)
def get_davpath(self):
@@ -262,6 +265,139 @@ class DAVHandler(HttpOptions, FixSendError, DAVRequestHandler):
except DAV_Error, (ec, dd):
return self.send_status(ec)
+ def do_UNLOCK(self):
+ """ Unlocks given resource """
+
+ dc = self.IFACE_CLASS
+ self.log_message('UNLOCKing resource %s' % self.headers)
+
+ uri = urlparse.urljoin(self.get_baseuri(dc), self.path)
+ uri = urllib.unquote(uri)
+
+ token = self.headers.get('Lock-Token', False)
+ if token:
+ token = token.strip()
+ if token[0] == '<' and token[-1] == '>':
+ token = token[1:-1]
+ else:
+ token = False
+
+ if not token:
+ return self.send_status(400, 'Bad lock token')
+
+ try:
+ res = dc.unlock(uri, token)
+ except DAV_Error, (ec, dd):
+ return self.send_status(ec, dd)
+
+ if res == True:
+ self.send_body(None, '204', 'OK', 'Resource unlocked.')
+ else:
+ # We just differentiate the description, for debugging purposes
+ self.send_body(None, '204', 'OK', 'Resource not locked.')
+
+ def do_LOCK(self):
+ """ Attempt to place a lock on the given resource.
+ """
+
+ dc = self.IFACE_CLASS
+ lock_data = {}
+
+ self.log_message('LOCKing resource %s' % self.headers)
+
+ body = None
+ if self.headers.has_key('Content-Length'):
+ l = self.headers['Content-Length']
+ body = self.rfile.read(atoi(l))
+
+ depth = self.headers.get('Depth', 'infinity')
+
+ uri = urlparse.urljoin(self.get_baseuri(dc), self.path)
+ uri = urllib.unquote(uri)
+ self.log_message('do_LOCK: uri = %s' % uri)
+
+ ifheader = self.headers.get('If')
+
+ if ifheader:
+ ldif = IfParser(ifheader)
+ if isinstance(ldif, list):
+ if len(ldif) !=1 or (not isinstance(ldif[0], TagList)) \
+ or len(ldif[0].list) != 1:
+ raise DAV_Error(400, "Cannot accept multiple tokens")
+ ldif = ldif[0].list[0]
+ if ldif[0] == '<' and ldif[-1] == '>':
+ ldif = ldif[1:-1]
+
+ lock_data['token'] = ldif
+
+ if not body:
+ lock_data['refresh'] = True
+ else:
+ lock_data['refresh'] = False
+ lock_data.update(self._lock_unlock_parse(body))
+
+ if lock_data['refresh'] and not lock_data.get('token', False):
+ raise DAV_Error(400, 'Lock refresh must specify token')
+
+ lock_data['depth'] = depth
+
+ try:
+ created, data, lock_token = dc.lock(uri, lock_data)
+ except DAV_Error, (ec, dd):
+ return self.send_status(ec, dd)
+
+ headers = {}
+ if not lock_data['refresh']:
+ headers['Lock-Token'] = '<%s>' % lock_token
+
+ if created:
+ self.send_body(data, '201', 'Created', ctype='text/xml', headers=headers)
+ else:
+ self.send_body(data, '200', 'OK', ctype='text/xml', headers=headers)
+
+ def _lock_unlock_parse(self, body):
+ # Override the python-webdav function, with some improvements
+ # Unlike the py-webdav one, we also parse the owner minidom elements into
+ # pure pythonic struct.
+ doc = minidom.parseString(body)
+
+ data = {}
+ owners = []
+ for info in doc.getElementsByTagNameNS('DAV:', 'lockinfo'):
+ for scope in info.getElementsByTagNameNS('DAV:', 'lockscope'):
+ for scc in scope.childNodes:
+ if scc.nodeType == info.ELEMENT_NODE \
+ and scc.namespaceURI == 'DAV:':
+ data['lockscope'] = scc.localName
+ break
+ for ltype in info.getElementsByTagNameNS('DAV:', 'locktype'):
+ for ltc in ltype.childNodes:
+ if ltc.nodeType == info.ELEMENT_NODE \
+ and ltc.namespaceURI == 'DAV:':
+ data['locktype'] = ltc.localName
+ break
+ for own in info.getElementsByTagNameNS('DAV:', 'owner'):
+ for ono in own.childNodes:
+ if ono.nodeType == info.TEXT_NODE:
+ if ono.data:
+ owners.append(ono.data)
+ elif ono.nodeType == info.ELEMENT_NODE \
+ and ono.namespaceURI == 'DAV:' \
+ and ono.localName == 'href':
+ href = ''
+ for hno in ono.childNodes:
+ if hno.nodeType == info.TEXT_NODE:
+ href += hno.data
+ owners.append(('href','DAV:', href))
+
+ if len(owners) == 1:
+ data['lockowner'] = owners[0]
+ elif not owners:
+ pass
+ else:
+ data['lockowner'] = owners
+ return data
+
from service.http_server import reg_http_service,OpenERPAuthProvider
class DAVAuthProvider(OpenERPAuthProvider):