Skip to content

Commit 50b397c

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

2 files changed

Lines changed: 76 additions & 29 deletions

File tree

apps/dav/lib/CardDAV/CardDavBackend.php

Lines changed: 42 additions & 12 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 OC\Search\Filter\DateTimeFilter;
5657
use PDO;
5758
use Sabre\CardDAV\Backend\BackendInterface;
5859
use Sabre\CardDAV\Backend\SyncSupport;
@@ -1109,7 +1110,15 @@ public function searchPrincipalUri(string $principalUri,
11091110
* @param string $pattern
11101111
* @param array $searchProperties
11111112
* @param array $options
1112-
* @psalm-param array{types?: bool, escape_like_param?: bool, limit?: int, offset?: int, wildcard?: bool} $options
1113+
* @psalm-param array{
1114+
* types?: bool,
1115+
* escape_like_param?: bool,
1116+
* limit?: int,
1117+
* offset?: int,
1118+
* wildcard?: bool,
1119+
* since?: DateTimeFilter|null,
1120+
* until?: DateTimeFilter|null,
1121+
* } $options
11131122
* @return array
11141123
*/
11151124
private function searchByAddressBookIds(array $addressBookIds,
@@ -1130,32 +1139,31 @@ private function searchByAddressBookIds(array $addressBookIds,
11301139
return [];
11311140
}
11321141

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

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

1148-
$propertyOr->add($query2->expr()->eq('cp.name', $query2->createNamedParameter($property)));
1155+
return true;
1156+
});
11491157
}
11501158

1151-
if ($propertyOr->count() === 0) {
1159+
if (empty($searchProperties)) {
11521160
return [];
11531161
}
11541162

11551163
$query2->selectDistinct('cp.cardid')
11561164
->from($this->dbCardsPropertiesTable, 'cp')
11571165
->andWhere($addressBookOr)
1158-
->andWhere($propertyOr);
1166+
->andWhere($query2->expr()->in('cp.name', $query2->createNamedParameter($searchProperties, IQueryBuilder::PARAM_STR_ARRAY)));
11591167

11601168
// No need for like when the pattern is empty
11611169
if ('' !== $pattern) {
@@ -1167,14 +1175,36 @@ private function searchByAddressBookIds(array $addressBookIds,
11671175
$query2->andWhere($query2->expr()->ilike('cp.value', $query2->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%')));
11681176
}
11691177
}
1170-
11711178
if (isset($options['limit'])) {
11721179
$query2->setMaxResults($options['limit']);
11731180
}
11741181
if (isset($options['offset'])) {
11751182
$query2->setFirstResult($options['offset']);
11761183
}
11771184

1185+
if (isset($options['since']) || isset($options['until'])) {
1186+
$query2->join('cp', $this->dbCardsPropertiesTable, 'cp_bday', 'cp.cardid = cp_bday.cardid');
1187+
$query2->andWhere($query2->expr()->eq('cp_bday.name', $query2->createNamedParameter('BDAY')));
1188+
/**
1189+
* FIXME Find a way to match only 4 last digits
1190+
* BDAY can be --1018 without year or 20001019 with it
1191+
* $bDayOr = $query2->expr()->orX();
1192+
* if ($options['since'] instanceof DateTimeFilter) {
1193+
* $bDayOr->add(
1194+
* $query2->expr()->gte('SUBSTR(cp_bday.value, -4)',
1195+
* $query2->createNamedParameter($options['since']->get()->format('md')))
1196+
* );
1197+
* }
1198+
* if ($options['until'] instanceof DateTimeFilter) {
1199+
* $bDayOr->add(
1200+
* $query2->expr()->lte('SUBSTR(cp_bday.value, -4)',
1201+
* $query2->createNamedParameter($options['until']->get()->format('md')))
1202+
* );
1203+
* }
1204+
* $query2->andWhere($bDayOr);
1205+
*/
1206+
}
1207+
11781208
$result = $query2->execute();
11791209
$matches = $result->fetchAll();
11801210
$result->closeCursor();
@@ -1410,7 +1440,7 @@ public function pruneOutdatedSyncTokens(int $keep = 10_000): int {
14101440
$maxId = (int) $result->fetchOne();
14111441
$result->closeCursor();
14121442
if (!$maxId || $maxId < $keep) {
1413-
return 0;
1443+
return 0;
14141444
}
14151445

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

apps/dav/lib/Search/ContactsSearchProvider.php

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -32,19 +32,23 @@
3232
use OCP\IL10N;
3333
use OCP\IURLGenerator;
3434
use OCP\IUser;
35-
use OCP\Search\IProvider;
35+
use OCP\Search\FilterDefinition;
36+
use OCP\Search\IFilteringProvider;
3637
use OCP\Search\ISearchQuery;
3738
use OCP\Search\SearchResult;
3839
use OCP\Search\SearchResultEntry;
3940
use Sabre\VObject\Component\VCard;
4041
use Sabre\VObject\Reader;
4142

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

44-
/**
45-
* @var string[]
46-
*/
47-
private static $searchProperties = [
51+
private static array $searchProperties = [
4852
'N',
4953
'FN',
5054
'NICKNAME',
@@ -78,19 +82,13 @@ public function getName(): string {
7882
return $this->l10n->t('Contacts');
7983
}
8084

81-
/**
82-
* @inheritDoc
83-
*/
8485
public function getOrder(string $route, array $routeParameters): int {
8586
if ($route === 'contacts.Page.index') {
8687
return -1;
8788
}
8889
return 25;
8990
}
9091

91-
/**
92-
* @inheritDoc
93-
*/
9492
public function search(IUser $user, ISearchQuery $query): SearchResult {
9593
if (!$this->appManager->isEnabledForUser('contacts', $user)) {
9694
return SearchResult::complete($this->getName(), []);
@@ -106,13 +104,15 @@ public function search(IUser $user, ISearchQuery $query): SearchResult {
106104
$searchResults = $this->backend->searchPrincipalUri(
107105
$principalUri,
108106
$query->getFilter('term')?->get() ?? '',
109-
self::$searchProperties,
107+
$query->getFilter('title-only')?->get() ? self::$searchPropertiesRestricted : self::$searchProperties,
110108
[
111109
'limit' => $query->getLimit(),
112110
'offset' => $query->getCursor(),
113111
'since' => $query->getFilter('since'),
114112
'until' => $query->getFilter('until'),
115-
]
113+
'person' => $query->getFilter('person'),
114+
'company' => $query->getFilter('company'),
115+
],
116116
);
117117
$formattedResults = \array_map(function (array $contactRow) use ($addressBooksById):SearchResultEntry {
118118
$addressBook = $addressBooksById[$contactRow['addressbookid']];
@@ -165,9 +165,6 @@ protected function getDeepLinkToContactsApp(
165165
);
166166
}
167167

168-
/**
169-
* @param VCard $vCard
170-
*/
171168
protected function generateSubline(VCard $vCard): string {
172169
$emailAddresses = $vCard->select('EMAIL');
173170
if (!is_array($emailAddresses) || empty($emailAddresses)) {
@@ -176,4 +173,24 @@ protected function generateSubline(VCard $vCard): string {
176173

177174
return (string)$emailAddresses[0];
178175
}
176+
177+
public function getSupportedFilters(): array {
178+
return [
179+
'term',
180+
'since',
181+
'until',
182+
'person',
183+
'title-only',
184+
];
185+
}
186+
187+
public function getAlternateIds(): array {
188+
return [];
189+
}
190+
191+
public function getCustomFilters(): array {
192+
return [
193+
new FilterDefinition('company'),
194+
];
195+
}
179196
}

0 commit comments

Comments
 (0)