Launchpad automatic translations update.
[odoo/odoo.git] / addons / document_webdav / nodes.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2010 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22
23 from document import document as nodes
24 from openerp.tools.safe_eval import safe_eval as eval
25 import time
26 import urllib
27 import uuid
28 from openerp import SUPERUSER_ID
29
30 def dict_filter(srcdic, keys, res=None):
31     ''' Return a copy of srcdic that has only keys set.
32     If any of keys are missing from srcdic, the result won't have them, 
33     either.
34     @param res If given, result will be updated there, instead of a new dict.
35     '''
36     if res is None:
37         res = {}
38     for k in keys:
39         if k in srcdic:
40             res[k] = srcdic[k]
41     return res
42
43 class node_acl_mixin(object):
44     def _get_dav_owner(self, cr):
45         return self.uuser
46
47     def _get_dav_group(self, cr):
48         return self.ugroup
49         
50     def _get_dav_supported_privilege_set(self, cr):
51         return '' # TODO
52     
53     def _get_dav_current_user_privilege_set(self, cr):
54         return '' # TODO
55
56     def _get_dav_props_hlpr(self, cr, par_class, prop_model, 
57                             prop_ref_field, res_id):
58         """ Helper for dav properties, usable in subclasses
59         
60         @param par_class The parent class
61         @param prop_model The name of the orm model holding the properties
62         @param prop_ref_field The name of the field at prop_model pointing to us
63         @param res_id the id of self in the corresponing orm table, that should
64                         match prop_model.prop_ref_field
65         """
66         ret = par_class.get_dav_props(self, cr)
67         if prop_model:
68             propobj = self.context._dirobj.pool.get(prop_model)
69             uid = self.context.uid
70             ctx = self.context.context.copy()
71             ctx.update(self.dctx)
72             # Not really needed because we don't do eval here:
73             # ctx.update({'uid': uid, 'dbname': self.context.dbname })
74             # dict_filter(self.context.extra_ctx, ['username', 'groupname', 'webdav_path'], ctx)
75             sdomain = [(prop_ref_field, '=', False),]
76             if res_id:
77                 sdomain = ['|', (prop_ref_field, '=', res_id)] + sdomain
78             prop_ids = propobj.search(cr, uid, sdomain, context=ctx)
79             if prop_ids:
80                 ret = ret.copy()
81                 for pbro in propobj.browse(cr, uid, prop_ids, context=ctx):
82                     ret[pbro.namespace] = ret.get(pbro.namespace, ()) + \
83                         (pbro.name,)
84                     # Note that we cannot have properties to conditionally appear
85                     # on the context, yet.
86                 
87         return ret
88
89     def _get_dav_eprop_hlpr(self, cr, ns, prop,
90                             par_class, prop_model, 
91                             prop_ref_field, res_id):
92         """ Helper for get dav eprop, usable in subclasses
93         
94         @param namespace the one to search for
95         @param name Name to search for
96         @param par_class The parent class
97         @param prop_model The name of the orm model holding the properties
98         @param prop_ref_field The name of the field at prop_model pointing to us
99         @param res_id the id of self in the corresponing orm table, that should
100                         match prop_model.prop_ref_field
101         """
102         ret = par_class.get_dav_eprop(self, cr, ns, prop)
103         if ret is not None:
104             return ret
105         if prop_model:
106             propobj = self.context._dirobj.pool.get(prop_model)
107             uid = self.context.uid
108             ctx = self.context.context.copy()
109             ctx.update(self.dctx)
110             ctx.update({'uid': uid, 'dbname': self.context.dbname })
111             ctx['node_classname'] = "%s.%s" % (self.__class__.__module__, self.__class__.__name__)
112             dict_filter(self.context.extra_ctx, ['username', 'groupname', 'webdav_path'], ctx)
113             sdomain = [(prop_ref_field, '=', False),('namespace', '=', ns), ('name','=', prop)]
114             if res_id:
115                 sdomain = ['|', (prop_ref_field, '=', res_id)] + sdomain
116             prop_ids = propobj.search(cr, uid, sdomain, context=ctx)
117             if prop_ids:
118                 pbro = propobj.browse(cr, uid, prop_ids[0], context=ctx)
119                 val = pbro.value
120                 if pbro.do_subst:
121                     if val.startswith("('") and val.endswith(")"):
122                         glbls = { 'urlquote': urllib.quote, }
123                         val = eval(val, glbls, ctx)
124                     else:
125                         val = val % ctx
126                 return val
127         return None
128
129     def _dav_lock_hlpr(self, cr, lock_data, par_class, prop_model,
130                             prop_ref_field, res_id):
131         """ Helper, which uses the dav properties table for placing locks
132         
133         @param lock_data a dictionary of input to this function.
134         @return list of tuples, DAV:activelock _contents_ structure.
135                 See webdav.py:class Prop2Xml() for semantics
136         
137         Note: although the DAV response shall be an <activelock/>, this
138         function will only return the elements inside the activelock,
139         because the calling function needs to append the <lockroot/> in
140         it. See webdav.py:mk_lock_response()
141         
142         In order to reuse code, this function can be called with 
143         lock_data['unlock_mode']=True, in order to unlock.
144         
145         @return bool in unlock mode, (davstruct, prop_id, token) in lock/refresh,
146                     or (False, prop_id, token) if already locked,
147                     or (False, False, False) if lock not found to refresh
148         """
149         assert prop_model
150         assert res_id
151         assert isinstance(lock_data, dict), '%r' % lock_data
152         propobj = self.context._dirobj.pool.get(prop_model)
153         uid = self.context.uid
154         ctx = self.context.context.copy()
155         ctx.update(self.dctx)
156         ctx.update({'uid': uid, 'dbname': self.context.dbname })
157         ctx['node_classname'] = "%s.%s" % (self.__class__.__module__, self.__class__.__name__)
158         dict_filter(self.context.extra_ctx, ['username', 'groupname', 'webdav_path'], ctx)
159         sdomain = [(prop_ref_field, '=', res_id), ('namespace', '=', 'DAV:'),
160                     ('name','=', 'lockdiscovery')]
161         props_to_delete = []
162         lock_found = False
163         lock_val = None
164         tmout2 = int(lock_data.get('timeout', 3*3600))
165         
166         prop_ids = propobj.search(cr, uid, sdomain, context=ctx)
167         if prop_ids:
168             for pbro in propobj.browse(cr, uid, prop_ids, context=ctx):
169                 val = pbro.value
170                 if pbro.do_subst:
171                     if val.startswith("('") and val.endswith(")"):
172                         glbls = { 'urlquote': urllib.quote, }
173                         val = eval(val, glbls, ctx)
174                     else:
175                         # all locks should be at "subst" format
176                         continue
177                 if not (val and isinstance(val, tuple) 
178                         and val[0:2] == ( 'activelock','DAV:')):
179                     # print "Value is not activelock:", val
180                     continue
181                 
182                 old_token = False
183                 old_owner = False
184                 try:
185                     # discover the timeout. If anything goes wrong, delete
186                     # the lock (cleanup)
187                     tmout = False
188                     for parm in val[2]:
189                         if parm[1] != 'DAV:':
190                             continue
191                         if parm[0] == 'timeout':
192                             if isinstance(parm[2], basestring) \
193                                     and parm[2].startswith('Second-'):
194                                 tmout = int(parm[2][7:])
195                         elif parm[0] == 'locktoken':
196                             if isinstance(parm[2], basestring):
197                                 old_token = parm[2]
198                             elif isinstance(parm[2], tuple) and \
199                                 parm[2][0:2] == ('href','DAV:'):
200                                     old_token = parm[2][2]
201                             else:
202                                 # print "Mangled token in DAV property: %r" % parm[2]
203                                 props_to_delete.append(pbro.id)
204                                 continue
205                         elif parm[0] == 'owner':
206                             old_owner = parm[2] # not used yet
207                     if tmout:
208                         mdate = pbro.write_date or pbro.create_date
209                         mdate = time.mktime(time.strptime(mdate,'%Y-%m-%d %H:%M:%S'))
210                         if mdate + tmout < time.time():
211                             props_to_delete.append(pbro.id)
212                             continue
213                     else:
214                         props_to_delete.append(pbro.id)
215                         continue
216                 except ValueError:
217                     props_to_delete.append(pbro.id)
218                     continue
219                 
220                 # A valid lock is found here
221                 if lock_data.get('refresh', False):
222                     if old_token != lock_data.get('token'):
223                         continue
224                     # refresh mode. Just touch anything and the ORM will update
225                     # the write uid+date, won't it?
226                     # Note: we don't update the owner, because incoming refresh
227                     # wouldn't have a body, anyway.
228                     propobj.write(cr, uid, [pbro.id,], { 'name': 'lockdiscovery'})
229                 elif lock_data.get('unlock_mode', False):
230                     if old_token != lock_data.get('token'):
231                         continue
232                     props_to_delete.append(pbro.id)
233                 
234                 lock_found = pbro.id
235                 lock_val = val
236
237         if tmout2 > 3*3600: # 3 hours maximum
238             tmout2 = 3*3600
239         elif tmout2 < 300:
240             # 5 minutes minimum, but an unlock request can always
241             # break it at any time. Ensures no negative values, either.
242             tmout2 = 300
243         
244         if props_to_delete:
245             # explicitly delete, as admin, any of the ids we have identified.
246             propobj.unlink(cr, SUPERUSER_ID, props_to_delete)
247         
248         if lock_data.get('unlock_mode', False):
249             return lock_found and True
250         elif (not lock_found) and not (lock_data.get('refresh', False)):
251             # Create a new lock, attach and return it.
252             new_token = uuid.uuid4().urn
253             lock_val = ('activelock', 'DAV:', 
254                     [ ('locktype', 'DAV:', (lock_data.get('locktype',False) or 'write','DAV:')),
255                       ('lockscope', 'DAV:', (lock_data.get('lockscope',False) or 'exclusive','DAV:')),
256                       # ? ('depth', 'DAV:', lock_data.get('depth','0') ),
257                       ('timeout','DAV:', 'Second-%d' % tmout2),
258                       ('locktoken', 'DAV:', ('href', 'DAV:', new_token)),
259                       # ('lockroot', 'DAV: ..., we don't store that, appended by caller
260                     ])
261             new_owner = lock_data.get('lockowner',False) or ctx.get('username', False)
262             if new_owner:
263                 lock_val[2].append( ('owner', 'DAV:',  new_owner) )
264             prop_id = propobj.create(cr, uid, { prop_ref_field: res_id,
265                     'namespace': 'DAV:', 'name': 'lockdiscovery',
266                     'do_subst': True, 'value': repr(lock_val) })
267             return (lock_val[2], prop_id, new_token )
268         elif not lock_found: # and refresh
269             return (False, False, False)
270         elif lock_found and not lock_data.get('refresh', False):
271             # already locked
272             return (False, lock_found, old_token)
273         else:
274             return (lock_val[2], lock_found, old_token )
275
276 class node_dir(node_acl_mixin, nodes.node_dir):
277     """ override node_dir and add DAV functionality
278     """
279     DAV_PROPS = { "DAV:": ('owner', 'group', 
280                             'supported-privilege-set', 
281                             'current-user-privilege-set'), 
282                 }
283     DAV_M_NS = { "DAV:" : '_get_dav',}
284     http_options = { 'DAV': ['access-control',] }
285
286     def get_dav_resourcetype(self, cr):
287         return ('collection', 'DAV:')
288
289     def get_dav_props(self, cr):
290         return self._get_dav_props_hlpr(cr, nodes.node_dir, 
291                 'document.webdav.dir.property', 'dir_id', self.dir_id)
292
293     def get_dav_eprop(self, cr, ns, prop):
294         return self._get_dav_eprop_hlpr(cr, ns, prop, nodes.node_dir,
295                 'document.webdav.dir.property', 'dir_id', self.dir_id)
296
297
298 class node_file(node_acl_mixin, nodes.node_file):
299     DAV_PROPS = { "DAV:": ('owner', 'group', 
300                             'supported-privilege-set', 
301                             'current-user-privilege-set',
302                             ), 
303                 }
304     DAV_M_NS = { "DAV:" : '_get_dav',}
305     http_options = { 'DAV': ['access-control', ] }
306     pass
307
308     def get_dav_resourcetype(self, cr):
309         return ''
310
311     def get_dav_props(self, cr):
312         return self._get_dav_props_hlpr(cr, nodes.node_file, 
313                 'document.webdav.file.property', 'file_id', self.file_id)
314
315     def dav_lock(self, cr, lock_data):
316         """ Locks or unlocks the node, using DAV semantics.
317         
318         Unlocking will be done when lock_data['unlock_mode'] == True
319         
320         See _dav_lock_hlpr() for calling details.
321         
322         It is fundamentally OK to use this function from non-DAV endpoints,
323         but they will all have to emulate the tuple-in-list structure of
324         the DAV lock data. RFC if this translation should be done inside
325         the _dav_lock_hlpr (to ease other protocols).
326         """
327         return self._dav_lock_hlpr(cr, lock_data, nodes.node_file, 
328                 'document.webdav.file.property', 'file_id', self.file_id)
329
330     def dav_unlock(self, cr, token):
331         """Releases the token lock held for the node
332         
333         This is a utility complement of dav_lock()
334         """
335         lock_data = { 'token': token, 'unlock_mode': True }
336         return self._dav_lock_hlpr(cr, lock_data, nodes.node_file, 
337                 'document.webdav.file.property', 'file_id', self.file_id)
338
339     def get_dav_eprop(self, cr, ns, prop):
340         if ns == 'DAV:' and prop == 'supportedlock':
341             return [ ('lockentry', 'DAV:', 
342                         [ ('lockscope','DAV:', ('shared', 'DAV:')),
343                           ('locktype','DAV:', ('write', 'DAV:')),
344                         ]),
345                    ('lockentry', 'DAV:', 
346                         [ ('lockscope','DAV:', ('exclusive', 'DAV:')),
347                           ('locktype','DAV:', ('write', 'DAV:')),
348                         ] )
349                    ]
350         return self._get_dav_eprop_hlpr(cr, ns, prop, nodes.node_file,
351                 'document.webdav.file.property', 'file_id', self.file_id)
352
353 class node_database(nodes.node_database):
354     def get_dav_resourcetype(self, cr):
355         return ('collection', 'DAV:')
356
357     def get_dav_props(self, cr):
358         return self._get_dav_props_hlpr(cr, nodes.node_database,
359                 'document.webdav.dir.property', 'dir_id', False)
360
361     def get_dav_eprop(self, cr, ns, prop):
362         return self._get_dav_eprop_hlpr(cr, nodes.node_database, ns, prop,
363                 'document.webdav.dir.property', 'dir_id', False)
364
365 class node_res_obj(node_acl_mixin, nodes.node_res_obj):
366     DAV_PROPS = { "DAV:": ('owner', 'group', 
367                             'supported-privilege-set', 
368                             'current-user-privilege-set'), 
369                 }
370     DAV_M_NS = { "DAV:" : '_get_dav',}
371     http_options = { 'DAV': ['access-control',] }
372
373     def get_dav_resourcetype(self, cr):
374         return ('collection', 'DAV:')
375
376     def get_dav_props(self, cr):
377         return self._get_dav_props_hlpr(cr, nodes.node_res_obj, 
378                 'document.webdav.dir.property', 'dir_id', self.dir_id)
379
380     def get_dav_eprop(self, cr, ns, prop):
381         return self._get_dav_eprop_hlpr(cr, ns, prop, nodes.node_res_obj,
382                 'document.webdav.dir.property', 'dir_id', self.dir_id)
383
384
385 class node_res_dir(node_acl_mixin, nodes.node_res_dir):
386     DAV_PROPS = { "DAV:": ('owner', 'group', 
387                             'supported-privilege-set', 
388                             'current-user-privilege-set'), 
389                 }
390     DAV_M_NS = { "DAV:" : '_get_dav',}
391     http_options = { 'DAV': ['access-control',] }
392     res_obj_class = node_res_obj
393
394     def get_dav_resourcetype(self, cr):
395         return ('collection', 'DAV:')
396
397     def get_dav_props(self, cr):
398         return self._get_dav_props_hlpr(cr, nodes.node_res_dir, 
399                 'document.webdav.dir.property', 'dir_id', self.dir_id)
400
401     def get_dav_eprop(self, cr, ns, prop):
402         return self._get_dav_eprop_hlpr(cr, ns, prop, nodes.node_res_dir,
403                 'document.webdav.dir.property', 'dir_id', self.dir_id)
404
405 # Some copies, so that this module can replace 'from document import nodes'
406 get_node_context = nodes.get_node_context
407 node_context = nodes.node_context
408 node_class = nodes.node_class
409 node_descriptor = nodes.node_descriptor
410
411
412 #eof
413
414 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: