Skip to content

Commit 3fdcf9f

Browse files
committed
fixup! fix(caldav): event search with limit and timerange
Signed-off-by: Daniel Kesselberg <mail@danielkesselberg.de>
1 parent b9ca585 commit 3fdcf9f

File tree

1 file changed

+103
-69
lines changed

1 file changed

+103
-69
lines changed

apps/dav/lib/CalDAV/CalDavBackend.php

Lines changed: 103 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1879,12 +1879,6 @@ public function search(
18791879
$limit,
18801880
$offset
18811881
) {
1882-
1883-
/**
1884-
* Limiting the results with sql only works without a timerange filter.
1885-
*/
1886-
$canUseSqlLimit = true;
1887-
18881882
$outerQuery = $this->db->getQueryBuilder();
18891883
$innerQuery = $this->db->getQueryBuilder();
18901884

@@ -1926,16 +1920,34 @@ public function search(
19261920
$this->db->escapeLikeParameter($pattern) . '%')));
19271921
}
19281922

1923+
$start = null;
1924+
$end = null;
1925+
1926+
$hasLimit = is_int($limit);
1927+
$hasTimeRange = false;
1928+
19291929
if (isset($options['timerange'])) {
19301930
if (isset($options['timerange']['start']) && $options['timerange']['start'] instanceof DateTimeInterface) {
1931-
$outerQuery->andWhere($outerQuery->expr()->gt('lastoccurence',
1932-
$outerQuery->createNamedParameter($options['timerange']['start']->getTimeStamp())));
1933-
$canUseSqlLimit = false;
1931+
/** @var DateTimeInterface $start */
1932+
$start = $options['timerange']['start'];
1933+
$outerQuery->andWhere(
1934+
$outerQuery->expr()->gt(
1935+
'lastoccurence',
1936+
$outerQuery->createNamedParameter($start->getTimestamp())
1937+
)
1938+
);
1939+
$hasTimeRange = true;
19341940
}
19351941
if (isset($options['timerange']['end']) && $options['timerange']['end'] instanceof DateTimeInterface) {
1936-
$outerQuery->andWhere($outerQuery->expr()->lt('firstoccurence',
1937-
$outerQuery->createNamedParameter($options['timerange']['end']->getTimeStamp())));
1938-
$canUseSqlLimit = false;
1942+
/** @var DateTimeInterface $end */
1943+
$end = $options['timerange']['end'];
1944+
$outerQuery->andWhere(
1945+
$outerQuery->expr()->lt(
1946+
'firstoccurence',
1947+
$outerQuery->createNamedParameter($end->getTimestamp())
1948+
)
1949+
);
1950+
$hasTimeRange = true;
19391951
}
19401952
}
19411953

