diff options
Diffstat (limited to 'apps/dav/lib')
-rw-r--r-- | apps/dav/lib/AppInfo/Application.php | 6 | ||||
-rw-r--r-- | apps/dav/lib/CardDAV/CardDavBackend.php | 61 | ||||
-rw-r--r-- | apps/dav/lib/Search/ContactsSearchProvider.php | 189 | ||||
-rw-r--r-- | apps/dav/lib/Search/ContactsSearchResultEntry.php | 30 |
4 files changed, 278 insertions, 8 deletions
diff --git a/apps/dav/lib/AppInfo/Application.php b/apps/dav/lib/AppInfo/Application.php index bf1e6146330..6f2f7b29153 100644 --- a/apps/dav/lib/AppInfo/Application.php +++ b/apps/dav/lib/AppInfo/Application.php @@ -54,6 +54,7 @@ use OCA\DAV\CardDAV\ContactsManager; 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; @@ -96,6 +97,11 @@ class Application extends App implements IBootstrap { * Register capabilities */ $context->registerCapability(Capabilities::class); + + /* + * Register Search Providers + */ + $context->registerSearchProvider(ContactsSearchProvider::class); } public function boot(IBootContext $context): void { diff --git a/apps/dav/lib/CardDAV/CardDavBackend.php b/apps/dav/lib/CardDAV/CardDavBackend.php index 9d602025c7a..3b3474cdd01 100644 --- a/apps/dav/lib/CardDAV/CardDavBackend.php +++ b/apps/dav/lib/CardDAV/CardDavBackend.php @@ -951,7 +951,7 @@ class CardDavBackend implements BackendInterface, SyncSupport { } /** - * search contact + * Search contacts in a specific address-book * * @param int $addressBookId * @param string $pattern which should match within the $searchProperties @@ -962,11 +962,55 @@ class CardDavBackend implements BackendInterface, SyncSupport { * - '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) { @@ -980,17 +1024,17 @@ class CardDavBackend implements BackendInterface, SyncSupport { } } - $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) { @@ -1016,7 +1060,7 @@ class CardDavBackend implements BackendInterface, SyncSupport { }, $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))); @@ -1026,6 +1070,7 @@ class CardDavBackend implements BackendInterface, SyncSupport { $result->closeCursor(); return array_map(function ($array) { + $array['addressbookid'] = (int) $array['addressbookid']; $modified = false; $array['carddata'] = $this->readBlob($array['carddata'], $modified); if ($modified) { diff --git a/apps/dav/lib/Search/ContactsSearchProvider.php b/apps/dav/lib/Search/ContactsSearchProvider.php new file mode 100644 index 00000000000..656b484c2b9 --- /dev/null +++ b/apps/dav/lib/Search/ContactsSearchProvider.php @@ -0,0 +1,189 @@ +<?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 'contacts-dav'; + } + + /** + * @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.contacts.direct', [ + 'contact' => $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]; + } +} diff --git a/apps/dav/lib/Search/ContactsSearchResultEntry.php b/apps/dav/lib/Search/ContactsSearchResultEntry.php new file mode 100644 index 00000000000..698fc1b3f4a --- /dev/null +++ b/apps/dav/lib/Search/ContactsSearchResultEntry.php @@ -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 { +} |