Skip to content

Commit

Permalink
Implement Contacts Backend for Unified Search
Browse files Browse the repository at this point in the history
Signed-off-by: Georg Ehrke <developer@georgehrke.com>
  • Loading branch information
georgehrke committed Jul 30, 2020
1 parent 4cac0f6 commit 7ee97f5
Show file tree
Hide file tree
Showing 7 changed files with 561 additions and 8 deletions.
2 changes: 2 additions & 0 deletions apps/dav/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,8 @@
'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningNode' => $baseDir . '/../lib/Provisioning/Apple/AppleProvisioningNode.php',
'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningPlugin' => $baseDir . '/../lib/Provisioning/Apple/AppleProvisioningPlugin.php',
'OCA\\DAV\\RootCollection' => $baseDir . '/../lib/RootCollection.php',
'OCA\\DAV\\Search\\ContactsSearchProvider' => $baseDir . '/../lib/Search/ContactsSearchProvider.php',
'OCA\\DAV\\Search\\ContactsSearchResultEntry' => $baseDir . '/../lib/Search/ContactsSearchResultEntry.php',
'OCA\\DAV\\Server' => $baseDir . '/../lib/Server.php',
'OCA\\DAV\\Settings\\CalDAVSettings' => $baseDir . '/../lib/Settings/CalDAVSettings.php',
'OCA\\DAV\\Storage\\PublicOwnerWrapper' => $baseDir . '/../lib/Storage/PublicOwnerWrapper.php',
Expand Down
2 changes: 2 additions & 0 deletions apps/dav/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,8 @@ class ComposerStaticInitDAV
'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningNode' => __DIR__ . '/..' . '/../lib/Provisioning/Apple/AppleProvisioningNode.php',
'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningPlugin' => __DIR__ . '/..' . '/../lib/Provisioning/Apple/AppleProvisioningPlugin.php',
'OCA\\DAV\\RootCollection' => __DIR__ . '/..' . '/../lib/RootCollection.php',
'OCA\\DAV\\Search\\ContactsSearchProvider' => __DIR__ . '/..' . '/../lib/Search/ContactsSearchProvider.php',
'OCA\\DAV\\Search\\ContactsSearchResultEntry' => __DIR__ . '/..' . '/../lib/Search/ContactsSearchResultEntry.php',
'OCA\\DAV\\Server' => __DIR__ . '/..' . '/../lib/Server.php',
'OCA\\DAV\\Settings\\CalDAVSettings' => __DIR__ . '/..' . '/../lib/Settings/CalDAVSettings.php',
'OCA\\DAV\\Storage\\PublicOwnerWrapper' => __DIR__ . '/..' . '/../lib/Storage/PublicOwnerWrapper.php',
Expand Down
6 changes: 6 additions & 0 deletions apps/dav/lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
use OCA\DAV\CardDAV\PhotoCache;
use OCA\DAV\CardDAV\SyncService;
use OCA\DAV\HookManager;
use OCA\DAV\Search\ContactsSearchProvider;
use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootContext;
use OCP\AppFramework\Bootstrap\IBootstrap;
Expand Down Expand Up @@ -96,6 +97,11 @@ public function register(IRegistrationContext $context): void {
* Register capabilities
*/
$context->registerCapability(Capabilities::class);

/*
* Register Search Providers
*/
$context->registerSearchProvider(ContactsSearchProvider::class);
}

