From 2289282c8b11443a135a6fae6098e54c1178c049 Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Sun, 1 Aug 2010 11:37:22 +0300 Subject: [PATCH 1/3] Doc, webdav: new API for the DAV:resourcetype property. This property is used in DAV folders, CalDAV, GroupDAV and CardDAV, with different extensions at each one. So, it needs an extensible API for the value it may return. Typical response in a calendar dir: The last two elements have to be added by subclassing the node in the caldav module. Conflicts: document_webdav/dav_fs.py bzr revid: p_christ@hol.gr-20100801083722-nh4ty58fy0hobrgk --- addons/document/nodes.py | 22 +++++++++ addons/document_webdav/dav_fs.py | 26 +++++++--- addons/document_webdav/webdav.py | 81 +++++++++++++++++++++++++------- 3 files changed, 107 insertions(+), 22 deletions(-) diff --git a/addons/document/nodes.py b/addons/document/nodes.py index de2a83285f9..228f104574d 100644 --- a/addons/document/nodes.py +++ b/addons/document/nodes.py @@ -314,6 +314,14 @@ class node_class(object): logger.debug('Property %s not supported' % prop, exc_info=True) return None + def get_dav_resourcetype(self, cr): + """ Get the DAV resource type. + + Is here because some nodes may exhibit special behaviour, like + CalDAV/GroupDAV collections + """ + raise NotImplementedError + def move_to(self, cr, ndir_node, new_name=False, fil_obj=None, ndir_obj=None, in_write=False): """ Move this node to a new parent directory. @param ndir_node the collection that this node should be moved under @@ -456,6 +464,9 @@ class node_database(node_class): def _get_ttag(self,cr): return 'db-%s' % cr.dbname + def get_dav_resourcetype(self, cr): + return ('collection', 'DAV:') + def mkdosname(company_name, default='noname'): """ convert a string to a dos-like name""" if not company_name: @@ -778,6 +789,9 @@ class node_res_dir(node_class): def _get_ttag(self,cr): return 'rdir-%d' % self.dir_id + def get_dav_resourcetype(self, cr): + return ('collection', 'DAV:') + class node_res_obj(node_class): """ A special sibling to node_dir, which does only contain dynamically created folders foreach resource in the foreign model. @@ -1034,6 +1048,9 @@ class node_res_obj(node_class): def _get_ttag(self,cr): return 'rodir-%d-%d' % (self.dir_id, self.res_id) + def get_dav_resourcetype(self, cr): + return ('collection', 'DAV:') + class node_file(node_class): our_type = 'file' def __init__(self, path, parent, context, fil): @@ -1168,6 +1185,9 @@ class node_file(node_class): def _get_ttag(self,cr): return 'file-%d' % self.file_id + def get_dav_resourcetype(self, cr): + return '' + def move_to(self, cr, ndir_node, new_name=False, fil_obj=None, ndir_obj=None, in_write=False): if ndir_node.context != self.context: raise NotImplementedError("Cannot move files between contexts") @@ -1313,6 +1333,8 @@ class node_content(node_class): def _get_ttag(self,cr): return 'cnt-%d%s' % (self.cnt_id,(self.act_id and ('-' + str(self.act_id))) or '') + def get_dav_resourcetype(self, cr): + return '' class nodefd_content(StringIO, node_descriptor): """ A descriptor to content nodes diff --git a/addons/document_webdav/dav_fs.py b/addons/document_webdav/dav_fs.py index 78812ff8cf5..af8738ef0da 100644 --- a/addons/document_webdav/dav_fs.py +++ b/addons/document_webdav/dav_fs.py @@ -370,19 +370,22 @@ class openerp_dav_handler(dav_interface): if cr: cr.close() @memoize(CACHE_SIZE) - def _get_dav_resourcetype(self,uri): + def _get_dav_resourcetype(self, uri): """ return type of object """ self.parent.log_message('get RT: %s' % uri) cr, uid, pool, dbname, uri2 = self.get_cr(uri) try: if not dbname: return COLLECTION - node = self.uri2object(cr,uid,pool, uri2) + node = self.uri2object(cr, uid, pool, uri2) if not node: raise DAV_NotFound2(uri2) - if node.type in ('collection','database'): - return COLLECTION - return OBJECT + try: + return node.get_dav_resourcetype(cr) + except NotImplementedError: + if node.type in ('collection','database'): + return ('collection', 'DAV:') + return '' finally: if cr: cr.close() @@ -739,6 +742,17 @@ class openerp_dav_handler(dav_interface): @memoize(CACHE_SIZE) def is_collection(self, uri): """ test if the given uri is a collection """ - return self._get_dav_resourcetype(uri)==COLLECTION + cr, uid, pool, dbname, uri2 = self.get_cr(uri) + try: + if not dbname: + return True + node = self.uri2object(cr,uid,pool, uri2) + if not node: + raise DAV_NotFound2(uri2) + if node.type in ('collection','database'): + return True + return False + finally: + if cr: cr.close() #eof diff --git a/addons/document_webdav/webdav.py b/addons/document_webdav/webdav.py index 1502ff204f8..1fd530cbdca 100644 --- a/addons/document_webdav/webdav.py +++ b/addons/document_webdav/webdav.py @@ -48,6 +48,70 @@ def mk_prop_response(self, uri, good_props, bad_props, doc): re.setAttribute("xmlns:ns"+str(nsnum),nsname) nsnum=nsnum+1 + def _prop_child(xnode, ns, prop, value): + """Append a property xml node to xnode, with value + + And a little smarter than that, it will consider namespace and + also allow nested properties etc. + + :param ns the namespace of the node + :param prop the name of the property + :param value the value. Can be: + string: text node + tuple ('elem', 'ns') for empty sub-node + tuple ('elem', 'ns', sub-elems) for sub-node with elements + list, of above tuples + """ + if ns == 'DAV:': + ns_prefix = 'D:' + else: + ns_prefix="ns"+str(namespaces.index(ns))+":" + + pe=doc.createElement(ns_prefix+str(prop)) + if hasattr(value, '__class__') and value.__class__.__name__ == 'Element': + pe.appendChild(value) + else: + if ns == 'DAV:' and prop=="resourcetype" and isinstance(value, int): + # hack, to go.. + if value == 1: + ve=doc.createElement("D:collection") + pe.appendChild(ve) + else: + _prop_elem_child(pe, ns, value, ns_prefix) + + xnode.appendChild(pe) + + def _prop_elem_child(pnode, pns, v, pns_prefix): + + if isinstance(v, list): + for vit in v: + _prop_elem_child(pnode, pns, vit, pns_prefix) + elif isinstance(v,tuple): + need_ns = False + if v[1] == pns: + ns_prefix = pns_prefix + elif v[1] == 'DAV:': + ns_prefix = 'D:' + elif v[1] in namespaces: + ns_prefix="ns"+str(namespaces.index(v[1]))+":" + else: + # namespaces.append(v[1]) + # nsnum += 1 + ns_prefix="ns"+str(nsnum)+":" + need_ns = True + + ve=doc.createElement(ns_prefix+v[0]) + if need_ns: + ve.setAttribute("xmlns:ns"+str(nsnum), v[1]) + if len(v) > 2 and isinstance(v[2], list): + # support nested elements like: + # ( 'elem', 'ns:', [('sub-elem1', 'ns1'), ...] + _prop_elem_child(ve, v[1], v[2], ns_prefix) + pnode.appendChild(ve) + else: + ve=doc.createTextNode(tools.ustr(v)) + pnode.appendChild(ve) + # write href information uparts=urlparse.urlparse(uri) fileloc=uparts[2] @@ -74,22 +138,7 @@ def mk_prop_response(self, uri, good_props, bad_props, doc): for p,v in good_props[ns].items(): if not v: continue - pe=doc.createElement(ns_prefix+str(p)) - if hasattr(v, '__class__') and v.__class__.__name__ == 'Element': - pe.appendChild(v) - else: - if ns == 'DAV:' and p=="resourcetype": - if v == 1: - ve=doc.createElement("D:collection") - pe.appendChild(ve) - elif isinstance(v,tuple) and v[1] == ns: - ve=doc.createElement(ns_prefix+v[0]) - pe.appendChild(ve) - else: - ve=doc.createTextNode(tools.ustr(v)) - pe.appendChild(ve) - - gp.appendChild(pe) + _prop_child(gp, ns, p, v) ps.appendChild(gp) s=doc.createElement("D:status") From f4de6ab5f02c0bd0fc417fb311dc2c9d9fef9fb1 Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Sun, 1 Aug 2010 11:37:26 +0300 Subject: [PATCH 2/3] CalDAV: use the new API for DAV:resourcetype bzr revid: p_christ@hol.gr-20100801083726-y8tkn68gwe8vmoxf --- addons/caldav/caldav.py | 20 +------------------- addons/caldav/caldav_node.py | 23 ++++------------------- 2 files changed, 5 insertions(+), 38 deletions(-) diff --git a/addons/caldav/caldav.py b/addons/caldav/caldav.py index 3aa5dfd14dc..2517f88f789 100644 --- a/addons/caldav/caldav.py +++ b/addons/caldav/caldav.py @@ -20,26 +20,8 @@ ############################################################################## from document_webdav import webdav -import tools -from DAV.propfind import PROPFIND import urlparse urlparse.uses_netloc.append('caldav') urlparse.uses_netloc.append('caldavs') -super_mk_prop_response = webdav.mk_prop_response -def mk_prop_response(self,uri,good_props,bad_props,doc): - res = super_mk_prop_response(self, uri,good_props,bad_props,doc) - uris = uri.split('/') - calendar = False - if 'http://calendarserver.org/ns/' in good_props or 'http://calendarserver.org/ns/' in bad_props: - calendar = True - if calendar: - ad = doc.createElement('calendar') - ad.setAttribute('xmlns', 'urn:ietf:params:xml:ns:caldav') - cols = res.getElementsByTagName('D:collection') - if cols: - cols[0].parentNode.appendChild(ad) - #cols[0].parentNode.appendChild(vc) - return res - -PROPFIND.mk_prop_response = mk_prop_response +#EOF diff --git a/addons/caldav/caldav_node.py b/addons/caldav/caldav_node.py index 3ae37ea3e7e..b931b0f0328 100644 --- a/addons/caldav/caldav_node.py +++ b/addons/caldav/caldav_node.py @@ -133,25 +133,10 @@ class node_calendar(nodes.node_class): result = self._get_ttag(cr) + ':' + str(time.time()) return str(result) - def _get_gdav_resourcetype(self, cr): - return (str(self.cal_type + '-collection'), 'http://groupdav.org/') - - def removeme_match_dav_eprop(self, cr, match, ns, prop): - # Why? - if ns == "DAV:" and prop == "getetag": - dirobj = self.context._dirobj - uid = self.context.uid - ctx = self.context.context.copy() - tem, dav_time = tuple(match.split(':')) - model, res_id = tuple(tem.split('_')) - model_obj = dirobj.pool.get(model) - model = model_obj.browse(cr, uid, res_id, context=ctx) - write_time = model.write_date or model.create_date - wtime = time.mktime(time.strptime(write_time,'%Y-%m-%d %H:%M:%S')) - if float(dav_time) == float(wtime): - return True - return False - res = super(node_calendar, self).match_dav_eprop(cr, match, ns, prop) + def get_dav_resourcetype(self, cr): + res = [ ('collection', 'DAV:'), + (str(self.cal_type + '-collection'), 'http://groupdav.org/'), + ('calendar', 'urn:ietf:params:xml:ns:caldav') ] return res def get_domain(self, cr, filters): From c02cd022e4531ac05001ca7909fbd037cb4d4627 Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Sun, 1 Aug 2010 11:38:28 +0300 Subject: [PATCH 3/3] WebDAV: Add keep-alive in headers, fix If-Match We must check the "If-Match" header. Its value is quoted, so try to remove quotes (crude way), and also consume any body before we respond with 4xx. Conflicts: document_webdav/webdav_server.py bzr revid: p_christ@hol.gr-20100801083828-lh9htiqlwewvhloh --- addons/document_webdav/webdav_server.py | 32 +++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/addons/document_webdav/webdav_server.py b/addons/document_webdav/webdav_server.py index 0f306a70612..34c255a2b3e 100644 --- a/addons/document_webdav/webdav_server.py +++ b/addons/document_webdav/webdav_server.py @@ -131,6 +131,22 @@ class DAVHandler(HttpOptions, FixSendError, DAVRequestHandler): self.close_connection = 1 DAVRequestHandler.send_header(self, key, value) + def send_body(self, DATA, code = None, msg = None, desc = None, ctype='application/octet-stream', headers=None): + if headers and 'Connection' in headers: + pass + elif self.request_version in ('HTTP/1.0', 'HTTP/0.9'): + pass + elif self.close_connection == 1: # close header already sent + pass + else: + if headers is None: + headers = {} + if self.headers.get('Connection',False) == 'Keep-Alive': + headers['Connection'] = 'keep-alive' + + DAVRequestHandler.send_body(self, DATA, code=code, msg=msg, desc=desc, + ctype=ctype, headers=headers) + def do_PUT(self): dc=self.IFACE_CLASS uri=urlparse.urljoin(self.get_baseuri(dc), self.path) @@ -141,6 +157,8 @@ class DAVHandler(HttpOptions, FixSendError, DAVRequestHandler): etag = None for match in self.headers['If-Match'].split(','): + if match.startswith('"') and match.endswith('"'): + match = match[1:-1] if match == '*': if dc.exists(uri): test = True @@ -150,6 +168,7 @@ class DAVHandler(HttpOptions, FixSendError, DAVRequestHandler): test = True break if not test: + self._get_body() self.send_status(412) return @@ -167,6 +186,7 @@ class DAVHandler(HttpOptions, FixSendError, DAVRequestHandler): test = False break if not test: + self._get_body() self.send_status(412) return @@ -179,10 +199,7 @@ class DAVHandler(HttpOptions, FixSendError, DAVRequestHandler): self._flush() # read the body - body=None - if self.headers.has_key("Content-Length"): - l=self.headers['Content-Length'] - body=self.rfile.read(atoi(l)) + body=self._get_body() # locked resources are not allowed to be overwritten if self._l_isLocked(uri): @@ -208,6 +225,13 @@ class DAVHandler(HttpOptions, FixSendError, DAVRequestHandler): self.send_body(None, '201', 'Created', '', headers=headers) + def _get_body(self): + body = None + if self.headers.has_key("Content-Length"): + l=self.headers['Content-Length'] + body=self.rfile.read(atoi(l)) + return body + def do_DELETE(self): try: DAVRequestHandler.do_DELETE(self)