diff --git a/radicale/xmlutils.py b/radicale/xmlutils.py
index fb75dff..fb06aa5 100644
--- a/radicale/xmlutils.py
+++ b/radicale/xmlutils.py
@@ -117,6 +117,108 @@ def _href(collection, href):
href.lstrip("/"))
+def _comp_match(item, filter_, scope="collection"):
+ """Check whether the ``item`` matches the comp ``filter_``.
+
+ If ``scope`` is ``"collection"``, the filter is applied on the
+ item's collection. Otherwise, it's applied on the item.
+
+ See rfc4791-9.7.1.
+
+ """
+ filter_length = len(filter_)
+ if scope == "collection":
+ tag = item.collection.get_meta("tag")
+ else:
+ for component in item.components():
+ if component.name in ("VTODO", "VEVENT", "VJOURNAL"):
+ tag = component.name
+ if filter_length == 0:
+ # Point #1 of rfc4791-9.7.1
+ return filter_.get("name") == tag
+ else:
+ if filter_length == 1:
+ if filter_[0].tag == _tag("C", "is-not-defined"):
+ # Point #2 of rfc4791-9.7.1
+ return filter_.get("name") != tag
+ if filter_[0].tag == _tag("C", "time-range"):
+ # Point #3 of rfc4791-9.7.1
+ if not _time_range_match(item, filter_[0]):
+ return False
+ filter_.remove(filter_[0])
+ # Point #4 of rfc4791-9.7.1
+ return all(
+ _prop_match(item, child) if child.tag == _tag("C", "prop-filter")
+ else _comp_match(item, child, scope="component")
+ for child in filter_)
+
+
+def _prop_match(item, filter_):
+ """Check whether the ``item`` matches the prop ``filter_``.
+
+ See rfc4791-9.7.2 and rfc6352-10.5.1.
+
+ """
+ filter_length = len(filter_)
+ if item.collection.get_meta("tag") == "VCALENDAR":
+ for component in item.components():
+ if component.name in ("VTODO", "VEVENT", "VJOURNAL"):
+ vobject_item = component
+ else:
+ vobject_item = item.item
+ if filter_length == 0:
+ # Point #1 of rfc4791-9.7.2
+ return filter_.get("name").lower() in vobject_item.contents
+ else:
+ if filter_length == 1:
+ if filter_[0].tag == _tag("C", "is-not-defined"):
+ # Point #2 of rfc4791-9.7.2
+ return filter_.get("name").lower() not in vobject_item.contents
+ if filter_[0].tag == _tag("C", "time-range"):
+ # Point #3 of rfc4791-9.7.2
+ if not _time_range_match(item, filter_[0]):
+ return False
+ filter_.remove(filter_[0])
+ elif filter_[0].tag == _tag("C", "text-match"):
+ # Point #4 of rfc4791-9.7.2
+ if not _text_match(item, filter_[0]):
+ return False
+ filter_.remove(filter_[0])
+ return all(
+ _param_filter_match(item, param_filter)
+ for param_filter in filter_)
+
+
+def _time_range_match(item, _filter):
+ """Check whether the ``item`` matches the time-range ``filter_``.
+
+ See rfc4791-9.9.
+
+ """
+ # TODO: implement this
+ return True
+
+
+def _text_match(item, _filter):
+ """Check whether the ``item`` matches the text-match ``filter_``.
+
+ See rfc4791-9.7.3.
+
+ """
+ # TODO: implement this
+ return True
+
+
+def _param_filter_match(item, _filter):
+ """Check whether the ``item`` matches the param-filter ``filter_``.
+
+ See rfc4791-9.7.3.
+
+ """
+ # TODO: implement this
+ return True
+
+
def name_from_path(path, collection):
"""Return Radicale item name from ``path``."""
collection_parts = collection.path.strip("/").split("/")
@@ -491,16 +593,11 @@ def report(path, xml_request, collection):
hreferences.add(href_path[len(base_prefix) - 1:])
else:
hreferences = (path,)
- # TODO: handle other filters
- # TODO: handle the nested comp-filters correctly
- # Read rfc4791-9.7.1 for info
- tag_filters = set(
- element.get("name").upper() for element
- in root.findall(".//%s" % _tag("C", "comp-filter")))
- tag_filters.discard("VCALENDAR")
+ filters = (
+ root.findall(".//%s" % _tag("C", "filter")) +
+ root.findall(".//%s" % _tag("CR", "filter")))
else:
- hreferences = ()
- tag_filters = set()
+ hreferences = filters = ()
# Writing answer
multistatus = ET.Element(_tag("D", "multistatus"))
@@ -526,10 +623,12 @@ def report(path, xml_request, collection):
items = [collection.get(href) for href, etag in collection.list()]
for item in items:
- if (tag_filters and
- item.name not in tag_filters and
- not {tag.upper() for tag in item.contents} & tag_filters):
- continue
+ if filters:
+ match = (
+ _comp_match if collection.get_meta("tag") == "VCALENDAR"
+ else _prop_match)
+ if not all(match(item, filter_[0]) for filter_ in filters):
+ continue
found_props = []
not_found_props = []
diff --git a/tests/test_base.py b/tests/test_base.py
index a02fd53..131ce3a 100644
--- a/tests/test_base.py
+++ b/tests/test_base.py
@@ -93,6 +93,129 @@ class BaseRequests:
status, headers, answer = self.request("GET", "/calendar.ics/")
assert "VEVENT" not in answer
+ def _test_filter(self, filters):
+ filters_text = "".join(
+ "%s" % filter_ for filter_ in filters)
+ self.request(
+ "PUT", "/calendar.ics/", "BEGIN:VCALENDAR\r\nEND:VCALENDAR")
+ event = get_file_content("event.ics")
+ self.request("PUT", "/calendar.ics/event.ics", event)
+ status, headers, answer = self.request(
+ "REPORT", "/calendar.ics",
+ """
+
+
+
+
+ %s
+ """ % filters_text)
+ return answer
+
+ def test_calendar_tag_filter(self):
+ """Report request with tag-based filter on calendar."""
+ assert "href>/calendar.ics/event.ics" in self._test_filter(["""
+ """])
+
+ def test_item_tag_filter(self):
+ """Report request with tag-based filter on an item."""
+ assert "href>/calendar.ics/event.ics" in self._test_filter(["""
+
+
+ """])
+ assert "href>/calendar.ics/event.ics" not in self._test_filter(["""
+
+
+ """])
+
+ def test_item_not_tag_filter(self):
+ """Report request with tag-based is-not filter on an item."""
+ assert "href>/calendar.ics/event.ics" not in self._test_filter(["""
+
+
+
+
+ """])
+ assert "href>/calendar.ics/event.ics" in self._test_filter(["""
+
+
+
+
+ """])
+
+ def test_item_prop_filter(self):
+ """Report request with prop-based filter on an item."""
+ assert "href>/calendar.ics/event.ics" in self._test_filter(["""
+
+
+
+
+ """])
+ assert "href>/calendar.ics/event.ics" not in self._test_filter(["""
+
+
+
+
+ """])
+
+ def test_item_not_prop_filter(self):
+ """Report request with prop-based is-not filter on an item."""
+ assert "href>/calendar.ics/event.ics" not in self._test_filter(["""
+
+
+
+
+
+
+ """])
+ assert "href>/calendar.ics/event.ics" in self._test_filter(["""
+
+
+
+
+
+
+ """])
+
+ def test_mutiple_filters(self):
+ """Report request with multiple filters on an item."""
+ assert "href>/calendar.ics/event.ics" not in self._test_filter(["""
+
+
+
+
+
+
+ """, """
+
+
+
+
+
+
+ """])
+ assert "href>/calendar.ics/event.ics" in self._test_filter(["""
+
+
+
+
+ """, """
+
+
+
+
+
+
+ """])
+ assert "href>/calendar.ics/event.ics" in self._test_filter(["""
+
+
+
+
+
+
+
+ """])
+
class TestMultiFileSystem(BaseRequests, BaseTest):
"""Base class for filesystem tests."""