Skip to content

Commit 1f3f5e3

Browse files
committed
feat(carddav): Allow advanced search for contacts
Signed-off-by: Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
1 parent 434b761 commit 1f3f5e3

File tree

2 files changed

+68
-18
lines changed

2 files changed

+68
-18
lines changed

apps/dav/lib/CardDAV/CardDavBackend.php

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
use OCP\IDBConnection;
5454
use OCP\IGroupManager;
5555
use OCP\IUserManager;
56+
use OCP\Search\Filter\DateTimeFilter;
5657
use PDO;
5758
use Sabre\CardDAV\Backend\BackendInterface;
5859
use Sabre\CardDAV\Backend\SyncSupport;
@@ -1130,32 +1131,31 @@ private function searchByAddressBookIds(array $addressBookIds,
11301131
return [];
11311132
}
11321133

1133-
$propertyOr = $query2->expr()->orX();
1134-
foreach ($searchProperties as $property) {
1135-
if ($escapePattern) {
1134+
if ($escapePattern) {
1135+
$searchProperties = array_filter($searchProperties, function ($property) use ($pattern) {
11361136
if ($property === 'EMAIL' && str_contains($pattern, ' ')) {
11371137
// There can be no spaces in emails
1138-
continue;
1138+
return false;
11391139
}
11401140

11411141
if ($property === 'CLOUD' && preg_match('/[^a-zA-Z0-9 :_.@\/\-\']/', $pattern) === 1) {
11421142
// There can be no chars in cloud ids which are not valid for user ids plus :/
11431143
// worst case: CA61590A-BBBC-423E-84AF-E6DF01455A53@https://my.nxt/srv/
1144-
continue;
1144+
return false;
11451145
}
1146-
}
11471146

1148-
$propertyOr->add($query2->expr()->eq('cp.name', $query2->createNamedParameter($property)));
1147+
return true;
1148+
});
11491149
}
11501150

1151-
if ($propertyOr->count() === 0) {
1151+
if (empty($searchProperties)) {
11521152
return [];
11531153
}
11541154

11551155
$query2->selectDistinct('cp.cardid')
11561156
->from($this->dbCardsPropertiesTable, 'cp')
11571157
->andWhere($addressBookOr)
1158-
->andWhere($propertyOr);
1158+
->andWhere($query2->expr()->in('cp.name', $query2->createNamedParameter($searchProperties, IQueryBuilder::PARAM_STR_ARRAY)));
11591159

11601160
// No need for like when the pattern is empty
11611161
if ('' !== $pattern) {
@@ -1167,14 +1167,39 @@ private function searchByAddressBookIds(array $addressBookIds,
11671167
$query2->andWhere($query2->expr()->ilike('cp.value', $query2->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%')));
11681168
}
11691169
}
1170-
11711170
if (isset($options['limit'])) {
11721171
$query2->setMaxResults($options['limit']);
11731172
}
11741173
if (isset($options['offset'])) {
11751174
$query2->setFirstResult($options['offset']);
11761175
}
11771176

1177+
if (
1178+
$options['since'] instanceof DateTimeFilter
1179+
|| $options['until'] instanceof DateTimeFilter
1180+
) {
1181+
$query2->join('cp', $this->dbCardsPropertiesTable, 'cp_bday', 'cp.cardid = cp_bday.cardid');
1182+
$query2->andWhere($query2->expr()->eq('cp_bday.name', $query2->createNamedParameter('BDAY')));
1183+
/**
1184+
* FIXME Find a way to match only 4 last digits
1185+
* BDAY can be --1018 without year or 20001019 with it
1186+
* $bDayOr = $query2->expr()->orX();
1187+
* if ($options['since'] instanceof DateTimeFilter) {
1188+
* $bDayOr->add(
1189+
* $query2->expr()->gte('SUBSTR(cp_bday.value, -4)',
1190+
* $query2->createNamedParameter($options['since']->get()->format('md')))
1191+
* );
1192+
* }
1193+
* if ($options['until'] instanceof DateTimeFilter) {
1194+
* $bDayOr->add(
1195+
* $query2->expr()->lte('SUBSTR(cp_bday.value, -4)',
1196+
* $query2->createNamedParameter($options['until']->get()->format('md')))
1197+
* );
1198+
* }
1199+
* $query2->andWhere($bDayOr);
1200+
*/
1201+
}
1202+
11781203
$result = $query2->execute();
11791204
$matches = $result->fetchAll();
11801205
$result->closeCursor();
@@ -1410,7 +1435,7 @@ public function pruneOutdatedSyncTokens(int $keep = 10_000): int {
14101435
$maxId = (int) $result->fetchOne();
14111436
$result->closeCursor();
14121437
if (!$maxId || $maxId < $keep) {
1413-
return 0;
1438+
return 0;
14141439
}
14151440

14161441
$query = $this->db->getQueryBuilder();

apps/dav/lib/Search/ContactsSearchProvider.php

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,18 +32,21 @@
3232
use OCP\IL10N;
3333
use OCP\IURLGenerator;
3434
use OCP\IUser;
35-
use OCP\Search\IProvider;
35+
use OCP\Search\IProviderV2;
3636
use OCP\Search\ISearchQuery;
3737
use OCP\Search\SearchResult;
3838
use OCP\Search\SearchResultEntry;
3939
use Sabre\VObject\Component\VCard;
4040
use Sabre\VObject\Reader;
4141

42-
class ContactsSearchProvider implements IProvider {
42+
class ContactsSearchProvider implements IProviderV2 {
43+
private static $searchPropertiesRestricted = [
44+
'N',
45+
'FN',
46+
'NICKNAME',
47+
'EMAIL',
48+
];
4349

44-
/**
45-
* @var string[]
46-
*/
4750
private static $searchProperties = [
4851
'N',
4952
'FN',
@@ -106,13 +109,15 @@ public function search(IUser $user, ISearchQuery $query): SearchResult {
106109
$searchResults = $this->backend->searchPrincipalUri(
107110
$principalUri,
108111
$query->getFilter('term')?->get() ?? '',
109-
self::$searchProperties,
112+
$query->getFilter('title-only')?->get() ? self::$searchPropertiesRestricted : self::$searchProperties,
110113
[
111114
'limit' => $query->getLimit(),
112115
'offset' => $query->getCursor(),
113116
'since' => $query->getFilter('since'),
114117
'until' => $query->getFilter('until'),
115-
]
118+
'person' => $query->getFilter('person'),
119+
'company' => $query->getFilter('company'),
120+
],
116121
);
117122
$formattedResults = \array_map(function (array $contactRow) use ($addressBooksById):SearchResultEntry {
118123
$addressBook = $addressBooksById[$contactRow['addressbookid']];
@@ -176,4 +181,24 @@ protected function generateSubline(VCard $vCard): string {
176181

177182
return (string)$emailAddresses[0];
178183
}
184+
185+
public function getSupportedFilters(): array {
186+
return [
187+
'term',
188+
'since',
189+
'until',
190+
'person',
191+
'title-only',
192+
];
193+
}
194+
195+
public function getAlternateIds(): array {
196+
return [];
197+
}
198+
199+
public function registerCustomFilters(): array {
200+
return [
201+
'company' => 'string'
202+
];
203+
}
179204
}

0 commit comments

Comments
 (0)