From e557c0cd5e2af5ea8d998d00d2f3dfc42ad17fa9 Mon Sep 17 00:00:00 2001 From: TowyTowy Date: Sat, 11 Jul 2026 15:58:39 +0200 Subject: [PATCH] Fix: time-range filter matches VEVENT with whole-day DURATION A calendar-query REPORT with a time-range filter failed to return a VEVENT that has a whole-day DURATION (e.g. DURATION:P1D or P2D) whenever the queried range fell inside the event but after DTSTART. The VEVENT time-range logic in radicale/item/filter.py gated the "non-zero duration" branch (rfc4791-9.9 line 2) on timedelta.seconds instead of timedelta.total_seconds(). For a duration that is an exact multiple of 24h, timedelta.seconds is 0 (the days component holds the value), so the event was treated as zero-length (line 3) and only matched a one-second window at its start. An identical event expressed with DTEND matched correctly, confirming the defect is isolated to the DURATION path. Use total_seconds() so multi-day durations are handled correctly. Adds a regression test (event11, DURATION:P2D) covering both an inside-range match and an outside-range non-match. Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 3 +++ pyproject.toml | 2 +- radicale/item/filter.py | 4 ++-- radicale/tests/static/event11.ics | 11 +++++++++++ radicale/tests/test_base.py | 25 +++++++++++++++++++++++++ 5 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 radicale/tests/static/event11.ics diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ac34598..3c6b0b37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## 3.7.7.dev +* Fix: time-range filter treated a VEVENT with a whole-day DURATION (e.g. P1D, P2D) as zero-length (timedelta.seconds instead of total_seconds), so such events were missing from calendar-query REPORT results + ## 3.7.6 * Extension: item verification on commandline * Improvement: catch lack of support of PERIOD in vobject <= 0.9.9 diff --git a/pyproject.toml b/pyproject.toml index 20a94c9d..8db3b7ae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "Radicale" # When the version is updated, a new section in the CHANGELOG.md file must be # added too. readme = "README.md" -version = "3.7.6" +version = "3.7.7.dev" authors = [{name = "Guillaume Ayoub", email = "guillaume.ayoub@kozea.fr"}, {name = "Unrud", email = "unrud@outlook.com"}, {name = "Peter Bieringer", email = "pb@bieringer.de"}] license = {text = "GNU GPL v3"} description = "CalDAV and CardDAV Server" diff --git a/radicale/item/filter.py b/radicale/item/filter.py index 5f990075..46462487 100644 --- a/radicale/item/filter.py +++ b/radicale/item/filter.py @@ -402,8 +402,8 @@ def visit_time_ranges(vobject_item: vobject.base.Component, child_name: str, return elif duration is not None: if original_duration is None: - original_duration = duration.seconds - if duration.seconds > 0: + original_duration = duration.total_seconds() + if duration.total_seconds() > 0: # Line 2 if range_fn(dtstart, dtstart + duration, is_recurrence): diff --git a/radicale/tests/static/event11.ics b/radicale/tests/static/event11.ics new file mode 100644 index 00000000..47b00cb4 --- /dev/null +++ b/radicale/tests/static/event11.ics @@ -0,0 +1,11 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Radicale//NONSGML Radicale Server//EN +BEGIN:VEVENT +DTSTAMP:20130901T000000Z +UID:event11 +SUMMARY:event11 +DTSTART:20130901T000000Z +DURATION:P2D +END:VEVENT +END:VCALENDAR diff --git a/radicale/tests/test_base.py b/radicale/tests/test_base.py index 3d80a259..381d6bed 100644 --- a/radicale/tests/test_base.py +++ b/radicale/tests/test_base.py @@ -1789,6 +1789,31 @@ permissions: RrWw""") """], items=(9,)) assert "/calendar.ics/event9.ics" not in answer + def test_time_range_filter_events_whole_day_duration(self) -> None: + """Report time-range filter on an event with a whole-day DURATION. + + event11 starts 2013-09-01T00:00:00Z and lasts DURATION:P2D, i.e. it is + ongoing until 2013-09-03T00:00:00Z. A time-range that falls inside the + span (but after DTSTART) must match it, exactly as it would for an + equivalent event expressed with DTEND. + """ + # Time-range fully inside the 2-day event, after DTSTART. + answer = self._test_filter(["""\ + + + + +"""], items=(11,)) + assert "/calendar.ics/event11.ics" in answer + # Time-range fully after the event must not match. + answer = self._test_filter(["""\ + + + + +"""], items=(11,)) + assert "/calendar.ics/event11.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(["""\