calquery: don't drop events beyond the capped expansion window on open-ended time-range
What
An open-ended CalDAV calendar-query <C:time-range> — one with a start
but no end, which per RFC 4791 §9.9 means +infinity — silently drops any
event whose next occurrence lies more than 730 days after start.
How I ran into it
A family member added an event to a shared calendar. It showed up on other devices but was missing from my Android calendar. It turned out I had recently configured DAVx5 to include events for sync only up to an age of 730 days. Removing that age limitation immediately brought back all the missing future events.
Analysis traced it to this calendar-query time-range handling: DAVx5's age
limit makes it send a time-range with start = now − 730d and no end,
which is exactly the shape that triggers the bug.
Reproduction
Against a real shared collection (counting <href> results):
open (start only): 297
bounded (start + end=2999): 324 ← 27 events silently missingCause
The SQL prefilter is not at fault — for an open range it correctly emits only
the lower-bound condition. The drop happens in the post-fetch loop
(inc/caldav-REPORT-calquery.php): each resource is expanded into
[expand_range_start, expand_range_end] and skipped when the expansion is
empty (ComponentCount() == 0). For an open-ended range expand_range_end
defaults to time_range_start + 730 days, so an event whose only instance is
beyond that cap is discarded — even though the preceding
getVCalendarRange()->overlaps($range_filter) test, which treats a null upper
bound as +infinity, has already matched it.
This became user-visible in 1.1.12 via b40c96de ("If time-range is set, only
return matching events."), which extended the empty-expansion skip — previously
reached only for explicit <C:expand> requests — to every time-range query
through isset($range_filter).
Fix
Only enforce the empty-expansion skip when the upper bound is real
(range_filter->until is set) or the client explicitly requested expansion
(need_expansion). For an open range the exact overlaps() test above stands
and the unexpanded master component is returned as normal. One line changed;
bounded and <C:expand> queries are byte-identical.
Testing
Tested on Debian GNU/Linux 13 (trixie), package davical 1.1.12-2.1. After
patching, the open-ended query returns the same set as the far-bounded one
(324), the previously-missing future events reappear, and bounded / no-filter
responses are unchanged (verified with a scripted before/after <href>-set
diff). The change also applies cleanly to current master.
Note on the test change (second commit)
0555-iPhone-REPORT is itself an open-ended query
(<time-range start="20090827T120000Z"/>, no end). Its expected result
had encoded the pre-fix behaviour: it omitted two events dated 2011-10
(0544-gzip-PUT.ics, 0545-deflate-PUT.ics) which fall just beyond the
former start + 730d cap (2011-08-27) but do match [start, +∞). The
second commit updates that fixture to the corrected output, so the
existing test now doubles as the regression test for this fix.