sogo/Tests/Integration/webdavlib.py

581 lines
19 KiB
Python

# webdavlib.py - A versatile WebDAV Python Library
#
# Copyright (C) 2009, 2010 Inverse inc.
#
# Author: Wolfgang Sourdeau <wsourdeau@inverse.ca>
#
# webdavlib is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation; either version 2, or (at your option) any later
# version.
#
# webdavlib is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with webdavlib; see the file COPYING. If not, write to the Free
# Software Foundation, 59 Temple Place - Suite 330, Boston, MA 02111-1307,
# USA.
import httplib
import re
import time
import xml.dom.expatbuilder
import xml.etree.cElementTree
import xml.sax.saxutils
import sys
xmlns_dav = "DAV:"
xmlns_caldav = "urn:ietf:params:xml:ns:caldav"
xmlns_carddav = "urn:ietf:params:xml:ns:carddav"
xmlns_inversedav = "urn:inverse:params:xml:ns:inverse-dav"
url_re = None
class HTTPUnparsedURL:
def __init__(self, url):
self._parse(url)
def _parse(self, url):
# ((proto)://((username(:(password)?)@)?hostname(:(port))))(path)?
# if url_re is None:
url_parts = url.split("?")
alpha_match = "[a-zA-Z0-9%\._-]+"
num_match = "[0-9]+"
pattern = ("((%s)://(((%s)(:(%s))?@)?(%s)(:(%s))?))?(/.*)"
% (alpha_match, alpha_match, alpha_match,
alpha_match, num_match))
url_re = re.compile(pattern)
re_match = url_re.match(url_parts[0])
if re_match is None:
raise Exception, "URL expression could not be parsed: %s" % url
(trash, self.protocol, trash, trash, self.username, trash,
self.password, self.hostname, trash, self.port, self.path) = re_match.groups()
self.parameters = {}
if len(url_parts) > 1:
param_elms = url_parts[1].split("&")
for param_pair in param_elms:
parameter = param_pair.split("=")
self.parameters[parameter[0]] = parameter[1]
class WebDAVClient:
user_agent = "Mozilla/5.0"
def __init__(self, hostname, port, username = None, password = "",
forcessl = False):
if int(port) == 443 or forcessl:
import M2Crypto.httpslib
self.conn = M2Crypto.httpslib.HTTPSConnection(hostname, int(port),
True)
else:
self.conn = httplib.HTTPConnection(hostname, port, True)
if username is None:
self.simpleauth_hash = None
else:
self.simpleauth_hash = (("%s:%s" % (username, password))
.encode('base64')[:-1])
def prepare_headers(self, query, body):
headers = { "User-Agent": self.user_agent }
if self.simpleauth_hash is not None:
headers["authorization"] = "Basic %s" % self.simpleauth_hash
if body is not None:
headers["content-length"] = len(body)
if query.__dict__.has_key("depth") and query.depth is not None:
headers["depth"] = query.depth
if query.__dict__.has_key("content_type"):
headers["content-type"] = query.content_type
if not query.__dict__.has_key("accept-language"):
headers["accept-language"] = 'en-us,en;q=0.5'
query_headers = query.prepare_headers()
if query_headers is not None:
for key in query_headers.keys():
headers[key] = query_headers[key]
return headers
def execute(self, query):
body = query.render()
query.start = time.time()
self.conn.request(query.method, query.url,
body, self.prepare_headers(query, body))
try:
query.set_response(self.conn.getresponse());
except httplib.BadStatusLine, e:
print e
query.set_response(self.conn.getresponse());
query.duration = time.time() - query.start
class HTTPSimpleQuery:
method = None
def __init__(self, url):
self.url = url
self.response = None
self.start = -1
self.duration = -1
def prepare_headers(self):
return {}
def render(self):
return None
def set_response(self, http_response):
headers = {}
for rk, rv in http_response.getheaders():
k = rk.lower()
headers[k] = rv
self.response = { "headers": headers,
"status": http_response.status,
"version": http_response.version,
"body": http_response.read() }
class HTTPGET(HTTPSimpleQuery):
method = "GET"
cookie = None
def prepare_headers (self):
headers = HTTPSimpleQuery.prepare_headers(self)
if self.cookie:
headers["Cookie"] = self.cookie
return headers
class HTTPOPTIONS(HTTPSimpleQuery):
method = "OPTIONS"
class HTTPQuery(HTTPSimpleQuery):
def __init__(self, url):
HTTPSimpleQuery.__init__(self, url)
self.content_type = "application/octet-stream"
class HTTPPUT(HTTPQuery):
method = "PUT"
def __init__(self, url, content,
content_type="application/octet-stream",
exclusive=False):
HTTPQuery.__init__(self, url)
self.content = content
self.content_type = content_type
self.exclusive = exclusive
def render(self):
return self.content
def prepare_headers(self):
headers = HTTPQuery.prepare_headers(self)
if self.exclusive:
headers["if-none-match"] = "*"
return headers
class HTTPPOST(HTTPPUT):
method = "POST"
cookie = None
def prepare_headers (self):
headers = HTTPPUT.prepare_headers(self)
if self.cookie:
headers["Cookie"] = self.cookie
return headers
class WebDAVQuery(HTTPQuery):
method = None
def __init__(self, url, depth = None):
HTTPQuery.__init__(self, url)
self.content_type = "application/xml; charset=\"utf-8\""
self.depth = depth
self.ns_mgr = _WD_XMLNS_MGR()
self.top_node = None
# helper for PROPFIND and REPORT (only)
def _initProperties(self, properties):
props = _WD_XMLTreeElement("prop")
self.top_node.append(props)
for prop in properties:
prop_tag = self.render_tag(prop)
props.append(_WD_XMLTreeElement(prop_tag))
def render(self):
if self.top_node is not None:
text = ("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n%s"
% self.top_node.render(self.ns_mgr.render()))
else:
text = ""
return text
def render_tag(self, tag):
cb = tag.find("}")
if cb > -1:
ns = tag[1:cb]
real_tag = tag[cb+1:]
new_tag = self.ns_mgr.register(real_tag, ns)
else:
new_tag = tag
return new_tag
def set_response(self, http_response):
HTTPQuery.set_response(self, http_response)
headers = self.response["headers"]
if (headers.has_key("content-type")
and headers.has_key("content-length")
and (headers["content-type"].startswith("application/xml")
or headers["content-type"].startswith("text/xml"))
and int(headers["content-length"]) > 0):
document = xml.etree.cElementTree.fromstring(self.response["body"])
self.response["document"] = document
class WebDAVMKCOL(WebDAVQuery):
method = "MKCOL"
class WebDAVDELETE(WebDAVQuery):
method = "DELETE"
class WebDAVREPORT(WebDAVQuery):
method = "REPORT"
class WebDAVGET(WebDAVQuery):
method = "GET"
class WebDAVPROPFIND(WebDAVQuery):
method = "PROPFIND"
def __init__(self, url, properties, depth = None):
WebDAVQuery.__init__(self, url, depth)
self.top_node = _WD_XMLTreeElement("propfind")
if properties is not None and len(properties) > 0:
self._initProperties(properties)
class WebDAVPROPPATCH(WebDAVQuery):
method = "PROPPATCH"
# <x0:propertyupdate xmlns:x1="urn:ietf:params:xml:ns:caldav" xmlns:x0="DAV:"><x0:set><x0:prop>
def __init__(self, url, properties):
WebDAVQuery.__init__(self, url, None)
self.top_node = _WD_XMLTreeElement("propertyupdate")
set_node = _WD_XMLTreeElement("set")
self.top_node.append(set_node)
prop_node = _WD_XMLTreeElement("prop")
set_node.append(prop_node)
prop_node.appendSubtree(self, properties)
class WebDAVMOVE(WebDAVQuery):
method = "MOVE"
destination = None
host = None
def prepare_headers(self):
headers = WebDAVQuery.prepare_headers(self)
print "DESTINATION", self.destination
if self.destination is not None:
headers["Destination"] = self.destination
if self.host is not None:
headers["Host"] = self.host
return headers
class WebDAVPrincipalPropertySearch(WebDAVREPORT):
def __init__(self, url, properties, matches):
WebDAVQuery.__init__(self, url)
ppsearch_tag = self.ns_mgr.register("principal-property-search",
xmlns_dav)
self.top_node = _WD_XMLTreeElement(ppsearch_tag)
self._initMatches(matches)
if properties is not None and len(properties) > 0:
self._initProperties(properties)
def _initMatches(self, matches):
for match in matches:
psearch = _WD_XMLTreeElement("property-search")
self.top_node.append(psearch)
prop = _WD_XMLTreeElement("prop")
psearch.append(prop)
match_tag = self.render_tag(match[0])
prop.append(_WD_XMLTreeElement(match_tag))
match_tag = _WD_XMLTreeElement("match")
psearch.append(match_tag)
match_tag.appendSubtree(self, match[1])
class WebDAVSyncQuery(WebDAVREPORT):
def __init__(self, url, token, properties):
WebDAVQuery.__init__(self, url)
self.top_node = _WD_XMLTreeElement("sync-collection")
sync_token = _WD_XMLTreeElement("sync-token")
self.top_node.append(sync_token)
if token is not None:
sync_token.append(_WD_XMLTreeTextNode(token))
if properties is not None and len(properties) > 0:
self._initProperties(properties)
class WebDAVExpandProperty(WebDAVREPORT):
def _parseTag(self, tag):
result = []
cb = tag.find("}")
if cb > -1:
result.append(tag[cb+1:])
result.append(tag[1:cb])
else:
result.append(tag)
result.append("DAV:")
return result;
def _propElement(self, tag):
parsedTag = self._parseTag(tag)
parameters = { "name": parsedTag[0] }
if len(parsedTag) > 1:
parameters["namespace"] = parsedTag[1]
return _WD_XMLTreeElement("property", parameters)
def __init__(self, url, query_properties, properties):
WebDAVQuery.__init__(self, url)
self.top_node = _WD_XMLTreeElement("expand-property")
for query_tag in query_properties:
property_query = self._propElement(query_tag)
self.top_node.append(property_query)
for tag in properties:
property = self._propElement(tag)
property_query.append(property)
class CalDAVPOST(WebDAVQuery):
method = "POST"
def __init__(self, url, content,
originator = None, recipients = None):
WebDAVQuery.__init__(self, url)
self.content_type = "text/calendar; charset=utf-8"
self.originator = originator
self.recipients = recipients
self.content = content
def prepare_headers(self):
headers = WebDAVQuery.prepare_headers(self)
if self.originator is not None:
headers["originator"] = self.originator
if self.recipients is not None:
headers["recipient"] = ",".join(self.recipients)
return headers
def render(self):
return self.content
class CalDAVCalendarMultiget(WebDAVREPORT):
def __init__(self, url, properties, hrefs, depth = None):
WebDAVQuery.__init__(self, url, depth)
multiget_tag = self.ns_mgr.register("calendar-multiget", xmlns_caldav)
self.top_node = _WD_XMLTreeElement(multiget_tag)
if properties is not None and len(properties) > 0:
self._initProperties(properties)
for href in hrefs:
href_node = _WD_XMLTreeElement("href")
self.top_node.append(href_node)
href_node.append(_WD_XMLTreeTextNode(href))
class CalDAVCalendarQuery(WebDAVREPORT):
def __init__(self, url, properties, component = None, timerange = None):
WebDAVQuery.__init__(self, url)
multiget_tag = self.ns_mgr.register("calendar-query", xmlns_caldav)
self.top_node = _WD_XMLTreeElement(multiget_tag)
if properties is not None and len(properties) > 0:
self._initProperties(properties)
if component is not None:
filter_tag = self.ns_mgr.register("filter",
xmlns_caldav)
compfilter_tag = self.ns_mgr.register("comp-filter",
xmlns_caldav)
filter_node = _WD_XMLTreeElement(filter_tag)
cal_filter_node = _WD_XMLTreeElement(compfilter_tag,
{ "name": "VCALENDAR" })
comp_node = _WD_XMLTreeElement(compfilter_tag,
{ "name": component })
## TODO
# if timerange is not None:
cal_filter_node.append(comp_node)
filter_node.append(cal_filter_node)
self.top_node.append(filter_node)
class CardDAVAddressBookQuery(WebDAVREPORT):
def __init__(self, url, properties, searchProperty = None, searchValue = None):
WebDAVQuery.__init__(self, url)
query_tag = self.ns_mgr.register("addressbook-query", xmlns_carddav)
ns_key = self.ns_mgr.xmlns[xmlns_carddav]
self.top_node = _WD_XMLTreeElement(query_tag)
if properties is not None and len(properties) > 0:
self._initProperties(properties)
if searchProperty is not None:
filter_node = _WD_XMLTreeElement("%s:filter" % ns_key)
self.top_node.append(filter_node)
propfilter_node = _WD_XMLTreeElement("%s:prop-filter" % ns_key, { "name": searchProperty })
filter_node.append(propfilter_node)
match_node = _WD_XMLTreeElement("%s:text-match" % ns_key,
{ "collation": "i;unicasemap", "match-type": "starts-with" })
propfilter_node.append(match_node)
match_node.appendSubtree(None, searchValue)
class MailDAVMailQuery(WebDAVREPORT):
def __init__(self, url, properties, filters = None,
sort = None, ascending = True):
WebDAVQuery.__init__(self, url)
mailquery_tag = self.ns_mgr.register("mail-query",
xmlns_inversedav)
self.top_node = _WD_XMLTreeElement(mailquery_tag)
if properties is not None and len(properties) > 0:
self._initProperties(properties)
if filters is not None and len(filters) > 0:
self._initFilters(filters)
if sort is not None and len(sort) > 0:
self._initSort(sort, ascending)
def _initFilters(self, filters):
mailfilter_tag = self.ns_mgr.register("mail-filters",
xmlns_inversedav)
mailfilter_node = _WD_XMLTreeElement(mailfilter_tag)
self.top_node.append(mailfilter_node)
for filterk in filters.keys():
filter_tag = self.ns_mgr.register(filterk,
xmlns_inversedav)
filter_node = _WD_XMLTreeElement(filter_tag,
filters[filterk])
mailfilter_node.append(filter_node)
def _initSort(self, sort, ascending):
sort_tag = self.ns_mgr.register("sort", xmlns_inversedav)
if ascending:
sort_attrs = { "order": "ascending" }
else:
sort_attrs = { "order": "descending" }
sort_node = _WD_XMLTreeElement(sort_tag, sort_attrs)
self.top_node.append(sort_node)
for item in sort:
sort_subnode = _WD_XMLTreeElement(self.render_tag(item))
sort_node.append(sort_subnode)
# private classes to handle XML stuff
class _WD_XMLNS_MGR:
def __init__(self):
self.xmlns = {}
self.counter = 0
def render(self):
text = " xmlns=\"DAV:\""
for k in self.xmlns:
text = text + " xmlns:%s=\"%s\"" % (self.xmlns[k], k)
return text
def create_key(self, namespace):
new_nssym = "n%d" % self.counter
self.counter = self.counter + 1
self.xmlns[namespace] = new_nssym
return new_nssym
def register(self, tag, namespace):
if namespace != xmlns_dav:
if self.xmlns.has_key(namespace):
key = self.xmlns[namespace]
else:
key = self.create_key(namespace)
else:
key = None
if key is not None:
newTag = "%s:%s" % (key, tag)
else:
newTag = tag
return newTag
class _WD_XMLTreeElement:
typeNum = type(0)
typeStr = type("")
typeUnicode = type(u"")
typeList = type([])
typeDict = type({})
def __init__(self, tag, attributes = {}):
self.tag = tag
self.children = []
self.attributes = attributes
def append(self, child):
self.children.append(child)
def appendSubtree(self, query, subtree):
if type(subtree) == self.typeNum:
strValue = "%d" % subtree
textNode = _WD_XMLTreeTextNode(strValue)
self.append(textNode)
elif type(subtree) == self.typeUnicode:
textNode = _WD_XMLTreeTextNode(subtree.encode("utf-8"))
self.append(textNode)
elif type(subtree) == self.typeStr:
textNode = _WD_XMLTreeTextNode(subtree)
self.append(textNode)
elif type(subtree) == self.typeList:
for x in subtree:
self.appendSubtree(query, x)
elif type(subtree) == self.typeDict:
for x in subtree.keys():
tag = query.render_tag(x)
node = _WD_XMLTreeElement(tag)
node.appendSubtree(query, subtree[x])
self.append(node)
else:
None
def render(self, ns_text = None):
text = "<" + self.tag
if ns_text is not None:
text = text + ns_text
for k in self.attributes:
text = text + " %s=\"%s\"" % (k, self.attributes[k])
if len(self.children) > 0:
text = text + ">"
for child in self.children:
text = text + child.render()
text = text + "</" + self.tag + ">"
else:
text = text + "/>"
return text
class _WD_XMLTreeTextNode:
def __init__(self, text):
self.text = xml.sax.saxutils.escape(text)
def render(self):
return self.text