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 ##############################################################################
30 from DAV.constants import COLLECTION #, OBJECT
31 from DAV.errors import DAV_Error, DAV_Forbidden, DAV_NotFound
32 from DAV.iface import dav_interface
35 from DAV.davcmd import copyone, copytree, moveone, movetree, delone, deltree
36 from cache import memoize
37 from tools import misc
39 from webdav import mk_lock_response
42 from tools.dict_tools import dict_merge2
44 from document.dict_tools import dict_merge2
48 #hack for urlparse: add webdav in the net protocols
49 urlparse.uses_netloc.append('webdav')
50 urlparse.uses_netloc.append('webdavs')
52 day_names = { 0: 'Mon', 1: 'Tue' , 2: 'Wed', 3: 'Thu', 4: 'Fri', 5: 'Sat', 6: 'Sun' }
53 month_names = { 1: 'Jan', 2: 'Feb', 3: 'Mar', 4: 'Apr', 5: 'May', 6: 'Jun',
54 7: 'Jul', 8: 'Aug', 9: 'Sep', 10: 'Oct', 11: 'Nov', 12: 'Dec' }
56 class DAV_NotFound2(DAV_NotFound):
57 """404 exception, that accepts our list uris
59 def __init__(self, *args):
60 if len(args) and isinstance(args[0], (tuple, list)):
61 path = ''.join([ '/' + x for x in args[0]])
63 DAV_NotFound.__init__(self, *args)
67 """ Convert a string with time representation (from db) into time (float)
72 if isinstance(cre, basestring) and '.' in cre:
74 frac = float(cre[fdot:])
76 return time.mktime(time.strptime(cre,'%Y-%m-%d %H:%M:%S')) + frac
78 class BoundStream2(object):
79 """Wraps around a seekable buffer, reads a determined range of data
81 Note that the supplied stream object MUST support a size() which
82 should return its data length (in bytes).
84 A variation of the class in websrv_lib.py
87 def __init__(self, stream, offset=None, length=None, chunk_size=None):
89 self._offset = offset or 0
90 self._length = length or self._stream.size()
91 self._rem_length = length
92 assert length and isinstance(length, (int, long))
93 assert length and length >= 0, length
94 self._chunk_size = chunk_size
95 if offset is not None:
96 self._stream.seek(offset)
98 def read(self, size=-1):
100 raise IOError(errno.EBADF, "read() without stream")
102 if self._rem_length == 0:
104 elif self._rem_length < 0:
107 rsize = self._rem_length
108 if size > 0 and size < rsize:
110 if self._chunk_size and self._chunk_size < rsize:
111 rsize = self._chunk_size
113 data = self._stream.read(rsize)
114 self._rem_length -= len(data)
122 res = self._stream.tell()
131 return self.read(65536)
133 def seek(self, pos, whence=os.SEEK_SET):
134 """ Seek, computing our limited range
136 if whence == os.SEEK_SET:
137 if pos < 0 or pos > self._length:
138 raise IOError(errno.EINVAL,"Cannot seek")
139 self._stream.seek(pos - self._offset)
140 self._rem_length = self._length - pos
141 elif whence == os.SEEK_CUR:
143 if pos > self._rem_length:
144 raise IOError(errno.EINVAL,"Cannot seek past end")
148 raise IOError(errno.EINVAL,"Cannot seek before start")
149 self._stream.seek(pos, os.SEEK_CUR)
150 self._rem_length -= pos
151 elif whence == os.SEEK_END:
153 raise IOError(errno.EINVAL,"Cannot seek past end")
155 if self._length + pos < 0:
156 raise IOError(errno.EINVAL,"Cannot seek before start")
157 newpos = self._offset + self._length + pos
158 self._stream.seek(newpos, os.SEEK_SET)
159 self._rem_length = 0 - pos
161 class openerp_dav_handler(dav_interface):
163 This class models a OpenERP interface for the DAV server
165 PROPS={'DAV:': dav_interface.PROPS['DAV:'],}
167 M_NS={ "DAV:" : dav_interface.M_NS['DAV:'],}
169 def __init__(self, parent, verbose=False):
172 self.baseuri = parent.baseuri
173 self.verbose = verbose
175 def get_propnames(self, uri):
177 self.parent.log_message('get propnames: %s' % uri)
178 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
181 # TODO: maybe limit props for databases..?
183 node = self.uri2object(cr, uid, pool, uri2)
185 props = dict_merge2(props, node.get_dav_props(cr))
189 def _try_function(self, funct, args, opname='run function', cr=None,
190 default_exc=DAV_Forbidden):
191 """ Try to run a function, and properly convert exceptions to DAV ones.
193 @objname the name of the operation being performed
194 @param cr if given, the cursor to close at exceptions
202 except NotImplementedError, e:
205 self.parent.log_error("Cannot %s: %s", opname, str(e))
206 self.parent.log_message("Exc: %s",traceback.format_exc())
207 # see par 9.3.1 of rfc
208 raise DAV_Error(403, str(e) or 'Not supported at this path')
209 except EnvironmentError, err:
212 self.parent.log_error("Cannot %s: %s", opname, err.strerror)
213 self.parent.log_message("Exc: %s",traceback.format_exc())
214 raise default_exc(err.strerror)
218 self.parent.log_error("Cannot %s: %s", opname, str(e))
219 self.parent.log_message("Exc: %s",traceback.format_exc())
220 raise default_exc("Operation failed")
222 def _get_dav_lockdiscovery(self, uri):
223 """ We raise that so that the node API is used """
226 def _get_dav_supportedlock(self, uri):
227 """ We raise that so that the node API is used """
230 def match_prop(self, uri, match, ns, propname):
231 if self.M_NS.has_key(ns):
232 return match == dav_interface.get_prop(self, uri, ns, propname)
233 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
237 node = self.uri2object(cr, uid, pool, uri2)
241 res = node.match_dav_eprop(cr, match, ns, propname)
245 def prep_http_options(self, uri, opts):
246 """see HttpOptions._prep_OPTIONS """
247 self.parent.log_message('get options: %s' % uri)
248 cr, uid, pool, dbname, uri2 = self.get_cr(uri, allow_last=True)
253 node = self.uri2object(cr, uid, pool, uri2[:])
259 if hasattr(node, 'http_options'):
261 for key, val in node.http_options.items():
262 if isinstance(val, basestring):
265 ret[key] = ret[key][:] # copy the orig. array
270 self.parent.log_message('options: %s' % ret)
276 def reduce_useragent(self):
277 ua = self.parent.headers.get('User-Agent', False)
281 ctx['DAV-client'] = 'iPhone'
282 elif 'Konqueror' in ua:
283 ctx['DAV-client'] = 'GroupDAV'
286 def get_prop(self, uri, ns, propname):
287 """ return the value of a given property
289 uri -- uri of the object to get the property of
290 ns -- namespace of the property
291 pname -- name of the property
293 if self.M_NS.has_key(ns):
295 # if it's not in the interface class, a "DAV:" property
296 # may be at the node class. So shouldn't give up early.
297 return dav_interface.get_prop(self, uri, ns, propname)
300 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
305 node = self.uri2object(cr, uid, pool, uri2)
308 res = node.get_dav_eprop(cr, ns, propname)
313 def get_db(self, uri, rest_ret=False, allow_last=False):
314 """Parse the uri and get the dbname and the rest.
315 Db name should be the first component in the unix-like
316 path supplied in uri.
318 @param rest_ret Instead of the db_name, return (db_name, rest),
319 where rest is the remaining path
320 @param allow_last If the dbname is the last component in the
321 path, allow it to be resolved. The default False value means
322 we will not attempt to use the db, unless there is more
325 @return db_name or (dbname, rest) depending on rest_ret,
326 will return dbname=False when component is not found.
329 uri2 = self.uri2local(uri)
330 if uri2.startswith('/'):
332 names=uri2.split('/',1)
339 if len(names) > ll and names[0]:
350 def urijoin(self,*ajoin):
351 """ Return the base URI of this request, or even join it with the
354 return self.parent.get_baseuri(self) + '/'.join(ajoin)
358 s = netsvc.ExportService.getService('db')
359 result = s.exp_list()
361 for db_name in result:
364 db = pooler.get_db_only(db_name)
366 cr.execute("SELECT id FROM ir_module_module WHERE name = 'document' AND state='installed' ")
369 self.db_name_list.append(db_name)
371 self.parent.log_error("Exception in db list: %s" % e)
375 return self.db_name_list
377 def get_childs(self,uri, filters=None):
378 """ return the child objects as self.baseuris for the given URI """
379 self.parent.log_message('get children: %s' % uri)
380 cr, uid, pool, dbname, uri2 = self.get_cr(uri, allow_last=True)
384 res = map(lambda x: self.urijoin(x), self.db_list())
387 node = self.uri2object(cr, uid, pool, uri2[:])
391 raise DAV_NotFound2(uri2)
393 fp = node.full_path()
396 self.parent.log_message('children for: %s' % fp)
401 domain = node.get_domain(cr, filters)
403 if hasattr(filters, 'getElementsByTagNameNS'):
404 hrefs = filters.getElementsByTagNameNS('DAV:', 'href')
406 ul = self.parent.davpath + self.uri2local(uri)
409 for tx in hr.childNodes:
410 if tx.nodeType == hr.TEXT_NODE:
412 if not turi.startswith('/'):
413 # it may be an absolute URL, decode to the
414 # relative part, because ul is relative, anyway
415 uparts=urlparse.urlparse(turi)
418 turi += ';' + uparts[3]
419 if turi.startswith(ul):
420 result.append( turi[len(self.parent.davpath):])
422 self.parent.log_error("ignore href %s because it is not under request path %s", turi, ul)
424 # We don't want to continue with the children found below
425 # Note the exceptions and that 'finally' will close the
427 for d in node.children(cr, domain):
428 self.parent.log_message('child: %s' % d.path)
430 result.append( self.urijoin(dbname,fp,d.path) )
432 result.append( self.urijoin(dbname,d.path) )
436 self.parent.log_error("cannot get_children: "+ str(e))
442 def uri2local(self, uri):
443 uparts=urlparse.urlparse(uri)
446 reluri += ';'+uparts[3]
447 if reluri and reluri[-1]=="/":
452 # pos: -1 to get the parent of the uri
454 def get_cr(self, uri, allow_last=False):
455 """ Split the uri, grab a cursor for that db
457 pdb = self.parent.auth_proxy.last_auth
458 dbname, uri2 = self.get_db(uri, rest_ret=True, allow_last=allow_last)
459 uri2 = (uri2 and uri2.split('/')) or []
461 return None, None, None, False, uri2
462 # if dbname was in our uri, we should have authenticated
464 assert pdb == dbname, " %s != %s" %(pdb, dbname)
465 res = self.parent.auth_proxy.auth_creds.get(dbname, False)
467 self.parent.auth_proxy.checkRequest(self.parent, uri, dbname)
468 res = self.parent.auth_proxy.auth_creds[dbname]
469 user, passwd, dbn2, uid = res
470 db,pool = pooler.get_db_and_pool(dbname)
472 return cr, uid, pool, dbname, uri2
474 def uri2object(self, cr, uid, pool, uri):
477 context = self.reduce_useragent()
478 return pool.get('document.directory').get_object(cr, uid, uri, context=context)
480 def get_data(self,uri, rrange=None):
481 self.parent.log_message('GET: %s' % uri)
482 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
486 node = self.uri2object(cr, uid, pool, uri2)
488 raise DAV_NotFound2(uri2)
489 # TODO: if node is a collection, for some specific set of
490 # clients ( web browsers; available in node context),
491 # we may return a pseydo-html page with the directory listing.
493 res = node.open_data(cr,'r')
495 assert isinstance(rrange, (tuple,list))
496 start, end = map(long, rrange)
500 if end and end < start:
501 self.parent.log_error("Invalid range for data: %s-%s" %(start, end))
502 raise DAV_Error(416, "Invalid range for data")
504 if end >= res.size():
505 raise DAV_Error(416, "Requested data exceeds available size")
506 length = (end + 1) - start
508 length = res.size() - start
509 res = BoundStream2(res, offset=start, length=length)
512 # for the collections that return this error, the DAV standard
513 # says we'd better just return 200 OK with empty data
515 except IndexError,e :
516 self.parent.log_error("GET IndexError: %s", str(e))
517 raise DAV_NotFound2(uri2)
520 self.parent.log_error("GET exception: %s",str(e))
521 self.parent.log_message("Exc: %s", traceback.format_exc())
528 def _get_dav_resourcetype(self, uri):
529 """ return type of object """
530 self.parent.log_message('get RT: %s' % uri)
531 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
535 node = self.uri2object(cr, uid, pool, uri2)
537 raise DAV_NotFound2(uri2)
539 return node.get_dav_resourcetype(cr)
540 except NotImplementedError:
541 if node.type in ('collection','database'):
542 return ('collection', 'DAV:')
547 def _get_dav_displayname(self,uri):
548 self.parent.log_message('get DN: %s' % uri)
549 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
552 # at root, dbname, just return the last component
554 if uri2 and len(uri2) < 2:
557 node = self.uri2object(cr, uid, pool, uri2)
560 raise DAV_NotFound2(uri2)
562 return node.displayname
565 def _get_dav_getcontentlength(self, uri):
566 """ return the content length of an object """
567 self.parent.log_message('get length: %s' % uri)
569 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
573 node = self.uri2object(cr, uid, pool, uri2)
576 raise DAV_NotFound2(uri2)
577 result = node.content_length or 0
582 def _get_dav_getetag(self,uri):
583 """ return the ETag of an object """
584 self.parent.log_message('get etag: %s' % uri)
586 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
590 node = self.uri2object(cr, uid, pool, uri2)
593 raise DAV_NotFound2(uri2)
594 result = self._try_function(node.get_etag ,(cr,), "etag %s" %uri, cr=cr)
599 def get_lastmodified(self, uri):
600 """ return the last modified date of the object """
601 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
605 node = self.uri2object(cr, uid, pool, uri2)
607 raise DAV_NotFound2(uri2)
608 return _str2time(node.write_date)
612 def _get_dav_getlastmodified(self,uri):
613 """ return the last modified date of a resource
615 d=self.get_lastmodified(uri)
616 # format it. Note that we explicitly set the day, month names from
617 # an array, so that strftime() doesn't use its own locale-aware
620 return time.strftime("%%s, %d %%s %Y %H:%M:%S GMT", gmt ) % \
621 (day_names[gmt.tm_wday], month_names[gmt.tm_mon])
624 def get_creationdate(self, uri):
625 """ return the last modified date of the object """
626 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
630 node = self.uri2object(cr, uid, pool, uri2)
632 raise DAV_NotFound2(uri2)
634 return _str2time(node.create_date)
639 def _get_dav_getcontenttype(self,uri):
640 self.parent.log_message('get contenttype: %s' % uri)
641 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
644 return 'httpd/unix-directory'
646 node = self.uri2object(cr, uid, pool, uri2)
648 raise DAV_NotFound2(uri2)
649 result = str(node.mimetype)
651 #raise DAV_NotFound, 'Could not find %s' % path
656 """ create a new collection
657 see par. 9.3 of rfc4918
659 self.parent.log_message('MKCOL: %s' % uri)
660 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
663 raise DAV_Error(409, "Cannot create nameless collection")
667 node = self.uri2object(cr,uid,pool, uri2[:-1])
670 raise DAV_Error(409, "Parent path %s does not exist" % uri2[:-1])
671 nc = node.child(cr, uri2[-1])
674 raise DAV_Error(405, "Path already exists")
675 self._try_function(node.create_child_collection, (cr, uri2[-1]),
676 "create col %s" % uri2[-1], cr=cr)
681 def put(self, uri, data, content_type=None):
682 """ put the object into the filesystem """
683 self.parent.log_message('Putting %s (%d), %s'%( misc.ustr(uri), data and len(data) or 0, content_type))
684 cr, uid, pool,dbname, uri2 = self.get_cr(uri)
689 node = self.uri2object(cr, uid, pool, uri2[:])
693 objname = misc.ustr(uri2[-1])
697 dir_node = self.uri2object(cr, uid, pool, uri2[:-1])
700 raise DAV_NotFound('Parent folder not found')
702 newchild = self._try_function(dir_node.create_child, (cr, objname, data),
703 "create %s" % objname, cr=cr)
707 raise DAV_Error(400, "Failed to create resource")
709 uparts=urlparse.urlparse(uri)
710 fileloc = '/'.join(newchild.full_path())
711 if isinstance(fileloc, unicode):
712 fileloc = fileloc.encode('utf-8')
713 # the uri we get is a mangled one, where the davpath has been removed
714 davpath = self.parent.get_davpath()
716 surl = '%s://%s' % (uparts[0], uparts[1])
717 uloc = urllib.quote(fileloc)
719 if uri != ('/'+uloc) and uri != (surl + '/' + uloc):
720 hurl = '%s%s/%s/%s' %(surl, davpath, dbname, uloc)
723 etag = str(newchild.get_etag(cr))
725 self.parent.log_error("Cannot get etag for node: %s" % e)
726 ret = (str(hurl), etag)
728 self._try_function(node.set_data, (cr, data), "save %s" % objname, cr=cr)
735 """ delete a collection """
736 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
741 node = self.uri2object(cr, uid, pool, uri2)
742 self._try_function(node.rmcol, (cr,), "rmcol %s" % uri, cr=cr)
749 cr, uid, pool,dbname, uri2 = self.get_cr(uri)
753 node = self.uri2object(cr, uid, pool, uri2)
754 res = self._try_function(node.rm, (cr,), "rm %s" % uri, cr=cr)
757 raise OSError(1, 'Operation not permited.')
762 ### DELETE handlers (examples)
763 ### (we use the predefined methods in davcmd instead of doing
767 def delone(self, uri):
768 """ delete a single resource
770 You have to return a result dict of the form
772 or None if everything's ok
775 if uri[-1]=='/':uri=uri[:-1]
777 # parent='/'.join(uri.split('/')[:-1])
780 def deltree(self, uri):
781 """ delete a collection
783 You have to return a result dict of the form
785 or None if everything's ok
787 if uri[-1]=='/':uri=uri[:-1]
788 res=deltree(self, uri)
789 # parent='/'.join(uri.split('/')[:-1])
794 ### MOVE handlers (examples)
797 def moveone(self, src, dst, overwrite):
798 """ move one resource with Depth=0
800 an alternative implementation would be
805 r=os.system("rm -f '%s'" %dst)
807 r=os.system("mv '%s' '%s'" %(src,dst))
811 (untested!). This would not use the davcmd functions
812 and thus can only detect errors directly on the root node.
814 res=moveone(self, src, dst, overwrite)
817 def movetree(self, src, dst, overwrite):
818 """ move a collection with Depth=infinity
820 an alternative implementation would be
825 r=os.system("rm -rf '%s'" %dst)
827 r=os.system("mv '%s' '%s'" %(src,dst))
831 (untested!). This would not use the davcmd functions
832 and thus can only detect errors directly on the root node"""
834 res=movetree(self, src, dst, overwrite)
841 def copyone(self, src, dst, overwrite):
842 """ copy one resource with Depth=0
844 an alternative implementation would be
849 r=os.system("rm -f '%s'" %dst)
851 r=os.system("cp '%s' '%s'" %(src,dst))
855 (untested!). This would not use the davcmd functions
856 and thus can only detect errors directly on the root node.
858 res=copyone(self, src, dst, overwrite)
861 def copytree(self, src, dst, overwrite):
862 """ copy a collection with Depth=infinity
864 an alternative implementation would be
869 r=os.system("rm -rf '%s'" %dst)
871 r=os.system("cp -r '%s' '%s'" %(src,dst))
875 (untested!). This would not use the davcmd functions
876 and thus can only detect errors directly on the root node"""
877 res=copytree(self, src, dst, overwrite)
882 ### This methods actually copy something. low-level
883 ### They are called by the davcmd utility functions
884 ### copytree and copyone (not the above!)
885 ### Look in davcmd.py for further details.
888 def copy(self, src, dst):
889 src=urllib.unquote(src)
890 dst=urllib.unquote(dst)
891 ct = self._get_dav_getcontenttype(src)
892 data = self.get_data(src)
893 self.put(dst, data, ct)
896 def copycol(self, src, dst):
897 """ copy a collection.
899 As this is not recursive (the davserver recurses itself)
900 we will only create a new directory here. For some more
901 advanced systems we might also have to copy properties from
902 the source to the destination.
904 return self.mkcol(dst)
907 def exists(self, uri):
908 """ test if a resource exists """
910 cr, uid, pool,dbname, uri2 = self.get_cr(uri)
915 node = self.uri2object(cr, uid, pool, uri2)
923 def unlock(self, uri, token):
924 """ Unlock a resource from that token
926 @return True if unlocked, False if no lock existed, Exceptions
928 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
933 node = self.uri2object(cr, uid, pool, uri2)
935 node_fn = node.dav_unlock
936 except AttributeError:
937 # perhaps the node doesn't support locks
939 raise DAV_Error(400, 'No locks for this resource')
941 res = self._try_function(node_fn, (cr, token), "unlock %s" % uri, cr=cr)
946 def lock(self, uri, lock_data):
947 """ Lock (may create) resource.
948 Data is a dict, may contain:
949 depth, token, refresh, lockscope, locktype, owner
951 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
958 node = self.uri2object(cr, uid, pool, uri2[:])
962 objname = misc.ustr(uri2[-1])
965 dir_node = self.uri2object(cr, uid, pool, uri2[:-1])
968 raise DAV_NotFound('Parent folder not found')
970 # We create a new node (file) but with empty data=None,
971 # as in RFC4918 p. 9.10.4
972 node = self._try_function(dir_node.create_child, (cr, objname, None),
973 "create %s" % objname, cr=cr)
977 raise DAV_Error(400, "Failed to create resource")
982 node_fn = node.dav_lock
983 except AttributeError:
984 # perhaps the node doesn't support locks
986 raise DAV_Error(400, 'No locks for this resource')
988 # Obtain the lock on the node
989 lres, pid, token = self._try_function(node_fn, (cr, lock_data), "lock %s" % objname, cr=cr)
994 raise DAV_Error(423, "Resource already locked")
996 assert isinstance(lres, list), 'lres: %s' % repr(lres)
999 data = mk_lock_response(self, uri, lres)
1006 return created, data, token
1008 @memoize(CACHE_SIZE)
1009 def is_collection(self, uri):
1010 """ test if the given uri is a collection """
1011 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
1015 node = self.uri2object(cr,uid,pool, uri2)
1017 raise DAV_NotFound2(uri2)
1018 if node.type in ('collection','database'):