Files
odoo_source/addons/document_webdav/nodes.py
T
P. Christeas 6b2c832f5f 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
2010-12-07 15:40:41 +02:00

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