##############################################################################
#
# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (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 69167 2006-07-17 23:36:49Z sidnei $
"""
import sys
from common import absattr, aq_base, urlfix, urlbase, urljoin
from OFS.PropertySheets import DAVProperties
from LockItem import LockItem
from WriteLockInterface import WriteLockInterface
from Acquisition import aq_parent
from xmltools import XmlParser
from cStringIO import StringIO
from urllib import quote
from AccessControl import getSecurityManager
from zExceptions import BadRequest, Forbidden
from common import isDavCollection
from common import PreconditionFailed
import transaction
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 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 = 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 \
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 = 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()