Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 35 additions & 24 deletions apps/dav/lib/CalDAV/CalDavBackend.php
Original file line number Diff line number Diff line change
Expand Up @@ -2473,13 +2473,20 @@
}

/**
* Search calendar objects across a principal's calendars.
*
* This returns the stored calendar objects and does not expand recurring
* events. Callers that need the concrete occurrence for a requested time
* range must expand recurrences from `calendardata` themselves.
*
* @param string $principalUri
* @param string $pattern
* @param array $componentTypes
* @param array $searchProperties
* @param array $searchParameters
* @param array $options
* @return array
*
* @return list<array{uri: string, calendarid: int, calendartype: 0|1, calendardata: string}>

Check failure on line 2489 in apps/dav/lib/CalDAV/CalDavBackend.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

MoreSpecificReturnType

apps/dav/lib/CalDAV/CalDavBackend.php:2489:13: MoreSpecificReturnType: The declared return type 'list<array{calendardata: string, calendarid: int, calendartype: 0|1, uri: string}>' for OCA\DAV\CalDAV\CalDavBackend::searchPrincipalUri is more specific than the inferred return type 'array<array-key, mixed>' (see https://psalm.dev/070)
*/
public function searchPrincipalUri(string $principalUri,
string $pattern,
Expand All @@ -2488,13 +2495,18 @@
array $searchParameters,
array $options = [],
): array {
return $this->atomic(function () use ($principalUri, $pattern, $componentTypes, $searchProperties, $searchParameters, $options) {

Check failure on line 2498 in apps/dav/lib/CalDAV/CalDavBackend.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

LessSpecificReturnStatement

apps/dav/lib/CalDAV/CalDavBackend.php:2498:10: LessSpecificReturnStatement: The type 'array<array-key, mixed>' is more general than the declared return type 'list<array{calendardata: string, calendarid: int, calendartype: 0|1, uri: string}>' for OCA\DAV\CalDAV\CalDavBackend::searchPrincipalUri (see https://psalm.dev/129)
$escapePattern = !\array_key_exists('escape_like_param', $options) || $options['escape_like_param'] !== false;

$calendarObjectIdQuery = $this->db->getQueryBuilder();
$calendarOr = [];
$searchOr = [];

$start = null;
$end = null;

// Todo: The retries when $hasLimit && $hasTimeRange from https://github.com/nextcloud/server/pull/45222 should also be applied here to the calendarObjectIdQuery

// Fetch calendars and subscription
$calendars = $this->getCalendarsForUser($principalUri);
$subscriptions = $this->getSubscriptionsForUser($principalUri);
Expand Down Expand Up @@ -2573,19 +2585,21 @@
if (isset($options['offset'])) {
$calendarObjectIdQuery->setFirstResult($options['offset']);
}
if (isset($options['timerange'])) {
if (isset($options['timerange']['start']) && $options['timerange']['start'] instanceof DateTimeInterface) {
$calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->gt(
'lastoccurence',
$calendarObjectIdQuery->createNamedParameter($options['timerange']['start']->getTimeStamp()),
));
}
if (isset($options['timerange']['end']) && $options['timerange']['end'] instanceof DateTimeInterface) {
$calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->lt(
'firstoccurence',
$calendarObjectIdQuery->createNamedParameter($options['timerange']['end']->getTimeStamp()),
));
}
if (isset($options['timerange']['start']) && $options['timerange']['start'] instanceof DateTimeInterface) {
/** @var DateTimeInterface $start */
$start = $options['timerange']['start'];
$calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->gt(
'lastoccurence',
$calendarObjectIdQuery->createNamedParameter($start->getTimestamp()),
));
}
if (isset($options['timerange']['end']) && $options['timerange']['end'] instanceof DateTimeInterface) {
/** @var DateTimeInterface $end */
$end = $options['timerange']['end'];
$calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->lt(
'firstoccurence',
$calendarObjectIdQuery->createNamedParameter($end->getTimestamp()),
));
}

$result = $calendarObjectIdQuery->executeQuery();
Expand All @@ -2600,17 +2614,14 @@
->from('calendarobjects')
->where($query->expr()->in('id', $query->createNamedParameter($matches, IQueryBuilder::PARAM_INT_ARRAY)));

$result = $query->executeQuery();
$calendarObjects = [];
while (($array = $result->fetchAssociative()) !== false) {
$array['calendarid'] = (int)$array['calendarid'];
$array['calendartype'] = (int)$array['calendartype'];
$array['calendardata'] = $this->readBlob($array['calendardata']);
$calendarObjects = $this->searchCalendarObjects($query, $start, $end);

$calendarObjects[] = $array;
}
$result->closeCursor();
return $calendarObjects;
return array_map(function ($event) {
$event['calendarid'] = (int)$event['calendarid'];
$event['calendartype'] = (int)$event['calendartype'];
$event['calendardata'] = $this->readBlob($event['calendardata']);
return $event;
}, $calendarObjects);
}, $this->db);
}

Expand Down
51 changes: 49 additions & 2 deletions apps/dav/lib/Search/ACalendarSearchProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
use OCP\IURLGenerator;
use OCP\Search\IProvider;
use Sabre\VObject\Component;
use Sabre\VObject\Component\VCalendar;
use Sabre\VObject\InvalidDataException;
use Sabre\VObject\Reader;

/**
Expand Down Expand Up @@ -87,10 +89,41 @@ protected function getSortedSubscriptions(string $principalUri): array {
* @param string $componentName
* @return Component
*/
protected function getPrimaryComponent(string $calendarData, string $componentName): Component {
protected function getPrimaryComponent(string $calendarData, string $componentName, \DateTimeInterface|null $since, \DateTimeInterface|null $until): Component {
$vCalendar = Reader::read($calendarData, Reader::OPTION_FORGIVING);

$components = $vCalendar->select($componentName);
$originalTimeZone = null;
if ($vCalendar instanceof VCalendar && isset($since, $until)) {
// expand() rewrites every occurrence's DTSTART/DTEND to UTC, so remember
// the event's original timezone to display the occurrence in local time.
$baseComponent = $vCalendar->getBaseComponent($componentName);
if ($baseComponent !== null && isset($baseComponent->DTSTART) && $baseComponent->DTSTART->hasTime()) {
$originalTimeZone = $baseComponent->DTSTART->getDateTime()->getTimezone();
}

try {
$vCalendar = $vCalendar->expand($since, $until);
} catch (InvalidDataException $e) {
// fallback to the original event without expanding, leave its timezone untouched
$originalTimeZone = null;
}
}

$component = $this->selectPrimaryComponent($vCalendar->select($componentName));

if ($originalTimeZone !== null) {
$this->applyTimeZone($component, $originalTimeZone);
}

return $component;
}

/**
* @param Component[] $components
*/
private function selectPrimaryComponent(array $components): Component {
// Expanded results: every instance has a RECURRENCE-ID; just take the first in-range occurrence.
// Stored objects: a recurrence-set is the master (no RECURRENCE-ID) plus override exceptions.
if (count($components) === 1) {
return $components[0];
}
Expand All @@ -106,4 +139,18 @@ protected function getPrimaryComponent(string $calendarData, string $componentNa
// In case of error, just fallback to the first element in the set
return $components[0];
}

/**
* Move the occurrence back into the event's original timezone after expand()
* has rewritten it to UTC, so the rendered time matches the user's local time.
*/
private function applyTimeZone(Component $component, \DateTimeZone $timeZone): void {
foreach (['DTSTART', 'DTEND'] as $name) {
if (isset($component->$name) && $component->$name->hasTime()) {
$component->$name->setDateTime(
$component->$name->getDateTime()->setTimezone($timeZone),
);
}
}
}
}
29 changes: 19 additions & 10 deletions apps/dav/lib/Search/EventsSearchProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

namespace OCA\DAV\Search;

use DateTimeImmutable;
use OCA\DAV\CalDAV\CalDavBackend;
use OCP\IUser;
use OCP\Search\IFilteringProvider;
Expand Down Expand Up @@ -101,6 +102,20 @@ public function search(

/** @var string|null $term */
$term = $query->getFilter('term')?->get();

$since = $query->getFilter('since')?->get();
$until = $query->getFilter('until')?->get();

if ($since !== null && $until === null) {
$until = new DateTimeImmutable('now', new \DateTimeZone('Z'));
}

/** @var array{start: DateTimeImmutable|null, end: DateTimeImmutable|null} $timeRange */
$timeRange = [
'start' => $since,
'end' => $until,
];

if ($term === null) {
$searchResults = [];
} else {
Expand All @@ -113,10 +128,7 @@ public function search(
[
'limit' => $query->getLimit(),
'offset' => $query->getCursor(),
'timerange' => [
'start' => $query->getFilter('since')?->get(),
'end' => $query->getFilter('until')?->get(),
],
'timerange' => $timeRange,
]
);
}
Expand All @@ -133,10 +145,7 @@ public function search(
[
'limit' => $query->getLimit(),
'offset' => $query->getCursor(),
'timerange' => [
'start' => $query->getFilter('since')?->get(),
'end' => $query->getFilter('until')?->get(),
],
'timerange' => $timeRange,
],
);

Expand All @@ -152,8 +161,8 @@ public function search(
$searchResults[] = $attendeeResult;
}
}
$formattedResults = \array_map(function (array $eventRow) use ($calendarsById, $subscriptionsById): SearchResultEntry {
$component = $this->getPrimaryComponent($eventRow['calendardata'], self::COMPONENT_TYPE);
$formattedResults = \array_map(function (array $eventRow) use ($calendarsById, $subscriptionsById, $since, $until): SearchResultEntry {
$component = $this->getPrimaryComponent($eventRow['calendardata'], self::COMPONENT_TYPE, $since, $until);
$title = (string)($component->SUMMARY ?? $this->l10n->t('Untitled event'));

if ($eventRow['calendartype'] === CalDavBackend::CALENDAR_TYPE_CALENDAR) {
Expand Down
2 changes: 1 addition & 1 deletion apps/dav/lib/Search/TasksSearchProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ public function search(
]
);
$formattedResults = \array_map(function (array $taskRow) use ($calendarsById, $subscriptionsById):SearchResultEntry {
$component = $this->getPrimaryComponent($taskRow['calendardata'], self::COMPONENT_TYPE);
$component = $this->getPrimaryComponent($taskRow['calendardata'], self::COMPONENT_TYPE, null, null);
$title = (string)($component->SUMMARY ?? $this->l10n->t('Untitled task'));

if ($taskRow['calendartype'] === CalDavBackend::CALENDAR_TYPE_CALENDAR) {
Expand Down
Loading
Loading