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
403 lines
17 KiB
Python
403 lines
17 KiB
Python
# -*- coding: utf-8 -*-
|
|
##############################################################################
|
|
#
|
|
# OpenERP, Open Source Management Solution
|
|
# Copyright (C) 2010 Tiny SPRL (<http://tiny.be>).
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU Affero General Public License as
|
|
# published by the Free Software Foundation, either version 3 of the
|
|
# License, or (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Affero General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
#
|
|
##############################################################################
|
|
|
|
|
|
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
|
|
|
|
class node_acl_mixin(object):
|
|
def _get_dav_owner(self, cr):
|
|
return self.uuser
|
|
|
|
def _get_dav_group(self, cr):
|
|
return self.ugroup
|
|
|
|
def _get_dav_supported_privilege_set(self, cr):
|
|
return '' # TODO
|
|
|
|
def _get_dav_current_user_privilege_set(self, cr):
|
|
return '' # TODO
|
|
|
|
def _get_dav_props_hlpr(self, cr, par_class, prop_model,
|
|
prop_ref_field, res_id):
|
|
""" Helper for dav properties, usable in subclasses
|
|
|
|
@param par_class The parent class
|
|
@param prop_model The name of the orm model holding the properties
|
|
@param prop_ref_field The name of the field at prop_model pointing to us
|
|
@param res_id the id of self in the corresponing orm table, that should
|
|
match prop_model.prop_ref_field
|
|
"""
|
|
ret = par_class.get_dav_props(self, cr)
|
|
if prop_model:
|
|
propobj = self.context._dirobj.pool.get(prop_model)
|
|
uid = self.context.uid
|
|
ctx = self.context.context.copy()
|
|
ctx.update(self.dctx)
|
|
# Not really needed because we don't do eval here:
|
|
# ctx.update({'uid': uid, 'dbname': self.context.dbname })
|
|
# dict_filter(self.context.extra_ctx, ['username', 'groupname', 'webdav_path'], ctx)
|
|
sdomain = [(prop_ref_field, '=', False),]
|
|
if res_id:
|
|
sdomain = ['|', (prop_ref_field, '=', res_id)] + sdomain
|
|
prop_ids = propobj.search(cr, uid, sdomain, context=ctx)
|
|
if prop_ids:
|
|
ret = ret.copy()
|
|
for pbro in propobj.browse(cr, uid, prop_ids, context=ctx):
|
|
ret[pbro.namespace] = ret.get(pbro.namespace, ()) + \
|
|
(pbro.name,)
|
|
# Note that we cannot have properties to conditionally appear
|
|
# on the context, yet.
|
|
|
|
return ret
|
|
|
|
def _get_dav_eprop_hlpr(self, cr, ns, prop,
|
|
par_class, prop_model,
|
|
prop_ref_field, res_id):
|
|
""" Helper for get dav eprop, usable in subclasses
|
|
|
|
@param namespace the one to search for
|
|
@param name Name to search for
|
|
@param par_class The parent class
|
|
@param prop_model The name of the orm model holding the properties
|
|
@param prop_ref_field The name of the field at prop_model pointing to us
|
|
@param res_id the id of self in the corresponing orm table, that should
|
|
match prop_model.prop_ref_field
|
|
"""
|
|
ret = par_class.get_dav_eprop(self, cr, ns, prop)
|
|
if ret is not None:
|
|
return ret
|
|
if prop_model:
|
|
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, '=', False),('namespace', '=', ns), ('name','=', prop)]
|
|
if res_id:
|
|
sdomain = ['|', (prop_ref_field, '=', res_id)] + sdomain
|
|
prop_ids = propobj.search(cr, uid, sdomain, context=ctx)
|
|
if prop_ids:
|
|
pbro = propobj.browse(cr, uid, prop_ids[0], 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:
|
|
val = val % ctx
|
|
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
|
|
"""
|
|
DAV_PROPS = { "DAV:": ('owner', 'group',
|
|
'supported-privilege-set',
|
|
'current-user-privilege-set'),
|
|
}
|
|
DAV_M_NS = { "DAV:" : '_get_dav',}
|
|
http_options = { 'DAV': ['access-control',] }
|
|
|
|
def get_dav_resourcetype(self, cr):
|
|
return ('collection', 'DAV:')
|
|
|
|
def get_dav_props(self, cr):
|
|
return self._get_dav_props_hlpr(cr, nodes.node_dir,
|
|
'document.webdav.dir.property', 'dir_id', self.dir_id)
|
|
|
|
def get_dav_eprop(self, cr, ns, prop):
|
|
return self._get_dav_eprop_hlpr(cr, ns, prop, nodes.node_dir,
|
|
'document.webdav.dir.property', 'dir_id', self.dir_id)
|
|
|
|
|
|
class node_file(node_acl_mixin, nodes.node_file):
|
|
DAV_PROPS = { "DAV:": ('owner', 'group',
|
|
'supported-privilege-set',
|
|
'current-user-privilege-set',
|
|
),
|
|
}
|
|
DAV_M_NS = { "DAV:" : '_get_dav',}
|
|
http_options = { 'DAV': ['access-control', ] }
|
|
pass
|
|
|
|
def get_dav_resourcetype(self, cr):
|
|
return ''
|
|
|
|
def get_dav_props(self, cr):
|
|
return self._get_dav_props_hlpr(cr, nodes.node_dir,
|
|
'document.webdav.file.property', 'file_id', self.file_id)
|
|
|
|
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):
|
|
return ('collection', 'DAV:')
|
|
|
|
def get_dav_props(self, cr):
|
|
return self._get_dav_props_hlpr(cr, nodes.node_dir,
|
|
'document.webdav.dir.property', 'dir_id', False)
|
|
|
|
def get_dav_eprop(self, cr, ns, prop):
|
|
return self._get_dav_eprop_hlpr(cr, nodes.node_dir, ns, prop,
|
|
'document.webdav.dir.property', 'dir_id', False)
|
|
|
|
class node_res_obj(node_acl_mixin, nodes.node_res_obj):
|
|
DAV_PROPS = { "DAV:": ('owner', 'group',
|
|
'supported-privilege-set',
|
|
'current-user-privilege-set'),
|
|
}
|
|
DAV_M_NS = { "DAV:" : '_get_dav',}
|
|
http_options = { 'DAV': ['access-control',] }
|
|
|
|
def get_dav_resourcetype(self, cr):
|
|
return ('collection', 'DAV:')
|
|
|
|
def get_dav_props(self, cr):
|
|
return self._get_dav_props_hlpr(cr, nodes.node_res_obj,
|
|
'document.webdav.dir.property', 'dir_id', self.dir_id)
|
|
|
|
def get_dav_eprop(self, cr, ns, prop):
|
|
return self._get_dav_eprop_hlpr(cr, ns, prop, nodes.node_res_obj,
|
|
'document.webdav.dir.property', 'dir_id', self.dir_id)
|
|
|
|
|
|
class node_res_dir(node_acl_mixin, nodes.node_res_dir):
|
|
DAV_PROPS = { "DAV:": ('owner', 'group',
|
|
'supported-privilege-set',
|
|
'current-user-privilege-set'),
|
|
}
|
|
DAV_M_NS = { "DAV:" : '_get_dav',}
|
|
http_options = { 'DAV': ['access-control',] }
|
|
res_obj_class = node_res_obj
|
|
|
|
def get_dav_resourcetype(self, cr):
|
|
return ('collection', 'DAV:')
|
|
|
|
def get_dav_props(self, cr):
|
|
return self._get_dav_props_hlpr(cr, nodes.node_res_dir,
|
|
'document.webdav.dir.property', 'dir_id', self.dir_id)
|
|
|
|
def get_dav_eprop(self, cr, ns, prop):
|
|
return self._get_dav_eprop_hlpr(cr, ns, prop, nodes.node_res_dir,
|
|
'document.webdav.dir.property', 'dir_id', self.dir_id)
|
|
|
|
# Some copies, so that this module can replace 'from document import nodes'
|
|
get_node_context = nodes.get_node_context
|
|
node_context = nodes.node_context
|
|
node_class = nodes.node_class
|
|
node_descriptor = nodes.node_descriptor
|
|
|
|
|
|
#eof
|