Skip to content

Commit 4cf0888

Browse files
authored
Merge pull request #60766 from nextcloud/bug/noid/expand-unified-search
fix(caldav): Expand recurring events for principal calendar search
2 parents 368f338 + 557f5f8 commit 4cf0888

7 files changed

Lines changed: 1178 additions & 93 deletions

File tree

apps/dav/lib/CalDAV/CalDavBackend.php

Lines changed: 45 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2391,6 +2391,10 @@ private function searchCalendarObjects(IQueryBuilder $query, ?DateTimeInterface
23912391
}
23922392

23932393
try {
2394+
// The time-range filter is hardcoded to VEVENT: Sabre only
2395+
// expands VEVENT recurrences (EventIterator is VEVENT-only and
2396+
// VTodo::isInTimeRange ignores RRULE), so other component types
2397+
// would not be filtered correctly here.
23942398
$isValid = $this->validateFilterForObject($row, [
23952399
'name' => 'VCALENDAR',
23962400
'comp-filters' => [
@@ -2494,13 +2498,24 @@ private function transformSearchProperty(Property $prop) {
24942498
}
24952499

24962500
/**
2501+
* Search calendar objects across a principal's calendars.
2502+
*
2503+
* This returns the stored calendar objects and does not expand recurring
2504+
* events. Callers that need the concrete occurrence for a requested time
2505+
* range must expand recurrences from `calendardata` themselves.
2506+
*
2507+
* Note: when a `timerange` option is given, the precise filtering assumes
2508+
* VEVENT components (see searchCalendarObjects()). Passing other component
2509+
* types together with a `timerange` would drop all results.
2510+
*
24972511
* @param string $principalUri
24982512
* @param string $pattern
24992513
* @param array $componentTypes
25002514
* @param array $searchProperties
25012515
* @param array $searchParameters
25022516
* @param array $options
2503-
* @return array
2517+
*
2518+
* @return list<array{uri: string, calendarid: int, calendartype: int, calendardata: string}>
25042519
*/
25052520
public function searchPrincipalUri(string $principalUri,
25062521
string $pattern,
@@ -2516,6 +2531,11 @@ public function searchPrincipalUri(string $principalUri,
25162531
$calendarOr = [];
25172532
$searchOr = [];
25182533

2534+
$start = null;
2535+
$end = null;
2536+
2537+
// Todo: The retries when $hasLimit && $hasTimeRange from https://github.com/nextcloud/server/pull/45222 should also be applied here to the calendarObjectIdQuery
2538+
25192539
// Fetch calendars and subscription
25202540
$calendars = $this->getCalendarsForUser($principalUri);
25212541
$subscriptions = $this->getSubscriptionsForUser($principalUri);
@@ -2594,19 +2614,21 @@ public function searchPrincipalUri(string $principalUri,
25942614
if (isset($options['offset'])) {
25952615
$calendarObjectIdQuery->setFirstResult($options['offset']);
25962616
}
2597-
if (isset($options['timerange'])) {
2598-
if (isset($options['timerange']['start']) && $options['timerange']['start'] instanceof DateTimeInterface) {
2599-
$calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->gt(
2600-
'lastoccurence',
2601-
$calendarObjectIdQuery->createNamedParameter($options['timerange']['start']->getTimeStamp()),
2602-
));
2603-
}
2604-
if (isset($options['timerange']['end']) && $options['timerange']['end'] instanceof DateTimeInterface) {
2605-
$calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->lt(
2606-
'firstoccurence',
2607-
$calendarObjectIdQuery->createNamedParameter($options['timerange']['end']->getTimeStamp()),
2608-
));
2609-
}
2617+
if (isset($options['timerange']['start']) && $options['timerange']['start'] instanceof DateTimeInterface) {
2618+
/** @var DateTimeInterface $start */
2619+
$start = $options['timerange']['start'];
2620+
$calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->gt(
2621+
'lastoccurence',
2622+
$calendarObjectIdQuery->createNamedParameter($start->getTimestamp()),
2623+
));
2624+
}
2625+
if (isset($options['timerange']['end']) && $options['timerange']['end'] instanceof DateTimeInterface) {
2626+
/** @var DateTimeInterface $end */
2627+
$end = $options['timerange']['end'];
2628+
$calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->lt(
2629+
'firstoccurence',
2630+
$calendarObjectIdQuery->createNamedParameter($end->getTimestamp()),
2631+
));
26102632
}
26112633

26122634
$result = $calendarObjectIdQuery->executeQuery();
@@ -2621,17 +2643,16 @@ public function searchPrincipalUri(string $principalUri,
26212643
->from('calendarobjects')
26222644
->where($query->expr()->in('id', $query->createNamedParameter($matches, IQueryBuilder::PARAM_INT_ARRAY)));
26232645

2624-
$result = $query->executeQuery();
2625-
$calendarObjects = [];
2626-
while (($array = $result->fetchAssociative()) !== false) {
2627-
$array['calendarid'] = (int)$array['calendarid'];
2628-
$array['calendartype'] = (int)$array['calendartype'];
2629-
$array['calendardata'] = $this->readBlob($array['calendardata']);
2646+
$calendarObjects = $this->searchCalendarObjects($query, $start, $end);
26302647

2631-
$calendarObjects[] = $array;
2632-
}
2633-
$result->closeCursor();
2634-
return $calendarObjects;
2648+
return array_values(array_map(function ($event) {
2649+
return [
2650+
'uri' => (string)$event['uri'],
2651+
'calendarid' => (int)$event['calendarid'],
2652+
'calendartype' => (int)$event['calendartype'],
2653+
'calendardata' => (string)$this->readBlob($event['calendardata']),
2654+
];
2655+
}, $calendarObjects));
26352656
}, $this->db);
26362657
}
26372658

apps/dav/lib/Search/ACalendarSearchProvider.php

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
use OCP\IURLGenerator;
1616
use OCP\Search\IProvider;
1717
use Sabre\VObject\Component;
18-
use Sabre\VObject\Reader;
18+
use Sabre\VObject\Component\VCalendar;
1919

2020
/**
2121
* Class ACalendarSearchProvider
@@ -76,34 +76,32 @@ protected function getSortedSubscriptions(string $principalUri): array {
7676

7777
/**
7878
* Returns the primary VEvent / VJournal / VTodo component
79+
*
7980
* If it's a component with recurrence-ids, it will return
8081
* the primary component
8182
*
8283
* TODO: It would be a nice enhancement to show recurrence-exceptions
8384
* as individual search-results.
85+
*
8486
* For now we will just display the primary element of a recurrence-set.
8587
*
86-
* @param string $calendarData
88+
* Returns null when the calendar has no component of the requested type.
89+
*
90+
* @param VCalendar $vCalendar
8791
* @param string $componentName
88-
* @return Component
92+
* @return Component|null
8993
*/
90-
protected function getPrimaryComponent(string $calendarData, string $componentName): Component {
91-
$vCalendar = Reader::read($calendarData, Reader::OPTION_FORGIVING);
92-
93-
$components = $vCalendar->select($componentName);
94-
if (count($components) === 1) {
95-
return $components[0];
96-
}
97-
98-
// If it's a recurrence-set, take the primary element
99-
foreach ($components as $component) {
94+
protected function getPrimaryComponent(VCalendar $vCalendar, string $componentName): ?Component {
95+
$first = null;
96+
foreach ($vCalendar->select($componentName) as $component) {
10097
/** @var Component $component */
98+
// Prefer the recurrence-set master (no RECURRENCE-ID); otherwise the first element.
99+
$first ??= $component;
101100
if (!$component->{'RECURRENCE-ID'}) {
102101
return $component;
103102
}
104103
}
105104

106-
// In case of error, just fallback to the first element in the set
107-
return $components[0];
105+
return $first;
108106
}
109107
}

0 commit comments

Comments
 (0)