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)
417 if turi.startswith(ul):
418 result.append( turi[len(self.parent.davpath):])
420 self.parent.log_error("ignore href %s because it is not under request path %s", turi, ul)
422 # We don't want to continue with the children found below
423 # Note the exceptions and that 'finally' will close the
425 for d in node.children(cr, domain):
426 self.parent.log_message('child: %s' % d.path)
428 result.append( self.urijoin(dbname,fp,d.path) )
430 result.append( self.urijoin(dbname,d.path) )
434 self.parent.log_error("cannot get_children: "+ str(e))
440 def uri2local(self, uri):
441 uparts=urlparse.urlparse(uri)
443 if reluri and reluri[-1]=="/":
448 # pos: -1 to get the parent of the uri
450 def get_cr(self, uri, allow_last=False):
451 """ Split the uri, grab a cursor for that db
453 pdb = self.parent.auth_proxy.last_auth
454 dbname, uri2 = self.get_db(uri, rest_ret=True, allow_last=allow_last)
455 uri2 = (uri2 and uri2.split('/')) or []
457 return None, None, None, False, uri2
458 # if dbname was in our uri, we should have authenticated
460 assert pdb == dbname, " %s != %s" %(pdb, dbname)
461 res = self.parent.auth_proxy.auth_creds.get(dbname, False)
463 self.parent.auth_proxy.checkRequest(self.parent, uri, dbname)
464 res = self.parent.auth_proxy.auth_creds[dbname]
465 user, passwd, dbn2, uid = res
466 db,pool = pooler.get_db_and_pool(dbname)
468 return cr, uid, pool, dbname, uri2
470 def uri2object(self, cr, uid, pool, uri):
473 context = self.reduce_useragent()
474 return pool.get('document.directory').get_object(cr, uid, uri, context=context)
476 def get_data(self,uri, rrange=None):
477 self.parent.log_message('GET: %s' % uri)
478 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
482 node = self.uri2object(cr, uid, pool, uri2)
484 raise DAV_NotFound2(uri2)
485 # TODO: if node is a collection, for some specific set of
486 # clients ( web browsers; available in node context),
487 # we may return a pseydo-html page with the directory listing.
489 res = node.open_data(cr,'r')
491 assert isinstance(rrange, (tuple,list))
492 start, end = map(long, rrange)
496 if end and end < start:
497 self.parent.log_error("Invalid range for data: %s-%s" %(start, end))
498 raise DAV_Error(416, "Invalid range for data")
500 if end >= res.size():
501 raise DAV_Error(416, "Requested data exceeds available size")
502 length = (end + 1) - start
504 length = res.size() - start
505 res = BoundStream2(res, offset=start, length=length)
508 # for the collections that return this error, the DAV standard
509 # says we'd better just return 200 OK with empty data
511 except IndexError,e :
512 self.parent.log_error("GET IndexError: %s", str(e))
513 raise DAV_NotFound2(uri2)
516 self.parent.log_error("GET exception: %s",str(e))
517 self.parent.log_message("Exc: %s", traceback.format_exc())
524 def _get_dav_resourcetype(self, uri):
525 """ return type of object """
526 self.parent.log_message('get RT: %s' % uri)
527 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
531 node = self.uri2object(cr, uid, pool, uri2)
533 raise DAV_NotFound2(uri2)
535 return node.get_dav_resourcetype(cr)
536 except NotImplementedError:
537 if node.type in ('collection','database'):
538 return ('collection', 'DAV:')
543 def _get_dav_displayname(self,uri):
544 self.parent.log_message('get DN: %s' % uri)
545 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
548 # at root, dbname, just return the last component
550 if uri2 and len(uri2) < 2:
553 node = self.uri2object(cr, uid, pool, uri2)
556 raise DAV_NotFound2(uri2)
558 return node.displayname
561 def _get_dav_getcontentlength(self, uri):
562 """ return the content length of an object """
563 self.parent.log_message('get length: %s' % uri)
565 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
569 node = self.uri2object(cr, uid, pool, uri2)
572 raise DAV_NotFound2(uri2)
573 result = node.content_length or 0
578 def _get_dav_getetag(self,uri):
579 """ return the ETag of an object """
580 self.parent.log_message('get etag: %s' % uri)
582 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
586 node = self.uri2object(cr, uid, pool, uri2)
589 raise DAV_NotFound2(uri2)
590 result = self._try_function(node.get_etag ,(cr,), "etag %s" %uri, cr=cr)
595 def get_lastmodified(self, uri):
596 """ return the last modified date of the object """
597 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
601 node = self.uri2object(cr, uid, pool, uri2)
603 raise DAV_NotFound2(uri2)
604 return _str2time(node.write_date)
608 def _get_dav_getlastmodified(self,uri):
609 """ return the last modified date of a resource
611 d=self.get_lastmodified(uri)
612 # format it. Note that we explicitly set the day, month names from
613 # an array, so that strftime() doesn't use its own locale-aware
616 return time.strftime("%%s, %d %%s %Y %H:%M:%S GMT", gmt ) % \
617 (day_names[gmt.tm_wday], month_names[gmt.tm_mon])
620 def get_creationdate(self, uri):
621 """ return the last modified date of the object """
622 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
626 node = self.uri2object(cr, uid, pool, uri2)
628 raise DAV_NotFound2(uri2)
630 return _str2time(node.create_date)
635 def _get_dav_getcontenttype(self,uri):
636 self.parent.log_message('get contenttype: %s' % uri)
637 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
640 return 'httpd/unix-directory'
642 node = self.uri2object(cr, uid, pool, uri2)
644 raise DAV_NotFound2(uri2)
645 result = str(node.mimetype)
647 #raise DAV_NotFound, 'Could not find %s' % path
652 """ create a new collection
653 see par. 9.3 of rfc4918
655 self.parent.log_message('MKCOL: %s' % uri)
656 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
659 raise DAV_Error(409, "Cannot create nameless collection")
663 node = self.uri2object(cr,uid,pool, uri2[:-1])
666 raise DAV_Error(409, "Parent path %s does not exist" % uri2[:-1])
667 nc = node.child(cr, uri2[-1])
670 raise DAV_Error(405, "Path already exists")
671 self._try_function(node.create_child_collection, (cr, uri2[-1]),
672 "create col %s" % uri2[-1], cr=cr)
677 def put(self, uri, data, content_type=None):
678 """ put the object into the filesystem """
679 self.parent.log_message('Putting %s (%d), %s'%( misc.ustr(uri), data and len(data) or 0, content_type))
680 cr, uid, pool,dbname, uri2 = self.get_cr(uri)
685 node = self.uri2object(cr, uid, pool, uri2[:])
689 objname = misc.ustr(uri2[-1])
693 dir_node = self.uri2object(cr, uid, pool, uri2[:-1])
696 raise DAV_NotFound('Parent folder not found')
698 newchild = self._try_function(dir_node.create_child, (cr, objname, data),
699 "create %s" % objname, cr=cr)
703 raise DAV_Error(400, "Failed to create resource")
705 uparts=urlparse.urlparse(uri)
706 fileloc = '/'.join(newchild.full_path())
707 if isinstance(fileloc, unicode):
708 fileloc = fileloc.encode('utf-8')
709 # the uri we get is a mangled one, where the davpath has been removed
710 davpath = self.parent.get_davpath()
712 surl = '%s://%s' % (uparts[0], uparts[1])
713 uloc = urllib.quote(fileloc)
715 if uri != ('/'+uloc) and uri != (surl + '/' + uloc):
716 hurl = '%s%s/%s/%s' %(surl, davpath, dbname, uloc)
719 etag = str(newchild.get_etag(cr))
721 self.parent.log_error("Cannot get etag for node: %s" % e)
722 ret = (str(hurl), etag)
724 self._try_function(node.set_data, (cr, data), "save %s" % objname, cr=cr)
731 """ delete a collection """
732 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
737 node = self.uri2object(cr, uid, pool, uri2)
738 self._try_function(node.rmcol, (cr,), "rmcol %s" % uri, cr=cr)
745 cr, uid, pool,dbname, uri2 = self.get_cr(uri)
749 node = self.uri2object(cr, uid, pool, uri2)
750 res = self._try_function(node.rm, (cr,), "rm %s" % uri, cr=cr)
753 raise OSError(1, 'Operation not permited.')
758 ### DELETE handlers (examples)
759 ### (we use the predefined methods in davcmd instead of doing
763 def delone(self, uri):
764 """ delete a single resource
766 You have to return a result dict of the form
768 or None if everything's ok
771 if uri[-1]=='/':uri=uri[:-1]
773 # parent='/'.join(uri.split('/')[:-1])
776 def deltree(self, uri):
777 """ delete a collection
779 You have to return a result dict of the form
781 or None if everything's ok
783 if uri[-1]=='/':uri=uri[:-1]
784 res=deltree(self, uri)
785 # parent='/'.join(uri.split('/')[:-1])
790 ### MOVE handlers (examples)
793 def moveone(self, src, dst, overwrite):
794 """ move one resource with Depth=0
796 an alternative implementation would be
801 r=os.system("rm -f '%s'" %dst)
803 r=os.system("mv '%s' '%s'" %(src,dst))
807 (untested!). This would not use the davcmd functions
808 and thus can only detect errors directly on the root node.
810 res=moveone(self, src, dst, overwrite)
813 def movetree(self, src, dst, overwrite):
814 """ move a collection with Depth=infinity
816 an alternative implementation would be
821 r=os.system("rm -rf '%s'" %dst)
823 r=os.system("mv '%s' '%s'" %(src,dst))
827 (untested!). This would not use the davcmd functions
828 and thus can only detect errors directly on the root node"""
830 res=movetree(self, src, dst, overwrite)
837 def copyone(self, src, dst, overwrite):
838 """ copy one resource with Depth=0
840 an alternative implementation would be
845 r=os.system("rm -f '%s'" %dst)
847 r=os.system("cp '%s' '%s'" %(src,dst))
851 (untested!). This would not use the davcmd functions
852 and thus can only detect errors directly on the root node.
854 res=copyone(self, src, dst, overwrite)
857 def copytree(self, src, dst, overwrite):
858 """ copy a collection with Depth=infinity
860 an alternative implementation would be
865 r=os.system("rm -rf '%s'" %dst)
867 r=os.system("cp -r '%s' '%s'" %(src,dst))
871 (untested!). This would not use the davcmd functions
872 and thus can only detect errors directly on the root node"""
873 res=copytree(self, src, dst, overwrite)
878 ### This methods actually copy something. low-level
879 ### They are called by the davcmd utility functions
880 ### copytree and copyone (not the above!)
881 ### Look in davcmd.py for further details.
884 def copy(self, src, dst):
885 src=urllib.unquote(src)
886 dst=urllib.unquote(dst)
887 ct = self._get_dav_getcontenttype(src)
888 data = self.get_data(src)
889 self.put(dst, data, ct)
892 def copycol(self, src, dst):
893 """ copy a collection.
895 As this is not recursive (the davserver recurses itself)
896 we will only create a new directory here. For some more
897 advanced systems we might also have to copy properties from
898 the source to the destination.
900 return self.mkcol(dst)
903 def exists(self, uri):
904 """ test if a resource exists """
906 cr, uid, pool,dbname, uri2 = self.get_cr(uri)
911 node = self.uri2object(cr, uid, pool, uri2)
919 def unlock(self, uri, token):
920 """ Unlock a resource from that token
922 @return True if unlocked, False if no lock existed, Exceptions
924 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
929 node = self.uri2object(cr, uid, pool, uri2)
931 node_fn = node.dav_unlock
932 except AttributeError:
933 # perhaps the node doesn't support locks
935 raise DAV_Error(400, 'No locks for this resource')
937 res = self._try_function(node_fn, (cr, token), "unlock %s" % uri, cr=cr)
942 def lock(self, uri, lock_data):
943 """ Lock (may create) resource.
944 Data is a dict, may contain:
945 depth, token, refresh, lockscope, locktype, owner
947 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
954 node = self.uri2object(cr, uid, pool, uri2[:])
958 objname = misc.ustr(uri2[-1])
961 dir_node = self.uri2object(cr, uid, pool, uri2[:-1])
964 raise DAV_NotFound('Parent folder not found')
966 # We create a new node (file) but with empty data=None,
967 # as in RFC4918 p. 9.10.4
968 node = self._try_function(dir_node.create_child, (cr, objname, None),
969 "create %s" % objname, cr=cr)
973 raise DAV_Error(400, "Failed to create resource")
978 node_fn = node.dav_lock
979 except AttributeError:
980 # perhaps the node doesn't support locks
982 raise DAV_Error(400, 'No locks for this resource')
984 # Obtain the lock on the node
985 lres, pid, token = self._try_function(node_fn, (cr, lock_data), "lock %s" % objname, cr=cr)
990 raise DAV_Error(423, "Resource already locked")
992 assert isinstance(lres, list), 'lres: %s' % repr(lres)
995 data = mk_lock_response(self, uri, lres)
1002 return created, data, token
1004 @memoize(CACHE_SIZE)
1005 def is_collection(self, uri):
1006 """ test if the given uri is a collection """
1007 cr, uid, pool, dbname, uri2 = self.get_cr(uri)
1011 node = self.uri2object(cr,uid,pool, uri2)
1013 raise DAV_NotFound2(uri2)
1014 if node.type in ('collection','database'):