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 ##############################################################################
31 from DAV.constants import COLLECTION #, OBJECT
32 from DAV.errors import DAV_Error, DAV_Forbidden, DAV_NotFound
33 from DAV.iface import dav_interface
36 from DAV.davcmd import copyone, copytree, moveone, movetree, delone, deltree
37 from cache import memoize
38 from tools import misc
40 from webdav import mk_lock_response
43 from tools.dict_tools import dict_merge2
45 from document.dict_tools import dict_merge2
49 #hack for urlparse: add webdav in the net protocols
50 urlparse.uses_netloc.append('webdav')
51 urlparse.uses_netloc.append('webdavs')
53 day_names = { 0: 'Mon', 1: 'Tue' , 2: 'Wed', 3: 'Thu', 4: 'Fri', 5: 'Sat', 6: 'Sun' }
54 month_names = { 1: 'Jan', 2: 'Feb', 3: 'Mar', 4: 'Apr', 5: 'May', 6: 'Jun',
55 7: 'Jul', 8: 'Aug', 9: 'Sep', 10: 'Oct', 11: 'Nov', 12: 'Dec' }
57 class DAV_NotFound2(DAV_NotFound):
58 """404 exception, that accepts our list uris
60 def __init__(self, *args):
61 if len(args) and isinstance(args[0], (tuple, list)):
62 path = ''.join([ '/' + x for x in args[0]])
64 DAV_NotFound.__init__(self, *args)
68 """ Convert a string with time representation (from db) into time (float)
73 if isinstance(cre, basestring) and '.' in cre:
75 frac = float(cre[fdot:])
77 return time.mktime(time.strptime(cre,'%Y-%m-%d %H:%M:%S')) + frac
79 class BoundStream2(object):
80 """Wraps around a seekable buffer, reads a determined range of data
82 Note that the supplied stream object MUST support a size() which
83 should return its data length (in bytes).
85 A variation of the class in websrv_lib.py
88 def __init__(self, stream, offset=None, length=None, chunk_size=None):
90 self._offset = offset or 0
91 self._length = length or self._stream.size()
92 self._rem_length = length
93 assert length and isinstance(length, (int, long))
94 assert length and length >= 0, length
95 self._chunk_size = chunk_size
96 if offset is not None:
97 self._stream.seek(offset)
99 def read(self, size=-1):
101 raise IOError(errno.EBADF, "read() without stream.")
103 if self._rem_length == 0:
105 elif self._rem_length < 0:
108 rsize = self._rem_length
109 if size > 0 and size < rsize:
111 if self._chunk_size and self._chunk_size < rsize:
112 rsize = self._chunk_size
114 data = self._stream.read(rsize)
115 self._rem_length -= len(data)
123 res = self._stream.tell()
132 return self.read(65536)
134 def seek(self, pos, whence=os.SEEK_SET):
135 """ Seek, computing our limited range
137 if whence == os.SEEK_SET:
138 if pos < 0 or pos > self._length:
139 raise IOError(errno.EINVAL,"Cannot seek.")
140 self._stream.seek(pos - self._offset)
141 self._rem_length = self._length - pos
142 elif whence == os.SEEK_CUR:
144 if pos > self._rem_length:
145 raise IOError(errno.EINVAL,"Cannot seek past end.")
149 raise IOError(errno.EINVAL,"Cannot seek before start.")
150 self._stream.seek(pos, os.SEEK_CUR)
151 self._rem_length -= pos
152 elif whence == os.SEEK_END:
154 raise IOError(errno.EINVAL,"Cannot seek past end.")
156 if self._length + pos < 0:
157 raise IOError(errno.EINVAL,"Cannot seek before start.")
158 newpos = self._offset + self._length + pos
159 self._stream.seek(newpos, os.SEEK_SET)
160 self._rem_length = 0 - pos
162 class openerp_dav_handler(dav_interface):
164 This class models a OpenERP interface for the DAV server
166 PROPS={'DAV:': dav_interface.PROPS['DAV:'],}
168 M_NS={ "DAV:" : dav_interface.M_NS['DAV:'],}
170 def __init__(self, parent, verbose=False):
173 self.baseuri = parent.baseuri
174 self.verbose = verbose
176 def get_propnames(self, uri):
178 self.parent.log_message('get propnames: %s' % uri)
179 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
182 # TODO: maybe limit props for databases..?
184 node = self.uri2object(cr, uid, pool, uri2)
186 props = dict_merge2(props, node.get_dav_props(cr))
190 def _try_function(self, funct, args, opname='run function', cr=None,
191 default_exc=DAV_Forbidden):
192 """ Try to run a function, and properly convert exceptions to DAV ones.
194 @objname the name of the operation being performed
195 @param cr if given, the cursor to close at exceptions
203 except NotImplementedError, e:
206 self.parent.log_error("Cannot %s: %s", opname, str(e))
207 self.parent.log_message("Exc: %s",traceback.format_exc())
208 # see par 9.3.1 of rfc
209 raise DAV_Error(403, str(e) or 'Not supported at this path.')
210 except EnvironmentError, err:
213 self.parent.log_error("Cannot %s: %s", opname, err.strerror)
214 self.parent.log_message("Exc: %s",traceback.format_exc())
215 raise default_exc(err.strerror)
219 self.parent.log_error("Cannot %s: %s", opname, str(e))
220 self.parent.log_message("Exc: %s",traceback.format_exc())
221 raise default_exc("Operation failed.")
223 def _get_dav_lockdiscovery(self, uri):
224 """ We raise that so that the node API is used """
227 def _get_dav_supportedlock(self, uri):
228 """ We raise that so that the node API is used """
231 def match_prop(self, uri, match, ns, propname):
232 if self.M_NS.has_key(ns):
233 return match == dav_interface.get_prop(self, uri, ns, propname)
234 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
238 node = self.uri2object(cr, uid, pool, uri2)
242 res = node.match_dav_eprop(cr, match, ns, propname)
246 def prep_http_options(self, uri, opts):
247 """see HttpOptions._prep_OPTIONS """
248 self.parent.log_message('get options: %s' % uri)
249 cr, uid, pool, dbname, uri2 = self.get_cr(uri, allow_last=True)
254 node = self.uri2object(cr, uid, pool, uri2[:])
260 if hasattr(node, 'http_options'):
262 for key, val in node.http_options.items():
263 if isinstance(val, basestring):
266 ret[key] = ret[key][:] # copy the orig. array
271 self.parent.log_message('options: %s' % ret)
277 def reduce_useragent(self):
278 ua = self.parent.headers.get('User-Agent', False)
282 ctx['DAV-client'] = 'iPhone'
283 elif 'Konqueror' in ua:
284 ctx['DAV-client'] = 'GroupDAV'
287 def get_prop(self, uri, ns, propname):
288 """ return the value of a given property
290 uri -- uri of the object to get the property of
291 ns -- namespace of the property
292 pname -- name of the property
294 if self.M_NS.has_key(ns):
296 # if it's not in the interface class, a "DAV:" property
297 # may be at the node class. So shouldn't give up early.
298 return dav_interface.get_prop(self, uri, ns, propname)
301 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
306 node = self.uri2object(cr, uid, pool, uri2)
309 res = node.get_dav_eprop(cr, ns, propname)
314 def get_db(self, uri, rest_ret=False, allow_last=False):
315 """Parse the uri and get the dbname and the rest.
316 Db name should be the first component in the unix-like
317 path supplied in uri.
319 @param rest_ret Instead of the db_name, return (db_name, rest),
320 where rest is the remaining path
321 @param allow_last If the dbname is the last component in the
322 path, allow it to be resolved. The default False value means
323 we will not attempt to use the db, unless there is more
326 @return db_name or (dbname, rest) depending on rest_ret,
327 will return dbname=False when component is not found.
330 uri2 = self.uri2local(uri)
331 if uri2.startswith('/'):
333 names=uri2.split('/',1)
340 if len(names) > ll and names[0]:
351 def urijoin(self,*ajoin):
352 """ Return the base URI of this request, or even join it with the
355 return self.parent.get_baseuri(self) + '/'.join(ajoin)
359 s = netsvc.ExportService.getService('db')
360 result = s.exp_list()
362 for db_name in result:
365 db = sql_db.db_connect(db_name)
367 cr.execute("SELECT id FROM ir_module_module WHERE name = 'document' AND state='installed' ")
370 self.db_name_list.append(db_name)
372 self.parent.log_error("Exception in db list: %s" % e)
376 return self.db_name_list
378 def get_childs(self,uri, filters=None):
379 """ return the child objects as self.baseuris for the given URI """
380 self.parent.log_message('get children: %s' % uri)
381 cr, uid, pool, dbname, uri2 = self.get_cr(uri, allow_last=True)
385 res = map(lambda x: self.urijoin(x), self.db_list())
388 node = self.uri2object(cr, uid, pool, uri2[:])
392 raise DAV_NotFound2(uri2)
394 fp = node.full_path()
397 self.parent.log_message('children for: %s' % fp)
402 domain = node.get_domain(cr, filters)
404 if hasattr(filters, 'getElementsByTagNameNS'):
405 hrefs = filters.getElementsByTagNameNS('DAV:', 'href')
407 ul = self.parent.davpath + self.uri2local(uri)
410 for tx in hr.childNodes:
411 if tx.nodeType == hr.TEXT_NODE:
413 if not turi.startswith('/'):
414 # it may be an absolute URL, decode to the
415 # relative part, because ul is relative, anyway
416 uparts=urlparse.urlparse(turi)
419 turi += ';' + uparts[3]
420 if turi.startswith(ul):
421 result.append( turi[len(self.parent.davpath):])
423 self.parent.log_error("ignore href %s because it is not under request path %s", turi, ul)
425 # We don't want to continue with the children found below
426 # Note the exceptions and that 'finally' will close the
428 for d in node.children(cr, domain):
429 self.parent.log_message('child: %s' % d.path)
431 result.append( self.urijoin(dbname,fp,d.path) )
433 result.append( self.urijoin(dbname,d.path) )
437 self.parent.log_error("Cannot get_children: "+str(e)+".")
443 def uri2local(self, uri):
444 uparts=urlparse.urlparse(uri)
447 reluri += ';'+uparts[3]
448 if reluri and reluri[-1]=="/":
453 # pos: -1 to get the parent of the uri
455 def get_cr(self, uri, allow_last=False):
456 """ Split the uri, grab a cursor for that db
458 pdb = self.parent.auth_provider.last_auth
459 dbname, uri2 = self.get_db(uri, rest_ret=True, allow_last=allow_last)
460 uri2 = (uri2 and uri2.split('/')) or []
462 return None, None, None, False, uri2
463 # if dbname was in our uri, we should have authenticated
465 assert pdb == dbname, " %s != %s" %(pdb, dbname)
466 res = self.parent.auth_provider.auth_creds.get(dbname, False)
468 self.parent.auth_provider.checkRequest(self.parent, uri, dbname)
469 res = self.parent.auth_provider.auth_creds[dbname]
470 user, passwd, dbn2, uid = res
471 db,pool = pooler.get_db_and_pool(dbname)
473 return cr, uid, pool, dbname, uri2
475 def uri2object(self, cr, uid, pool, uri):
478 context = self.reduce_useragent()
479 return pool.get('document.directory').get_object(cr, uid, uri, context=context)
481 def get_data(self,uri, rrange=None):
482 self.parent.log_message('GET: %s' % uri)
483 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
487 node = self.uri2object(cr, uid, pool, uri2)
489 raise DAV_NotFound2(uri2)
490 # TODO: if node is a collection, for some specific set of
491 # clients ( web browsers; available in node context),
492 # we may return a pseydo-html page with the directory listing.
494 res = node.open_data(cr,'r')
496 assert isinstance(rrange, (tuple,list))
497 start, end = map(long, rrange)
501 if end and end < start:
502 self.parent.log_error("Invalid range for data: %s-%s" %(start, end))
503 raise DAV_Error(416, "Invalid range for data.")
505 if end >= res.size():
506 raise DAV_Error(416, "Requested data exceeds available size.")
507 length = (end + 1) - start
509 length = res.size() - start
510 res = BoundStream2(res, offset=start, length=length)
513 # for the collections that return this error, the DAV standard
514 # says we'd better just return 200 OK with empty data
516 except IndexError,e :
517 self.parent.log_error("GET IndexError: %s", str(e))
518 raise DAV_NotFound2(uri2)
521 self.parent.log_error("GET exception: %s",str(e))
522 self.parent.log_message("Exc: %s", traceback.format_exc())
529 def _get_dav_resourcetype(self, uri):
530 """ return type of object """
531 self.parent.log_message('get RT: %s' % uri)
532 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
536 node = self.uri2object(cr, uid, pool, uri2)
538 raise DAV_NotFound2(uri2)
540 return node.get_dav_resourcetype(cr)
541 except NotImplementedError:
542 if node.type in ('collection','database'):
543 return ('collection', 'DAV:')
548 def _get_dav_displayname(self,uri):
549 self.parent.log_message('get DN: %s' % uri)
550 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
553 # at root, dbname, just return the last component
555 if uri2 and len(uri2) < 2:
558 node = self.uri2object(cr, uid, pool, uri2)
561 raise DAV_NotFound2(uri2)
563 return node.displayname
566 def _get_dav_getcontentlength(self, uri):
567 """ return the content length of an object """
568 self.parent.log_message('get length: %s' % uri)
570 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
574 node = self.uri2object(cr, uid, pool, uri2)
577 raise DAV_NotFound2(uri2)
578 result = node.content_length or 0
583 def _get_dav_getetag(self,uri):
584 """ return the ETag of an object """
585 self.parent.log_message('get etag: %s' % uri)
587 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
591 node = self.uri2object(cr, uid, pool, uri2)
594 raise DAV_NotFound2(uri2)
595 result = self._try_function(node.get_etag ,(cr,), "etag %s" %uri, cr=cr)
600 def get_lastmodified(self, uri):
601 """ return the last modified date of the object """
602 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
606 node = self.uri2object(cr, uid, pool, uri2)
608 raise DAV_NotFound2(uri2)
609 return _str2time(node.write_date)
613 def _get_dav_getlastmodified(self,uri):
614 """ return the last modified date of a resource
616 d=self.get_lastmodified(uri)
617 # format it. Note that we explicitly set the day, month names from
618 # an array, so that strftime() doesn't use its own locale-aware
621 return time.strftime("%%s, %d %%s %Y %H:%M:%S GMT", gmt ) % \
622 (day_names[gmt.tm_wday], month_names[gmt.tm_mon])
625 def get_creationdate(self, uri):
626 """ return the last modified date of the object """
627 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
631 node = self.uri2object(cr, uid, pool, uri2)
633 raise DAV_NotFound2(uri2)
635 return _str2time(node.create_date)
640 def _get_dav_getcontenttype(self,uri):
641 self.parent.log_message('get contenttype: %s' % uri)
642 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
645 return 'httpd/unix-directory'
647 node = self.uri2object(cr, uid, pool, uri2)
649 raise DAV_NotFound2(uri2)
650 result = str(node.mimetype)
652 #raise DAV_NotFound, 'Could not find %s' % path
657 """ create a new collection
658 see par. 9.3 of rfc4918
660 self.parent.log_message('MKCOL: %s' % uri)
661 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
664 raise DAV_Error(409, "Cannot create nameless collection.")
668 node = self.uri2object(cr,uid,pool, uri2[:-1])
671 raise DAV_Error(409, "Parent path %s does not exist" % uri2[:-1])
672 nc = node.child(cr, uri2[-1])
675 raise DAV_Error(405, "Path already exists.")
676 self._try_function(node.create_child_collection, (cr, uri2[-1]),
677 "create col %s" % uri2[-1], cr=cr)
682 def put(self, uri, data, content_type=None):
683 """ put the object into the filesystem """
684 self.parent.log_message('Putting %s (%d), %s'%( misc.ustr(uri), data and len(data) or 0, content_type))
685 cr, uid, pool,dbname, uri2 = self.get_cr(uri)
690 node = self.uri2object(cr, uid, pool, uri2[:])
694 objname = misc.ustr(uri2[-1])
698 dir_node = self.uri2object(cr, uid, pool, uri2[:-1])
701 raise DAV_NotFound('Parent folder not found.')
703 newchild = self._try_function(dir_node.create_child, (cr, objname, data),
704 "create %s" % objname, cr=cr)
708 raise DAV_Error(400, "Failed to create resource.")
710 uparts=urlparse.urlparse(uri)
711 fileloc = '/'.join(newchild.full_path())
712 if isinstance(fileloc, unicode):
713 fileloc = fileloc.encode('utf-8')
714 # the uri we get is a mangled one, where the davpath has been removed
715 davpath = self.parent.get_davpath()
717 surl = '%s://%s' % (uparts[0], uparts[1])
718 uloc = urllib.quote(fileloc)
720 if uri != ('/'+uloc) and uri != (surl + '/' + uloc):
721 hurl = '%s%s/%s/%s' %(surl, davpath, dbname, uloc)
724 etag = str(newchild.get_etag(cr))
726 self.parent.log_error("Cannot get etag for node: %s" % e)
727 ret = (str(hurl), etag)
729 self._try_function(node.set_data, (cr, data), "save %s" % objname, cr=cr)
736 """ delete a collection """
737 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
742 node = self.uri2object(cr, uid, pool, uri2)
743 self._try_function(node.rmcol, (cr,), "rmcol %s" % uri, cr=cr)
750 cr, uid, pool,dbname, uri2 = self.get_cr(uri)
754 node = self.uri2object(cr, uid, pool, uri2)
755 res = self._try_function(node.rm, (cr,), "rm %s" % uri, cr=cr)
758 raise OSError(1, 'Invalid Action!')
763 ### DELETE handlers (examples)
764 ### (we use the predefined methods in davcmd instead of doing
768 def delone(self, uri):
769 """ delete a single resource
771 You have to return a result dict of the form
773 or None if everything's ok
776 if uri[-1]=='/':uri=uri[:-1]
778 # parent='/'.join(uri.split('/')[:-1])
781 def deltree(self, uri):
782 """ delete a collection
784 You have to return a result dict of the form
786 or None if everything's ok
788 if uri[-1]=='/':uri=uri[:-1]
789 res=deltree(self, uri)
790 # parent='/'.join(uri.split('/')[:-1])
795 ### MOVE handlers (examples)
798 def moveone(self, src, dst, overwrite):
799 """ move one resource with Depth=0
801 an alternative implementation would be
806 r=os.system("rm -f '%s'" %dst)
808 r=os.system("mv '%s' '%s'" %(src,dst))
812 (untested!). This would not use the davcmd functions
813 and thus can only detect errors directly on the root node.
815 res=moveone(self, src, dst, overwrite)
818 def movetree(self, src, dst, overwrite):
819 """ move a collection with Depth=infinity
821 an alternative implementation would be
826 r=os.system("rm -rf '%s'" %dst)
828 r=os.system("mv '%s' '%s'" %(src,dst))
832 (untested!). This would not use the davcmd functions
833 and thus can only detect errors directly on the root node"""
835 res=movetree(self, src, dst, overwrite)
842 def copyone(self, src, dst, overwrite):
843 """ copy one resource with Depth=0
845 an alternative implementation would be
850 r=os.system("rm -f '%s'" %dst)
852 r=os.system("cp '%s' '%s'" %(src,dst))
856 (untested!). This would not use the davcmd functions
857 and thus can only detect errors directly on the root node.
859 res=copyone(self, src, dst, overwrite)
862 def copytree(self, src, dst, overwrite):
863 """ copy a collection with Depth=infinity
865 an alternative implementation would be
870 r=os.system("rm -rf '%s'" %dst)
872 r=os.system("cp -r '%s' '%s'" %(src,dst))
876 (untested!). This would not use the davcmd functions
877 and thus can only detect errors directly on the root node"""
878 res=copytree(self, src, dst, overwrite)
883 ### This methods actually copy something. low-level
884 ### They are called by the davcmd utility functions
885 ### copytree and copyone (not the above!)
886 ### Look in davcmd.py for further details.
889 def copy(self, src, dst):
890 src=urllib.unquote(src)
891 dst=urllib.unquote(dst)
892 ct = self._get_dav_getcontenttype(src)
893 data = self.get_data(src)
894 self.put(dst, data, ct)
897 def copycol(self, src, dst):
898 """ copy a collection.
900 As this is not recursive (the davserver recurses itself)
901 we will only create a new directory here. For some more
902 advanced systems we might also have to copy properties from
903 the source to the destination.
905 return self.mkcol(dst)
908 def exists(self, uri):
909 """ test if a resource exists """
911 cr, uid, pool,dbname, uri2 = self.get_cr(uri)
916 node = self.uri2object(cr, uid, pool, uri2)
924 def unlock(self, uri, token):
925 """ Unlock a resource from that token
927 @return True if unlocked, False if no lock existed, Exceptions
929 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
934 node = self.uri2object(cr, uid, pool, uri2)
936 node_fn = node.dav_unlock
937 except AttributeError:
938 # perhaps the node doesn't support locks
940 raise DAV_Error(400, 'No locks for this resource.')
942 res = self._try_function(node_fn, (cr, token), "unlock %s" % uri, cr=cr)
947 def lock(self, uri, lock_data):
948 """ Lock (may create) resource.
949 Data is a dict, may contain:
950 depth, token, refresh, lockscope, locktype, owner
952 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
959 node = self.uri2object(cr, uid, pool, uri2[:])
963 objname = misc.ustr(uri2[-1])
966 dir_node = self.uri2object(cr, uid, pool, uri2[:-1])
969 raise DAV_NotFound('Parent folder not found.')
971 # We create a new node (file) but with empty data=None,
972 # as in RFC4918 p. 9.10.4
973 node = self._try_function(dir_node.create_child, (cr, objname, None),
974 "create %s" % objname, cr=cr)
978 raise DAV_Error(400, "Failed to create resource.")
983 node_fn = node.dav_lock
984 except AttributeError:
985 # perhaps the node doesn't support locks
987 raise DAV_Error(400, 'No locks for this resource.')
989 # Obtain the lock on the node
990 lres, pid, token = self._try_function(node_fn, (cr, lock_data), "lock %s" % objname, cr=cr)
995 raise DAV_Error(423, "Resource already locked.")
997 assert isinstance(lres, list), 'lres: %s' % repr(lres)
1000 data = mk_lock_response(self, uri, lres)
1007 return created, data, token
1009 @memoize(CACHE_SIZE)
1010 def is_collection(self, uri):
1011 """ test if the given uri is a collection """
1012 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
1016 node = self.uri2object(cr,uid,pool, uri2)
1018 raise DAV_NotFound2(uri2)
1019 if node.type in ('collection','database'):
1027 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: