feat(caldav): Allow advanced search for events/tasks

Signed-off-by: Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
pull/40618/head
Benjamin Gaussorgues 2023-09-26 11:07:49 +07:00
parent a75a93af8e
commit 3545a1c613
No known key found for this signature in database
GPG Key ID: 5DAC1CAFAA6DB883
6 changed files with 96 additions and 135 deletions

@ -208,36 +208,22 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
*/ */
protected array $userDisplayNames; protected array $userDisplayNames;
private IDBConnection $db;
private Backend $calendarSharingBackend; private Backend $calendarSharingBackend;
private Principal $principalBackend;
private IUserManager $userManager;
private ISecureRandom $random;
private LoggerInterface $logger;
private IEventDispatcher $dispatcher;
private IConfig $config;
private bool $legacyEndpoint;
private string $dbObjectPropertiesTable = 'calendarobjects_props'; private string $dbObjectPropertiesTable = 'calendarobjects_props';
private array $cachedObjects = []; private array $cachedObjects = [];
public function __construct(IDBConnection $db, public function __construct(
Principal $principalBackend, private IDBConnection $db,
IUserManager $userManager, private Principal $principalBackend,
IGroupManager $groupManager, private IUserManager $userManager,
ISecureRandom $random, IGroupManager $groupManager,
LoggerInterface $logger, private ISecureRandom $random,
IEventDispatcher $dispatcher, private LoggerInterface $logger,
IConfig $config, private IEventDispatcher $dispatcher,
bool $legacyEndpoint = false) { private IConfig $config,
$this->db = $db; private bool $legacyEndpoint = false,
$this->principalBackend = $principalBackend; ) {
$this->userManager = $userManager;
$this->calendarSharingBackend = new Backend($this->db, $this->userManager, $groupManager, $principalBackend, 'calendar'); $this->calendarSharingBackend = new Backend($this->db, $this->userManager, $groupManager, $principalBackend, 'calendar');
$this->random = $random;
$this->logger = $logger;
$this->dispatcher = $dispatcher;
$this->config = $config;
$this->legacyEndpoint = $legacyEndpoint;
} }
/** /**
@ -1855,8 +1841,14 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
* *
* @return array * @return array
*/ */
public function search(array $calendarInfo, $pattern, array $searchProperties, public function search(
array $options, $limit, $offset) { array $calendarInfo,
$pattern,
array $searchProperties,
array $options,
$limit,
$offset
) {
$outerQuery = $this->db->getQueryBuilder(); $outerQuery = $this->db->getQueryBuilder();
$innerQuery = $this->db->getQueryBuilder(); $innerQuery = $this->db->getQueryBuilder();
@ -2074,11 +2066,12 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
* @return array * @return array
*/ */
public function searchPrincipalUri(string $principalUri, public function searchPrincipalUri(string $principalUri,
string $pattern, string $pattern,
array $componentTypes, array $componentTypes,
array $searchProperties, array $searchProperties,
array $searchParameters, array $searchParameters,
array $options = []): array { array $options = []
): array {
return $this->atomic(function () use ($principalUri, $pattern, $componentTypes, $searchProperties, $searchParameters, $options) { return $this->atomic(function () use ($principalUri, $pattern, $componentTypes, $searchProperties, $searchParameters, $options) {
$escapePattern = !\array_key_exists('escape_like_param', $options) || $options['escape_like_param'] !== false; $escapePattern = !\array_key_exists('escape_like_param', $options) || $options['escape_like_param'] !== false;
@ -2160,6 +2153,20 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
if (isset($options['offset'])) { if (isset($options['offset'])) {
$calendarObjectIdQuery->setFirstResult($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()),
));
}
}
$result = $calendarObjectIdQuery->executeQuery(); $result = $calendarObjectIdQuery->executeQuery();
$matches = []; $matches = [];
@ -3187,7 +3194,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
$maxId = (int) $result->fetchOne(); $maxId = (int) $result->fetchOne();
$result->closeCursor(); $result->closeCursor();
if (!$maxId || $maxId < $keep) { if (!$maxId || $maxId < $keep) {
return 0; return 0;
} }
$query = $this->db->getQueryBuilder(); $query = $this->db->getQueryBuilder();

@ -41,18 +41,6 @@ use Sabre\VObject\Reader;
class ContactsSearchProvider implements IProvider { class ContactsSearchProvider implements IProvider {
/** @var IAppManager */
private $appManager;
/** @var IL10N */
private $l10n;
/** @var IURLGenerator */
private $urlGenerator;
/** @var CardDavBackend */
private $backend;
/** /**
* @var string[] * @var string[]
*/ */
@ -68,22 +56,12 @@ class ContactsSearchProvider implements IProvider {
'NOTE', 'NOTE',
]; ];
/** public function __construct(
* ContactsSearchProvider constructor. private IAppManager $appManager,
* private IL10N $l10n,
* @param IAppManager $appManager private IURLGenerator $urlGenerator,
* @param IL10N $l10n private CardDavBackend $backend,
* @param IURLGenerator $urlGenerator ) {
* @param CardDavBackend $backend
*/
public function __construct(IAppManager $appManager,
IL10N $l10n,
IURLGenerator $urlGenerator,
CardDavBackend $backend) {
$this->appManager = $appManager;
$this->l10n = $l10n;
$this->urlGenerator = $urlGenerator;
$this->backend = $backend;
} }
/** /**
@ -127,11 +105,13 @@ class ContactsSearchProvider implements IProvider {
$searchResults = $this->backend->searchPrincipalUri( $searchResults = $this->backend->searchPrincipalUri(
$principalUri, $principalUri,
$query->getTerm(), $query->getFilter('term')?->get() ?? '',
self::$searchProperties, self::$searchProperties,
[ [
'limit' => $query->getLimit(), 'limit' => $query->getLimit(),
'offset' => $query->getCursor(), 'offset' => $query->getCursor(),
'since' => $query->getFilter('since'),
'until' => $query->getFilter('until'),
] ]
); );
$formattedResults = \array_map(function (array $contactRow) use ($addressBooksById):SearchResultEntry { $formattedResults = \array_map(function (array $contactRow) use ($addressBooksById):SearchResultEntry {
@ -158,15 +138,11 @@ class ContactsSearchProvider implements IProvider {
); );
} }
/** protected function getDavUrlForContact(
* @param string $principalUri string $principalUri,
* @param string $addressBookUri string $addressBookUri,
* @param string $contactsUri string $contactsUri,
* @return string ): string {
*/
protected function getDavUrlForContact(string $principalUri,
string $addressBookUri,
string $contactsUri): string {
[, $principalType, $principalId] = explode('/', $principalUri, 3); [, $principalType, $principalId] = explode('/', $principalUri, 3);
return $this->urlGenerator->getAbsoluteURL( return $this->urlGenerator->getAbsoluteURL(
@ -178,13 +154,10 @@ class ContactsSearchProvider implements IProvider {
); );
} }
/** protected function getDeepLinkToContactsApp(
* @param string $addressBookUri string $addressBookUri,
* @param string $contactUid string $contactUid,
* @return string ): string {
*/
protected function getDeepLinkToContactsApp(string $addressBookUri,
string $contactUid): string {
return $this->urlGenerator->getAbsoluteURL( return $this->urlGenerator->getAbsoluteURL(
$this->urlGenerator->linkToRoute('contacts.contacts.direct', [ $this->urlGenerator->linkToRoute('contacts.contacts.direct', [
'contact' => $contactUid . '~' . $addressBookUri 'contact' => $contactUid . '~' . $addressBookUri
@ -194,7 +167,6 @@ class ContactsSearchProvider implements IProvider {
/** /**
* @param VCard $vCard * @param VCard $vCard
* @return string
*/ */
protected function generateSubline(VCard $vCard): string { protected function generateSubline(VCard $vCard): string {
$emailAddresses = $vCard->select('EMAIL'); $emailAddresses = $vCard->select('EMAIL');

@ -42,7 +42,6 @@ use Sabre\VObject\Property;
* @package OCA\DAV\Search * @package OCA\DAV\Search
*/ */
class EventsSearchProvider extends ACalendarSearchProvider { class EventsSearchProvider extends ACalendarSearchProvider {
/** /**
* @var string[] * @var string[]
*/ */
@ -95,8 +94,10 @@ class EventsSearchProvider extends ACalendarSearchProvider {
/** /**
* @inheritDoc * @inheritDoc
*/ */
public function search(IUser $user, public function search(
ISearchQuery $query): SearchResult { IUser $user,
ISearchQuery $query,
): SearchResult {
if (!$this->appManager->isEnabledForUser('calendar', $user)) { if (!$this->appManager->isEnabledForUser('calendar', $user)) {
return SearchResult::complete($this->getName(), []); return SearchResult::complete($this->getName(), []);
} }
@ -107,13 +108,17 @@ class EventsSearchProvider extends ACalendarSearchProvider {
$searchResults = $this->backend->searchPrincipalUri( $searchResults = $this->backend->searchPrincipalUri(
$principalUri, $principalUri,
$query->getTerm(), $query->getFilter('term')?->get() ?? '',
[self::$componentType], [self::$componentType],
self::$searchProperties, self::$searchProperties,
self::$searchParameters, self::$searchParameters,
[ [
'limit' => $query->getLimit(), 'limit' => $query->getLimit(),
'offset' => $query->getCursor(), 'offset' => $query->getCursor(),
'timerange' => [
'start' => $query->getFilter('since')?->get(),
'end' => $query->getFilter('until')?->get(),
],
] ]
); );
$formattedResults = \array_map(function (array $eventRow) use ($calendarsById, $subscriptionsById):SearchResultEntry { $formattedResults = \array_map(function (array $eventRow) use ($calendarsById, $subscriptionsById):SearchResultEntry {
@ -138,15 +143,11 @@ class EventsSearchProvider extends ACalendarSearchProvider {
); );
} }
/** protected function getDeepLinkToCalendarApp(
* @param string $principalUri string $principalUri,
* @param string $calendarUri string $calendarUri,
* @param string $calendarObjectUri string $calendarObjectUri,
* @return string ): string {
*/
protected function getDeepLinkToCalendarApp(string $principalUri,
string $calendarUri,
string $calendarObjectUri): string {
$davUrl = $this->getDavUrlForCalendarObject($principalUri, $calendarUri, $calendarObjectUri); $davUrl = $this->getDavUrlForCalendarObject($principalUri, $calendarUri, $calendarObjectUri);
// This route will automatically figure out what recurrence-id to open // This route will automatically figure out what recurrence-id to open
return $this->urlGenerator->getAbsoluteURL( return $this->urlGenerator->getAbsoluteURL(
@ -156,15 +157,11 @@ class EventsSearchProvider extends ACalendarSearchProvider {
); );
} }
/** protected function getDavUrlForCalendarObject(
* @param string $principalUri string $principalUri,
* @param string $calendarUri string $calendarUri,
* @param string $calendarObjectUri string $calendarObjectUri
* @return string ): string {
*/
protected function getDavUrlForCalendarObject(string $principalUri,
string $calendarUri,
string $calendarObjectUri): string {
[,, $principalId] = explode('/', $principalUri, 3); [,, $principalId] = explode('/', $principalUri, 3);
return $this->urlGenerator->linkTo('', 'remote.php') . '/dav/calendars/' return $this->urlGenerator->linkTo('', 'remote.php') . '/dav/calendars/'
@ -173,10 +170,6 @@ class EventsSearchProvider extends ACalendarSearchProvider {
. $calendarObjectUri; . $calendarObjectUri;
} }
/**
* @param Component $eventComponent
* @return string
*/
protected function generateSubline(Component $eventComponent): string { protected function generateSubline(Component $eventComponent): string {
$dtStart = $eventComponent->DTSTART; $dtStart = $eventComponent->DTSTART;
$dtEnd = $this->getDTEndForEvent($eventComponent); $dtEnd = $this->getDTEndForEvent($eventComponent);
@ -207,10 +200,6 @@ class EventsSearchProvider extends ACalendarSearchProvider {
return "$formattedStartDate $formattedStartTime - $formattedEndDate $formattedEndTime"; return "$formattedStartDate $formattedStartTime - $formattedEndDate $formattedEndTime";
} }
/**
* @param Component $eventComponent
* @return Property
*/
protected function getDTEndForEvent(Component $eventComponent):Property { protected function getDTEndForEvent(Component $eventComponent):Property {
if (isset($eventComponent->DTEND)) { if (isset($eventComponent->DTEND)) {
$end = $eventComponent->DTEND; $end = $eventComponent->DTEND;
@ -233,13 +222,10 @@ class EventsSearchProvider extends ACalendarSearchProvider {
return $end; return $end;
} }
/** protected function isDayEqual(
* @param \DateTime $dtStart \DateTime $dtStart,
* @param \DateTime $dtEnd \DateTime $dtEnd,
* @return bool ): bool {
*/
protected function isDayEqual(\DateTime $dtStart,
\DateTime $dtEnd) {
return $dtStart->format('Y-m-d') === $dtEnd->format('Y-m-d'); return $dtStart->format('Y-m-d') === $dtEnd->format('Y-m-d');
} }
} }

@ -41,7 +41,6 @@ use Sabre\VObject\Component;
* @package OCA\DAV\Search * @package OCA\DAV\Search
*/ */
class TasksSearchProvider extends ACalendarSearchProvider { class TasksSearchProvider extends ACalendarSearchProvider {
/** /**
* @var string[] * @var string[]
*/ */
@ -88,8 +87,10 @@ class TasksSearchProvider extends ACalendarSearchProvider {
/** /**
* @inheritDoc * @inheritDoc
*/ */
public function search(IUser $user, public function search(
ISearchQuery $query): SearchResult { IUser $user,
ISearchQuery $query,
): SearchResult {
if (!$this->appManager->isEnabledForUser('tasks', $user)) { if (!$this->appManager->isEnabledForUser('tasks', $user)) {
return SearchResult::complete($this->getName(), []); return SearchResult::complete($this->getName(), []);
} }
@ -100,13 +101,15 @@ class TasksSearchProvider extends ACalendarSearchProvider {
$searchResults = $this->backend->searchPrincipalUri( $searchResults = $this->backend->searchPrincipalUri(
$principalUri, $principalUri,
$query->getTerm(), $query->getFilter('term')?->get() ?? '',
[self::$componentType], [self::$componentType],
self::$searchProperties, self::$searchProperties,
self::$searchParameters, self::$searchParameters,
[ [
'limit' => $query->getLimit(), 'limit' => $query->getLimit(),
'offset' => $query->getCursor(), 'offset' => $query->getCursor(),
'since' => $query->getFilter('since'),
'until' => $query->getFilter('until'),
] ]
); );
$formattedResults = \array_map(function (array $taskRow) use ($calendarsById, $subscriptionsById):SearchResultEntry { $formattedResults = \array_map(function (array $taskRow) use ($calendarsById, $subscriptionsById):SearchResultEntry {
@ -131,13 +134,10 @@ class TasksSearchProvider extends ACalendarSearchProvider {
); );
} }
/** protected function getDeepLinkToTasksApp(
* @param string $calendarUri string $calendarUri,
* @param string $taskUri string $taskUri,
* @return string ): string {
*/
protected function getDeepLinkToTasksApp(string $calendarUri,
string $taskUri): string {
return $this->urlGenerator->getAbsoluteURL( return $this->urlGenerator->getAbsoluteURL(
$this->urlGenerator->linkToRoute('tasks.page.index') $this->urlGenerator->linkToRoute('tasks.page.index')
. '#/calendars/' . '#/calendars/'
@ -147,10 +147,6 @@ class TasksSearchProvider extends ACalendarSearchProvider {
); );
} }
/**
* @param Component $taskComponent
* @return string
*/
protected function generateSubline(Component $taskComponent): string { protected function generateSubline(Component $taskComponent): string {
if ($taskComponent->COMPLETED) { if ($taskComponent->COMPLETED) {
$completedDateTime = new \DateTime($taskComponent->COMPLETED->getDateTime()->format(\DateTimeInterface::ATOM)); $completedDateTime = new \DateTime($taskComponent->COMPLETED->getDateTime()->format(\DateTimeInterface::ATOM));

@ -328,10 +328,10 @@ class EventsSearchProviderTest extends TestCase {
]); ]);
$this->backend->expects($this->once()) $this->backend->expects($this->once())
->method('searchPrincipalUri') ->method('searchPrincipalUri')
->with('principals/users/john.doe', 'search term', ['VEVENT'], ->with('principals/users/john.doe', '', ['VEVENT'],
['SUMMARY', 'LOCATION', 'DESCRIPTION', 'ATTENDEE', 'ORGANIZER', 'CATEGORIES'], ['SUMMARY', 'LOCATION', 'DESCRIPTION', 'ATTENDEE', 'ORGANIZER', 'CATEGORIES'],
['ATTENDEE' => ['CN'], 'ORGANIZER' => ['CN']], ['ATTENDEE' => ['CN'], 'ORGANIZER' => ['CN']],
['limit' => 5, 'offset' => 20]) ['limit' => 5, 'offset' => 20, 'timerange' => ['start' => null, 'end' => null]])
->willReturn([ ->willReturn([
[ [
'calendarid' => 99, 'calendarid' => 99,

@ -213,10 +213,10 @@ class TasksSearchProviderTest extends TestCase {
]); ]);
$this->backend->expects($this->once()) $this->backend->expects($this->once())
->method('searchPrincipalUri') ->method('searchPrincipalUri')
->with('principals/users/john.doe', 'search term', ['VTODO'], ->with('principals/users/john.doe', '', ['VTODO'],
['SUMMARY', 'DESCRIPTION', 'CATEGORIES'], ['SUMMARY', 'DESCRIPTION', 'CATEGORIES'],
[], [],
['limit' => 5, 'offset' => 20]) ['limit' => 5, 'offset' => 20, 'since' => null, 'until' => null])
->willReturn([ ->willReturn([
[ [
'calendarid' => 99, 'calendarid' => 99,