Skip to content

Commit 5fb2562

Browse files
georgehrkeskjnldsv
authored andcommitted
Implement Contacts Backend for Unified Search
Signed-off-by: Georg Ehrke <[email protected]>
1 parent db5ac96 commit 5fb2562

File tree

7 files changed

+557
-8
lines changed

7 files changed

+557
-8
lines changed

apps/dav/composer/composer/autoload_classmap.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,8 @@
210210
'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningNode' => $baseDir . '/../lib/Provisioning/Apple/AppleProvisioningNode.php',
211211
'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningPlugin' => $baseDir . '/../lib/Provisioning/Apple/AppleProvisioningPlugin.php',
212212
'OCA\\DAV\\RootCollection' => $baseDir . '/../lib/RootCollection.php',
213+
'OCA\\DAV\\Search\\ContactsSearchProvider' => $baseDir . '/../lib/Search/ContactsSearchProvider.php',
214+
'OCA\\DAV\\Search\\ContactsSearchResultEntry' => $baseDir . '/../lib/Search/ContactsSearchResultEntry.php',
213215
'OCA\\DAV\\Server' => $baseDir . '/../lib/Server.php',
214216
'OCA\\DAV\\Settings\\CalDAVSettings' => $baseDir . '/../lib/Settings/CalDAVSettings.php',
215217
'OCA\\DAV\\Storage\\PublicOwnerWrapper' => $baseDir . '/../lib/Storage/PublicOwnerWrapper.php',

apps/dav/composer/composer/autoload_static.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,8 @@ class ComposerStaticInitDAV
225225
'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningNode' => __DIR__ . '/..' . '/../lib/Provisioning/Apple/AppleProvisioningNode.php',
226226
'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningPlugin' => __DIR__ . '/..' . '/../lib/Provisioning/Apple/AppleProvisioningPlugin.php',
227227
'OCA\\DAV\\RootCollection' => __DIR__ . '/..' . '/../lib/RootCollection.php',
228+
'OCA\\DAV\\Search\\ContactsSearchProvider' => __DIR__ . '/..' . '/../lib/Search/ContactsSearchProvider.php',
229+
'OCA\\DAV\\Search\\ContactsSearchResultEntry' => __DIR__ . '/..' . '/../lib/Search/ContactsSearchResultEntry.php',
228230
'OCA\\DAV\\Server' => __DIR__ . '/..' . '/../lib/Server.php',
229231
'OCA\\DAV\\Settings\\CalDAVSettings' => __DIR__ . '/..' . '/../lib/Settings/CalDAVSettings.php',
230232
'OCA\\DAV\\Storage\\PublicOwnerWrapper' => __DIR__ . '/..' . '/../lib/Storage/PublicOwnerWrapper.php',

apps/dav/lib/AppInfo/Application.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
use OCA\DAV\CardDAV\PhotoCache;
5555
use OCA\DAV\CardDAV\SyncService;
5656
use OCA\DAV\HookManager;
57+
use OCA\DAV\Search\ContactsSearchProvider;
5758
use OCP\AppFramework\App;
5859
use OCP\AppFramework\Bootstrap\IBootContext;
5960
use OCP\AppFramework\Bootstrap\IBootstrap;
@@ -96,6 +97,11 @@ public function register(IRegistrationContext $context): void {
9697
* Register capabilities
9798
*/
9899
$context->registerCapability(Capabilities::class);
100+
101+
/*
102+
* Register Search Providers
103+
*/
104+
$context->registerSearchProvider(ContactsSearchProvider::class);
99105
}
100106

