Fix: time-range filter on VTODO with DTSTART/DUE and CREATED/COMPLETED

visit_time_ranges() reuses a single "original_duration" variable for two
unrelated purposes: the DTSTART->DUE span and the CREATED->COMPLETED span.
When a VTODO carries all four properties (a completed task, which most
clients write with CREATED and COMPLETED), the second assignment clobbers
the first, and the DTSTART/DUE branch of the rfc4791-9.9 table then
reconstructs DUE as DTSTART + (COMPLETED - CREATED).

The elif chain already implements the RFC table correctly (DTSTART/DUE
take precedence over CREATED/COMPLETED), so the CREATED/COMPLETED value
is never wanted there. Keep it in its own variable.

Effect: such a VTODO is filtered against a bogus interval, both in
calendar-query REPORT and in item.find_time_range() (the enclosing range
cached for the storage prefilter), so completed tasks go missing from -
or wrongly appear in - client results.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
TowyTowy
2026-08-04 20:05:04 +02:00
parent 3ad8ae1d39
commit 0e15021c11
4 changed files with 40 additions and 2 deletions

View File

@@ -0,0 +1,13 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Radicale//NONSGML Radicale Server//EN
BEGIN:VTODO
UID:todo10
SUMMARY:todo10
DTSTART:20130901T180000Z
DUE:20130903T180000Z
CREATED:20130901T170000Z
COMPLETED:20130901T173000Z
STATUS:COMPLETED
END:VTODO
END:VCALENDAR

View File

@@ -2083,6 +2083,27 @@ permissions: RrWw""")
</C:comp-filter>"""], "todo", items=range(1, 9))
assert "/calendar.ics/todo7.ics" in answer
def test_time_range_filter_todos_dtstart_due_completed(self) -> None:
"""Report request with time-range filter on a completed todo which
also has DTSTART and DUE (rfc4791-9.9: DTSTART/DUE take
precedence over CREATED/COMPLETED)."""
# inside DTSTART..DUE, but outside CREATED..COMPLETED
answer = self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VTODO">
<C:time-range start="20130902T000000Z" end="20130903T000000Z"/>
</C:comp-filter>
</C:comp-filter>"""], "todo", items=(10,))
assert "/calendar.ics/todo10.ics" in answer
# entirely before DTSTART and CREATED
answer = self._test_filter(["""\
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VTODO">
<C:time-range start="20130801T000000Z" end="20130901T000000Z"/>
</C:comp-filter>
</C:comp-filter>"""], "todo", items=(10,))
assert "/calendar.ics/todo10.ics" not in answer
def test_time_range_filter_events_valarm(self) -> None:
"""Report request with time-range filter on events having absolute VALARM."""
answer = self._test_filter(["""\