############################################################################## # # Copyright (c) 2002 Zope Corporation and Contributors. All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """WebDAV xml request objects. $Id: davcmds.py 69164 2006-07-17 23:34:32Z sidnei $ """ import sys from cStringIO import StringIO from urllib import quote import transaction from AccessControl import getSecurityManager from Acquisition import aq_parent from OFS.PropertySheets import DAVProperties from zExceptions import BadRequest, Forbidden from common import absattr, aq_base, urlfix, urlbase, urljoin from common import isDavCollection from common import PreconditionFailed from interfaces import IWriteLock from LockItem import LockItem from WriteLockInterface import WriteLockInterface from xmltools import XmlParser def safe_quote(url, mark=r'%'): if url.find(mark) > -1: return url return quote(url) class DAVProps(DAVProperties): """Emulate required DAV properties for objects which do not themselves support properties. This is mainly so that non-PropertyManagers can appear to support DAV PROPFIND requests.""" def __init__(self, obj): self.__obj__=obj def v_self(self): return self.__obj__ p_self=v_self class PropFind: """Model a PROPFIND request.""" def __init__(self, request): self.request=request self.depth='infinity' self.allprop=0 self.propname=0 self.propnames=[] self.parse(request) def parse(self, request, dav='DAV:'): self.depth=request.get_header('Depth', 'infinity') if not (self.depth in ('0','1','infinity')): raise BadRequest, 'Invalid Depth header.' body=request.get('BODY', '') self.allprop=(not len(body)) if not body: return try: root=XmlParser().parse(body) except: raise BadRequest, sys.exc_info()[1] e=root.elements('propfind', ns=dav) if not e: raise BadRequest, 'Invalid xml request.' e=e[0] if e.elements('allprop', ns=dav): self.allprop=1 return if e.elements('propname', ns=dav): self.propname=1 return prop=e.elements('prop', ns=dav) if not prop: raise BadRequest, 'Invalid xml request.' prop=prop[0] for val in prop.elements(): self.propnames.append((val.name(), val.namespace())) if (not self.allprop) and (not self.propname) and \ (not self.propnames): raise BadRequest, 'Invalid xml request.' return def apply(self, obj, url=None, depth=0, result=None, top=1): if result is None: result=StringIO() depth=self.depth url=urlfix(self.request['URL'], 'PROPFIND') url=urlbase(url) result.write('\n' \ '\n') iscol=isDavCollection(obj) if iscol and url[-1] != '/': url=url+'/' result.write('\n%s\n' % safe_quote(url)) if hasattr(aq_base(obj), 'propertysheets'): propsets=obj.propertysheets.values() obsheets=obj.propertysheets else: davprops=DAVProps(obj) propsets=(davprops,) obsheets={'DAV:': davprops} if self.allprop: stats=[] for ps in propsets: if hasattr(aq_base(ps), 'dav__allprop'): stats.append(ps.dav__allprop()) stats=''.join(stats) or '200 OK\n' result.write(stats) elif self.propname: stats=[] for ps in propsets: if hasattr(aq_base(ps), 'dav__propnames'): stats.append(ps.dav__propnames()) stats=''.join(stats) or '200 OK\n' result.write(stats) elif self.propnames: rdict={} for name, ns in self.propnames: ps=obsheets.get(ns, None) if ps is not None and hasattr(aq_base(ps), 'dav__propstat'): stat=ps.dav__propstat(name, rdict) else: prop='' % (name, ns) code='404 Not Found' if not rdict.has_key(code): rdict[code]=[prop] else: rdict[code].append(prop) keys=rdict.keys() keys.sort() for key in keys: result.write('\n' \ ' \n' \ ) map(result.write, rdict[key]) result.write(' \n' \ ' HTTP/1.1 %s\n' \ '\n' % key ) else: raise BadRequest, 'Invalid request' result.write('\n') if depth in ('1', 'infinity') and iscol: for ob in obj.listDAVObjects(): if hasattr(ob,"meta_type"): if ob.meta_type=="Broken Because Product is Gone": continue dflag=hasattr(ob, '_p_changed') and (ob._p_changed == None) if hasattr(ob, '__locknull_resource__'): # Do nothing, a null resource shouldn't show up to DAV if dflag: ob._p_deactivate() elif hasattr(ob, '__dav_resource__'): uri = urljoin(url, absattr(ob.getId())) depth = depth=='infinity' and depth or 0 self.apply(ob, uri, depth, result, top=0) if dflag: ob._p_deactivate() if not top: return result result.write('') return result.getvalue() class PropPatch: """Model a PROPPATCH request.""" def __init__(self, request): self.request=request self.values=[] self.parse(request) def parse(self, request, dav='DAV:'): body=request.get('BODY', '') try: root=XmlParser().parse(body) except: raise BadRequest, sys.exc_info()[1] vals=self.values e=root.elements('propertyupdate', ns=dav) if not e: raise BadRequest, 'Invalid xml request.' e=e[0] for ob in e.elements(): if ob.name()=='set' and ob.namespace()==dav: proptag=ob.elements('prop', ns=dav) if not proptag: raise BadRequest, 'Invalid xml request.' proptag=proptag[0] for prop in proptag.elements(): # We have to ensure that all tag attrs (including # an xmlns attr for all xml namespaces used by the # element and its children) are saved, per rfc2518. name, ns=prop.name(), prop.namespace() e, attrs=prop.elements(), prop.attrs() if (not e) and (not attrs): # simple property item=(name, ns, prop.strval(), {}) vals.append(item) else: # xml property attrs={} prop.remap({ns:'n'}) prop.del_attr('xmlns:n') for attr in prop.attrs(): attrs[attr.qname()]=attr.value() md={'__xml_attrs__':attrs} item=(name, ns, prop.strval(), md) vals.append(item) if ob.name()=='remove' and ob.namespace()==dav: proptag=ob.elements('prop', ns=dav) if not proptag: raise BadRequest, 'Invalid xml request.' proptag=proptag[0] for prop in proptag.elements(): item=(prop.name(), prop.namespace()) vals.append(item) def apply(self, obj): url=urlfix(self.request['URL'], 'PROPPATCH') if isDavCollection(obj): url=url+'/' result=StringIO() errors=[] result.write('\n' \ '\n' \ '\n' \ '%s\n' % quote(url)) propsets=obj.propertysheets for value in self.values: status='200 OK' if len(value) > 2: name, ns, val, md=value propset=propsets.get(ns, None) if propset is None: propsets.manage_addPropertySheet('', ns) propset=propsets.get(ns) if propset.hasProperty(name): try: propset._updateProperty(name, val, meta=md) except: errors.append(str(sys.exc_info()[1])) status='409 Conflict' else: try: propset._setProperty(name, val, meta=md) except: errors.append(str(sys.exc_info()[1])) status='409 Conflict' else: name, ns=value propset=propsets.get(ns, None) if propset is None or not propset.hasProperty(name): # removing a non-existing property is not an error! # according to RFC 2518 status='200 OK' else: try: propset._delProperty(name) except: errors.append('%s cannot be deleted.' % name) status='409 Conflict' result.write('\n' \ ' \n' \ ' \n' \ ' \n' \ ' HTTP/1.1 %s\n' \ '\n' % (ns, name, status)) errmsg='\n'.join(errors) or 'The operation succeded.' result.write('\n' \ '%s\n' \ '\n' \ '\n' \ '' % errmsg) result=result.getvalue() if not errors: return result # This is lame, but I cant find a way to keep ZPublisher # from sticking a traceback into my xml response :( transaction.abort() result=result.replace( '200 OK', '424 Failed Dependency') return result class Lock: """Model a LOCK request.""" def __init__(self, request): self.request = request data = request.get('BODY', '') self.scope = 'exclusive' self.type = 'write' self.owner = '' timeout = request.get_header('Timeout', 'infinite') self.timeout = timeout.split(',')[-1].strip() self.parse(data) def parse(self, data, dav='DAV:'): root = XmlParser().parse(data) info = root.elements('lockinfo', ns=dav)[0] ls = info.elements('lockscope', ns=dav)[0] self.scope = ls.elements()[0].name() lt = info.elements('locktype', ns=dav)[0] self.type = lt.elements()[0].name() lockowner = info.elements('owner', ns=dav) if lockowner: # Since the Owner element may contain children in different # namespaces (or none at all), we have to find them for potential # remapping. Note that Cadaver doesn't use namespaces in the # XML it sends. lockowner = lockowner[0] for el in lockowner.elements(): name, elns = el.name(), el.namespace() if not elns: # There's no namespace, so we have to add one lockowner.remap({dav:'ot'}) el.__nskey__ = 'ot' for subel in el.elements(): if not subel.namespace(): el.__nskey__ = 'ot' else: el.remap({dav:'o'}) self.owner = lockowner.strval() def apply(self, obj, creator=None, depth='infinity', token=None, result=None, url=None, top=1): """ Apply, built for recursion (so that we may lock subitems of a collection if requested """ if result is None: result = StringIO() url = urlfix(self.request['URL'], 'LOCK') url = urlbase(url) iscol = isDavCollection(obj) if iscol and url[-1] != '/': url = url + '/' errmsg = None lock = None try: lock = LockItem(creator, self.owner, depth, self.timeout, self.type, self.scope, token) if token is None: token = lock.getLockToken() except ValueError: errmsg = "412 Precondition Failed" except: errmsg = "403 Forbidden" try: if not (IWriteLock.providedBy(obj) or WriteLockInterface.isImplementedBy(obj)): if top: # This is the top level object in the apply, so we # do want an error errmsg = "405 Method Not Allowed" else: # We're in an infinity request and a subobject does # not support locking, so we'll just pass pass elif obj.wl_isLocked(): errmsg = "423 Locked" else: method = getattr(obj, 'wl_setLock') vld = getSecurityManager().validate(None, obj, 'wl_setLock', method) if vld and token and (lock is not None): obj.wl_setLock(token, lock) else: errmsg = "403 Forbidden" except: errmsg = "403 Forbidden" if errmsg: if top and ((depth in (0, '0')) or (not iscol)): # We don't need to raise multistatus errors raise errmsg[4:] elif not result.getvalue(): # We haven't had any errors yet, so our result is empty # and we need to set up the XML header result.write('\n' \ '\n') result.write('\n %s\n' % url) result.write(' HTTP/1.1 %s\n' % errmsg) result.write('\n') if depth == 'infinity' and iscol: for ob in obj.objectValues(): if hasattr(obj, '__dav_resource__'): uri = urljoin(url, absattr(ob.getId())) self.apply(ob, creator, depth, token, result, uri, top=0) if not top: return token, result if result.getvalue(): # One or more subitems probably failed, so close the multistatus # element and clear out all succesful locks result.write('') transaction.abort() # This *SHOULD* clear all succesful locks return token, result.getvalue() class Unlock: """ Model an Unlock request """ def apply(self, obj, token, url=None, result=None, top=1): if result is None: result = StringIO() url = urlfix(url, 'UNLOCK') url = urlbase(url) iscol = isDavCollection(obj) if iscol and url[-1] != '/': url = url + '/' errmsg = None islockable = IWriteLock.providedBy(obj) or \ WriteLockInterface.isImplementedBy(obj) if islockable and obj.wl_hasLock(token): method = getattr(obj, 'wl_delLock') vld = getSecurityManager().validate(None,obj,'wl_delLock',method) if vld: obj.wl_delLock(token) else: errmsg = "403 Forbidden" elif not islockable: # Only set an error message if the command is being applied # to a top level object. Otherwise, we're descending a tree # which may contain many objects that don't implement locking, # so we just want to avoid them if top: errmsg = "405 Method Not Allowed" if errmsg: if top and (not iscol): # We don't need to raise multistatus errors if errmsg[:3] == '403': raise Forbidden else: raise PreconditionFailed elif not result.getvalue(): # We haven't had any errors yet, so our result is empty # and we need to set up the XML header result.write('\n' \ '\n') result.write('\n %s\n' % url) result.write(' HTTP/1.1 %s\n' % errmsg) result.write('\n') if iscol: for ob in obj.objectValues(): if hasattr(ob, '__dav_resource__') and \ (IWriteLock.providedBy(ob) or WriteLockInterface.isImplementedBy(ob)): uri = urljoin(url, absattr(ob.getId())) self.apply(ob, token, uri, result, top=0) if not top: return result if result.getvalue(): # One or more subitems probably failed, so close the multistatus # element and clear out all succesful unlocks result.write('') transaction.abort() return result.getvalue() class DeleteCollection: """ With WriteLocks in the picture, deleting a collection involves checking *all* descendents (deletes on collections are always of depth infinite) for locks and if the locks match. """ def apply(self, obj, token, user, url=None, result=None, top=1): if result is None: result = StringIO() url = urlfix(url, 'DELETE') url = urlbase(url) iscol = isDavCollection(obj) errmsg = None parent = aq_parent(obj) islockable = IWriteLock.providedBy(obj) or \ WriteLockInterface.isImplementedBy(obj) if parent and (not user.has_permission('Delete objects', parent)): # User doesn't have permission to delete this object errmsg = "403 Forbidden" elif islockable and obj.wl_isLocked(): if token and obj.wl_hasLock(token): # Object is locked, and the token matches (no error) errmsg = "" else: errmsg = "423 Locked" if errmsg: if top and (not iscol): err = errmsg[4:] raise err elif not result.getvalue(): # We haven't had any errors yet, so our result is empty # and we need to set up the XML header result.write('\n' \ '\n') result.write('\n %s\n' % url) result.write(' HTTP/1.1 %s\n' % errmsg) result.write('\n') if iscol: for ob in obj.objectValues(): dflag = hasattr(ob,'_p_changed') and (ob._p_changed == None) if hasattr(ob, '__dav_resource__'): uri = urljoin(url, absattr(ob.getId())) self.apply(ob, token, user, uri, result, top=0) if dflag: ob._p_deactivate() if not top: return result if result.getvalue(): # One or more subitems can't be delted, so close the multistatus # element result.write('\n') return result.getvalue()