Merge pull request #22011 from nextcloud/feature/20919/contacts_search
Implement Contacts Backend for Unified Search
This commit is contained in:
commit
03778d6463
|
@ -210,6 +210,8 @@ return array(
|
||||||
'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningNode' => $baseDir . '/../lib/Provisioning/Apple/AppleProvisioningNode.php',
|
'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningNode' => $baseDir . '/../lib/Provisioning/Apple/AppleProvisioningNode.php',
|
||||||
'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningPlugin' => $baseDir . '/../lib/Provisioning/Apple/AppleProvisioningPlugin.php',
|
'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningPlugin' => $baseDir . '/../lib/Provisioning/Apple/AppleProvisioningPlugin.php',
|
||||||
'OCA\\DAV\\RootCollection' => $baseDir . '/../lib/RootCollection.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\\Server' => $baseDir . '/../lib/Server.php',
|
||||||
'OCA\\DAV\\Settings\\CalDAVSettings' => $baseDir . '/../lib/Settings/CalDAVSettings.php',
|
'OCA\\DAV\\Settings\\CalDAVSettings' => $baseDir . '/../lib/Settings/CalDAVSettings.php',
|
||||||
'OCA\\DAV\\Storage\\PublicOwnerWrapper' => $baseDir . '/../lib/Storage/PublicOwnerWrapper.php',
|
'OCA\\DAV\\Storage\\PublicOwnerWrapper' => $baseDir . '/../lib/Storage/PublicOwnerWrapper.php',
|
||||||
|
|
|
@ -225,6 +225,8 @@ class ComposerStaticInitDAV
|
||||||
'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningNode' => __DIR__ . '/..' . '/../lib/Provisioning/Apple/AppleProvisioningNode.php',
|
'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningNode' => __DIR__ . '/..' . '/../lib/Provisioning/Apple/AppleProvisioningNode.php',
|
||||||
'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningPlugin' => __DIR__ . '/..' . '/../lib/Provisioning/Apple/AppleProvisioningPlugin.php',
|
'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningPlugin' => __DIR__ . '/..' . '/../lib/Provisioning/Apple/AppleProvisioningPlugin.php',
|
||||||
'OCA\\DAV\\RootCollection' => __DIR__ . '/..' . '/../lib/RootCollection.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\\Server' => __DIR__ . '/..' . '/../lib/Server.php',
|
||||||
'OCA\\DAV\\Settings\\CalDAVSettings' => __DIR__ . '/..' . '/../lib/Settings/CalDAVSettings.php',
|
'OCA\\DAV\\Settings\\CalDAVSettings' => __DIR__ . '/..' . '/../lib/Settings/CalDAVSettings.php',
|
||||||
'OCA\\DAV\\Storage\\PublicOwnerWrapper' => __DIR__ . '/..' . '/../lib/Storage/PublicOwnerWrapper.php',
|
'OCA\\DAV\\Storage\\PublicOwnerWrapper' => __DIR__ . '/..' . '/../lib/Storage/PublicOwnerWrapper.php',
|
||||||
|
|
|
@ -54,6 +54,7 @@ use OCA\DAV\CardDAV\ContactsManager;
|
||||||
use OCA\DAV\CardDAV\PhotoCache;
|
use OCA\DAV\CardDAV\PhotoCache;
|
||||||
use OCA\DAV\CardDAV\SyncService;
|
use OCA\DAV\CardDAV\SyncService;
|
||||||
use OCA\DAV\HookManager;
|
use OCA\DAV\HookManager;
|
||||||
|
use OCA\DAV\Search\ContactsSearchProvider;
|
||||||
use OCP\AppFramework\App;
|
use OCP\AppFramework\App;
|
||||||
use OCP\AppFramework\Bootstrap\IBootContext;
|
use OCP\AppFramework\Bootstrap\IBootContext;
|
||||||
use OCP\AppFramework\Bootstrap\IBootstrap;
|
use OCP\AppFramework\Bootstrap\IBootstrap;
|
||||||
|
@ -96,6 +97,11 @@ class Application extends App implements IBootstrap {
|
||||||
* Register capabilities
|
* Register capabilities
|
||||||
*/
|
*/
|
||||||
$context->registerCapability(Capabilities::class);
|
$context->registerCapability(Capabilities::class);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Register Search Providers
|
||||||
|
*/
|
||||||
|
$context->registerSearchProvider(ContactsSearchProvider::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function boot(IBootContext $context): void {
|
public function boot(IBootContext $context): void {
|
||||||
|
|
|
@ -951,7 +951,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* search contact
|
* Search contacts in a specific address-book
|
||||||
*
|
*
|
||||||
* @param int $addressBookId
|
* @param int $addressBookId
|
||||||
* @param string $pattern which should match within the $searchProperties
|
* @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
|
* - 'offset' - Set the offset for the limited search results
|
||||||
* @return array an array of contacts which are arrays of key-value-pairs
|
* @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;
|
$escapePattern = !\array_key_exists('escape_like_param', $options) || $options['escape_like_param'] !== false;
|
||||||
|
|
||||||
$query2 = $this->db->getQueryBuilder();
|
$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) {
|
foreach ($searchProperties as $property) {
|
||||||
if ($escapePattern) {
|
if ($escapePattern) {
|
||||||
if ($property === 'EMAIL' && strpos($pattern, ' ') !== false) {
|
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 [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
$query2->selectDistinct('cp.cardid')
|
$query2->selectDistinct('cp.cardid')
|
||||||
->from($this->dbCardsPropertiesTable, 'cp')
|
->from($this->dbCardsPropertiesTable, 'cp')
|
||||||
->andWhere($query2->expr()->eq('cp.addressbookid', $query2->createNamedParameter($addressBookId)))
|
->andWhere($addressBookOr)
|
||||||
->andWhere($or);
|
->andWhere($propertyOr);
|
||||||
|
|
||||||
// No need for like when the pattern is empty
|
// No need for like when the pattern is empty
|
||||||
if ('' !== $pattern) {
|
if ('' !== $pattern) {
|
||||||
|
@ -1016,7 +1060,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
|
||||||
}, $matches);
|
}, $matches);
|
||||||
|
|
||||||
$query = $this->db->getQueryBuilder();
|
$query = $this->db->getQueryBuilder();
|
||||||
$query->select('c.carddata', 'c.uri')
|
$query->select('c.addressbookid', 'c.carddata', 'c.uri')
|
||||||
->from($this->dbCardsTable, 'c')
|
->from($this->dbCardsTable, 'c')
|
||||||
->where($query->expr()->in('c.id', $query->createNamedParameter($matches, IQueryBuilder::PARAM_INT_ARRAY)));
|
->where($query->expr()->in('c.id', $query->createNamedParameter($matches, IQueryBuilder::PARAM_INT_ARRAY)));
|
||||||
|
|
||||||
|
@ -1026,6 +1070,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
|
||||||
$result->closeCursor();
|
$result->closeCursor();
|
||||||
|
|
||||||
return array_map(function ($array) {
|
return array_map(function ($array) {
|
||||||
|
$array['addressbookid'] = (int) $array['addressbookid'];
|
||||||
$modified = false;
|
$modified = false;
|
||||||
$array['carddata'] = $this->readBlob($array['carddata'], $modified);
|
$array['carddata'] = $this->readBlob($array['carddata'], $modified);
|
||||||
if ($modified) {
|
if ($modified) {
|
||||||
|
|
|
@ -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];
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
}
|
|
@ -0,0 +1,275 @@
|
||||||
|
<?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\Tests\unit;
|
||||||
|
|
||||||
|
use OCA\DAV\CardDAV\CardDavBackend;
|
||||||
|
use OCA\DAV\Search\ContactsSearchProvider;
|
||||||
|
use OCA\DAV\Search\ContactsSearchResultEntry;
|
||||||
|
use OCP\App\IAppManager;
|
||||||
|
use OCP\IL10N;
|
||||||
|
use OCP\IURLGenerator;
|
||||||
|
use OCP\IUser;
|
||||||
|
use OCP\Search\ISearchQuery;
|
||||||
|
use OCP\Search\SearchResult;
|
||||||
|
use Sabre\VObject\Reader;
|
||||||
|
use Test\TestCase;
|
||||||
|
|
||||||
|
class ContactsSearchProviderTest extends TestCase {
|
||||||
|
|
||||||
|
/** @var IAppManager|\PHPUnit\Framework\MockObject\MockObject */
|
||||||
|
private $appManager;
|
||||||
|
|
||||||
|
/** @var IL10N|\PHPUnit\Framework\MockObject\MockObject */
|
||||||
|
private $l10n;
|
||||||
|
|
||||||
|
/** @var IURLGenerator|\PHPUnit\Framework\MockObject\MockObject */
|
||||||
|
private $urlGenerator;
|
||||||
|
|
||||||
|
/** @var CardDavBackend|\PHPUnit\Framework\MockObject\MockObject */
|
||||||
|
private $backend;
|
||||||
|
|
||||||
|
/** @var ContactsSearchProvider */
|
||||||
|
private $provider;
|
||||||
|
|
||||||
|
private $vcardTest0 = 'BEGIN:VCARD'.PHP_EOL.
|
||||||
|
'VERSION:3.0'.PHP_EOL.
|
||||||
|
'PRODID:-//Sabre//Sabre VObject 4.1.2//EN'.PHP_EOL.
|
||||||
|
'UID:Test'.PHP_EOL.
|
||||||
|
'FN:FN of Test'.PHP_EOL.
|
||||||
|
'N:Test;;;;'.PHP_EOL.
|
||||||
|
'EMAIL:forrestgump@example.com'.PHP_EOL.
|
||||||
|
'END:VCARD';
|
||||||
|
|
||||||
|
private $vcardTest1 = 'BEGIN:VCARD'.PHP_EOL.
|
||||||
|
'VERSION:3.0'.PHP_EOL.
|
||||||
|
'PRODID:-//Sabre//Sabre VObject 4.1.2//EN'.PHP_EOL.
|
||||||
|
'PHOTO;ENCODING=b;TYPE=image/jpeg:'.PHP_EOL.
|
||||||
|
'UID:Test2'.PHP_EOL.
|
||||||
|
'FN:FN of Test2'.PHP_EOL.
|
||||||
|
'N:Test2;;;;'.PHP_EOL.
|
||||||
|
'END:VCARD';
|
||||||
|
|
||||||
|
protected function setUp(): void {
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$this->appManager = $this->createMock(IAppManager::class);
|
||||||
|
$this->l10n = $this->createMock(IL10N::class);
|
||||||
|
$this->urlGenerator = $this->createMock(IURLGenerator::class);
|
||||||
|
$this->backend = $this->createMock(CardDavBackend::class);
|
||||||
|
|
||||||
|
$this->provider = new ContactsSearchProvider(
|
||||||
|
$this->appManager,
|
||||||
|
$this->l10n,
|
||||||
|
$this->urlGenerator,
|
||||||
|
$this->backend
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetId(): void {
|
||||||
|
$this->assertEquals('contacts-dav', $this->provider->getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetName(): void {
|
||||||
|
$this->l10n->expects($this->exactly(1))
|
||||||
|
->method('t')
|
||||||
|
->with('Contacts')
|
||||||
|
->willReturnArgument(0);
|
||||||
|
|
||||||
|
$this->assertEquals('Contacts', $this->provider->getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSearchAppDisabled(): void {
|
||||||
|
$user = $this->createMock(IUser::class);
|
||||||
|
$query = $this->createMock(ISearchQuery::class);
|
||||||
|
$this->appManager->expects($this->once())
|
||||||
|
->method('isEnabledForUser')
|
||||||
|
->with('contacts', $user)
|
||||||
|
->willReturn(false);
|
||||||
|
$this->l10n->expects($this->exactly(1))
|
||||||
|
->method('t')
|
||||||
|
->with('Contacts')
|
||||||
|
->willReturnArgument(0);
|
||||||
|
$this->backend->expects($this->never())
|
||||||
|
->method('getAddressBooksForUser');
|
||||||
|
$this->backend->expects($this->never())
|
||||||
|
->method('searchPrincipalUri');
|
||||||
|
|
||||||
|
$actual = $this->provider->search($user, $query);
|
||||||
|
$data = $actual->jsonSerialize();
|
||||||
|
$this->assertInstanceOf(SearchResult::class, $actual);
|
||||||
|
$this->assertEquals('Contacts', $data['name']);
|
||||||
|
$this->assertEmpty($data['entries']);
|
||||||
|
$this->assertFalse($data['isPaginated']);
|
||||||
|
$this->assertNull($data['cursor']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSearch(): void {
|
||||||
|
$user = $this->createMock(IUser::class);
|
||||||
|
$user->method('getUID')->willReturn('john.doe');
|
||||||
|
$query = $this->createMock(ISearchQuery::class);
|
||||||
|
$query->method('getTerm')->willReturn('search term');
|
||||||
|
$query->method('getLimit')->willReturn(5);
|
||||||
|
$query->method('getCursor')->willReturn(20);
|
||||||
|
$this->appManager->expects($this->once())
|
||||||
|
->method('isEnabledForUser')
|
||||||
|
->with('contacts', $user)
|
||||||
|
->willReturn(true);
|
||||||
|
$this->l10n->expects($this->exactly(1))
|
||||||
|
->method('t')
|
||||||
|
->with('Contacts')
|
||||||
|
->willReturnArgument(0);
|
||||||
|
|
||||||
|
$this->backend->expects($this->once())
|
||||||
|
->method('getAddressBooksForUser')
|
||||||
|
->with('principals/users/john.doe')
|
||||||
|
->willReturn([
|
||||||
|
[
|
||||||
|
'id' => 99,
|
||||||
|
'principaluri' => 'principals/users/john.doe',
|
||||||
|
'uri' => 'addressbook-uri-99',
|
||||||
|
], [
|
||||||
|
'id' => 123,
|
||||||
|
'principaluri' => 'principals/users/john.doe',
|
||||||
|
'uri' => 'addressbook-uri-123',
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
$this->backend->expects($this->once())
|
||||||
|
->method('searchPrincipalUri')
|
||||||
|
->with('principals/users/john.doe', 'search term',
|
||||||
|
['N', 'FN', 'NICKNAME', 'EMAIL', 'ADR'],
|
||||||
|
['limit' => 5, 'offset' => 20])
|
||||||
|
->willReturn([
|
||||||
|
[
|
||||||
|
'addressbookid' => 99,
|
||||||
|
'uri' => 'vcard0.vcf',
|
||||||
|
'carddata' => $this->vcardTest0,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'addressbookid' => 123,
|
||||||
|
'uri' => 'vcard1.vcf',
|
||||||
|
'carddata' => $this->vcardTest1,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$provider = $this->getMockBuilder(ContactsSearchProvider::class)
|
||||||
|
->setConstructorArgs([
|
||||||
|
$this->appManager,
|
||||||
|
$this->l10n,
|
||||||
|
$this->urlGenerator,
|
||||||
|
$this->backend,
|
||||||
|
])
|
||||||
|
->setMethods([
|
||||||
|
'getDavUrlForContact',
|
||||||
|
'getDeepLinkToContactsApp',
|
||||||
|
'generateSubline',
|
||||||
|
])
|
||||||
|
->getMock();
|
||||||
|
|
||||||
|
$provider->expects($this->once())
|
||||||
|
->method('getDavUrlForContact')
|
||||||
|
->with('principals/users/john.doe', 'addressbook-uri-123', 'vcard1.vcf')
|
||||||
|
->willReturn('absolute-thumbnail-url');
|
||||||
|
|
||||||
|
$provider->expects($this->exactly(2))
|
||||||
|
->method('generateSubline')
|
||||||
|
->willReturn('subline');
|
||||||
|
$provider->expects($this->exactly(2))
|
||||||
|
->method('getDeepLinkToContactsApp')
|
||||||
|
->withConsecutive(
|
||||||
|
['addressbook-uri-99', 'Test'],
|
||||||
|
['addressbook-uri-123', 'Test2']
|
||||||
|
)
|
||||||
|
->willReturn('deep-link-to-contacts');
|
||||||
|
|
||||||
|
$actual = $provider->search($user, $query);
|
||||||
|
$data = $actual->jsonSerialize();
|
||||||
|
$this->assertInstanceOf(SearchResult::class, $actual);
|
||||||
|
$this->assertEquals('Contacts', $data['name']);
|
||||||
|
$this->assertCount(2, $data['entries']);
|
||||||
|
$this->assertTrue($data['isPaginated']);
|
||||||
|
$this->assertEquals(22, $data['cursor']);
|
||||||
|
|
||||||
|
$result0 = $data['entries'][0];
|
||||||
|
$result0Data = $result0->jsonSerialize();
|
||||||
|
$result1 = $data['entries'][1];
|
||||||
|
$result1Data = $result1->jsonSerialize();
|
||||||
|
|
||||||
|
$this->assertInstanceOf(ContactsSearchResultEntry::class, $result0);
|
||||||
|
$this->assertEquals('', $result0Data['thumbnailUrl']);
|
||||||
|
$this->assertEquals('FN of Test', $result0Data['title']);
|
||||||
|
$this->assertEquals('subline', $result0Data['subline']);
|
||||||
|
$this->assertEquals('deep-link-to-contacts', $result0Data['resourceUrl']);
|
||||||
|
$this->assertEquals('icon-contacts-dark', $result0Data['iconClass']);
|
||||||
|
$this->assertTrue($result0Data['rounded']);
|
||||||
|
|
||||||
|
$this->assertInstanceOf(ContactsSearchResultEntry::class, $result0);
|
||||||
|
$this->assertEquals('absolute-thumbnail-url?photo', $result1Data['thumbnailUrl']);
|
||||||
|
$this->assertEquals('FN of Test2', $result1Data['title']);
|
||||||
|
$this->assertEquals('subline', $result1Data['subline']);
|
||||||
|
$this->assertEquals('deep-link-to-contacts', $result1Data['resourceUrl']);
|
||||||
|
$this->assertEquals('icon-contacts-dark', $result1Data['iconClass']);
|
||||||
|
$this->assertTrue($result1Data['rounded']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetDavUrlForContact(): void {
|
||||||
|
$this->urlGenerator->expects($this->once())
|
||||||
|
->method('linkTo')
|
||||||
|
->with('', 'remote.php')
|
||||||
|
->willReturn('link-to-remote.php');
|
||||||
|
$this->urlGenerator->expects($this->once())
|
||||||
|
->method('getAbsoluteURL')
|
||||||
|
->with('link-to-remote.php/dav/addressbooks/users/john.doe/foo/bar.vcf')
|
||||||
|
->willReturn('absolute-url-link-to-remote.php/dav/addressbooks/users/john.doe/foo/bar.vcf');
|
||||||
|
|
||||||
|
$actual = self::invokePrivate($this->provider, 'getDavUrlForContact', ['principals/users/john.doe', 'foo', 'bar.vcf']);
|
||||||
|
|
||||||
|
$this->assertEquals('absolute-url-link-to-remote.php/dav/addressbooks/users/john.doe/foo/bar.vcf', $actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetDeepLinkToContactsApp(): void {
|
||||||
|
$this->urlGenerator->expects($this->once())
|
||||||
|
->method('linkToRoute')
|
||||||
|
->with('contacts.contacts.direct', ['contact' => 'uid123~uri-john.doe'])
|
||||||
|
->willReturn('link-to-route-contacts.contacts.direct/direct/uid123~uri-john.doe');
|
||||||
|
$this->urlGenerator->expects($this->once())
|
||||||
|
->method('getAbsoluteURL')
|
||||||
|
->with('link-to-route-contacts.contacts.direct/direct/uid123~uri-john.doe')
|
||||||
|
->willReturn('absolute-url-link-to-route-contacts.contacts.direct/direct/uid123~uri-john.doe');
|
||||||
|
|
||||||
|
$actual = self::invokePrivate($this->provider, 'getDeepLinkToContactsApp', ['uri-john.doe', 'uid123']);
|
||||||
|
$this->assertEquals('absolute-url-link-to-route-contacts.contacts.direct/direct/uid123~uri-john.doe', $actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGenerateSubline(): void {
|
||||||
|
$vCard0 = Reader::read($this->vcardTest0);
|
||||||
|
$vCard1 = Reader::read($this->vcardTest1);
|
||||||
|
|
||||||
|
$actual1 = self::invokePrivate($this->provider, 'generateSubline', [$vCard0]);
|
||||||
|
$actual2 = self::invokePrivate($this->provider, 'generateSubline', [$vCard1]);
|
||||||
|
|
||||||
|
$this->assertEquals('forrestgump@example.com', $actual1);
|
||||||
|
$this->assertEquals('', $actual2);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue