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 tools.dict_tools import dict_merge2
41 from document.dict_tools import dict_merge2
45 #hack for urlparse: add webdav in the net protocols
46 urlparse.uses_netloc.append('webdav')
47 urlparse.uses_netloc.append('webdavs')
49 day_names = { 0: 'Mon', 1: 'Tue' , 2: 'Wed', 3: 'Thu', 4: 'Fri', 5: 'Sat', 6: 'Sun' }
50 month_names = { 1: 'Jan', 2: 'Feb', 3: 'Mar', 4: 'Apr', 5: 'May', 6: 'Jun',
51 7: 'Jul', 8: 'Aug', 9: 'Sep', 10: 'Oct', 11: 'Nov', 12: 'Dec' }
53 class DAV_NotFound2(DAV_NotFound):
54 """404 exception, that accepts our list uris
56 def __init__(self, *args):
57 if len(args) and isinstance(args[0], (tuple, list)):
58 path = ''.join([ '/' + x for x in args[0]])
60 DAV_NotFound.__init__(self, *args)
64 """ Convert a string with time representation (from db) into time (float)
69 if isinstance(cre, basestring) and '.' in cre:
71 frac = float(cre[fdot:])
73 return time.mktime(time.strptime(cre,'%Y-%m-%d %H:%M:%S')) + frac
75 class BoundStream2(object):
76 """Wraps around a seekable buffer, reads a determined range of data
78 Note that the supplied stream object MUST support a size() which
79 should return its data length (in bytes).
81 A variation of the class in websrv_lib.py
84 def __init__(self, stream, offset=None, length=None, chunk_size=None):
86 self._offset = offset or 0
87 self._length = length or self._stream.size()
88 self._rem_length = length
89 assert length and isinstance(length, (int, long))
90 assert length and length >= 0, length
91 self._chunk_size = chunk_size
92 if offset is not None:
93 self._stream.seek(offset)
95 def read(self, size=-1):
97 raise IOError(errno.EBADF, "read() without stream")
99 if self._rem_length == 0:
101 elif self._rem_length < 0:
104 rsize = self._rem_length
105 if size > 0 and size < rsize:
107 if self._chunk_size and self._chunk_size < rsize:
108 rsize = self._chunk_size
110 data = self._stream.read(rsize)
111 self._rem_length -= len(data)
119 res = self._stream.tell()
128 return self.read(65536)
130 def seek(self, pos, whence=os.SEEK_SET):
131 """ Seek, computing our limited range
133 if whence == os.SEEK_SET:
134 if pos < 0 or pos > self._length:
135 raise IOError(errno.EINVAL,"Cannot seek")
136 self._stream.seek(pos - self._offset)
137 self._rem_length = self._length - pos
138 elif whence == os.SEEK_CUR:
140 if pos > self._rem_length:
141 raise IOError(errno.EINVAL,"Cannot seek past end")
145 raise IOError(errno.EINVAL,"Cannot seek before start")
146 self._stream.seek(pos, os.SEEK_CUR)
147 self._rem_length -= pos
148 elif whence == os.SEEK_END:
150 raise IOError(errno.EINVAL,"Cannot seek past end")
152 if self._length + pos < 0:
153 raise IOError(errno.EINVAL,"Cannot seek before start")
154 newpos = self._offset + self._length + pos
155 self._stream.seek(newpos, os.SEEK_SET)
156 self._rem_length = 0 - pos
158 class openerp_dav_handler(dav_interface):
160 This class models a OpenERP interface for the DAV server
162 PROPS={'DAV:': dav_interface.PROPS['DAV:'],}
164 M_NS={ "DAV:" : dav_interface.M_NS['DAV:'],}
166 def __init__(self, parent, verbose=False):
169 self.baseuri = parent.baseuri
170 self.verbose = verbose
172 def get_propnames(self, uri):
174 self.parent.log_message('get propnames: %s' % uri)
175 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
178 # TODO: maybe limit props for databases..?
180 node = self.uri2object(cr, uid, pool, uri2)
182 props = dict_merge2(props, node.get_dav_props(cr))
186 def _try_function(self, funct, args, opname='run function', cr=None,
187 default_exc=DAV_Forbidden):
188 """ Try to run a function, and properly convert exceptions to DAV ones.
190 @objname the name of the operation being performed
191 @param cr if given, the cursor to close at exceptions
199 except NotImplementedError, e:
202 self.parent.log_error("Cannot %s: %s", opname, str(e))
203 self.parent.log_message("Exc: %s",traceback.format_exc())
204 # see par 9.3.1 of rfc
205 raise DAV_Error(403, str(e) or 'Not supported at this path')
206 except EnvironmentError, err:
209 self.parent.log_error("Cannot %s: %s", opname, err.strerror)
210 self.parent.log_message("Exc: %s",traceback.format_exc())
211 raise default_exc(err.strerror)
215 self.parent.log_error("Cannot %s: %s", opname, str(e))
216 self.parent.log_message("Exc: %s",traceback.format_exc())
217 raise default_exc("Operation failed")
219 #def _get_dav_lockdiscovery(self, uri):
222 #def A_get_dav_supportedlock(self, uri):
225 def match_prop(self, uri, match, ns, propname):
226 if self.M_NS.has_key(ns):
227 return match == dav_interface.get_prop(self, uri, ns, propname)
228 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
232 node = self.uri2object(cr, uid, pool, uri2)
236 res = node.match_dav_eprop(cr, match, ns, propname)
240 def prep_http_options(self, uri, opts):
241 """see HttpOptions._prep_OPTIONS """
242 self.parent.log_message('get options: %s' % uri)
243 cr, uid, pool, dbname, uri2 = self.get_cr(uri, allow_last=True)
248 node = self.uri2object(cr, uid, pool, uri2[:])
254 if hasattr(node, 'http_options'):
256 for key, val in node.http_options.items():
257 if isinstance(val, basestring):
260 ret[key] = ret[key][:] # copy the orig. array
265 self.parent.log_message('options: %s' % ret)
271 def reduce_useragent(self):
272 ua = self.parent.headers.get('User-Agent', False)
276 ctx['DAV-client'] = 'iPhone'
277 elif 'Konqueror' in ua:
278 ctx['DAV-client'] = 'GroupDAV'
281 def get_prop(self, uri, ns, propname):
282 """ return the value of a given property
284 uri -- uri of the object to get the property of
285 ns -- namespace of the property
286 pname -- name of the property
288 if self.M_NS.has_key(ns):
290 # if it's not in the interface class, a "DAV:" property
291 # may be at the node class. So shouldn't give up early.
292 return dav_interface.get_prop(self, uri, ns, propname)
295 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
300 node = self.uri2object(cr, uid, pool, uri2)
303 res = node.get_dav_eprop(cr, ns, propname)
308 def get_db(self, uri, rest_ret=False, allow_last=False):
309 """Parse the uri and get the dbname and the rest.
310 Db name should be the first component in the unix-like
311 path supplied in uri.
313 @param rest_ret Instead of the db_name, return (db_name, rest),
314 where rest is the remaining path
315 @param allow_last If the dbname is the last component in the
316 path, allow it to be resolved. The default False value means
317 we will not attempt to use the db, unless there is more
320 @return db_name or (dbname, rest) depending on rest_ret,
321 will return dbname=False when component is not found.
324 uri2 = self.uri2local(uri)
325 if uri2.startswith('/'):
327 names=uri2.split('/',1)
334 if len(names) > ll and names[0]:
345 def urijoin(self,*ajoin):
346 """ Return the base URI of this request, or even join it with the
349 return self.baseuri+ '/'.join(ajoin)
353 s = netsvc.ExportService.getService('db')
354 result = s.exp_list()
356 for db_name in result:
359 db = pooler.get_db_only(db_name)
361 cr.execute("SELECT id FROM ir_module_module WHERE name = 'document' AND state='installed' ")
364 self.db_name_list.append(db_name)
366 self.parent.log_error("Exception in db list: %s" % e)
370 return self.db_name_list
372 def get_childs(self, uri, filters=None):
373 """ return the child objects as self.baseuris for the given URI """
374 self.parent.log_message('get childs: %s' % uri)
375 cr, uid, pool, dbname, uri2 = self.get_cr(uri, allow_last=True)
379 res = map(lambda x: self.urijoin(x), self.db_list())
382 node = self.uri2object(cr, uid, pool, uri2[:])
386 raise DAV_NotFound2(uri2)
388 fp = node.full_path()
391 self.parent.log_message('childs for: %s' % fp)
396 domain = node.get_domain(cr, filters)
398 if hasattr(filters, 'getElementsByTagNameNS'):
399 hrefs = filters.getElementsByTagNameNS('DAV:', 'href')
401 ul = self.parent.davpath + self.uri2local(uri)
404 for tx in hr.childNodes:
405 if tx.nodeType == hr.TEXT_NODE:
407 if not turi.startswith('/'):
408 # it may be an absolute URL, decode to the
409 # relative part, because ul is relative, anyway
410 uparts=urlparse.urlparse(turi)
412 if turi.startswith(ul):
413 result.append( turi[len(self.parent.davpath):])
415 self.parent.log_error("ignore href %s because it is not under request path %s", turi, ul)
417 # We don't want to continue with the children found below
418 # Note the exceptions and that 'finally' will close the
420 for d in node.children(cr, domain):
421 self.parent.log_message('child: %s' % d.path)
423 result.append( self.urijoin(dbname,fp,d.path) )
425 result.append( self.urijoin(dbname,d.path) )
429 self.parent.log_error("cannot get_childs: "+ str(e))
435 def uri2local(self, uri):
436 uparts=urlparse.urlparse(uri)
438 if reluri and reluri[-1]=="/":
443 # pos: -1 to get the parent of the uri
445 def get_cr(self, uri, allow_last=False):
446 """ Split the uri, grab a cursor for that db
448 pdb = self.parent.auth_proxy.last_auth
449 dbname, uri2 = self.get_db(uri, rest_ret=True, allow_last=allow_last)
450 uri2 = (uri2 and uri2.split('/')) or []
452 return None, None, None, False, uri2
453 # if dbname was in our uri, we should have authenticated
455 assert pdb == dbname, " %s != %s" %(pdb, dbname)
456 res = self.parent.auth_proxy.auth_creds.get(dbname, False)
458 self.parent.auth_proxy.checkRequest(self.parent, uri, dbname)
459 res = self.parent.auth_proxy.auth_creds[dbname]
460 user, passwd, dbn2, uid = res
461 db,pool = pooler.get_db_and_pool(dbname)
463 return cr, uid, pool, dbname, uri2
465 def uri2object(self, cr, uid, pool, uri):
468 context = self.reduce_useragent()
469 return pool.get('document.directory').get_object(cr, uid, uri, context=context)
471 def get_data(self,uri, rrange=None):
472 self.parent.log_message('GET: %s' % uri)
473 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
477 node = self.uri2object(cr, uid, pool, uri2)
479 raise DAV_NotFound2(uri2)
480 # TODO: if node is a collection, for some specific set of
481 # clients ( web browsers; available in node context),
482 # we may return a pseydo-html page with the directory listing.
484 res = node.open_data(cr,'r')
486 assert isinstance(rrange, (tuple,list))
487 start, end = map(long, rrange)
491 if end and end < start:
492 self.parent.log_error("Invalid range for data: %s-%s" %(start, end))
493 raise DAV_Error(416, "Invalid range for data")
495 if end >= res.size():
496 raise DAV_Error(416, "Requested data exceeds available size")
497 length = (end + 1) - start
499 length = res.size() - start
500 res = BoundStream2(res, offset=start, length=length)
503 # for the collections that return this error, the DAV standard
504 # says we'd better just return 200 OK with empty data
506 except IndexError,e :
507 self.parent.log_error("GET IndexError: %s", str(e))
508 raise DAV_NotFound2(uri2)
511 self.parent.log_error("GET exception: %s",str(e))
512 self.parent.log_message("Exc: %s", traceback.format_exc())
519 def _get_dav_resourcetype(self, uri):
520 """ return type of object """
521 self.parent.log_message('get RT: %s' % uri)
522 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
526 node = self.uri2object(cr, uid, pool, uri2)
528 raise DAV_NotFound2(uri2)
530 return node.get_dav_resourcetype(cr)
531 except NotImplementedError:
532 if node.type in ('collection','database'):
533 return ('collection', 'DAV:')
538 def _get_dav_displayname(self,uri):
539 self.parent.log_message('get DN: %s' % uri)
540 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
543 # at root, dbname, just return the last component
545 if uri2 and len(uri2) < 2:
548 node = self.uri2object(cr, uid, pool, uri2)
551 raise DAV_NotFound2(uri2)
553 return node.displayname
556 def _get_dav_getcontentlength(self, uri):
557 """ return the content length of an object """
558 self.parent.log_message('get length: %s' % uri)
560 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
564 node = self.uri2object(cr, uid, pool, uri2)
567 raise DAV_NotFound2(uri2)
568 result = node.content_length or 0
573 def _get_dav_getetag(self,uri):
574 """ return the ETag of an object """
575 self.parent.log_message('get etag: %s' % uri)
577 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
581 node = self.uri2object(cr, uid, pool, uri2)
584 raise DAV_NotFound2(uri2)
585 result = self._try_function(node.get_etag ,(cr,), "etag %s" %uri, cr=cr)
590 def get_lastmodified(self, uri):
591 """ return the last modified date of the object """
592 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
596 node = self.uri2object(cr, uid, pool, uri2)
598 raise DAV_NotFound2(uri2)
599 return _str2time(node.write_date)
603 def _get_dav_getlastmodified(self,uri):
604 """ return the last modified date of a resource
606 d=self.get_lastmodified(uri)
607 # format it. Note that we explicitly set the day, month names from
608 # an array, so that strftime() doesn't use its own locale-aware
611 return time.strftime("%%s, %d %%s %Y %H:%M:%S GMT", gmt ) % \
612 (day_names[gmt.tm_wday], month_names[gmt.tm_mon])
615 def get_creationdate(self, uri):
616 """ return the last modified date of the object """
617 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
621 node = self.uri2object(cr, uid, pool, uri2)
623 raise DAV_NotFound2(uri2)
625 return _str2time(node.create_date)
630 def _get_dav_getcontenttype(self,uri):
631 self.parent.log_message('get contenttype: %s' % uri)
632 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
635 return 'httpd/unix-directory'
637 node = self.uri2object(cr, uid, pool, uri2)
639 raise DAV_NotFound2(uri2)
640 result = str(node.mimetype)
642 #raise DAV_NotFound, 'Could not find %s' % path
647 """ create a new collection
648 see par. 9.3 of rfc4918
650 self.parent.log_message('MKCOL: %s' % uri)
651 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
654 raise DAV_Error(409, "Cannot create nameless collection")
658 node = self.uri2object(cr,uid,pool, uri2[:-1])
661 raise DAV_Error(409, "Parent path %s does not exist" % uri2[:-1])
662 nc = node.child(cr, uri2[-1])
665 raise DAV_Error(405, "Path already exists")
666 self._try_function(node.create_child_collection, (cr, uri2[-1]),
667 "create col %s" % uri2[-1], cr=cr)
672 def put(self, uri, data, content_type=None):
673 """ put the object into the filesystem """
674 self.parent.log_message('Putting %s (%d), %s'%( misc.ustr(uri), data and len(data) or 0, content_type))
675 cr, uid, pool,dbname, uri2 = self.get_cr(uri)
680 node = self.uri2object(cr, uid, pool, uri2[:])
688 dir_node = self.uri2object(cr, uid, pool, uri2[:-1])
691 raise DAV_NotFound('Parent folder not found')
693 newchild = self._try_function(dir_node.create_child, (cr, objname, data),
694 "create %s" % objname, cr=cr)
698 raise DAV_Error(400, "Failed to create resource")
700 uparts=urlparse.urlparse(uri)
701 fileloc = '/'.join(newchild.full_path())
702 if isinstance(fileloc, unicode):
703 fileloc = fileloc.encode('utf-8')
704 # the uri we get is a mangled one, where the davpath has been removed
705 davpath = self.parent.get_davpath()
707 surl = '%s://%s' % (uparts[0], uparts[1])
708 uloc = urllib.quote(fileloc)
710 if uri != ('/'+uloc) and uri != (surl + '/' + uloc):
711 hurl = '%s%s/%s/%s' %(surl, davpath, dbname, uloc)
714 etag = str(newchild.get_etag(cr))
716 self.parent.log_error("Cannot get etag for node: %s" % e)
719 self._try_function(node.set_data, (cr, data), "save %s" % objname, cr=cr)
726 """ delete a collection """
727 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
732 node = self.uri2object(cr, uid, pool, uri2)
733 self._try_function(node.rmcol, (cr,), "rmcol %s" % uri, cr=cr)
740 cr, uid, pool,dbname, uri2 = self.get_cr(uri)
744 node = self.uri2object(cr, uid, pool, uri2)
745 res = self._try_function(node.rm, (cr,), "rm %s" % uri, cr=cr)
748 raise OSError(1, 'Operation not permited.')
753 ### DELETE handlers (examples)
754 ### (we use the predefined methods in davcmd instead of doing
758 def delone(self, uri):
759 """ delete a single resource
761 You have to return a result dict of the form
763 or None if everything's ok
766 if uri[-1]=='/':uri=uri[:-1]
768 # parent='/'.join(uri.split('/')[:-1])
771 def deltree(self, uri):
772 """ delete a collection
774 You have to return a result dict of the form
776 or None if everything's ok
778 if uri[-1]=='/':uri=uri[:-1]
779 res=deltree(self, uri)
780 # parent='/'.join(uri.split('/')[:-1])
785 ### MOVE handlers (examples)
788 def moveone(self, src, dst, overwrite):
789 """ move one resource with Depth=0
791 an alternative implementation would be
796 r=os.system("rm -f '%s'" %dst)
798 r=os.system("mv '%s' '%s'" %(src,dst))
802 (untested!). This would not use the davcmd functions
803 and thus can only detect errors directly on the root node.
805 res=moveone(self, src, dst, overwrite)
808 def movetree(self, src, dst, overwrite):
809 """ move a collection with Depth=infinity
811 an alternative implementation would be
816 r=os.system("rm -rf '%s'" %dst)
818 r=os.system("mv '%s' '%s'" %(src,dst))
822 (untested!). This would not use the davcmd functions
823 and thus can only detect errors directly on the root node"""
825 res=movetree(self, src, dst, overwrite)
832 def copyone(self, src, dst, overwrite):
833 """ copy one resource with Depth=0
835 an alternative implementation would be
840 r=os.system("rm -f '%s'" %dst)
842 r=os.system("cp '%s' '%s'" %(src,dst))
846 (untested!). This would not use the davcmd functions
847 and thus can only detect errors directly on the root node.
849 res=copyone(self, src, dst, overwrite)
852 def copytree(self, src, dst, overwrite):
853 """ copy a collection with Depth=infinity
855 an alternative implementation would be
860 r=os.system("rm -rf '%s'" %dst)
862 r=os.system("cp -r '%s' '%s'" %(src,dst))
866 (untested!). This would not use the davcmd functions
867 and thus can only detect errors directly on the root node"""
868 res=copytree(self, src, dst, overwrite)
873 ### This methods actually copy something. low-level
874 ### They are called by the davcmd utility functions
875 ### copytree and copyone (not the above!)
876 ### Look in davcmd.py for further details.
879 def copy(self, src, dst):
880 src=urllib.unquote(src)
881 dst=urllib.unquote(dst)
882 ct = self._get_dav_getcontenttype(src)
883 data = self.get_data(src)
884 self.put(dst, data, ct)
887 def copycol(self, src, dst):
888 """ copy a collection.
890 As this is not recursive (the davserver recurses itself)
891 we will only create a new directory here. For some more
892 advanced systems we might also have to copy properties from
893 the source to the destination.
895 return self.mkcol(dst)
898 def exists(self, uri):
899 """ test if a resource exists """
901 cr, uid, pool,dbname, uri2 = self.get_cr(uri)
906 node = self.uri2object(cr, uid, pool, uri2)
915 def is_collection(self, uri):
916 """ test if the given uri is a collection """
917 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
921 node = self.uri2object(cr,uid,pool, uri2)
923 raise DAV_NotFound2(uri2)
924 if node.type in ('collection','database'):