From bf0f92842c74d5b9eee627ae1d1b7c6985460e4b Mon Sep 17 00:00:00 2001 From: Michael Braun Date: Mon, 27 Jul 2026 18:48:24 +0200 Subject: [PATCH] calquery: don't drop events beyond the capped expansion window on open-ended time-range A calendar-query REPORT whose has a start but no end (RFC 4791 section 9.9: an absent end means +infinity) dropped every event whose next occurrence fell beyond time_range_start + 730 days: future single events and sparse recurrences (e.g. FREQ=YEARLY;INTERVAL=4). Clients such as DAVx5 send exactly this shape when they limit how far into the past they sync, so those events silently disappeared. 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: for any time-range query each resource is expanded into [expand_range_start, expand_range_end] and skipped when the expansion is empty (ComponentCount() == 0). For an open range expand_range_end is capped at start + 730 days, so an event with no instance inside that cap is discarded even though the preceding getVCalendarRange()->overlaps($range_filter) test -- which treats a null upper bound as +infinity -- already proved that it matches. This became user-visible in 1.1.12: commit b40c96de ("If time-range is set, only return matching events.") extended the empty-expansion skip, previously reached only for explicit requests, to every time-range query via isset($range_filter). 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. Bounded and queries are unchanged. Co-Authored-By: Claude Opus 4.8 --- inc/caldav-REPORT-calquery.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/inc/caldav-REPORT-calquery.php b/inc/caldav-REPORT-calquery.php index c0ec60a9..8e062d94 100644 --- a/inc/caldav-REPORT-calquery.php +++ b/inc/caldav-REPORT-calquery.php @@ -456,7 +456,15 @@ if ( $qry->Exec("calquery",__LINE__,__FILE__) && $qry->rows() > 0 ) { $expanded = expand_event_instances($vResource, $expand_range_start, $expand_range_end, $expand_as_floating , $dav_object->collection_tzid); - if ( $expanded->ComponentCount() == 0 ) continue; + # An empty expansion only proves a non-match when the expansion + # window equals the requested range. For an open-ended time-range + # ($range_filter->until is null) the window is artificially capped + # (expand_range_end defaults to time_range_start + 730 days), so a + # future or sparsely-recurring event whose next instance lies beyond + # the cap must NOT be dropped here -- the getVCalendarRange()->overlaps() + # test above is already exact for open ranges. Only enforce the + # empty-window skip for a real upper bound or an explicit expansion. + if ( $expanded->ComponentCount() == 0 && ( $need_expansion || isset($range_filter->until) ) ) continue; # We only keep the expanded instaneces if need_expansion is set, # RFC4791 Section 7.4. Otherwise we should return the original