##############################################################################
#
# 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()