101107
public function boot(IBootContext $context): void {

apps/dav/lib/CardDAV/CardDavBackend.php

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -951,7 +951,7 @@ public function updateShares(IShareable $shareable, $add, $remove) {
951951
}
952952

953953
/**
954-
* search contact
954+
* Search contacts in a specific address-book
955955
*
956956
* @param int $addressBookId
957957
* @param string $pattern which should match within the $searchProperties
@@ -962,11 +962,55 @@ public function updateShares(IShareable $shareable, $add, $remove) {
962962
* - 'offset' - Set the offset for the limited search results
963963
* @return array an array of contacts which are arrays of key-value-pairs
964964
*/
965-
public function search($addressBookId, $pattern, $searchProperties, $options = []) {
965+
public function search($addressBookId, $pattern, $searchProperties, $options = []): array {
966+
return $this->searchByAddressBookIds([$addressBookId], $pattern, $searchProperties, $options);
967+
}
968+
969+
/**
970+
* Search contacts in all address-books accessible by a user
971+
*
972+
* @param string $principalUri
973+
* @param string $pattern
974+
* @param array $searchProperties
975+
* @param array $options
976+
* @return array
977+
*/
978+
public function searchPrincipalUri(string $principalUri,
979+
string $pattern,
980+
array $searchProperties,
981+
array $options = []): array {
982+
$addressBookIds = array_map(static function ($row):int {
983+
return (int) $row['id'];
984+
}, $this->getAddressBooksForUser($principalUri));
985+
986+
return $this->searchByAddressBookIds($addressBookIds, $pattern, $searchProperties, $options);
987+
}
988+
989+
/**
990+
* @param array $addressBookIds
991+
* @param string $pattern
992+
* @param array $searchProperties
993+
* @param array $options
994+
* @return array
995+
*/
996+
private function searchByAddressBookIds(array $addressBookIds,
997+
string $pattern,
998+
array $searchProperties,
999+
array $options = []): array {
9661000
$escapePattern = !\array_key_exists('escape_like_param', $options) || $options['escape_like_param'] !== false;
9671001

9681002
$query2 = $this->db->getQueryBuilder();
969-
$or = $query2->expr()->orX();
1003+
1004+
$addressBookOr = $query2->expr()->orX();
1005+
foreach ($addressBookIds as $addressBookId) {
1006+
$addressBookOr->add($query2->expr()->eq('cp.addressbookid', $query2->createNamedParameter($addressBookId)));
1007+
}
1008+
1009+
if ($addressBookOr->count() === 0) {
1010+
return [];
1011+
}
1012+
1013+
$propertyOr = $query2->expr()->orX();
9701014
foreach ($searchProperties as $property) {
9711015
if ($escapePattern) {
9721016
if ($property === 'EMAIL' && strpos($pattern, ' ') !== false) {
@@ -980,17 +1024,17 @@ public function search($addressBookId, $pattern, $searchProperties, $options = [
9801024
}
9811025
}
9821026

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

986-
if ($or->count() === 0) {
1030+
if ($propertyOr->count() === 0) {
9871031
return [];
9881032
}
9891033

9901034
$query2->selectDistinct('cp.cardid')
9911035
->from($this->dbCardsPropertiesTable, 'cp')
992-
->andWhere($query2->expr()->eq('cp.addressbookid', $query2->createNamedParameter($addressBookId)))
993-
->andWhere($or);
1036+
->andWhere($addressBookOr)
1037+
->andWhere($propertyOr);
9941038

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

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

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

10281072
return array_map(function ($array) {
1073+
$array['addressbookid'] = (int) $array['addressbookid'];
10291074
$modified = false;
10301075
$array['carddata'] = $this->readBlob($array['carddata'], $modified);
10311076
if ($modified) {
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* @copyright Copyright (c) 2020, Georg Ehrke
7+
*
8+
* @author Georg Ehrke <[email protected]>
9+
*
10+
* @license AGPL-3.0
11+
*
12+
* This code is free software: you can redistribute it and/or modify
13+
* it under the terms of the GNU Affero General Public License, version 3,
14+
* as published by the Free Software Foundation.
15+
*
16+
* This program is distributed in the hope that it will be useful,
17+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
18+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19+
* GNU Affero General Public License for more details.
20+
*
21+
* You should have received a copy of the GNU Affero General Public License, version 3,
22+
* along with this program. If not, see <http://www.gnu.org/licenses/>
23+
*
24+
*/
25+
namespace OCA\DAV\Search;
26+
27+
use OCA\DAV\CardDAV\CardDavBackend;
28+
use OCP\App\IAppManager;
29+
use OCP\IL10N;
30+
use OCP\IURLGenerator;
31+
use OCP\IUser;
32+
use OCP\Search\IProvider;
33+
use OCP\Search\ISearchQuery;
34+
use OCP\Search\SearchResult;
35+
use Sabre\VObject\Component\VCard;
36+
use Sabre\VObject\Reader;
37+
38+
class ContactsSearchProvider implements IProvider {
39+
40+
/** @var IAppManager */
41+
private $appManager;
42+
43+
/** @var IL10N */
44+
private $l10n;
45+
46+
/** @var IURLGenerator */
47+
private $urlGenerator;
48+
49+
/** @var CardDavBackend */
50+
private $backend;
51+
52+
/**
53+
* @var string[]
54+
*/
55+
private static $searchProperties = [
56+
'N',
57+
'FN',
58+
'NICKNAME',
59+
'EMAIL',
60+
'ADR',
61+
];
62+
63+
/**
64+
* ContactsSearchProvider constructor.
65+
*
66+
* @param IAppManager $appManager
67+
* @param IL10N $l10n
68+
* @param IURLGenerator $urlGenerator
69+
* @param CardDavBackend $backend
70+
*/
71+
public function __construct(IAppManager $appManager,
72+
IL10N $l10n,
73+
IURLGenerator $urlGenerator,
74+
CardDavBackend $backend) {
75+
$this->appManager = $appManager;
76+
$this->l10n = $l10n;
77+
$this->urlGenerator = $urlGenerator;
78+
$this->backend = $backend;
79+
}
80+
81+
/**
82+
* @inheritDoc
83+
*/
84+
public function getId(): string {
85+
return 'contacts-dav';
86+
}
87+
88+
/**
89+
* @inheritDoc
90+
*/
91+
public function getName(): string {
92+
return $this->l10n->t('Contacts');
93+
}
94+
95+
/**
96+
* @inheritDoc
97+
*/
98+
public function search(IUser $user, ISearchQuery $query): SearchResult {
99+
if (!$this->appManager->isEnabledForUser('contacts', $user)) {
100+
return SearchResult::complete($this->getName(), []);
101+
}
102+
103+
$principalUri = 'principals/users/' . $user->getUID();
104+
$addressBooks = $this->backend->getAddressBooksForUser($principalUri);
105+
$addressBooksById = [];
106+
foreach ($addressBooks as $addressBook) {
107+
$addressBooksById[(int) $addressBook['id']] = $addressBook;
108+
}
109+
110+
$searchResults = $this->backend->searchPrincipalUri(
111+
$principalUri,
112+
$query->getTerm(),
113+
self::$searchProperties,
114+
[
115+
'limit' => $query->getLimit(),
116+
'offset' => $query->getCursor(),
117+
]
118+
);
119+
$formattedResults = \array_map(function (array $contactRow) use ($addressBooksById):ContactsSearchResultEntry {
120+
$addressBook = $addressBooksById[$contactRow['addressbookid']];
121+
122+
/** @var VCard $vCard */
123+
$vCard = Reader::read($contactRow['carddata']);
124+
$thumbnailUrl = '';
125+
if ($vCard->PHOTO) {
126+
$thumbnailUrl = $this->getDavUrlForContact($addressBook['principaluri'], $addressBook['uri'], $contactRow['uri']) . '?photo';
127+
}
128+
129+
$title = (string)$vCard->FN;
130+
$subline = $this->generateSubline($vCard);
131+
$resourceUrl = $this->getDeepLinkToContactsApp($addressBook['uri'], (string) $vCard->UID);
132+
133+
return new ContactsSearchResultEntry($thumbnailUrl, $title, $subline, $resourceUrl, 'icon-contacts-dark', true);
134+
}, $searchResults);
135+
136+
return SearchResult::paginated(
137+
$this->getName(),
138+
$formattedResults,
139+
$query->getCursor() + count($formattedResults)
140+
);
141+
}
142+
143+
/**
144+
* @param string $principalUri
145+
* @param string $addressBookUri
146+
* @param string $contactsUri
147+
* @return string
148+
*/
149+
protected function getDavUrlForContact(string $principalUri,
150+
string $addressBookUri,
151+
string $contactsUri): string {
152+
[, $principalType, $principalId] = explode('/', $principalUri, 3);
153+
154+
return $this->urlGenerator->getAbsoluteURL(
155+
$this->urlGenerator->linkTo('', 'remote.php') . '/dav/addressbooks/'
156+
. $principalType . '/'
157+
. $principalId . '/'
158+
. $addressBookUri . '/'
159+
. $contactsUri
160+
);
161+
}
162+
163+
/**
164+
* @param string $addressBookUri
165+
* @param string $contactUid
166+
* @return string
167+
*/
168+
protected function getDeepLinkToContactsApp(string $addressBookUri,
169+
string $contactUid): string {
170+
return $this->urlGenerator->getAbsoluteURL(
171+
$this->urlGenerator->linkToRoute('contacts.contacts.direct', [
172+
'contact' => $contactUid . '~' . $addressBookUri
173+
])
174+
);
175+
}
176+
177+
/**
178+
* @param VCard $vCard
179+
* @return string
180+
*/
181+
protected function generateSubline(VCard $vCard): string {
182+
$emailAddresses = $vCard->select('EMAIL');
183+
if (!is_array($emailAddresses) || empty($emailAddresses)) {
184+
return '';
185+
}
186+
187+
return (string)$emailAddresses[0];
188+
}
189+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* @copyright Copyright (c) 2020, Georg Ehrke
7+
*
8+
* @author Georg Ehrke <[email protected]>
9+
*
10+
* @license AGPL-3.0
11+
*
12+
* This code is free software: you can redistribute it and/or modify
13+
* it under the terms of the GNU Affero General Public License, version 3,
14+
* as published by the Free Software Foundation.
15+
*
16+
* This program is distributed in the hope that it will be useful,
17+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
18+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19+
* GNU Affero General Public License for more details.
20+
*
21+
* You should have received a copy of the GNU Affero General Public License, version 3,
22+
* along with this program. If not, see <http://www.gnu.org/licenses/>
23+
*
24+
*/
25+
namespace OCA\DAV\Search;
26+
27+
use OCP\Search\ASearchResultEntry;
28+
29+
class ContactsSearchResultEntry extends ASearchResultEntry {
30+
}

0 commit comments

Comments
 (0)