#
# DAV client library
#

import httplib
import string
import md5
import base64
import types
import mimetypes
import xml.dom.core
import xml.dom.walker
import xml.dom.sax_builder
import xml.sax.saxexts

error = 'davlib.error'

INFINITY = 'infinity'
XML_DOC_HEADER = '<?xml version="1.0" encoding="utf-8"?>'

# block size for copying files up to the server
BLOCKSIZE = 16384


class HTTPConnectionAuth(httplib.HTTPConnection):
  def __init__(self, *args, **kw):
    apply(httplib.HTTPConnection.__init__, (self,) + args, kw)

    self.__username = None
    self.__password = None
    self.__nonce = None
    self.__opaque = None

  def setauth(self, username, password):
    self.__username = username
    self.__password = password

  def getreply(self):
    result = httplib.HTTPConnection.getreply(self)
    if result[0] != 401 or not self.__username:
      return result

    response = result[2]
    challenges = response.getallmatchingheaders('www-authenticate')
    assert challenges, 'HTTP violation: 401 with no WWW-Authenticate hdr'

    ### fill in stuff here...
    return result


_MSR_STATE_ROOT = 'ROOT'
_MSR_STATE_MULTISTATUS = 'MULTISTATUS'
_MSR_STATE_RESPONSE = 'RESPONSE'
_MSR_STATE_PROPSTAT = 'PROPSTAT'
_MSR_STATE_PROP = 'PROP'
_MSR_STATE_PROPVALUE = 'PROPVALUE'

def _textof(elem):
  s = ''
  for node in elem.get_childNodes():
    type = node.get_nodeType()
    if type == xml.dom.core.TEXT_NODE:
      s = s + node.get_nodeValue()
    elif type == xml.dom.core.ENTITY_REFERENCE_NODE:
      s = s + '&' + node.get_nodeName() + ';'
  return s

def _parse_status(elem):
  text = _textof(elem)
  idx1 = string.find(text, ' ')
  idx2 = string.find(text, ' ', idx1+1)
  return int(text[idx1:idx2]), text[idx2+1:]

class _MSR_PropStat:
  def __init__(self):
    self.prop = { }
    self.status = None
    self.responsedescription = None

class _MSR_Response:
  def __init__(self):
    self.href = [ ]
    self.status = None
    self.responsedescription = None
    self.propstat = [ ]

