Also catch ValueError when attempting to parse rrulset.
[jelmer/calypso.git] / calypso / xmlutils.py
index 6ebae47dcd32d425db90af1b179095c87143ce7e..fc59fe01ce0d0f104875e6ef007386885bf2719d 100644 (file)
@@ -1,6 +1,7 @@
 # -*- 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
 # Copyright © 2008 Pascal Halter
@@ -28,41 +29,35 @@ in them for XML requests (all but PUT).
 """
 
 import xml.etree.ElementTree as ET
+import time
+import dateutil
+import dateutil.parser
+import dateutil.rrule
+import dateutil.tz
+import datetime
+import email.utils
+import logging
 
-import urllib
+from . import client, config, webdav, paths, principal
+from .xmlutils_generic import _tag
 
-from calypso import client, config, ical
+__package__ = 'calypso.xmlutils'
 
-
-NAMESPACES = {
-    "C": "urn:ietf:params:xml:ns:caldav",
-    "D": "DAV:",
-    "CS": "http://calendarserver.org/ns/"}
-
-
-def _tag(short_name, local):
-    """Get XML Clark notation {uri(``short_name``)}``local``."""
-    return "{%s}%s" % (NAMESPACES[short_name], local)
+log = logging.getLogger(__name__)
 
 
 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"))
@@ -79,41 +74,76 @@ def delete(path, calendar):
 
     return ET.tostring(multistatus, config.get("encoding", "request"))
 
-
-def propfind(path, xml_request, calendar, depth):
+def propfind(path, xml_request, collection, resource, 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"))
-    prop_list = prop_element.getchildren()
-    props = [prop.tag for prop in prop_list]
+    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 = [_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]
+    if resource is not None:
+        items = resource.propfind_children(depth, context)
+    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
+            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)
 
         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"))
@@ -124,21 +154,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"):
-                element.text = "text/calendar"
-            elif tag == _tag("CS", "getctag") and is_calendar:
-                element.text = item.etag
+                if item.tag == 'VCARD':
+                    element.text = "text/vcard"
+                else:
+                    element.text = "text/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"))
@@ -147,7 +186,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")):
+                ):
+                # not meaningfully implemented yet
                 tag = ET.Element(_tag("D", "href"))
                 tag.text = path
                 element.append(tag)
@@ -158,10 +198,28 @@ def propfind(path, xml_request, calendar, depth):
                 comp = ET.Element(_tag("C", "comp"))
                 comp.set("name", "VEVENT")
                 element.append(comp)
+            elif tag == _tag("D", "supported-report-set"):
+                tag = ET.Element(_tag("C", "calendar-multiget"))
+                element.append(tag)
+                tag = ET.Element(_tag("C", "filter"))
+                element.append(tag)
             elif tag == _tag("D", "current-user-privilege-set"):
                 privilege = ET.Element(_tag("D", "privilege"))
                 privilege.append(ET.Element(_tag("D", "all")))
                 element.append(privilege)
+            elif tag == _tag("D", "getcontentlength"):
+                element.text = item.length
+            elif tag == _tag("D", "getlastmodified"):
+#                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"))
@@ -171,26 +229,102 @@ 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)
-    print ("put %s" % name)
-    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)
-
-
-def match(item, filter):
-    if not filter:
+        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):
+    if fe.tag == _tag("C", "comp-filter"):
+        comp = fe.get("name")
+        if comp:
+            if comp == vobject.name:
+                hassub = False
+                submatch = False
+                for fc in fe.getchildren():
+                    if match_filter_element(vobject, fc):
+                        submatch = True
+                        break
+                    for vc in vobject.getChildren():
+                        hassub = True
+                        if match_filter_element (vc, fc):
+                            submatch = True
+                            break
+                    if submatch:
+                        break
+                if not hassub or submatch:
+                    return True
+        return False
+    elif fe.tag == _tag("C", "time-range"):
+        try:
+            rruleset = vobject.rruleset
+        except (AttributeError, ValueError):
+            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
+            try:
+                dtstart = datetime.datetime.combine(dtstart, datetime.time())
+            except Exception:
+                0
+            if dtstart.tzinfo is None:
+                dtstart = dtstart.replace(tzinfo = dateutil.tz.tzlocal())
+            rruleset.rdate(dtstart)
+        start_datetime = dateutil.parser.parse(start)
+        if start_datetime.tzinfo is None:
+            start_datetime = start_datetime.replace(tzinfo = dateutil.tz.tzlocal())
+        end_datetime = dateutil.parser.parse(end)
+        if end_datetime.tzinfo is None:
+            end_datetime = end_datetime.replace(tzinfo = dateutil.tz.tzlocal())
+        try:
+            if rruleset.between(start_datetime, end_datetime, True):
+                return True
+        except TypeError:
+            start_datetime = start_datetime.replace(tzinfo = None)
+            end_datetime = end_datetime.replace(tzinfo = None)
+            try:
+                if rruleset.between(start_datetime, end_datetime, True):
+                    return True
+            except TypeError:
+                return True
+        return False
+    return True
+
+def match_filter(item, filter):
+    if filter is None:
         return True
-    for fe in filter.iter():
-        print ("filter element %s\n" % fe)
-    
+    if filter.tag != _tag("C", "filter"):
+        return True
+    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.
@@ -204,11 +338,9 @@ def report(path, xml_request, calendar):
     props = [prop.tag for prop in prop_list]
 
     filter_element = root.find(_tag("C", "filter"))
-    if filter_element:
-        print ("filter: %s\n" % filter_element.getchildren())
 
-    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"))))
@@ -221,25 +353,27 @@ 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:
+            if not match_filter(item, filter_element):
+                continue
+
             response = ET.Element(_tag("D", "response"))
             multistatus.append(response)
 
-#            print ("Returning from path %s" % item.path)
             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"))
@@ -254,6 +388,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"))
@@ -261,5 +397,5 @@ def report(path, xml_request, calendar):
             propstat.append(status)
 
     reply = ET.tostring(multistatus, config.get("encoding", "request"))
-    # print ("Report returns %s" % reply)
+        
     return reply