Skip to content

Commit

Permalink
feat(carddav): Allow advanced search for contacts
Browse files Browse the repository at this point in the history
Signed-off-by: Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
  • Loading branch information
Altahrim committed Oct 26, 2023
1 parent 7e422ba commit b4482bb
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 18 deletions.
47 changes: 36 additions & 11 deletions apps/dav/lib/CardDAV/CardDavBackend.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
use OCP\IDBConnection;
use OCP\IGroupManager;
use OCP\IUserManager;
use OCP\Search\Filter\DateTimeFilter;
use PDO;
use Sabre\CardDAV\Backend\BackendInterface;
use Sabre\CardDAV\Backend\SyncSupport;
Expand Down Expand Up @@ -1130,32 +1131,31 @@ private function searchByAddressBookIds(array $addressBookIds,
return [];
}

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

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

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

if ($propertyOr->count() === 0) {
if (empty($searchProperties)) {
return [];
}

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

// No need for like when the pattern is empty
if ('' !== $pattern) {
Expand All @@ -1167,14 +1167,39 @@ private function searchByAddressBookIds(array $addressBookIds,
$query2->andWhere($query2->expr()->ilike('cp.value', $query2->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%')));
}
}

if (isset($options['limit'])) {
$query2->setMaxResults($options['limit']);
}
if (isset($options['offset'])) {
$query2->setFirstResult($options['offset']);
}

if (
$options['since'] instanceof DateTimeFilter

Check failure

Code scanning / Psalm

InvalidArrayOffset Error

Cannot access value on variable $options using offset value of 'since', expecting 'types', 'escape_like_param', 'limit', 'offset' or 'wildcard'

Check failure

Code scanning / Psalm

UndefinedClass Error

Class, interface or enum named OCP\Search\Filter\DateTimeFilter does not exist

Check failure on line 1178 in apps/dav/lib/CardDAV/CardDavBackend.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

InvalidArrayOffset

apps/dav/lib/CardDAV/CardDavBackend.php:1178:4: InvalidArrayOffset: Cannot access value on variable $options using offset value of 'since', expecting 'types', 'escape_like_param', 'limit', 'offset' or 'wildcard' (see https://psalm.dev/115)

Check failure on line 1178 in apps/dav/lib/CardDAV/CardDavBackend.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

UndefinedClass

apps/dav/lib/CardDAV/CardDavBackend.php:1178:33: UndefinedClass: Class, interface or enum named OCP\Search\Filter\DateTimeFilter does not exist (see https://psalm.dev/019)
|| $options['until'] instanceof DateTimeFilter
) {
$query2->join('cp', $this->dbCardsPropertiesTable, 'cp_bday', 'cp.cardid = cp_bday.cardid');
$query2->andWhere($query2->expr()->eq('cp_bday.name', $query2->createNamedParameter('BDAY')));
/**
* FIXME Find a way to match only 4 last digits
* BDAY can be --1018 without year or 20001019 with it
* $bDayOr = $query2->expr()->orX();
* if ($options['since'] instanceof DateTimeFilter) {
* $bDayOr->add(
* $query2->expr()->gte('SUBSTR(cp_bday.value, -4)',
* $query2->createNamedParameter($options['since']->get()->format('md')))
* );
* }
* if ($options['until'] instanceof DateTimeFilter) {
* $bDayOr->add(
* $query2->expr()->lte('SUBSTR(cp_bday.value, -4)',
* $query2->createNamedParameter($options['until']->get()->format('md')))
* );
* }
* $query2->andWhere($bDayOr);
*/
}

$result = $query2->execute();
$matches = $result->fetchAll();
$result->closeCursor();
Expand Down Expand Up @@ -1410,7 +1435,7 @@ public function pruneOutdatedSyncTokens(int $keep = 10_000): int {
$maxId = (int) $result->fetchOne();
$result->closeCursor();
if (!$maxId || $maxId < $keep) {
return 0;
return 0;
}

$query = $this->db->getQueryBuilder();
Expand Down
40 changes: 33 additions & 7 deletions apps/dav/lib/Search/ContactsSearchProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,22 @@
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\Search\IProvider;
use OCP\Search\FilterDefinition;
use OCP\Search\IFilteringProvider;
use OCP\Search\ISearchQuery;
use OCP\Search\SearchResult;
use OCP\Search\SearchResultEntry;
use Sabre\VObject\Component\VCard;
use Sabre\VObject\Reader;

class ContactsSearchProvider implements IProvider {
class ContactsSearchProvider implements IFilteringProvider {
private static $searchPropertiesRestricted = [

Check notice

Code scanning / Psalm

MissingPropertyType Note

Property OCA\DAV\Search\ContactsSearchProvider::$searchPropertiesRestricted does not have a declared type - consider list{string, string, string, string}
'N',
'FN',
'NICKNAME',
'EMAIL',
];

/**
* @var string[]
*/
private static $searchProperties = [
'N',
'FN',
Expand Down Expand Up @@ -106,13 +110,15 @@ public function search(IUser $user, ISearchQuery $query): SearchResult {
$searchResults = $this->backend->searchPrincipalUri(
$principalUri,
$query->getFilter('term')?->get() ?? '',
self::$searchProperties,
$query->getFilter('title-only')?->get() ? self::$searchPropertiesRestricted : self::$searchProperties,
[
'limit' => $query->getLimit(),
'offset' => $query->getCursor(),
'since' => $query->getFilter('since'),
'until' => $query->getFilter('until'),
]
'person' => $query->getFilter('person'),
'company' => $query->getFilter('company'),
],
);
$formattedResults = \array_map(function (array $contactRow) use ($addressBooksById):SearchResultEntry {
$addressBook = $addressBooksById[$contactRow['addressbookid']];
Expand Down Expand Up @@ -176,4 +182,24 @@ protected function generateSubline(VCard $vCard): string {

return (string)$emailAddresses[0];
}

public function getSupportedFilters(): array {
return [
'term',
'since',
'until',
'person',
'title-only',
];
}

public function getAlternateIds(): array {
return [];
}

public function getCustomFilters(): array {
return [
new FilterDefinition('company'),
];
}
}

0 comments on commit b4482bb

Please sign in to comment.