@@ -1954,37 +1966,92 @@ public function search(
19541966

19551967
$outerQuery->andWhere($outerQuery->expr()->in('c.id', $outerQuery->createFunction($innerQuery->getSQL())));
19561968

1957-
if ($offset) {
1958-
$outerQuery->setFirstResult($offset);
1959-
}
1960-
if ($limit) {
1961-
if ($canUseSqlLimit) {
1962-
$outerQuery->setMaxResults($limit);
1963-
} else {
1964-
// @TODO Discuss number, add way for api consumer to increase
1965-
$outerQuery->setMaxResults(500);
1966-
}
1969+
if ($offset === null) {
1970+
$offset = 0;
1971+
}
1972+
1973+
if ($hasLimit && $hasTimeRange) {
1974+
/**
1975+
* Event recurrences are evaluated at runtime because the database only knows the first and last occurrence.
1976+
*
1977+
* Given, a user created 8 events with a yearly reoccurrence and two for events tomorrow.
1978+
* The upcoming event widget asks the CalDAV backend for 7 events within the next 14 days.
1979+
*
1980+
* If limit 7 is applied to the SQL query, we find the 7 events with a yearly reoccurrence
1981+
* and discard the events after evaluating the reoccurrence rules because they are not due within
1982+
* the next 14 days and end up with an empty result even if there are two events to show.
1983+
*
1984+
* The workaround for search requests with limit and time range is ask for more row than requested
1985+
* and retry if we have not reached the limit.
1986+
*
1987+
* 25 rows and 3 retries is entirely arbitrary.
1988+
* Send us a patch if you have something different in mind.
1989+
*/
1990+
$maxResults = max($limit, 25);
1991+
$attempts = 3;
1992+
} else {
1993+
$maxResults = $limit;
1994+
$attempts = 1;
19671995
}
19681996

1969-
$result = $outerQuery->executeQuery();
1997+
$outerQuery->setFirstResult($offset);
1998+
$outerQuery->setMaxResults($maxResults);
1999+
19702000
$calendarObjects = [];
1971-
$objectsCount = 0;
2001+
do {
2002+
$objectsCount = array_push($calendarObjects, ...$this->searchCalendarObjectsByQuery($outerQuery, $start, $end));
2003+
$outerQuery->setFirstResult($offset += $maxResults);
2004+
--$attempts;
2005+
} while ($attempts > 0 && $objectsCount < $limit);
19722006

1973-
$start = $options['timerange']['start'] ?? null;
1974-
$end = $options['timerange']['end'] ?? null;
2007+
return array_map(function ($o) use ($options) {
2008+
$calendarData = Reader::read($o['calendardata']);
19752009

1976-
$filterByTimeRange = ($start instanceof DateTimeInterface) || ($end instanceof DateTimeInterface);
1977-
$hasLimit = is_int($limit);
2010+
// Expand recurrences if an explicit time range is requested
2011+
if ($calendarData instanceof VCalendar
2012+
&& isset($options['timerange']['start'], $options['timerange']['end'])) {
2013+
$calendarData = $calendarData->expand(
2014+
$options['timerange']['start'],
2015+
$options['timerange']['end'],
2016+
);
2017+
}
19782018

1979-
while (($row = $result->fetch()) !== false) {
1980-
if ($hasLimit && $objectsCount > $limit) {
1981-
break;
2019+
$comps = $calendarData->getComponents();
2020+
$objects = [];
2021+
$timezones = [];
2022+
foreach ($comps as $comp) {
2023+
if ($comp instanceof VTimeZone) {
2024+
$timezones[] = $comp;
2025+
} else {
2026+
$objects[] = $comp;
2027+
}
19822028
}
19832029

2030+
return [
2031+
'id' => $o['id'],
2032+
'type' => $o['componenttype'],
2033+
'uid' => $o['uid'],
2034+
'uri' => $o['uri'],
2035+
'objects' => array_map(function ($c) {
2036+
return $this->transformSearchData($c);
2037+
}, $objects),
2038+
'timezones' => array_map(function ($c) {
2039+
return $this->transformSearchData($c);
2040+
}, $timezones),
2041+
];
2042+
}, $calendarObjects);
2043+
}
2044+
2045+
private function searchCalendarObjectsByQuery(IQueryBuilder $query, DateTimeInterface|null $start, DateTimeInterface|null $end): array {
2046+
$calendarObjects = [];
2047+
$filterByTimeRange = ($start instanceof DateTimeInterface) || ($end instanceof DateTimeInterface);
2048+
2049+
$result = $query->executeQuery();
2050+
2051+
while (($row = $result->fetch()) !== false) {
19842052
if ($filterByTimeRange === false) {
19852053
// No filter required
19862054
$calendarObjects[] = $row;
1987-
$objectsCount++;
19882055
continue;
19892056
}
19902057

@@ -2006,53 +2073,20 @@ public function search(
20062073
'is-not-defined' => false,
20072074
'time-range' => null,
20082075
]);
2076+
20092077
if (is_resource($row['calendardata'])) {
20102078
// Put the stream back to the beginning so it can be read another time
20112079
rewind($row['calendardata']);
20122080
}
2081+
20132082
if ($isValid) {
20142083
$calendarObjects[] = $row;
2015-
$objectsCount++;
20162084
}
20172085
}
2018-
$result->closeCursor();
2019-
2020-
return array_map(function ($o) use ($options) {
2021-
$calendarData = Reader::read($o['calendardata']);
2022-
2023-
// Expand recurrences if an explicit time range is requested
2024-
if ($calendarData instanceof VCalendar
2025-
&& isset($options['timerange']['start'], $options['timerange']['end'])) {
2026-
$calendarData = $calendarData->expand(
2027-
$options['timerange']['start'],
2028-
$options['timerange']['end'],
2029-
);
2030-
}
20312086

2032-
$comps = $calendarData->getComponents();
2033-
$objects = [];
2034-
$timezones = [];
2035-
foreach ($comps as $comp) {
2036-
if ($comp instanceof VTimeZone) {
2037-
$timezones[] = $comp;
2038-
} else {
2039-
$objects[] = $comp;
2040-
}
2041-
}
2087+
$result->closeCursor();
20422088

2043-
return [
2044-
'id' => $o['id'],
2045-
'type' => $o['componenttype'],
2046-
'uid' => $o['uid'],
2047-
'uri' => $o['uri'],
2048-
'objects' => array_map(function ($c) {
2049-
return $this->transformSearchData($c);
2050-
}, $objects),
2051-
'timezones' => array_map(function ($c) {
2052-
return $this->transformSearchData($c);
2053-
}, $timezones),
2054-
];
2055-
}, $calendarObjects);
2089+
return $calendarObjects;
20562090
}
20572091

20582092
/**

0 commit comments

Comments
 (0)