Procházet zdrojové kódy

Merge pull request #1831 from pbiering/fix-1824

Fix 1824
Peter Bieringer před 7 měsíci
rodič
revize
bb84b62c4a

+ 1 - 0
CHANGELOG.md

@@ -10,6 +10,7 @@
 * Fix: report with enabled expand honors now provided filter proper
 * Improve: add options [logging] trace_on_debug and trace_filter for supporting trace logging
 * Fix: catch case where getpwuid is not returning a username
+* Fix: add support for query without comp-type
 
 ## 3.5.4
 * Improve: item filter enhanced for 3rd level supporting VALARM and honoring TRIGGER (offset or absolute)

+ 26 - 0
radicale/item/filter.py

@@ -32,6 +32,7 @@ import vobject
 
 from radicale import item, xmlutils
 from radicale.log import logger
+from radicale.utils import format_ut
 
 DAY: timedelta = timedelta(days=1)
 SECOND: timedelta = timedelta(seconds=1)
@@ -98,6 +99,7 @@ def comp_match(item: "item.Item", filter_: ET.Element, level: int = 0) -> bool:
     # HACK: the filters are tested separately against all components
 
     name = filter_.get("name", "").upper()
+    logger.debug("TRACE/ITEM/FILTER/comp_match: name=%s level=%d", name, level)
 
     if level == 0:
         tag = item.name
@@ -142,13 +144,24 @@ def comp_match(item: "item.Item", filter_: ET.Element, level: int = 0) -> bool:
                 trigger = subcomp.trigger.value
     for child in filter_:
         if child.tag == xmlutils.make_clark("C:prop-filter"):
+            logger.debug("TRACE/ITEM/FILTER/comp_match: prop-filter level=%d", level)
             if not any(prop_match(comp, child, "C")
                        for comp in components):
                 return False
         elif child.tag == xmlutils.make_clark("C:time-range"):
+            logger.debug("TRACE/ITEM/FILTER/comp_match: time-range level=%d tag=%s", level, tag)
+            if (level == 0) and (name == "VCALENDAR"):
+                for name_try in ("VTODO", "VEVENT", "VJOURNAL"):
+                    try:
+                        if time_range_match(item.vobject_item, filter_[0], name_try, trigger):
+                            return True
+                    except Exception:
+                        continue
+                return False
             if not time_range_match(item.vobject_item, filter_[0], tag, trigger):
                 return False
         elif child.tag == xmlutils.make_clark("C:comp-filter"):
+            logger.debug("TRACE/ITEM/FILTER/comp_match: comp-filter level=%d", level)
             if not comp_match(item, child, level=level + 1):
                 return False
         else:
