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