implement principal resources
[jelmer/calypso.git] / calypso / xmlutils.py
index 3db1d326a7903909bf4493e7661a7232766b3617..10b39bf349660b28e54e89201bae005bf0254560 100644 (file)
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# This file is part of Calypso Server - Calendar Server
+# This file is part of Calypso - CalDAV/CardDAV/WebDAV Server
 # Copyright © 2011 Keith Packard
 # Copyright © 2008-2011 Guillaume Ayoub
 # Copyright © 2008 Nicolas Kandel
@@ -36,16 +36,20 @@ import dateutil.rrule
 import dateutil.tz
 import datetime
 import email.utils
-import urllib
+import logging
 
-from . import client, config, ical
+from . import client, config, webdav, paths, principal
 
+__package__ = 'calypso.xmlutils'
 
 NAMESPACES = {
     "C": "urn:ietf:params:xml:ns:caldav",
+    "A": "urn:ietf:params:xml:ns:carddav",
     "D": "DAV:",
+    "E": "http://apple.com/ns/ical/",
     "CS": "http://calendarserver.org/ns/"}
 
+log = logging.getLogger(__name__)
 
 def _tag(short_name, local):
     """Get XML Clark notation {uri(``short_name``)}``local``."""
@@ -56,20 +60,14 @@ def _response(code):
     """Return full W3C names from HTTP status codes."""
     return "HTTP/1.1 %i %s" % (code, client.responses[code])
 
-
-def name_from_path(path):
-    """Return Calypso item name from ``path``."""
-    path_parts = path.strip("/").split("/")
-    return urllib.unquote(path_parts[-1]) if len(path_parts) > 2 else None
-
-def delete(path, calendar):
+def delete(path, collection, context):
     """Read and answer DELETE requests.
 
     Read rfc4918-9.6 for info.
 
     """
     # Reading request
-    calendar.remove(name_from_path(path))
+    collection.remove(paths.resource_from_path(path), context=context)
 
     # Writing answer
     multistatus = ET.Element(_tag("D", "multistatus"))
@@ -86,46 +84,114 @@ def delete(path, calendar):
 
     return ET.tostring(multistatus, config.get("encoding", "request"))
 
+def identify_resource(path):
+    """Return a Resource object corresponding to the path (this is used for
+    everything that is not a collection, like Principal and HomeSet objects)"""
+
+    try:
+        left, right = config.get('server', 'user_principal').split('%(user)s')
+    except ValueError:
+        raise ValueError("user_principal setting must contain %(user)s.")
+
+    if not path.startswith(left):
+        return None
+
+    remainder = path[len(left):]
+    if right not in remainder:
+        return None
+
+    username = remainder[:remainder.index(right)]
+    remainder = remainder[remainder.index(right)+len(right):]
 
-def propfind(path, xml_request, calendar, depth):
+    if remainder == principal.AddressbookHomeSet.type_dependent_suffix + "/":
+        return principal.AddressbookHomeSet(username)
+    elif remainder == principal.CalendarHomeSet.type_dependent_suffix + "/":
+        return principal.CalendarHomeSet(username)
+    elif remainder == "":
+        return principal.Principal(username)
+    else:
+        return None
+
+def propfind(path, xml_request, collection, depth, context):
     """Read and answer PROPFIND requests.
 
     Read rfc4918-9.1 for info.
 
     """
-    # Reading request
-    root = ET.fromstring(xml_request)
 
-    prop_element = root.find(_tag("D", "prop"))
+    item_name = paths.resource_from_path(path)
+
+    if xml_request:
+        # Reading request
+        root = ET.fromstring(xml_request)
+
+        prop_element = root.find(_tag("D", "prop"))
+    else:
+        prop_element = None
+
     if prop_element is not None:
         prop_list = prop_element.getchildren()
         props = [prop.tag for prop in prop_list]
     else:
-        props = None
+        props = [_tag("D", "resourcetype"),
+                 _tag("D", "owner"),
+                 _tag("D", "getcontenttype"),
+                 _tag("D", "getetag"),
+                 _tag("D", "principal-collection-set"),
+                 _tag("C", "supported-calendar-component-set"),
+                 _tag("D", "supported-report-set"),
+                 _tag("D", "current-user-privilege-set"),
+                 _tag("D", "getcontentlength"),
+                 _tag("D", "getlastmodified")]
 
     
     # Writing answer
     multistatus = ET.Element(_tag("D", "multistatus"))
 