class MultiStatusResponse(xml.dom.walker.Walker):
  def __init__(self, xmldoc):
    self.__state = [ _MSR_STATE_ROOT ]
    self.__ns = [ ]

    self.responses = [ ]
    self.responsedescription = None

    self.walk(xmldoc)

  def __lookup_ns(self, name):
    idx = string.find(name, ':')
    if idx != -1:
      ns = name[:idx]
      name = name[idx+1:]
    else:
      ns = ''	# elem uses default namespace
    for nsmap in self.__ns:
      if nsmap.has_key(ns):
        return name, nsmap[ns]
    if ns:
      raise error, 'unknown namespace prefix: "%s"' % ns
    return name, None	# no namespace

  def startElement(self, node):
    nsmap = { }
    self.__ns.insert(0, nsmap)

    # scan the attributes for namespace declarations
    for key, value in node.get_attributes().items():
      if key[:6] == 'xmlns:':
        nsmap[key[6:]] = value.get_nodeValue()
        node.removeAttribute(key)
      elif key == 'xmlns':
        nsmap[''] = value.get_nodeValue()	# defining default namespace
        node.removeAttribute(key)

    # rescan the attributes to insert namespace information
    for key, value in node.get_attributes().items():
      key, ns_uri = self.__lookup_ns(key)
      ### this doesn't entirely rename the thing... internally, a Node
      ### uses a dict holding the key/value. we aren't modifying that
      ### key -- we're only modifying an attr on the value.
      value._node.name = key
      value._node.namespace = ns_uri

    # modify the element to include namespace information
    tag, ns_uri = self.__lookup_ns(node.get_tagName())
    node._node.name = tag
    node._node.namespace = ns_uri

    state = self.__state[-1]

    if state == _MSR_STATE_PROP:
      self.responses[-1].propstat[-1].prop[(ns_uri, tag)] = node
      state = _MSR_STATE_PROPVALUE
    elif ns_uri == 'DAV:':
      if state == _MSR_STATE_ROOT:
        if tag == 'multistatus':
          state = _MSR_STATE_MULTISTATUS
      elif state == _MSR_STATE_MULTISTATUS:
        if tag == 'responsedescription':
          self.responsedescription = _textof(node)
        elif tag == 'response':
          state = _MSR_STATE_RESPONSE
          self.responses.append(_MSR_Response())
      elif state == _MSR_STATE_RESPONSE:
        if tag == 'href':
          self.responses[-1].href.append(_textof(node))
        elif tag == 'status':
          self.responses[-1].status = _parse_status(node)
        elif tag == 'responsedescription':
          self.responses[-1].responsedescription = _textof(node)
        elif tag == 'propstat':
          state = _MSR_STATE_PROPSTAT
          self.responses[-1].propstat.append(_MSR_PropStat())
      elif state == _MSR_STATE_PROPSTAT:
        if tag == 'prop':
          state = _MSR_STATE_PROP
        elif tag == 'status':
          self.responses[-1].propstat[-1].status = _parse_status(node)
        elif tag == 'responsedescription':
          self.responses[-1].propstat[-1].responsedescription = _textof(node)

    self.__state.append(state)

  def endElement(self, node):
    self.__ns.pop(0)
    self.__state.pop()


class DAVResponse(httplib.HTTPResponse):
  def parse_multistatus(self):
    parser = xml.sax.saxexts.make_parser()
    handler = xml.dom.sax_builder.SaxBuilder()
    parser.setDocumentHandler(handler)
    parser.parseFile(self)
    self.doc = handler.document
    self.msr = MultiStatusResponse(handler.document)


