1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
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.
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.
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/>.
20 ##############################################################################
27 from string import joinfields, split, lower
32 from DAV.constants import COLLECTION, OBJECT
33 from DAV.errors import *
34 from DAV.iface import *
37 from DAV.davcmd import copyone, copytree, moveone, movetree, delone, deltree
38 from cache import memoize
39 from tools import misc
40 from tools.dict_tools import dict_merge2
44 #hack for urlparse: add webdav in the net protocols
45 urlparse.uses_netloc.append('webdav')
46 urlparse.uses_netloc.append('webdavs')
48 day_names = { 0: 'Mon', 1: 'Tue' , 2: 'Wed', 3: 'Thu', 4: 'Fri', 5: 'Sat', 6: 'Sun' }
49 month_names = { 1: 'Jan', 2: 'Feb', 3: 'Mar', 4: 'Apr', 5: 'May', 6: 'Jun',
50 7: 'Jul', 8: 'Aug', 9: 'Sep', 10: 'Oct', 11: 'Nov', 12: 'Dec' }
52 class DAV_NotFound2(DAV_NotFound):
53 """404 exception, that accepts our list uris
55 def __init__(self, *args):
56 if len(args) and isinstance(args[0], (tuple, list)):
57 path = ''.join([ '/' + x for x in args[0]])
59 DAV_NotFound.__init__(self, *args)
63 """ Convert a string with time representation (from db) into time (float)
68 if isinstance(cre, basestring) and '.' in cre:
70 frac = float(cre[fdot:])
72 return time.mktime(time.strptime(cre,'%Y-%m-%d %H:%M:%S')) + frac
74 class openerp_dav_handler(dav_interface):
76 This class models a OpenERP interface for the DAV server
78 PROPS={'DAV:': dav_interface.PROPS['DAV:'],}
80 M_NS={ "DAV:" : dav_interface.M_NS['DAV:'],}
82 def __init__(self, parent, verbose=False):
85 self.baseuri = parent.baseuri
86 self.verbose = verbose
88 def get_propnames(self, uri):
90 self.parent.log_message('get propnames: %s' % uri)
91 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
94 # TODO: maybe limit props for databases..?
96 node = self.uri2object(cr, uid, pool, uri2)
98 props = dict_merge2(props, node.get_dav_props(cr))
102 def _try_function(self, funct, args, opname='run function', cr=None,
103 default_exc=DAV_Forbidden):
104 """ Try to run a function, and properly convert exceptions to DAV ones.
106 @objname the name of the operation being performed
107 @param cr if given, the cursor to close at exceptions
115 except NotImplementedError, e:
118 self.parent.log_error("Cannot %s: %s", opname, str(e))
119 self.parent.log_message("Exc: %s",traceback.format_exc())
120 # see par 9.3.1 of rfc
121 raise DAV_Error(403, str(e) or 'Not supported at this path')
122 except EnvironmentError, err:
125 self.parent.log_error("Cannot %s: %s", opname, err.strerror)
126 self.parent.log_message("Exc: %s",traceback.format_exc())
127 raise default_exc(err.strerror)
131 self.parent.log_error("Cannot %s: %s", opname, str(e))
132 self.parent.log_message("Exc: %s",traceback.format_exc())
133 raise default_exc("Operation failed")
135 #def _get_dav_lockdiscovery(self, uri):
138 #def A_get_dav_supportedlock(self, uri):
141 def match_prop(self, uri, match, ns, propname):
142 if self.M_NS.has_key(ns):
143 return match == dav_interface.get_prop(self, uri, ns, propname)
144 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
148 node = self.uri2object(cr, uid, pool, uri2)
152 res = node.match_dav_eprop(cr, match, ns, propname)
156 def prep_http_options(self, uri, opts):
157 """see HttpOptions._prep_OPTIONS """
158 self.parent.log_message('get options: %s' % uri)
159 cr, uid, pool, dbname, uri2 = self.get_cr(uri, allow_last=True)
164 node = self.uri2object(cr, uid, pool, uri2[:])
170 if hasattr(node, 'http_options'):
172 for key, val in node.http_options.items():
173 if isinstance(val, basestring):
176 ret[key] = ret[key][:] # copy the orig. array
181 self.parent.log_message('options: %s' % ret)
187 def get_prop(self, uri, ns, propname):
188 """ return the value of a given property
190 uri -- uri of the object to get the property of
191 ns -- namespace of the property
192 pname -- name of the property
194 if self.M_NS.has_key(ns):
196 # if it's not in the interface class, a "DAV:" property
197 # may be at the node class. So shouldn't give up early.
198 return dav_interface.get_prop(self, uri, ns, propname)
201 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
205 node = self.uri2object(cr, uid, pool, uri2)
209 res = node.get_dav_eprop(cr, ns, propname)
213 def get_db(self, uri, rest_ret=False, allow_last=False):
214 """Parse the uri and get the dbname and the rest.
215 Db name should be the first component in the unix-like
216 path supplied in uri.
218 @param rest_ret Instead of the db_name, return (db_name, rest),
219 where rest is the remaining path
220 @param allow_last If the dbname is the last component in the
221 path, allow it to be resolved. The default False value means
222 we will not attempt to use the db, unless there is more
225 @return db_name or (dbname, rest) depending on rest_ret,
226 will return dbname=False when component is not found.
229 uri2 = self.uri2local(uri)
230 if uri2.startswith('/'):
232 names=uri2.split('/',1)
239 if len(names) > ll and names[0]:
250 def urijoin(self,*ajoin):
251 """ Return the base URI of this request, or even join it with the
254 return self.baseuri+ '/'.join(ajoin)
258 s = netsvc.ExportService.getService('db')
259 result = s.exp_list()
261 for db_name in result:
264 db = pooler.get_db_only(db_name)
266 cr.execute("SELECT id FROM ir_module_module WHERE name = 'document' AND state='installed' ")
269 self.db_name_list.append(db_name)
271 self.parent.log_error("Exception in db list: %s" % e)
275 return self.db_name_list
277 def get_childs(self, uri, filters=None):
278 """ return the child objects as self.baseuris for the given URI """
279 self.parent.log_message('get childs: %s' % uri)
280 cr, uid, pool, dbname, uri2 = self.get_cr(uri, allow_last=True)
284 res = map(lambda x: self.urijoin(x), self.db_list())
287 node = self.uri2object(cr, uid, pool, uri2[:])
291 raise DAV_NotFound2(uri2)
293 fp = node.full_path()
296 self.parent.log_message('childs for: %s' % fp)
301 domain = node.get_domain(cr, filters)
302 for d in node.children(cr, domain):
303 self.parent.log_message('child: %s' % d.path)
305 result.append( self.urijoin(dbname,fp,d.path) )
307 result.append( self.urijoin(dbname,d.path) )
312 def uri2local(self, uri):
313 uparts=urlparse.urlparse(uri)
315 if reluri and reluri[-1]=="/":
320 # pos: -1 to get the parent of the uri
322 def get_cr(self, uri, allow_last=False):
323 """ Split the uri, grab a cursor for that db
325 pdb = self.parent.auth_proxy.last_auth
326 dbname, uri2 = self.get_db(uri, rest_ret=True, allow_last=allow_last)
327 uri2 = (uri2 and uri2.split('/')) or []
329 return None, None, None, False, uri2
330 # if dbname was in our uri, we should have authenticated
332 assert pdb == dbname, " %s != %s" %(pdb, dbname)
333 res = self.parent.auth_proxy.auth_creds.get(dbname, False)
335 self.parent.auth_proxy.checkRequest(self.parent, uri, dbname)
336 res = self.parent.auth_proxy.auth_creds[dbname]
337 user, passwd, dbn2, uid = res
338 db,pool = pooler.get_db_and_pool(dbname)
340 return cr, uid, pool, dbname, uri2
342 def uri2object(self, cr, uid, pool, uri):
345 return pool.get('document.directory').get_object(cr, uid, uri)
347 def get_data(self,uri, rrange=None):
348 self.parent.log_message('GET: %s' % uri)
349 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
353 node = self.uri2object(cr, uid, pool, uri2)
355 raise DAV_NotFound2(uri2)
358 self.parent.log_error("Doc get_data cannot use range")
360 datas = node.get_data(cr)
362 # for the collections that return this error, the DAV standard
363 # says we'd better just return 200 OK with empty data
365 except IndexError,e :
366 self.parent.log_error("GET IndexError: %s", str(e))
367 raise DAV_NotFound2(uri2)
370 self.parent.log_error("GET exception: %s",str(e))
371 self.parent.log_message("Exc: %s", traceback.format_exc())
373 return str(datas) # FIXME!
378 def _get_dav_resourcetype(self, uri):
379 """ return type of object """
380 self.parent.log_message('get RT: %s' % uri)
381 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
385 node = self.uri2object(cr, uid, pool, uri2)
387 raise DAV_NotFound2(uri2)
389 return node.get_dav_resourcetype(cr)
390 except NotImplementedError:
391 if node.type in ('collection','database'):
392 return ('collection', 'DAV:')
397 def _get_dav_displayname(self,uri):
398 self.parent.log_message('get DN: %s' % uri)
399 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
402 # at root, dbname, just return the last component
404 if uri2 and len(uri2) < 2:
407 node = self.uri2object(cr, uid, pool, uri2)
410 raise DAV_NotFound2(uri2)
412 return node.displayname
415 def _get_dav_getcontentlength(self, uri):
416 """ return the content length of an object """
417 self.parent.log_message('get length: %s' % uri)
419 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
423 node = self.uri2object(cr, uid, pool, uri2)
426 raise DAV_NotFound2(uri2)
427 result = node.content_length or 0
432 def _get_dav_getetag(self,uri):
433 """ return the ETag of an object """
434 self.parent.log_message('get etag: %s' % uri)
436 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
440 node = self.uri2object(cr, uid, pool, uri2)
443 raise DAV_NotFound2(uri2)
444 result = self._try_function(node.get_etag ,(cr,), "etag %s" %uri, cr=cr)
449 def get_lastmodified(self, uri):
450 """ return the last modified date of the object """
451 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
455 node = self.uri2object(cr, uid, pool, uri2)
457 raise DAV_NotFound2(uri2)
458 return _str2time(node.write_date)
462 def _get_dav_getlastmodified(self,uri):
463 """ return the last modified date of a resource
465 d=self.get_lastmodified(uri)
466 # format it. Note that we explicitly set the day, month names from
467 # an array, so that strftime() doesn't use its own locale-aware
470 return time.strftime("%%s, %d %%s %Y %H:%M:%S GMT", gmt ) % \
471 (day_names[gmt.tm_wday], month_names[gmt.tm_mon])
474 def get_creationdate(self, uri):
475 """ return the last modified date of the object """
476 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
480 node = self.uri2object(cr, uid, pool, uri2)
482 raise DAV_NotFound2(uri2)
484 return _str2time(node.create_date)
489 def _get_dav_getcontenttype(self,uri):
490 self.parent.log_message('get contenttype: %s' % uri)
491 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
494 return 'httpd/unix-directory'
496 node = self.uri2object(cr, uid, pool, uri2)
498 raise DAV_NotFound2(uri2)
499 result = str(node.mimetype)
501 #raise DAV_NotFound, 'Could not find %s' % path
506 """ create a new collection
507 see par. 9.3 of rfc4918
509 self.parent.log_message('MKCOL: %s' % uri)
510 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
513 raise DAV_Error(409, "Cannot create nameless collection")
517 node = self.uri2object(cr,uid,pool, uri2[:-1])
520 raise DAV_Error(409, "Parent path %s does not exist" % uri2[:-1])
521 nc = node.child(cr, uri2[-1])
524 raise DAV_Error(405, "Path already exists")
525 self._try_function(node.create_child_collection, (cr, uri2[-1]),
526 "create col %s" % uri2[-1], cr=cr)
531 def put(self, uri, data, content_type=None):
532 """ put the object into the filesystem """
533 self.parent.log_message('Putting %s (%d), %s'%( misc.ustr(uri), data and len(data) or 0, content_type))
534 cr, uid, pool,dbname, uri2 = self.get_cr(uri)
539 node = self.uri2object(cr, uid, pool, uri2[:])
544 ext = objname.find('.') >0 and objname.split('.')[1] or False
548 dir_node = self.uri2object(cr, uid, pool, uri2[:-1])
551 raise DAV_NotFound('Parent folder not found')
553 newchild = self._try_function(dir_node.create_child, (cr, objname, data),
554 "create %s" % objname, cr=cr)
558 raise DAV_Error(400, "Failed to create resource")
560 uparts=urlparse.urlparse(uri)
561 fileloc = '/'.join(newchild.full_path())
562 if isinstance(fileloc, unicode):
563 fileloc = fileloc.encode('utf-8')
564 # the uri we get is a mangled one, where the davpath has been removed
565 davpath = self.parent.get_davpath()
567 surl = '%s://%s' % (uparts[0], uparts[1])
568 uloc = urllib.quote(fileloc)
570 if uri != ('/'+uloc) and uri != (surl + '/' + uloc):
571 hurl = '%s%s/%s/%s' %(surl, davpath, dbname, uloc)
574 etag = str(newchild.get_etag(cr))
576 self.parent.log_error("Cannot get etag for node: %s" % e)
579 self._try_function(node.set_data, (cr, data), "save %s" % objname, cr=cr)
586 """ delete a collection """
587 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
592 node = self.uri2object(cr, uid, pool, uri2)
593 self._try_function(node.rmcol, (cr,), "rmcol %s" % uri, cr=cr)
600 cr, uid, pool,dbname, uri2 = self.get_cr(uri)
604 node = self.uri2object(cr, uid, pool, uri2)
605 res = self._try_function(node.rm, (cr,), "rm %s" % uri, cr=cr)
608 raise OSError(1, 'Operation not permited.')
613 ### DELETE handlers (examples)
614 ### (we use the predefined methods in davcmd instead of doing
618 def delone(self, uri):
619 """ delete a single resource
621 You have to return a result dict of the form
623 or None if everything's ok
626 if uri[-1]=='/':uri=uri[:-1]
628 parent='/'.join(uri.split('/')[:-1])
631 def deltree(self, uri):
632 """ delete a collection
634 You have to return a result dict of the form
636 or None if everything's ok
638 if uri[-1]=='/':uri=uri[:-1]
639 res=deltree(self, uri)
640 parent='/'.join(uri.split('/')[:-1])
645 ### MOVE handlers (examples)
648 def moveone(self, src, dst, overwrite):
649 """ move one resource with Depth=0
651 an alternative implementation would be
656 r=os.system("rm -f '%s'" %dst)
658 r=os.system("mv '%s' '%s'" %(src,dst))
662 (untested!). This would not use the davcmd functions
663 and thus can only detect errors directly on the root node.
665 res=moveone(self, src, dst, overwrite)
668 def movetree(self, src, dst, overwrite):
669 """ move a collection with Depth=infinity
671 an alternative implementation would be
676 r=os.system("rm -rf '%s'" %dst)
678 r=os.system("mv '%s' '%s'" %(src,dst))
682 (untested!). This would not use the davcmd functions
683 and thus can only detect errors directly on the root node"""
685 res=movetree(self, src, dst, overwrite)
692 def copyone(self, src, dst, overwrite):
693 """ copy one resource with Depth=0
695 an alternative implementation would be
700 r=os.system("rm -f '%s'" %dst)
702 r=os.system("cp '%s' '%s'" %(src,dst))
706 (untested!). This would not use the davcmd functions
707 and thus can only detect errors directly on the root node.
709 res=copyone(self, src, dst, overwrite)
712 def copytree(self, src, dst, overwrite):
713 """ copy a collection with Depth=infinity
715 an alternative implementation would be
720 r=os.system("rm -rf '%s'" %dst)
722 r=os.system("cp -r '%s' '%s'" %(src,dst))
726 (untested!). This would not use the davcmd functions
727 and thus can only detect errors directly on the root node"""
728 res=copytree(self, src, dst, overwrite)
733 ### This methods actually copy something. low-level
734 ### They are called by the davcmd utility functions
735 ### copytree and copyone (not the above!)
736 ### Look in davcmd.py for further details.
739 def copy(self, src, dst):
740 src=urllib.unquote(src)
741 dst=urllib.unquote(dst)
742 ct = self._get_dav_getcontenttype(src)
743 data = self.get_data(src)
744 self.put(dst, data, ct)
747 def copycol(self, src, dst):
748 """ copy a collection.
750 As this is not recursive (the davserver recurses itself)
751 we will only create a new directory here. For some more
752 advanced systems we might also have to copy properties from
753 the source to the destination.
755 return self.mkcol(dst)
758 def exists(self, uri):
759 """ test if a resource exists """
761 cr, uid, pool,dbname, uri2 = self.get_cr(uri)
766 node = self.uri2object(cr, uid, pool, uri2)
775 def is_collection(self, uri):
776 """ test if the given uri is a collection """
777 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
781 node = self.uri2object(cr,uid,pool, uri2)
783 raise DAV_NotFound2(uri2)
784 if node.type in ('collection','database'):