-    if calendar:
-        if depth == "0":
-            items = [calendar]
+    resource = identify_resource(path)
+
+    if resource is not None:
+        items = resource.propfind_children(depth)
+    elif collection:
+        if item_name:
+            item = collection.get_item(item_name)
+            print "item_name %s item %s" % (item_name, item)
+            if item:
+                items = [item]
+            else:
+                items = []
         else:
-            # depth is 1, infinity or not specified
-            # we limit ourselves to depth == 1
-            items = [calendar] + calendar.items
-#            items = [calendar]
+            if depth == "0":
+                items = [collection]
+            else:
+                # depth is 1, infinity or not specified
+                # we limit ourselves to depth == 1
+                items = [collection] + collection.items
     else:
         items = []
 
     for item in items:
-        is_calendar = isinstance(item, ical.Calendar)
+        is_collection = isinstance(item, webdav.Collection)
+        is_resource = isinstance(item, principal.Resource)
+
+        if is_collection:
+            # parentcollectionhack. this is not the way to do it, but much of
+            # the below code relies on items which are collection members to
+            # have their parent collection in the collection variable. get rid
+            # of the collection propfind-global variable, and this and all
+            # other occurrences of "parentcollectionhack" can be dropped.
+            collection = item
 
         response = ET.Element(_tag("D", "response"))
         multistatus.append(response)
 
         href = ET.Element(_tag("D", "href"))
-        href.text = path if is_calendar else path + item.name
+        if is_collection:
+            href.text = item.urlpath
+        elif is_resource:
+            href.text = item.urlpath
+        else:
+            href.text = collection.urlpath + item.name
         response.append(href)
 
         propstat = ET.Element(_tag("D", "propstat"))
@@ -136,24 +202,30 @@ def propfind(path, xml_request, calendar, depth):
 
         for tag in props:
             element = ET.Element(tag)
-            if tag == _tag("D", "resourcetype") and is_calendar:
-                tag = ET.Element(_tag("C", "calendar"))
-                element.append(tag)
+            if tag == _tag("D", "resourcetype") and is_collection:
+                if collection.is_calendar:
+                    tag = ET.Element(_tag("C", "calendar"))
+                    element.append(tag)
+                if collection.is_addressbook:
+                    tag = ET.Element(_tag("A", "addressbook"))
+                    element.append(tag)
                 tag = ET.Element(_tag("D", "collection"))
                 element.append(tag)
             elif tag == _tag("D", "owner"):
-                element.text = calendar.owner
+                element.text = collection.owner
             elif tag == _tag("D", "getcontenttype"):
                 if item.tag == 'VCARD':
                     element.text = "text/vcard"
                 else:
                     element.text = "text/calendar"
-            elif tag == _tag("CS", "getctag") and is_calendar:
+            elif tag == _tag("CS", "getctag") and is_collection:
                 element.text = item.ctag
             elif tag == _tag("D", "getetag"):
                 element.text = item.etag
-            elif tag == _tag("D", "displayname") and is_calendar:
-                element.text = calendar.name
+            elif tag == _tag("D", "displayname") and is_collection:
+                element.text = collection.name
+            elif tag == _tag("E", "calendar-color") and is_collection:
+                element.text = collection.color
             elif tag == _tag("D", "principal-URL"):
                 # TODO: use a real principal URL, read rfc3744-4.2 for info
                 tag = ET.Element(_tag("D", "href"))
@@ -162,8 +234,8 @@ def propfind(path, xml_request, calendar, depth):
             elif tag in (
                 _tag("D", "principal-collection-set"),
                 _tag("C", "calendar-user-address-set"),
-                _tag("C", "calendar-home-set"),
-                _tag("C", "addressbook-home-set")):
+                ):
+                # not meaningfully implemented yet
                 tag = ET.Element(_tag("D", "href"))
                 tag.text = path
                 element.append(tag)
@@ -189,6 +261,13 @@ def propfind(path, xml_request, calendar, depth):
 #                element.text = time.strftime("%a, %d %b %Y %H:%M:%S +0000", item.last_modified)
 #                element.text = email.utils.formatdate(item.last_modified)
                 element.text = email.utils.formatdate(time.mktime(item.last_modified))
