Browse Source

Set hreferences for calendar items, fixing the PUT and DELETE requests.

Guillaume Ayoub 16 năm trước cách đây
mục cha
commit
a45ca25df9
3 tập tin đã thay đổi với 167 bổ sung121 xóa
  1. 8 6
      radicale/__init__.py
  2. 115 83
      radicale/ical.py
  3. 44 32
      radicale/xmlutils.py

+ 8 - 6
radicale/__init__.py

@@ -153,8 +153,9 @@ class CalendarHTTPHandler(server.BaseHTTPRequestHandler):
     @check_rights
     def do_DELETE(self):
         """Manage DELETE request."""
-        obj = self.headers.get("If-Match", None)
-        answer = xmlutils.delete(obj, self._calendar, self.path)
+        # TODO: Check etag before deleting
+        etag = self.headers.get("If-Match", None)
+        answer = xmlutils.delete(self.path, self._calendar)
 
         self.send_response(client.NO_CONTENT)
         self.send_header("Content-Length", len(answer))
@@ -171,7 +172,7 @@ class CalendarHTTPHandler(server.BaseHTTPRequestHandler):
     def do_PROPFIND(self):
         """Manage PROPFIND request."""
         xml_request = self.rfile.read(int(self.headers["Content-Length"]))
-        answer = xmlutils.propfind(xml_request, self._calendar, self.path)
+        answer = xmlutils.propfind(self.path, xml_request, self._calendar)
 
         self.send_response(client.MULTI_STATUS)
         self.send_header("DAV", "1, calendar-access")
@@ -182,10 +183,11 @@ class CalendarHTTPHandler(server.BaseHTTPRequestHandler):
     @check_rights
     def do_PUT(self):
         """Manage PUT request."""
+        # TODO: Check etag before putting
+        etag = self.headers.get("If-Match", None)
         ical_request = self._decode(
             self.rfile.read(int(self.headers["Content-Length"])))
-        obj = self.headers.get("If-Match", None)
-        xmlutils.put(ical_request, self._calendar, self.path, obj)
+        xmlutils.put(self.path, ical_request, self._calendar)
 
         self.send_response(client.CREATED)
 
@@ -193,7 +195,7 @@ class CalendarHTTPHandler(server.BaseHTTPRequestHandler):
     def do_REPORT(self):
         """Manage REPORT request."""
         xml_request = self.rfile.read(int(self.headers["Content-Length"]))
-        answer = xmlutils.report(xml_request, self._calendar, self.path)
+        answer = xmlutils.report(self.path, xml_request, self._calendar)
 
         self.send_response(client.MULTI_STATUS)
         self.send_header("Content-Length", len(answer))

+ 115 - 83
radicale/ical.py

@@ -42,67 +42,91 @@ def open(path, mode="r"):
 # pylint: enable-msg=W0622
 
 
-def serialize(headers=(), timezones=(), events=(), todos=()):
-    items = ["BEGIN:VCALENDAR"]
-    for part in (headers, timezones, todos, events):
+def serialize(headers=(), items=()):
+    """Return an iCal text corresponding to given ``headers`` and ``items``."""
+    lines = ["BEGIN:VCALENDAR"]
+    for part in (headers, items):
         if part:
-            items.append("\n".join(item.text for item in part))
-    items.append("END:VCALENDAR")
-    return "\n".join(items)
+            lines.append("\n".join(item.text for item in part))
+    lines.append("END:VCALENDAR")
+    return "\n".join(lines)
 
 
-class Header(object):
-    """Internal header class."""
-    def __init__(self, text):
-        """Initialize header from ``text``."""
+class Item(object):
+    """Internal iCal item."""
+    def __init__(self, text, name=None):
+        """Initialize object from ``text`` and different ``kwargs``."""
         self.text = text