@@ -233,6 +246,7 @@ def time_range_match(vobject_item: vobject.base.Component,
     def infinity_fn(start: datetime) -> bool:
         return False
 
+    logger.debug("TRACE/ITEM/FILTER/time_range_match: start=(%s) end=(%s) child_name=%s", start, end, child_name)
     visit_time_ranges(vobject_item, child_name, range_fn, infinity_fn)
     return matched
 
@@ -289,6 +303,8 @@ def visit_time_ranges(vobject_item: vobject.base.Component, child_name: str,
     # recurrences too. This is not respected and client don't seem to bother
     # either.
 
+    logger.debug("TRACE/ITEM/FILTER/visit_time_ranges: child_name=%s", child_name)
+
     def getrruleset(child: vobject.base.Component, ignore: Sequence[date]
                     ) -> Tuple[Iterable[date], bool]:
         infinite = False
@@ -516,6 +532,7 @@ def visit_time_ranges(vobject_item: vobject.base.Component, child_name: str,
 
     else:
         # Match a property
+        logger.debug("TRACE/ITEM/FILTER/get_children: child_name=%s property match", child_name)
         child = getattr(vobject_item, child_name.lower())
         if isinstance(child.value, date):
             child_is_datetime = isinstance(child.value, datetime)
@@ -605,6 +622,7 @@ def simplify_prefilters(filters: Iterable[ET.Element], collection_tag: str
     """
     flat_filters = list(chain.from_iterable(filters))
     simple = len(flat_filters) <= 1
+    logger.debug("TRACE/ITEM/FILTER/simplify_prefilters: collection_tag=%s", collection_tag)
     for col_filter in flat_filters:
         if collection_tag != "VCALENDAR":
             simple = False
@@ -615,7 +633,14 @@ def simplify_prefilters(filters: Iterable[ET.Element], collection_tag: str
             continue
         simple &= len(col_filter) <= 1
         for comp_filter in col_filter:
+            logger.debug("TRACE/ITEM/FILTER/simplify_prefilters: filter.tag=%s simple=%s", comp_filter.tag, simple)
+            if comp_filter.tag == xmlutils.make_clark("C:time-range") and simple is True:
+                # time-filter found on level 0
+                start, end = time_range_timestamps(comp_filter)
+                logger.debug("TRACE/ITEM/FILTER/simplify_prefilters: found time-filter on level 0 start=%r(%d) end=%r(%d) simple=%s", format_ut(start), start, format_ut(end), end, simple)
+                return None, start, end, simple
             if comp_filter.tag != xmlutils.make_clark("C:comp-filter"):
+                logger.debug("TRACE/ITEM/FILTER/simplify_prefilters: no comp-filter on level 0")
                 simple = False
                 continue
             tag = comp_filter.get("name", "").upper()
@@ -632,6 +657,7 @@ def simplify_prefilters(filters: Iterable[ET.Element], collection_tag: str
                     simple = False
                     continue
                 start, end = time_range_timestamps(time_filter)
+                logger.debug("TRACE/ITEM/FILTER/simplify_prefilters: found time-filter on level 1 tag=%s start=%d end=%d simple=%s", tag, start, end, simple)
                 return tag, start, end, simple
             return tag, TIMESTAMP_MIN, TIMESTAMP_MAX, simple
     return None, TIMESTAMP_MIN, TIMESTAMP_MAX, simple

+ 7 - 1
radicale/storage/__init__.py

@@ -2,7 +2,7 @@
 # Copyright © 2014 Jean-Marc Martins
 # Copyright © 2012-2017 Guillaume Ayoub
 # Copyright © 2017-2022 Unrud <unrud@outlook.com>
-# Copyright © 2024-2024 Peter Bieringer <pb@bieringer.de>
+# Copyright © 2024-2025 Peter Bieringer <pb@bieringer.de>
 #
 # This library is free software: you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -37,6 +37,7 @@ from radicale import item as radicale_item
 from radicale import types, utils
 from radicale.item import filter as radicale_filter
 from radicale.log import logger
+from radicale.utils import format_ut
 
 INTERNAL_TYPES: Sequence[str] = ("multifilesystem", "multifilesystem_nolock",)
 
@@ -153,12 +154,17 @@ class BaseCollection:
             return
         tag, start, end, simple = radicale_filter.simplify_prefilters(
             filters, self.tag)
+        logger.debug("TRACE/STORAGE/get_filtered: prefilter tag=%s start=%s end=%s simple=%s", tag, format_ut(start), format_ut(end), simple)
         for item in self.get_all():
+            logger.debug("TRACE/STORAGE/get_filtered: component_name=%s tag=%s", item.component_name, tag)
             if tag is not None and tag != item.component_name:
                 continue
             istart, iend = item.time_range
+            logger.debug("TRACE/STORAGE/get_filtered: istart=%s iend=%s", format_ut(istart), format_ut(iend))
             if istart >= end or iend <= start:
+                logger.debug("TRACE/STORAGE/get_filtered: skip iuid=%s", item.uid)
                 continue
+            logger.debug("TRACE/STORAGE/get_filtered: add iuid=%s", item.uid)
             yield item, simple and (start <= istart or iend <= end)
 
     def has_uid(self, uid: str) -> bool:

+ 84 - 0
radicale/tests/test_base.py

@@ -1214,6 +1214,90 @@ permissions: RrWw""")
 </C:comp-filter>"""], items=(9,))
         assert "/calendar.ics/event9.ics" not in answer
 
+    def test_time_range_filter_without_comp_filter(self) -> None:
+        """Report request with time-range filter without comp-filter on events."""
+        answer = self._test_filter(["""\
+<C:comp-filter name="VCALENDAR">
+        <C:time-range start="20130801T000000Z" end="20131001T000000Z"/>
+</C:comp-filter>"""], "event", items=range(1, 6))
+        assert "/calendar.ics/event1.ics" in answer
+        assert "/calendar.ics/event2.ics" in answer
+        assert "/calendar.ics/event3.ics" in answer
+        assert "/calendar.ics/event4.ics" in answer
+        assert "/calendar.ics/event5.ics" in answer
+        answer = self._test_filter(["""\
+<C:comp-filter name="VCALENDAR">
+        <C:time-range start="20130902T000000Z" end="20131001T000000Z"/>
+</C:comp-filter>"""], items=range(1, 6))
+        assert "/calendar.ics/event1.ics" not in answer
+        assert "/calendar.ics/event2.ics" in answer
+        assert "/calendar.ics/event3.ics" in answer
+        assert "/calendar.ics/event4.ics" in answer
+        assert "/calendar.ics/event5.ics" in answer
+        answer = self._test_filter(["""\
+<C:comp-filter name="VCALENDAR">
+        <C:time-range start="20130903T000000Z" end="20130908T000000Z"/>
+</C:comp-filter>"""], items=range(1, 6))
+        assert "/calendar.ics/event1.ics" not in answer
+        assert "/calendar.ics/event2.ics" not in answer
+        assert "/calendar.ics/event3.ics" in answer
+        assert "/calendar.ics/event4.ics" in answer
+        assert "/calendar.ics/event5.ics" in answer
+        answer = self._test_filter(["""\
+<C:comp-filter name="VCALENDAR">
+        <C:time-range start="20130903T000000Z" end="20130904T000000Z"/>
+</C:comp-filter>"""], items=range(1, 6))
+        assert "/calendar.ics/event1.ics" not in answer
+        assert "/calendar.ics/event2.ics" not in answer
+        assert "/calendar.ics/event3.ics" in answer
+        assert "/calendar.ics/event4.ics" not in answer
+        assert "/calendar.ics/event5.ics" not in answer
+        answer = self._test_filter(["""\
+<C:comp-filter name="VCALENDAR">
+        <C:time-range start="20130805T000000Z" end="20130810T000000Z"/>
+</C:comp-filter>"""], items=range(1, 6))
+        assert "/calendar.ics/event1.ics" not in answer
+        assert "/calendar.ics/event2.ics" not in answer
+        assert "/calendar.ics/event3.ics" not in answer
+        assert "/calendar.ics/event4.ics" not in answer
+        assert "/calendar.ics/event5.ics" not in answer
+        # HACK: VObject doesn't match RECURRENCE-ID to recurrences, the
+        # overwritten recurrence is still used for filtering.
+        answer = self._test_filter(["""\
+<C:comp-filter name="VCALENDAR">
+        <C:time-range start="20170601T063000Z" end="20170601T070000Z"/>
+</C:comp-filter>"""], items=(6, 7, 8, 9))
+        assert "/calendar.ics/event6.ics" in answer
+        assert "/calendar.ics/event7.ics" in answer
+        assert "/calendar.ics/event8.ics" in answer
+        assert "/calendar.ics/event9.ics" in answer
+        answer = self._test_filter(["""\
+<C:comp-filter name="VCALENDAR">
+        <C:time-range start="20170701T060000Z"/>
+</C:comp-filter>"""], items=(6, 7, 8, 9))
+        assert "/calendar.ics/event6.ics" in answer
+        assert "/calendar.ics/event7.ics" in answer
+        assert "/calendar.ics/event8.ics" in answer
+        assert "/calendar.ics/event9.ics" not in answer
+        answer = self._test_filter(["""\
+<C:comp-filter name="VCALENDAR">
+        <C:time-range start="20170702T070000Z" end="20170704T060000Z"/>
+</C:comp-filter>"""], items=(6, 7, 8, 9))
+        assert "/calendar.ics/event6.ics" not in answer
+        assert "/calendar.ics/event7.ics" not in answer
+        assert "/calendar.ics/event8.ics" not in answer
+        assert "/calendar.ics/event9.ics" not in answer
+        answer = self._test_filter(["""\
+<C:comp-filter name="VCALENDAR">
+        <C:time-range start="20170602T075959Z" end="20170602T080000Z"/>
+</C:comp-filter>"""], items=(9,))
+        assert "/calendar.ics/event9.ics" in answer
+        answer = self._test_filter(["""\
+<C:comp-filter name="VCALENDAR">
+        <C:time-range start="20170602T080000Z" end="20170603T083000Z"/>
+</C:comp-filter>"""], items=(9,))
+        assert "/calendar.ics/event9.ics" not in answer
+
     def test_time_range_filter_events_rrule(self) -> None:
         """Report request with time-range filter on events with rrules."""
         answer = self._test_filter(["""\

+ 20 - 0
radicale/utils.py

@@ -17,6 +17,7 @@
 # You should have received a copy of the GNU General Public License
 # along with Radicale.  If not, see <http://www.gnu.org/licenses/>.
 
+import datetime
 import os
 import ssl
 import sys
@@ -46,6 +47,10 @@ ADDRESS_TYPE = Union[Tuple[Union[str, bytes, bytearray], int],
                      Tuple[str, int, int, int]]
 
 
+# Max YEAR in datetime in unixtime
+DATETIME_MAX_UNIXTIME: int = (datetime.MAXYEAR - 1970) * 365 * 24 * 60 * 60
+
+
 def load_plugin(internal_types: Sequence[str], module_name: str,
                 class_name: str, base_class: Type[_T_co],
                 configuration: "config.Configuration") -> _T_co:
@@ -244,3 +249,18 @@ def user_groups_as_string():
         username = os.getlogin()
         s = "user=%s" % (username)
     return s
+
+
+def format_ut(unixtime: int) -> str:
+    if sys.platform == "win32":
+        # TODO check how to support this better
+        return str(unixtime)
+    if unixtime < DATETIME_MAX_UNIXTIME:
+        if sys.version_info < (3, 11):
+            dt = datetime.datetime.utcfromtimestamp(unixtime)
+        else:
+            dt = datetime.datetime.fromtimestamp(unixtime, datetime.UTC)
+        r = str(unixtime) + "(" + dt.strftime('%Y-%m-%dT%H:%M:%SZ') + ")"
+    else:
+        r = str(unixtime) + "(>MAX:" + str(DATETIME_MAX_UNIXTIME) + ")"
+    return r