Daniel 2 weeks ago committed by GitHub
commit b1170f2540
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1920,14 +1920,34 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
$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;
}
}
@ -1946,52 +1966,40 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
$outerQuery->andWhere($outerQuery->expr()->in('c.id', $outerQuery->createFunction($innerQuery->getSQL())));
if ($offset) {
$outerQuery->setFirstResult($offset);
}
if ($limit) {
$outerQuery->setMaxResults($limit);
}
$offset = (int)$offset;
$outerQuery->setFirstResult($offset);
$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;
}
$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;
}
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 a limit and time range is asking for more row than requested
* and retrying if we have not reached the limit.
*
* 25 rows and 3 retries is entirely arbitrary.
*/
$maxResults = (int)max($limit, 25);
$outerQuery->setMaxResults($maxResults);
for ($attempt = $objectsCount = 0; $attempt < 3 && $objectsCount < $limit; $attempt++) {
$objectsCount = array_push($calendarObjects, ...$this->searchCalendarObjects($outerQuery, $start, $end));
$outerQuery->setFirstResult($offset += $maxResults);
}
$calendarObjects = array_slice($calendarObjects, 0, $limit, false);
} else {
$outerQuery->setMaxResults($limit);
$calendarObjects = $this->searchCalendarObjects($outerQuery, $start, $end);
}
$result->closeCursor();
return array_map(function ($o) use ($options) {
$calendarData = Reader::read($o['calendardata']);
@ -2031,6 +2039,53 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
}, $calendarObjects);
}
private function searchCalendarObjects(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

@ -0,0 +1,17 @@
BEGIN:VCALENDAR
PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
VERSION:2.0
BEGIN:VEVENT
CREATED:20240507T105946Z
LAST-MODIFIED:20240507T121113Z
DTSTAMP:20240507T121113Z
UID:07514c7b-1014-425c-b1b8-2c35ab0eea1d
SUMMARY:Event A
RRULE:FREQ=YEARLY
DTSTART;TZID=Europe/Berlin:20240101T101500
DTEND;TZID=Europe/Berlin:20240101T111500
TRANSP:OPAQUE
X-MOZ-GENERATION:4
SEQUENCE:2
END:VEVENT
END:VCALENDAR

@ -0,0 +1,17 @@
BEGIN:VCALENDAR
PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
VERSION:2.0
BEGIN:VEVENT
CREATED:20240507T110122Z
LAST-MODIFIED:20240507T121120Z
DTSTAMP:20240507T121120Z
UID:67cf8134-ff10-49a7-913d-acfeda463db6
SUMMARY:Event B
RRULE:FREQ=YEARLY
DTSTART;TZID=Europe/Berlin:20240101T123000
DTEND;TZID=Europe/Berlin:20240101T133000
TRANSP:OPAQUE
X-MOZ-GENERATION:4
SEQUENCE:2
END:VEVENT
END:VCALENDAR

@ -0,0 +1,17 @@
BEGIN:VCALENDAR
PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
VERSION:2.0
BEGIN:VEVENT
CREATED:20240507T120352Z
LAST-MODIFIED:20240507T121128Z
DTSTAMP:20240507T121128Z
UID:59090ca1-e52b-447f-8e08-491d1da729fa
SUMMARY:Event C
RRULE:FREQ=YEARLY
DTSTART;TZID=Europe/Berlin:20240101T151000
DTEND;TZID=Europe/Berlin:20240101T161000
TRANSP:OPAQUE
X-MOZ-GENERATION:2
SEQUENCE:1
END:VEVENT
END:VCALENDAR

@ -0,0 +1,17 @@
BEGIN:VCALENDAR
PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
VERSION:2.0
BEGIN:VEVENT
CREATED:20240507T120414Z
LAST-MODIFIED:20240507T121134Z
DTSTAMP:20240507T121134Z
UID:b1814d32-9adf-4518-8535-37f2c037f423
SUMMARY:Event D
RRULE:FREQ=YEARLY
DTSTART;TZID=Europe/Berlin:20240101T164500
DTEND;TZID=Europe/Berlin:20240101T171500
TRANSP:OPAQUE
SEQUENCE:2
X-MOZ-GENERATION:3
END:VEVENT
END:VCALENDAR

@ -0,0 +1,14 @@
BEGIN:VCALENDAR
PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
VERSION:2.0
BEGIN:VEVENT
CREATED:20240507T122221Z
LAST-MODIFIED:20240507T122237Z
DTSTAMP:20240507T122237Z
UID:19c4e049-0b09-4101-a2ad-061a837e6a5e
SUMMARY:Cake Tasting
DTSTART;TZID=Europe/Berlin:20240509T151500
DTEND;TZID=Europe/Berlin:20240509T171500
TRANSP:OPAQUE
END:VEVENT
END:VCALENDAR

@ -0,0 +1,15 @@
BEGIN:VCALENDAR
PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
VERSION:2.0
BEGIN:VEVENT
CREATED:20240507T122246Z
LAST-MODIFIED:20240507T175258Z
DTSTAMP:20240507T175258Z
UID:60a7d310-aa7b-4974-8a8a-ff9339367e1d
SUMMARY:Pasta Day
DTSTART;TZID=Europe/Berlin:20240514T123000
DTEND;TZID=Europe/Berlin:20240514T133000
TRANSP:OPAQUE
X-MOZ-GENERATION:2
END:VEVENT
END:VCALENDAR

@ -1598,4 +1598,168 @@ EOD;
self::assertEqualsCanonicalizing([$uri1, $uri3], $changesAfter['modified']);
self::assertEquals([$uri2], $changesAfter['deleted']);
}
public function testSearchWithLimitAndTimeRange() {
$calendarId = $this->createTestCalendar();
$calendarInfo = [
'id' => $calendarId,
'principaluri' => 'user1',
'{http://owncloud.org/ns}owner-principal' => 'user1',
];
$testFiles = [
__DIR__ . '/../../misc/caldav-search-limit-timerange-1.ics',
__DIR__ . '/../../misc/caldav-search-limit-timerange-2.ics',
__DIR__ . '/../../misc/caldav-search-limit-timerange-3.ics',
__DIR__ . '/../../misc/caldav-search-limit-timerange-4.ics',
__DIR__ . '/../../misc/caldav-search-limit-timerange-5.ics',
__DIR__ . '/../../misc/caldav-search-limit-timerange-6.ics',
];
foreach ($testFiles as $testFile) {
$objectUri = static::getUniqueID('search-limit-timerange-');
$calendarData = \file_get_contents($testFile);
$this->backend->createCalendarObject($calendarId, $objectUri, $calendarData);
}
$start = new DateTimeImmutable('2024-05-06T00:00:00Z');
$end = $start->add(new DateInterval('P14D'));
$results = $this->backend->search(
$calendarInfo,
'',
[],
[
'timerange' => [
'start' => $start,
'end' => $end,
]
],
4,
null,
);
$this->assertCount(2, $results);
$this->assertEquals('Cake Tasting', $results[0]['objects'][0]['SUMMARY'][0]);
$this->assertGreaterThanOrEqual(
$start->getTimestamp(),
$results[0]['objects'][0]['DTSTART'][0]->getTimestamp(),
'Recurrence starting before requested start',
);
$this->assertEquals('Pasta Day', $results[1]['objects'][0]['SUMMARY'][0]);
$this->assertGreaterThanOrEqual(
$start->getTimestamp(),
$results[1]['objects'][0]['DTSTART'][0]->getTimestamp(),
'Recurrence starting before requested start',
);
}
public function testSearchWithLimitAndTimeRangeShouldNotReturnMoreObjectsThenLimit() {
$calendarId = $this->createTestCalendar();
$calendarInfo = [
'id' => $calendarId,
'principaluri' => 'user1',
'{http://owncloud.org/ns}owner-principal' => 'user1',
];
$testFiles = [
__DIR__ . '/../../misc/caldav-search-limit-timerange-1.ics',
__DIR__ . '/../../misc/caldav-search-limit-timerange-2.ics',
__DIR__ . '/../../misc/caldav-search-limit-timerange-3.ics',
__DIR__ . '/../../misc/caldav-search-limit-timerange-4.ics',
__DIR__ . '/../../misc/caldav-search-limit-timerange-5.ics',
__DIR__ . '/../../misc/caldav-search-limit-timerange-6.ics',
];
foreach ($testFiles as $testFile) {
$objectUri = static::getUniqueID('search-limit-timerange-');
$calendarData = \file_get_contents($testFile);
$this->backend->createCalendarObject($calendarId, $objectUri, $calendarData);
}
$start = new DateTimeImmutable('2024-05-06T00:00:00Z');
$end = $start->add(new DateInterval('P14D'));
$results = $this->backend->search(
$calendarInfo,
'',
[],
[
'timerange' => [
'start' => $start,
'end' => $end,
]
],
1,
null,
);
$this->assertCount(1, $results);
$this->assertEquals('Cake Tasting', $results[0]['objects'][0]['SUMMARY'][0]);
$this->assertGreaterThanOrEqual(
$start->getTimestamp(),
$results[0]['objects'][0]['DTSTART'][0]->getTimestamp(),
'Recurrence starting before requested start',
);
}
public function testSearchWithLimitAndTimeRangeShouldReturnObjectsInTheSameOrder() {
$calendarId = $this->createTestCalendar();
$calendarInfo = [
'id' => $calendarId,
'principaluri' => 'user1',
'{http://owncloud.org/ns}owner-principal' => 'user1',
];
$testFiles = [
__DIR__ . '/../../misc/caldav-search-limit-timerange-1.ics',
__DIR__ . '/../../misc/caldav-search-limit-timerange-2.ics',
__DIR__ . '/../../misc/caldav-search-limit-timerange-3.ics',
__DIR__ . '/../../misc/caldav-search-limit-timerange-4.ics',
__DIR__ . '/../../misc/caldav-search-limit-timerange-6.ics', // <-- intentional!
__DIR__ . '/../../misc/caldav-search-limit-timerange-5.ics',
];
foreach ($testFiles as $testFile) {
$objectUri = static::getUniqueID('search-limit-timerange-');
$calendarData = \file_get_contents($testFile);
$this->backend->createCalendarObject($calendarId, $objectUri, $calendarData);
}
$start = new DateTimeImmutable('2024-05-06T00:00:00Z');
$end = $start->add(new DateInterval('P14D'));
$results = $this->backend->search(
$calendarInfo,
'',
[],
[
'timerange' => [
'start' => $start,
'end' => $end,
]
],
2,
null,
);
$this->assertCount(2, $results);
$this->assertEquals('Cake Tasting', $results[0]['objects'][0]['SUMMARY'][0]);
$this->assertGreaterThanOrEqual(
$start->getTimestamp(),
$results[0]['objects'][0]['DTSTART'][0]->getTimestamp(),
'Recurrence starting before requested start',
);
$this->assertEquals('Pasta Day', $results[1]['objects'][0]['SUMMARY'][0]);
$this->assertGreaterThanOrEqual(
$start->getTimestamp(),
$results[1]['objects'][0]['DTSTART'][0]->getTimestamp(),
'Recurrence starting before requested start',
);
}
}

Loading…
Cancel
Save