+        self._name = name
+
+        # We must synchronize the name in the text and in the object.
+        # An item must have a name, determined in order by:
+        #
+        # - the ``name`` parameter
+        # - the ``X-RADICALE-NAME`` iCal property (for Events and Todos)
+        # - the ``UID`` iCal property (for Events and Todos)
+        # - the ``TZID`` iCal property (for Timezones)
+        if not self._name:
+            for line in self.text.splitlines():
+                if line.startswith("X-RADICALE-NAME:"):
+                    self._name = line.replace("X-RADICALE-NAME:", "").strip()
+                    break
+                elif line.startswith("TZID:"):
+                    self._name = line.replace("TZID:", "").strip()
+                    break
+                elif line.startswith("UID:"):
+                    self._name = line.replace("UID:", "").strip()
+                    # Do not break, a ``X-RADICALE-NAME`` can appear next
+
+        if "\nX-RADICALE-NAME:" in text:
+            for line in self.text.splitlines():
+                if line.startswith("X-RADICALE-NAME:"):
+                    self.text = self.text.replace(
+                        line, "X-RADICALE-NAME:%s" % self._name)
+        else:
+            self.text = self.text.replace(
+                "\nUID:", "\nX-RADICALE-NAME:%s\nUID:" % self._name)
 
+    @property
+    def etag(self):
+        """Item etag.
 
-class Event(object):
-    """Internal event class."""
-    tag = "VEVENT"
+        Etag is mainly used to know if an item has changed.
 
-    def __init__(self, text):
-        """Initialize event from ``text``."""
-        self.text = text
+        """
+        return '"%s"' % hash(self.text)
 
     @property
-    def etag(self):
-        """Etag from event."""
-        return '"%s"' % hash(self.text)
+    def name(self):
+        """Item name.
+
+        Name is mainly used to give an URL to the item.
+
+        """
+        return self._name
+
+
+class Header(Item):
+    """Internal header class."""
+
+
+class Event(Item):
+    """Internal event class."""
+    tag = "VEVENT"
 
 
-class Todo(object):
+class Todo(Item):
     """Internal todo class."""
     # This is not a TODO!
     # pylint: disable-msg=W0511
     tag = "VTODO"
     # pylint: enable-msg=W0511
 
-    def __init__(self, text):
-        """Initialize todo from ``text``."""
-        self.text = text
-
-    @property
-    def etag(self):
-        """Etag from todo."""
-        return '"%s"' % hash(self.text)
-
 
-class Timezone(object):
+class Timezone(Item):
     """Internal timezone class."""
     tag = "VTIMEZONE"
 
-    def __init__(self, text):
-        """Initialize timezone from ``text``."""
-        lines = text.splitlines()
-        for line in lines:
-            if line.startswith("TZID:"):
-                self.name = line.replace("TZID:", "")
-                break
-
-        self.text = text
-
 
 class Calendar(object):
     """Internal calendar class."""
@@ -115,81 +139,84 @@ class Calendar(object):
         self.ctag = self.etag
 
     @staticmethod
-    def _parse(text, obj):
-        """Find ``obj.tag`` items in ``text`` text.
+    def _parse(text, item_types, name=None):
+        """Find items with type in ``item_types`` in ``text`` text.
+
+        If ``name`` is given, give this name to new items in ``text``.
 
-        Return a list of items of type ``obj``.
+        Return a list of items.
 
         """
+        item_tags = {}
+        for item_type in item_types:
+            item_tags[item_type.tag] = item_type
+
         items = []
 
         lines = text.splitlines()
         in_item = False
-        item_lines = []
 
         for line in lines:
-            if line.startswith("BEGIN:%s" % obj.tag):
-                in_item = True
-                item_lines = []
+            if line.startswith("BEGIN:") and not in_item:
+                item_tag = line.replace("BEGIN:", "").strip()
+                if item_tag in item_tags:
+                    in_item = True
+                    item_lines = []
 
             if in_item:
                 item_lines.append(line)
-                if line.startswith("END:%s" % obj.tag):
-                    items.append(obj("\n".join(item_lines)))
+                if line.startswith("END:%s" % item_tag):
+                    in_item = False
+                    item_type = item_tags[item_tag]
+                    item_text = "\n".join(item_lines)
+                    item_name = None if item_tag == "VTIMEZONE" else name
+                    items.append(item_type(item_text, item_name))
 
         return items
 
-    def append(self, text):
-        """Append ``text`` to calendar."""
-        self.ctag = self.etag
+    def append(self, name, text):
+        """Append items from ``text`` to calendar.
 
-        timezones = self.timezones
-        events = self.events
-        todos = self.todos
+        If ``name`` is given, give this name to new items in ``text``.
 
