12a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)#   Copyright (c) 2006-2007 Open Source Applications Foundation
22a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)#
32a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)#   Licensed under the Apache License, Version 2.0 (the "License");
42a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)#   you may not use this file except in compliance with the License.
52a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)#   You may obtain a copy of the License at
62a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)#
72a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)#       http://www.apache.org/licenses/LICENSE-2.0
82a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)#
92a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)#   Unless required by applicable law or agreed to in writing, software
102a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)#   distributed under the License is distributed on an "AS IS" BASIS,
112a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
122a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)#   See the License for the specific language governing permissions and
132a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)#   limitations under the License.
142a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
152a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)import urlparse, httplib, copy, base64, StringIO
162a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)import urllib
172a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
182a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)try:
192a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    from xml.etree import ElementTree
202a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)except:
212a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    from elementtree import ElementTree
222a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
232a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)__all__ = ['DAVClient']
242a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
252a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)def object_to_etree(parent, obj, namespace=''):
262a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    """This function takes in a python object, traverses it, and adds it to an existing etree object"""
272a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
282a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    if type(obj) is int or type(obj) is float or type(obj) is str:
292a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        # If object is a string, int, or float just add it
302a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        obj = str(obj)
312a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        if obj.startswith('{') is False:
322a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            ElementTree.SubElement(parent, '{%s}%s' % (namespace, obj))
332a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        else:
342a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            ElementTree.SubElement(parent, obj)
352a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
362a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    elif type(obj) is dict:
372a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        # If the object is a dictionary we'll need to parse it and send it back recusively
382a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        for key, value in obj.items():
392a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            if key.startswith('{') is False:
402a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)                key_etree = ElementTree.SubElement(parent, '{%s}%s' % (namespace, key))
412a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)                object_to_etree(key_etree, value, namespace=namespace)
422a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            else:
432a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)                key_etree = ElementTree.SubElement(parent, key)
442a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)                object_to_etree(key_etree, value, namespace=namespace)
452a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
462a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    elif type(obj) is list:
472a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        # If the object is a list parse it and send it back recursively
482a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        for item in obj:
492a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            object_to_etree(parent, item, namespace=namespace)
502a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
512a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    else:
522a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        # If it's none of previous types then raise
532a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        raise TypeError, '%s is an unsupported type' % type(obj)
542a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
552a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
562a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)class DAVClient(object):
572a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
582a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    def __init__(self, url='http://localhost:8080'):
592a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        """Initialization"""
602a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
612a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        self._url = urlparse.urlparse(url)
622a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
632a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        self.headers = {'Host':self._url[1],
642a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)                        'User-Agent': 'python.davclient.DAVClient/0.1'}
652a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
662a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
672a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    def _request(self, method, path='', body=None, headers=None):
682a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        """Internal request method"""
692a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        self.response = None
702a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
712a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        if headers is None:
722a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            headers = copy.copy(self.headers)
732a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        else:
742a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            new_headers = copy.copy(self.headers)
752a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            new_headers.update(headers)
762a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            headers = new_headers
772a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
782a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        if self._url.scheme == 'http':
792a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            self._connection = httplib.HTTPConnection(self._url[1])
802a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        elif self._url.scheme == 'https':
812a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            self._connection = httplib.HTTPSConnection(self._url[1])
822a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        else:
832a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            raise Exception, 'Unsupported scheme'
842a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
852a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        self._connection.request(method, path, body, headers)
862a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
872a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        self.response = self._connection.getresponse()
882a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
892a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        self.response.body = self.response.read()
902a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
912a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        # Try to parse and get an etree
922a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        try:
932a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            self._get_response_tree()
942a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        except:
952a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            pass
962a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
972a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
982a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    def _get_response_tree(self):
992a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        """Parse the response body into an elementree object"""
1002a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        self.response.tree = ElementTree.fromstring(self.response.body)
1012a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        return self.response.tree
1022a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
1032a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    def set_basic_auth(self, username, password):
1042a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        """Set basic authentication"""
1052a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        auth = 'Basic %s' % base64.encodestring('%s:%s' % (username, password)).strip()
1062a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        self._username = username
1072a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        self._password = password
1082a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        self.headers['Authorization'] = auth
1092a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
1102a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    ## HTTP DAV methods ##
1112a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
1122a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    def get(self, path, headers=None):
1132a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        """Simple get request"""
1142a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        self._request('GET', path, headers=headers)
1152a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        return self.response.body
1162a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
1172a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    def head(self, path, headers=None):
1182a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        """Basic HEAD request"""
1192a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        self._request('HEAD', path, headers=headers)
1202a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
1212a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    def put(self, path, body=None, f=None, headers=None):
1222a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        """Put resource with body"""
1232a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        if f is not None:
1242a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            body = f.read()
1252a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
1262a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        self._request('PUT', path, body=body, headers=headers)
1272a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
1282a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    def post(self, path, body=None, headers=None):
1292a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        """POST resource with body"""
1302a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
1312a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        self._request('POST', path, body=body, headers=headers)
1322a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
1332a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    def mkcol(self, path, headers=None):
1342a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        """Make DAV collection"""
1352a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        self._request('MKCOL', path=path, headers=headers)
1362a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
1372a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    make_collection = mkcol
1382a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
1392a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    def delete(self, path, headers=None):
1402a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        """Delete DAV resource"""
1412a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        self._request('DELETE', path=path, headers=headers)
1422a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
1432a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    def copy(self, source, destination, body=None, depth='infinity', overwrite=True, headers=None):
1442a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        """Copy DAV resource"""
1452a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        # Set all proper headers
1462a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        if headers is None:
1472a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            headers = {'Destination':destination}
1482a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        else:
1492a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            headers['Destination'] = self._url.geturl() + destination
1502a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        if overwrite is False:
1512a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            headers['Overwrite'] = 'F'
1522a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        headers['Depth'] = depth
1532a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
1542a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        self._request('COPY', source, body=body, headers=headers)
1552a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
1562a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
1572a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    def copy_collection(self, source, destination, depth='infinity', overwrite=True, headers=None):
1582a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        """Copy DAV collection"""
1592a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        body = '<?xml version="1.0" encoding="utf-8" ?><d:propertybehavior xmlns:d="DAV:"><d:keepalive>*</d:keepalive></d:propertybehavior>'
1602a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
1612a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        # Add proper headers
1622a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        if headers is None:
1632a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            headers = {}
1642a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        headers['Content-Type'] = 'text/xml; charset="utf-8"'
1652a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
1662a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        self.copy(source, destination, body=unicode(body, 'utf-8'), depth=depth, overwrite=overwrite, headers=headers)
1672a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
1682a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
1692a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    def move(self, source, destination, body=None, depth='infinity', overwrite=True, headers=None):
1702a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        """Move DAV resource"""
1712a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        # Set all proper headers
1722a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        if headers is None:
1732a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            headers = {'Destination':destination}
1742a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        else:
1752a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            headers['Destination'] = self._url.geturl() + destination
1762a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        if overwrite is False:
1772a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            headers['Overwrite'] = 'F'
1782a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        headers['Depth'] = depth
1792a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
1802a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        self._request('MOVE', source, body=body, headers=headers)
1812a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
1822a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
1832a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    def move_collection(self, source, destination, depth='infinity', overwrite=True, headers=None):
1842a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        """Move DAV collection and copy all properties"""
1852a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        body = '<?xml version="1.0" encoding="utf-8" ?><d:propertybehavior xmlns:d="DAV:"><d:keepalive>*</d:keepalive></d:propertybehavior>'
1862a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
1872a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        # Add proper headers
1882a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        if headers is None:
1892a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            headers = {}
1902a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        headers['Content-Type'] = 'text/xml; charset="utf-8"'
1912a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
1922a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        self.move(source, destination, unicode(body, 'utf-8'), depth=depth, overwrite=overwrite, headers=headers)
1932a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
1942a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
1952a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    def propfind(self, path, properties='allprop', namespace='DAV:', depth=None, headers=None):
1962a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        """Property find. If properties arg is unspecified it defaults to 'allprop'"""
1972a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        # Build propfind xml
1982a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        root = ElementTree.Element('{DAV:}propfind')
1992a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        if type(properties) is str:
2002a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            ElementTree.SubElement(root, '{DAV:}%s' % properties)
2012a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        else:
2022a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            props = ElementTree.SubElement(root, '{DAV:}prop')
2032a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            object_to_etree(props, properties, namespace=namespace)
2042a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        tree = ElementTree.ElementTree(root)
2052a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
2062a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        # Etree won't just return a normal string, so we have to do this
2072a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        body = StringIO.StringIO()
2082a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        tree.write(body)
2092a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        body = body.getvalue()
2102a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
2112a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        # Add proper headers
2122a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        if headers is None:
2132a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            headers = {}
2142a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        if depth is not None:
2152a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            headers['Depth'] = depth
2162a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        headers['Content-Type'] = 'text/xml; charset="utf-8"'
2172a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
2182a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        # Body encoding must be utf-8, 207 is proper response
2192a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        self._request('PROPFIND', path, body=unicode('<?xml version="1.0" encoding="utf-8" ?>\n'+body, 'utf-8'), headers=headers)
2202a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
2212a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        if self.response is not None and hasattr(self.response, 'tree') is True:
2222a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            property_responses = {}
2232a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            for response in self.response.tree._children:
2242a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)                property_href = response.find('{DAV:}href')
2252a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)                property_stat = response.find('{DAV:}propstat')
2262a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
2272a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)                def parse_props(props):
2282a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)                    property_dict = {}
2292a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)                    for prop in props:
2302a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)                        if prop.tag.find('{DAV:}') is not -1:
2312a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)                            name = prop.tag.split('}')[-1]
2322a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)                        else:
2332a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)                            name = prop.tag
2342a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)                        if len(prop._children) is not 0:
2352a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)                            property_dict[name] = parse_props(prop._children)
2362a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)                        else:
2372a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)                            property_dict[name] = prop.text
2382a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)                    return property_dict
2392a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
2402a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)                if property_href is not None and property_stat is not None:
2412a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)                    property_dict = parse_props(property_stat.find('{DAV:}prop')._children)
2422a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)                    property_responses[property_href.text] = property_dict
2432a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            return property_responses
2442a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
2452a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    def proppatch(self, path, set_props=None, remove_props=None, namespace='DAV:', headers=None):
2462a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        """Patch properties on a DAV resource. If namespace is not specified the DAV namespace is used for all properties"""
2472a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        root = ElementTree.Element('{DAV:}propertyupdate')
2482a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
2492a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        if set_props is not None:
2502a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            prop_set = ElementTree.SubElement(root, '{DAV:}set')
2512a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            object_to_etree(prop_set, set_props, namespace=namespace)
2522a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        if remove_props is not None:
2532a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            prop_remove = ElementTree.SubElement(root, '{DAV:}remove')
2542a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            object_to_etree(prop_remove, remove_props, namespace=namespace)
2552a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
2562a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        tree = ElementTree.ElementTree(root)
2572a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
2582a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        # Add proper headers
2592a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        if headers is None:
2602a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            headers = {}
2612a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        headers['Content-Type'] = 'text/xml; charset="utf-8"'
2622a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
2632a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        self._request('PROPPATCH', path, body=unicode('<?xml version="1.0" encoding="utf-8" ?>\n'+body, 'utf-8'), headers=headers)
2642a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
2652a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
2662a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    def set_lock(self, path, owner, locktype='exclusive', lockscope='write', depth=None, headers=None):
2672a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        """Set a lock on a dav resource"""
2682a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        root = ElementTree.Element('{DAV:}lockinfo')
2692a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        object_to_etree(root, {'locktype':locktype, 'lockscope':lockscope, 'owner':{'href':owner}}, namespace='DAV:')
2702a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        tree = ElementTree.ElementTree(root)
2712a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
2722a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        # Add proper headers
2732a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        if headers is None:
2742a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            headers = {}
2752a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        if depth is not None:
2762a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            headers['Depth'] = depth
2772a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        headers['Content-Type'] = 'text/xml; charset="utf-8"'
2782a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        headers['Timeout'] = 'Infinite, Second-4100000000'
2792a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
2802a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        self._request('LOCK', path, body=unicode('<?xml version="1.0" encoding="utf-8" ?>\n'+body, 'utf-8'), headers=headers)
2812a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
2822a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        locks = self.response.etree.finall('.//{DAV:}locktoken')
2832a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        lock_list = []
2842a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        for lock in locks:
2852a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            lock_list.append(lock.getchildren()[0].text.strip().strip('\n'))
2862a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        return lock_list
2872a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
2882a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
2892a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    def refresh_lock(self, path, token, headers=None):
2902a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        """Refresh lock with token"""
2912a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
2922a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        if headers is None:
2932a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            headers = {}
2942a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        headers['If'] = '(<%s>)' % token
2952a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        headers['Timeout'] = 'Infinite, Second-4100000000'
2962a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
2972a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        self._request('LOCK', path, body=None, headers=headers)
2982a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
2992a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
3002a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    def unlock(self, path, token, headers=None):
3012a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        """Unlock DAV resource with token"""
3022a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        if headers is None:
3032a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            headers = {}
3042a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        headers['Lock-Tocken'] = '<%s>' % token
3052a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
3062a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        self._request('UNLOCK', path, body=None, headers=headers)
3072a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
3082a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
3092a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
3102a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
3112a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
3122a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
313