+            elif tag == _tag("D", "current-user-principal"):
+                tag = ET.Element(_tag("D", "href"))
+                tag.text = config.get("server", "user_principal") % context
+                element.append(tag)
+            elif tag in (_tag("A", "addressbook-description"),
+                         _tag("C", "calendar-description")) and is_collection:
+                element.text = collection.get_description()
             prop.append(element)
 
         status = ET.Element(_tag("D", "status"))
@@ -198,15 +277,18 @@ def propfind(path, xml_request, calendar, depth):
     return ET.tostring(multistatus, config.get("encoding", "request"))
 
 
-def put(path, ical_request, calendar):
+def put(path, webdav_request, collection, context):
     """Read PUT requests."""
-    name = name_from_path(path)
-    if name in (item.name for item in calendar.items):
+    name = paths.resource_from_path(path)
+    log.debug('xmlutils put path %s name %s', path, name)
+    if name in (item.name for item in collection.items):
         # PUT is modifying an existing item
-        calendar.replace(name, ical_request)
+        log.debug('Replacing item named %s', name)
+        return collection.replace(name, webdav_request, context=context)
     else:
         # PUT is adding a new item
-        calendar.append(name, ical_request)
+        log.debug('Putting a new item, because name %s is not known', name)
+        return collection.append(name, webdav_request, context=context)
 
 
 def match_filter_element(vobject, fe):
@@ -237,6 +319,19 @@ def match_filter_element(vobject, fe):
             return False
         start = fe.get("start")
         end = fe.get("end")
+        # According to RFC 4791, one of start and stop must be set,
+        # but the other can be empty.  If both are empty, the
+        # specification is violated.
+        if start is None and end is None:
+            msg = "time-range missing both start and stop attribute (required by RFC 4791)"
+            log.error(msg)
+            raise ValueError(msg)
+        # RFC 4791 state if start is missing, assume it is -infinity
+        if start is None:
+            start = "00010101T000000Z"  # start of year one
+        # RFC 4791 state if end is missing, assume it is +infinity
+        if end is None:
+            end = "99991231T235959Z"  # last date with four digit year
         if rruleset is None:
             rruleset = dateutil.rrule.rruleset()
             dtstart = vobject.dtstart.value
@@ -275,8 +370,9 @@ def match_filter(item, filter):
     for fe in filter.getchildren():
         if match_filter_element(item.object, fe):
             return True
+    return False
 
-def report(path, xml_request, calendar):
+def report(path, xml_request, collection):
     """Read and answer REPORT requests.
 
     Read rfc3253-3.6 for info.
@@ -291,8 +387,8 @@ def report(path, xml_request, calendar):
 
     filter_element = root.find(_tag("C", "filter"))
 
-    if calendar:
-        if root.tag == _tag("C", "calendar-multiget"):
+    if collection:
+        if root.tag == _tag("C", "calendar-multiget") or root.tag == _tag('A', 'addressbook-multiget'):
             # Read rfc4791-7.9 for info
             hreferences = set((href_element.text for href_element
                                in root.findall(_tag("D", "href"))))
@@ -305,16 +401,16 @@ def report(path, xml_request, calendar):
     multistatus = ET.Element(_tag("D", "multistatus"))
 
     for hreference in hreferences:
-        # Check if the reference is an item or a calendar
-        name = name_from_path(hreference)
+        # Check if the reference is an item or a collection
+        name = paths.resource_from_path(hreference)
         if name:
             # Reference is an item
-            path = "/".join(hreference.split("/")[:-1]) + "/"
-            items = (item for item in calendar.items if item.name == name)
+            path = paths.collection_from_path(hreference) + "/"
+            items = (item for item in collection.items if item.name == name)
         else:
-            # Reference is a calendar
+            # Reference is a collection
             path = hreference
-            items = calendar.items
+            items = collection.items
 
         
         for item in items:
@@ -325,7 +421,7 @@ def report(path, xml_request, calendar):
             multistatus.append(response)
 
             href = ET.Element(_tag("D", "href"))
-            href.text = path + item.name
+            href.text = path.rstrip('/') + '/' + item.name
             response.append(href)
 
             propstat = ET.Element(_tag("D", "propstat"))
@@ -340,6 +436,8 @@ def report(path, xml_request, calendar):
                     element.text = item.etag
                 elif tag == _tag("C", "calendar-data"):
                     element.text = item.text
+                elif tag == _tag("A", "address-data"):
+                    element.text = item.text
                 prop.append(element)
 
             status = ET.Element(_tag("D", "status"))