-        for new_timezone in self._parse(text, Timezone):
-            if new_timezone.name not in [timezone.name
-                                         for timezone in timezones]:
-                timezones.append(new_timezone)
+        """
+        self.ctag = self.etag
 
-        for new_event in self._parse(text, Event):
-            if new_event.etag not in [event.etag for event in events]:
-                events.append(new_event)
+        items = self.items
 
-        for new_todo in self._parse(text, Todo):
-            if new_todo.etag not in [todo.etag for todo in todos]:
-                todos.append(new_todo)
+        for new_item in self._parse(text, (Timezone, Event, Todo), name):
+            if new_item.name not in (item.name for item in items):
+                items.append(new_item)
 
-        self.write(timezones=timezones, events=events, todos=todos)
+        self.write(items=items)
 
-    def remove(self, etag):
-        """Remove object named ``etag`` from the calendar."""
+    def remove(self, name):
+        """Remove object named ``name`` from calendar."""
         self.ctag = self.etag
-        todos = [todo for todo in self.todos if todo.etag != etag]
-        events = [event for event in self.events if event.etag != etag]
+        todos = [todo for todo in self.todos if todo.name != name]
+        events = [event for event in self.events if event.name != name]
 
-        self.write(todos=todos, events=events)
+        items = self.timezones + todos + events
+        self.write(items=items)
 
-    def replace(self, etag, text):
-        """Replace objet named ``etag`` by ``text`` in the calendar."""
+    def replace(self, name, text):
+        """Replace content by ``text`` in objet named ``name`` in calendar."""
         self.ctag = self.etag
-        self.remove(etag)
-        self.append(text)
+        self.remove(name)
+        self.append(name, text)
 
-    def write(self, headers=None, timezones=None, events=None, todos=None):
+    def write(self, headers=None, items=None):
         """Write calendar with given parameters."""
         headers = headers or self.headers or (
             Header("PRODID:-//Radicale//NONSGML Radicale Server//EN"),
             Header("VERSION:2.0"))
-        timezones = timezones or self.timezones
-        events = events or self.events
-        todos = todos or self.todos
+        items = items or self.items
 
         # Create folder if absent
         if not os.path.exists(os.path.dirname(self.path)):
             os.makedirs(os.path.dirname(self.path))
         
-        text = serialize(headers, timezones, events, todos)
+        text = serialize(headers, items)
         return open(self.path, "w").write(text)
 
     @property
@@ -220,17 +247,22 @@ class Calendar(object):
 
         return header_lines
 
+    @property
+    def items(self):
+        """Get list of all items in calendar."""
+        return self._parse(self.text, (Event, Todo, Timezone))
+
     @property
     def events(self):
         """Get list of ``Event`` items in calendar."""
-        return self._parse(self.text, Event)
+        return self._parse(self.text, (Event,))
 
     @property
     def todos(self):
         """Get list of ``Todo`` items in calendar."""
-        return self._parse(self.text, Todo)
+        return self._parse(self.text, (Todo,))
 
     @property
     def timezones(self):
         """Get list of ``Timezome`` items in calendar."""
-        return self._parse(self.text, Timezone)
+        return self._parse(self.text, (Timezone,))

+ 44 - 32
radicale/xmlutils.py

@@ -50,14 +50,19 @@ def _response(code):
     return "HTTP/1.1 %i %s" % (code, client.responses[code])
 
 
-def delete(obj, calendar, url):
+def _name_from_path(path):
+    """Return Radicale item name from ``path``."""
+    return path.split("/")[-1]
+
+
+def delete(path, calendar):
     """Read and answer DELETE requests.
 
     Read rfc4918-9.6 for info.
 
     """
     # Reading request
-    calendar.remove(obj)
+    calendar.remove(_name_from_path(path))
 
     # Writing answer
     multistatus = ET.Element(_tag("D", "multistatus"))
@@ -65,7 +70,7 @@ def delete(obj, calendar, url):
     multistatus.append(response)
 
     href = ET.Element(_tag("D", "href"))
-    href.text = url
+    href.text = path
     response.append(href)
 
     status = ET.Element(_tag("D", "status"))
@@ -74,7 +79,8 @@ def delete(obj, calendar, url):
 
     return ET.tostring(multistatus, config.get("encoding", "request"))
 
-def propfind(xml_request, calendar, url):
+
+def propfind(path, xml_request, calendar):
     """Read and answer PROPFIND requests.
 
     Read rfc4918-9.1 for info.