class DAV(HTTPConnectionAuth):

  response_class = DAVResponse

  def get(self, url):
    return self._request('GET', url)

  def head(self, url):
    return self._request('HEAD', url)

  def post(self, url, data={ }, body=None, extra_hdrs={ }):
    headers = { }
    headers.update(extra_hdrs)

    assert body or data, "body or data must be supplied"
    assert not (body and data), "cannot supply both body and data"
    if data:
      body = ''
      for key, value in data.items():
        if type(value) == types.ListType:
          for item in value:
            body = body + '&' + key + '=' + urllib.quote(str(item))
        else:
          body = body + '&' + key + '=' + urllib.quote(str(value))
      body = body[1:]
      headers['Content-Type'] = 'application/x-www-form-urlencoded'

    return self._request('POST', url, body, headers)

  def options(self, url='*'):
    return self._request('OPTIONS', url)

  def trace(self, url):
    return self._request('TRACE', url)

  def put(self, url, contents, content_type=None, content_enc=None):
    if not content_type:
      if type(contents) is types.FileType:
        content_type, content_enc = mimetypes.guess_type(contents.name)
      else:
        content_type, content_enc = mimetypes.guess_type(url)
    headers = { }
    if content_type:
      headers['Content-Type'] = content_type
    if content_enc:
      headers['Content-Encoding'] = content_enc
    return self._request('PUT', url, contents, headers)

  def delete(self, url):
    return self._request('DELETE', url)

  def propfind(self, url, body=None, depth=None):
    extra_hdrs = { 'Content-Type' : 'text/xml; charset="utf-8"' }
    if depth is not None:
      extra_hdrs['Depth'] = str(depth)
    return self._request('PROPFIND', url, body, extra_hdrs)

  def proppatch(self, url, body):
    extra_hdrs = { 'Content-Type' : 'text/xml; charset="utf-8"' }
    return self._request('PROPPATCH', url, body, extra_hdrs)

  def mkcol(self, url):
    return self._request('MKCOL', url)

  def move(self, src, dst):
    return self._request('MOVE', src, extra_hdrs={ 'Destination' : dst })

  def copy(self, src, dst, depth=None):
    extra_hdrs = { 'Destination' : dst }
    if depth is not None:
      extra_hdrs['Depth'] = str(depth)
    return self._request('COPY', src, extra_hdrs=extra_hdrs)

  def lock(self, url):
    raise error, 'Not yet implemented'

  def unlock(self, url):
    raise error, 'Not yet implemented'

  def _request(self, method, url, body=None, extra_hdrs={}):
    "Internal method for sending a request."
    self.putrequest(method, url)

    if body:
      self.putheader('Content-Length', str(len(body)))
    for hdr, value in extra_hdrs.items():
      self.putheader(hdr, value)

    self.endheaders()

    if body:
      if type(body) is types.FileType:
        while 1:
          block = body.read(BLOCKSIZE)
          if not block:
            break
          self.send(block)
      else:
        self.send(body)

    errcode, errmsg, response = self.getreply()
    if errcode == -1:
      raise error, (errmsg, response)

    if errcode == 207:	# Multi Status Response
      response.parse_multistatus()

    response.errcode = errcode
    response.errmsg = errmsg

    return response


  #
  # Higher-level methods for typical client use
  #

  def allprops(self, url, depth=None):
    return self.propfind(url, depth=depth)

  def propnames(self, url, depth=None):
    body = XML_DOC_HEADER + \
           '<DAV:propfind xmlns:DAV="DAV:"><DAV:propname/></DAV:propfind>'
    return self.propfind(url, body, depth)

  def getprops(self, url, *names, **kw):
    assert names, 'at least one property name must be provided'
    if kw.has_key('ns'):
      xmlns = ' xmlns:NS="' + kw['ns'] + '"'
      ns = 'NS:'
      del kw['ns']
    else:
      xmlns = ns = ''
    if kw.has_key('depth'):
      depth = kw['depth']
      del kw['depth']
    else:
      depth = 0
    assert not kw, 'unknown arguments'
    body = XML_DOC_HEADER + \
           '<DAV:propfind xmlns:DAV="DAV:"' + xmlns + '><DAV:prop><' + ns + \
           string.joinfields(names, '/><' + ns) + \
           '/></DAV:prop></DAV:propfind>'
    return self.propfind(url, body, depth)

  def delprops(self, url, *names, **kw):
    assert names, 'at least one property name must be provided'
    if kw.has_key('ns'):
      xmlns = ' xmlns:NS="' + kw['ns'] + '"'
      ns = 'NS:'
      del kw['ns']
    else:
      xmlns = ns = ''
    assert not kw, 'unknown arguments'
    body = XML_DOC_HEADER + \
           '<DAV:propertyupdate xmlns:DAV="DAV:"' + xmlns + \
           '><DAV:remove><DAV:prop><' + ns + \
           string.joinfields(names, '/><' + ns) + \
           '/></DAV:prop></DAV:remove></DAV:propertyupdate>'
    return self.proppatch(url, body)

  def setprops(self, url, *xmlprops, **props):
    assert xmlprops or props, 'at least one property must be provided'
    elems = string.joinfields(xmlprops, '')
    if props.has_key('ns'):
      xmlns = ' xmlns:NS="' + props['ns'] + '"'
      ns = 'NS:'
      del props['ns']
    else:
      xmlns = ns = ''
    for key, value in props.items():
      if value:
        elems = '%s<%s%s>%s</%s%s>' % (elems, ns, key, value, ns, key)
      else:
        elems = '%s<%s%s/>' % (elems, ns, key)
    body = XML_DOC_HEADER + \
           '<DAV:propertyupdate xmlns:DAV="DAV:"' + xmlns + \
           '><DAV:set><DAV:prop>' + \
           elems + \
           '</DAV:prop></DAV:set></DAV:propertyupdate>'
    return self.proppatch(url, body)