public function boot(IBootContext $context): void {
Expand Down
61 changes: 53 additions & 8 deletions apps/dav/lib/CardDAV/CardDavBackend.php
Original file line number Diff line number Diff line change
Expand Up @@ -951,7 +951,7 @@ public function updateShares(IShareable $shareable, $add, $remove) {
}

/**
* search contact
* Search contacts in a specific address-book
*
* @param int $addressBookId
* @param string $pattern which should match within the $searchProperties
Expand All @@ -962,11 +962,55 @@ public function updateShares(IShareable $shareable, $add, $remove) {
* - 'offset' - Set the offset for the limited search results
* @return array an array of contacts which are arrays of key-value-pairs
*/
public function search($addressBookId, $pattern, $searchProperties, $options = []) {
public function search($addressBookId, $pattern, $searchProperties, $options = []): array {
return $this->searchByAddressBookIds([$addressBookId], $pattern, $searchProperties, $options);
}

/**
* Search contacts in all address-books accessible by a user
*
* @param string $principalUri
* @param string $pattern
* @param array $searchProperties
* @param array $options
* @return array
*/
public function searchPrincipalUri(string $principalUri,
string $pattern,
array $searchProperties,
array $options = []): array {
$addressBookIds = array_map(static function ($row):int {
return (int) $row['id'];
}, $this->getAddressBooksForUser($principalUri));

return $this->searchByAddressBookIds($addressBookIds, $pattern, $searchProperties, $options);
}

/**
* @param array $addressBookIds
* @param string $pattern
* @param array $searchProperties
* @param array $options
* @return array
*/
private function searchByAddressBookIds(array $addressBookIds,
string $pattern,
array $searchProperties,
array $options = []): array {
$escapePattern = !\array_key_exists('escape_like_param', $options) || $options['escape_like_param'] !== false;

$query2 = $this->db->getQueryBuilder();
$or = $query2->expr()->orX();

$addressBookOr = $query2->expr()->orX();
foreach ($addressBookIds as $addressBookId) {
$addressBookOr->add($query2->expr()->eq('cp.addressbookid', $query2->createNamedParameter($addressBookId)));
}

if ($addressBookOr->count() === 0) {
return [];
}

$propertyOr = $query2->expr()->orX();
foreach ($searchProperties as $property) {
if ($escapePattern) {
if ($property === 'EMAIL' && strpos($pattern, ' ') !== false) {
Expand All @@ -980,17 +1024,17 @@ public function search($addressBookId, $pattern, $searchProperties, $options = [
}
}

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

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

$query2->selectDistinct('cp.cardid')
->from($this->dbCardsPropertiesTable, 'cp')
->andWhere($query2->expr()->eq('cp.addressbookid', $query2->createNamedParameter($addressBookId)))
->andWhere($or);
->andWhere($addressBookOr)
->andWhere($propertyOr);

// No need for like when the pattern is empty
if ('' !== $pattern) {
Expand All @@ -1016,7 +1060,7 @@ public function search($addressBookId, $pattern, $searchProperties, $options = [
}, $matches);

$query = $this->db->getQueryBuilder();
$query->select('c.carddata', 'c.uri')
$query->select('c.addressbookid', 'c.carddata', 'c.uri')
->from($this->dbCardsTable, 'c')
->where($query->expr()->in('c.id', $query->createNamedParameter($matches, IQueryBuilder::PARAM_INT_ARRAY)));

Expand All @@ -1026,6 +1070,7 @@ public function search($addressBookId, $pattern, $searchProperties, $options = [
$result->closeCursor();

return array_map(function ($array) {
$array['addressbookid'] = (int) $array['addressbookid'];
$modified = false;
$array['carddata'] = $this->readBlob($array['carddata'], $modified);
if ($modified) {
Expand Down
190 changes: 190 additions & 0 deletions apps/dav/lib/Search/ContactsSearchProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
<?php

declare(strict_types=1);

/**
* @copyright Copyright (c) 2020, Georg Ehrke
*
* @author Georg Ehrke <oc.list@georgehrke.com>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\DAV\Search;

use OCA\DAV\CardDAV\CardDavBackend;
use OCP\App\IAppManager;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\Search\IProvider;
use OCP\Search\ISearchQuery;
use OCP\Search\SearchResult;
use Sabre\VObject\Component\VCard;
use Sabre\VObject\Reader;

class ContactsSearchProvider implements IProvider {

/** @var IAppManager */
private $appManager;

/** @var IL10N */
private $l10n;

/** @var IURLGenerator */
private $urlGenerator;

/** @var CardDavBackend */
private $backend;

/**
* @var string[]
*/
private static $searchProperties = [
'N',
'FN',
'NICKNAME',
'EMAIL',
'ADR',
];

/**
* ContactsSearchProvider constructor.
*
* @param IAppManager $appManager
* @param IL10N $l10n
* @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;
}

/**
* @inheritDoc
*/
public function getId(): string {
return 'dav-contacts';
}

/**
* @inheritDoc
*/
public function getName(): string {
return $this->l10n->t('Contacts');
}

/**
* @inheritDoc
*/
public function search(IUser $user, ISearchQuery $query): SearchResult {
if (!$this->appManager->isEnabledForUser('contacts', $user)) {
return SearchResult::complete($this->getName(), []);
}

$principalUri = 'principals/users/' . $user->getUID();
$addressBooks = $this->backend->getAddressBooksForUser($principalUri);
$addressBooksById = [];
foreach ($addressBooks as $addressBook) {
$addressBooksById[(int) $addressBook['id']] = $addressBook;
}

$searchResults = $this->backend->searchPrincipalUri(
$principalUri,
$query->getTerm(),
self::$searchProperties,
[
'limit' => $query->getLimit(),
'offset' => $query->getCursor(),
]
);
$formattedResults = \array_map(function (array $contactRow) use ($addressBooksById):ContactsSearchResultEntry {
$addressBook = $addressBooksById[$contactRow['addressbookid']];

/** @var VCard $vCard */
$vCard = Reader::read($contactRow['carddata']);
$thumbnailUrl = '';
if ($vCard->PHOTO) {
$thumbnailUrl = $this->getDavUrlForContact($addressBook['principaluri'], $addressBook['uri'], $contactRow['uri']) . '?photo';
}

$title = (string)$vCard->FN;
$subline = $this->generateSubline($vCard);
$resourceUrl = $this->getDeepLinkToContactsApp($addressBook['uri'], (string) $vCard->UID);

return new ContactsSearchResultEntry($thumbnailUrl, $title, $subline, $resourceUrl, 'icon-contacts-dark', true);
}, $searchResults);

return SearchResult::paginated(
$this->getName(),
$formattedResults,
$query->getCursor() + count($formattedResults)
);
}

/**
* @param string $principalUri
* @param string $addressBookUri
* @param string $contactsUri
* @return string
*/
protected function getDavUrlForContact(string $principalUri,
string $addressBookUri,
string $contactsUri): string {
[, $principalType, $principalId] = explode('/', $principalUri, 3);

return $this->urlGenerator->getAbsoluteURL(
$this->urlGenerator->linkTo('', 'remote.php') . '/dav/addressbooks/'
. $principalType . '/'
. $principalId . '/'
. $addressBookUri . '/'
. $contactsUri
);
}

/**
* @param string $addressBookUri
* @param string $contactUid
* @return string
*/
protected function getDeepLinkToContactsApp(string $addressBookUri,
string $contactUid): string {
return $this->urlGenerator->getAbsoluteURL(
$this->urlGenerator->linkToRoute('contacts.page.index')
. 'All%20contacts/' // TODO - remove
. $contactUid . '~'
. $addressBookUri
);
}

/**
* @param VCard $vCard
* @return string
*/
protected function generateSubline(VCard $vCard): string {
$emailAddresses = $vCard->select('EMAIL');
if (!is_array($emailAddresses) || empty($emailAddresses)) {
return '';
}

return (string)$emailAddresses[0];
}
}
30 changes: 30 additions & 0 deletions apps/dav/lib/Search/ContactsSearchResultEntry.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

/**
* @copyright Copyright (c) 2020, Georg Ehrke
*
* @author Georg Ehrke <oc.list@georgehrke.com>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\DAV\Search;

use OCP\Search\ASearchResultEntry;

class ContactsSearchResultEntry extends ASearchResultEntry {
}
Loading

0 comments on commit 7ee97f5

Please sign in to comment.