@@ -93,7 +99,7 @@ def propfind(xml_request, calendar, url):
     multistatus.append(response)
 
     href = ET.Element(_tag("D", "href"))
-    href.text = url
+    href.text = path
     response.append(href)
 
     propstat = ET.Element(_tag("D", "propstat"))
@@ -133,17 +139,19 @@ def propfind(xml_request, calendar, url):
 
     return ET.tostring(multistatus, config.get("encoding", "request"))
 
-def put(ical_request, calendar, url, obj):
+
+def put(path, ical_request, calendar):
     """Read PUT requests."""
-    # TODO: use url to set hreference
-    if obj:
-        # PUT is modifying obj
-        calendar.replace(obj, ical_request)
+    name = _name_from_path(path)
+    if name in (item.name for item in calendar.items):
+        # PUT is modifying an existing item
+        calendar.replace(name, ical_request)
     else:
-        # PUT is adding a new object
-        calendar.append(ical_request)
+        # PUT is adding a new item
+        calendar.append(name, ical_request)
+
 
-def report(xml_request, calendar, url):
+def report(path, xml_request, calendar):
     """Read and answer REPORT requests.
 
     Read rfc3253-3.6 for info.
@@ -158,41 +166,45 @@ def report(xml_request, calendar, url):
 
     if root.tag == _tag("C", "calendar-multiget"):
         # Read rfc4791-7.9 for info
-        hreferences = set([href_element.text for href_element
-                           in root.findall(_tag("D", "href"))])
+        hreferences = set((href_element.text for href_element
+                           in root.findall(_tag("D", "href"))))
     else:
-        hreferences = [url]
+        hreferences = (path,)
 
     # Writing answer
     multistatus = ET.Element(_tag("D", "multistatus"))
 
-    # TODO: WTF, sunbird needs one response by object,
-    #       is that really what is needed?
-    #       Read rfc4791-9.[6|10] for info
     for hreference in hreferences:
-        objects = calendar.events + calendar.todos
-
-        if not objects:
+        # Check if the reference is an item or a calendar
+        name = hreference.split("/")[-1]
+        if name:
+            # Reference is an item
+            path = "/".join(hreference.split("/")[:-1]) + "/"
+            items = (item for item in calendar.items if item.name == name)
+        else:
+            # Reference is a calendar
+            path = hreference
+            items = calendar.events + calendar.todos
+
+        if not items:
             # TODO: Read rfc4791-9.[6|10] to find a right answer
             response = ET.Element(_tag("D", "response"))
             multistatus.append(response)
 
             href = ET.Element(_tag("D", "href"))
-            href.text = url
+            href.text = path
             response.append(href)
 
             status = ET.Element(_tag("D", "status"))
             status.text = _response(204)
             response.append(status)
 
-        for obj in objects:
-            # TODO: Use the hreference to read data and create href.text
-            #       We assume here that hreference is url
+        for item in items:
             response = ET.Element(_tag("D", "response"))
             multistatus.append(response)
 
             href = ET.Element(_tag("D", "href"))
-            href.text = url
+            href.text = path + item.name
             response.append(href)
 
             propstat = ET.Element(_tag("D", "propstat"))
@@ -203,17 +215,17 @@ def report(xml_request, calendar, url):
 
             if _tag("D", "getetag") in props:
                 element = ET.Element(_tag("D", "getetag"))
-                element.text = obj.etag
+                element.text = item.etag
                 prop.append(element)
 
             if _tag("C", "calendar-data") in props:
                 element = ET.Element(_tag("C", "calendar-data"))
-                if isinstance(obj, ical.Event):
+                if isinstance(item, ical.Event):
                     element.text = ical.serialize(
-                        calendar.headers, calendar.timezones, events=[obj])
-                elif isinstance(obj, ical.Todo):
+                        calendar.headers, calendar.timezones + [item])
+                elif isinstance(item, ical.Todo):
                     element.text = ical.serialize(
-                        calendar.headers, calendar.timezones, todos=[obj])
+                        calendar.headers, calendar.timezones + [item])
                 prop.append(element)
 
             status = ET.Element(_tag("D", "status"))