Skip to content

Commit

Permalink
fix(caldav): event search with limit and timerange
Browse files Browse the repository at this point in the history
  • Loading branch information
kesselb committed May 7, 2024
1 parent 6fc2c47 commit d335b1b
Showing 1 changed file with 105 additions and 47 deletions.
152 changes: 105 additions & 47 deletions apps/dav/lib/CalDAV/CalDavBackend.php
Original file line number Diff line number Diff line change
Expand Up @@ -1914,14 +1914,34 @@ public function search(
$this->db->escapeLikeParameter($pattern) . '%')));
}

$start = null;
$end = null;

$hasLimit = is_int($limit);
$hasTimeRange = false;

if (isset($options['timerange'])) {
if (isset($options['timerange']['start']) && $options['timerange']['start'] instanceof DateTimeInterface) {
$outerQuery->andWhere($outerQuery->expr()->gt('lastoccurence',
$outerQuery->createNamedParameter($options['timerange']['start']->getTimeStamp())));
/** @var DateTimeInterface $start */
$start = $options['timerange']['start'];
$outerQuery->andWhere(
$outerQuery->expr()->gt(
'lastoccurence',
$outerQuery->createNamedParameter($start->getTimestamp())
)
);
$hasTimeRange = true;
}
if (isset($options['timerange']['end']) && $options['timerange']['end'] instanceof DateTimeInterface) {
$outerQuery->andWhere($outerQuery->expr()->lt('firstoccurence',
$outerQuery->createNamedParameter($options['timerange']['end']->getTimeStamp())));
/** @var DateTimeInterface $end */
$end = $options['timerange']['end'];
$outerQuery->andWhere(
$outerQuery->expr()->lt(
'firstoccurence',
$outerQuery->createNamedParameter($end->getTimestamp())
)
);
$hasTimeRange = true;
}
}

Expand All @@ -1940,52 +1960,43 @@ public function search(

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

if ($offset) {
$outerQuery->setFirstResult($offset);
}
if ($limit) {
$outerQuery->setMaxResults($limit);
if ($offset === null) {
$offset = 0;
}

if ($hasLimit && $hasTimeRange) {
/**
* Event recurrences are evaluated at runtime because the database only knows the first and last occurrence.
*
* Given, a user created 8 events with a yearly reoccurrence and two for events tomorrow.
* The upcoming event widget asks the CalDAV backend for 7 events within the next 14 days.
*
* If limit 7 is applied to the SQL query, we find the 7 events with a yearly reoccurrence
* and discard the events after evaluating the reoccurrence rules because they are not due within
* the next 14 days and end up with an empty result even if there are two events to show.
*
* The workaround for search requests with limit and time range is ask for more row than requested
* and retry if we have not reached the limit.
*
* 25 rows and 3 retries is entirely arbitrary.
* Send us a patch if you have something different in mind.
*/
$maxResults = max($limit, 25);
$attempts = 3;
} else {
$maxResults = $limit;
$attempts = 1;
}

$result = $outerQuery->executeQuery();
$calendarObjects = [];
while (($row = $result->fetch()) !== false) {
$start = $options['timerange']['start'] ?? null;
$end = $options['timerange']['end'] ?? null;

if ($start === null || !($start instanceof DateTimeInterface) || $end === null || !($end instanceof DateTimeInterface)) {
// No filter required
$calendarObjects[] = $row;
continue;
}
$outerQuery->setFirstResult($offset);
$outerQuery->setMaxResults($maxResults);

$isValid = $this->validateFilterForObject($row, [
'name' => 'VCALENDAR',
'comp-filters' => [
[
'name' => 'VEVENT',
'comp-filters' => [],
'prop-filters' => [],
'is-not-defined' => false,
'time-range' => [
'start' => $start,
'end' => $end,
],
],
],
'prop-filters' => [],
'is-not-defined' => false,
'time-range' => null,
]);
if (is_resource($row['calendardata'])) {
// Put the stream back to the beginning so it can be read another time
rewind($row['calendardata']);
}
if ($isValid) {
$calendarObjects[] = $row;
}
}
$result->closeCursor();
$calendarObjects = [];
do {
$objectsCount = array_push($calendarObjects, ...$this->searchCalendarObjectsByQuery($outerQuery, $start, $end));
$outerQuery->setFirstResult($offset += $maxResults);
--$attempts;
} while ($attempts > 0 && $objectsCount < $limit);

return array_map(function ($o) use ($options) {
$calendarData = Reader::read($o['calendardata']);
Expand Down Expand Up @@ -2025,6 +2036,53 @@ public function search(
}, $calendarObjects);
}

private function searchCalendarObjectsByQuery(IQueryBuilder $query, DateTimeInterface|null $start, DateTimeInterface|null $end): array {
$calendarObjects = [];
$filterByTimeRange = ($start instanceof DateTimeInterface) || ($end instanceof DateTimeInterface);

$result = $query->executeQuery();

while (($row = $result->fetch()) !== false) {
if ($filterByTimeRange === false) {
// No filter required
$calendarObjects[] = $row;
continue;
}

$isValid = $this->validateFilterForObject($row, [
'name' => 'VCALENDAR',
'comp-filters' => [
[
'name' => 'VEVENT',
'comp-filters' => [],
'prop-filters' => [],
'is-not-defined' => false,
'time-range' => [
'start' => $start,
'end' => $end,
],
],
],
'prop-filters' => [],
'is-not-defined' => false,
'time-range' => null,
]);

if (is_resource($row['calendardata'])) {
// Put the stream back to the beginning so it can be read another time
rewind($row['calendardata']);
}

if ($isValid) {
$calendarObjects[] = $row;
}
}

$result->closeCursor();

return $calendarObjects;
}

/**
* @param Component $comp
* @return array
Expand Down

0 comments on commit d335b1b

Please sign in to comment.