ContactsSearchProvider.php 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
  5. * SPDX-License-Identifier: AGPL-3.0-or-later
  6. */
  7. namespace OCA\DAV\Search;
  8. use OCA\DAV\CardDAV\CardDavBackend;
  9. use OCP\App\IAppManager;
  10. use OCP\IL10N;
  11. use OCP\IURLGenerator;
  12. use OCP\IUser;
  13. use OCP\Search\FilterDefinition;
  14. use OCP\Search\IFilter;
  15. use OCP\Search\IFilteringProvider;
  16. use OCP\Search\ISearchQuery;
  17. use OCP\Search\SearchResult;
  18. use OCP\Search\SearchResultEntry;
  19. use Sabre\VObject\Component\VCard;
  20. use Sabre\VObject\Reader;
  21. class ContactsSearchProvider implements IFilteringProvider {
  22. private static array $searchPropertiesRestricted = [
  23. 'N',
  24. 'FN',
  25. 'NICKNAME',
  26. 'EMAIL',
  27. ];
  28. private static array $searchProperties = [
  29. 'N',
  30. 'FN',
  31. 'NICKNAME',
  32. 'EMAIL',
  33. 'TEL',
  34. 'ADR',
  35. 'TITLE',
  36. 'ORG',
  37. 'NOTE',
  38. ];
  39. public function __construct(
  40. private IAppManager $appManager,
  41. private IL10N $l10n,
  42. private IURLGenerator $urlGenerator,
  43. private CardDavBackend $backend,
  44. ) {
  45. }
  46. /**
  47. * @inheritDoc
  48. */
  49. public function getId(): string {
  50. return 'contacts';
  51. }
  52. /**
  53. * @inheritDoc
  54. */
  55. public function getName(): string {
  56. return $this->l10n->t('Contacts');
  57. }
  58. public function getOrder(string $route, array $routeParameters): ?int {
  59. if ($this->appManager->isEnabledForUser('contacts')) {
  60. return $route === 'contacts.Page.index' ? -1 : 25;
  61. }
  62. return null;
  63. }
  64. public function search(IUser $user, ISearchQuery $query): SearchResult {
  65. if (!$this->appManager->isEnabledForUser('contacts', $user)) {
  66. return SearchResult::complete($this->getName(), []);
  67. }
  68. $principalUri = 'principals/users/' . $user->getUID();
  69. $addressBooks = $this->backend->getAddressBooksForUser($principalUri);
  70. $addressBooksById = [];
  71. foreach ($addressBooks as $addressBook) {
  72. $addressBooksById[(int) $addressBook['id']] = $addressBook;
  73. }
  74. $searchResults = $this->backend->searchPrincipalUri(
  75. $principalUri,
  76. $query->getFilter('term')?->get() ?? '',
  77. $query->getFilter('title-only')?->get() ? self::$searchPropertiesRestricted : self::$searchProperties,
  78. [
  79. 'limit' => $query->getLimit(),
  80. 'offset' => $query->getCursor(),
  81. 'since' => $query->getFilter('since'),
  82. 'until' => $query->getFilter('until'),
  83. 'person' => $this->getPersonDisplayName($query->getFilter('person')),
  84. 'company' => $query->getFilter('company'),
  85. ],
  86. );
  87. $formattedResults = \array_map(function (array $contactRow) use ($addressBooksById):SearchResultEntry {
  88. $addressBook = $addressBooksById[$contactRow['addressbookid']];
  89. /** @var VCard $vCard */
  90. $vCard = Reader::read($contactRow['carddata']);
  91. $thumbnailUrl = '';
  92. if ($vCard->PHOTO) {
  93. $thumbnailUrl = $this->getDavUrlForContact($addressBook['principaluri'], $addressBook['uri'], $contactRow['uri']) . '?photo';
  94. }
  95. $title = (string)$vCard->FN;
  96. $subline = $this->generateSubline($vCard);
  97. $resourceUrl = $this->getDeepLinkToContactsApp($addressBook['uri'], (string) $vCard->UID);
  98. return new SearchResultEntry($thumbnailUrl, $title, $subline, $resourceUrl, 'icon-contacts-dark', true);
  99. }, $searchResults);
  100. return SearchResult::paginated(
  101. $this->getName(),
  102. $formattedResults,
  103. $query->getCursor() + count($formattedResults)
  104. );
  105. }
  106. private function getPersonDisplayName(?IFilter $person): ?string {
  107. $user = $person?->get();
  108. if ($user instanceof IUser) {
  109. return $user->getDisplayName();
  110. }
  111. return null;
  112. }
  113. protected function getDavUrlForContact(
  114. string $principalUri,
  115. string $addressBookUri,
  116. string $contactsUri,
  117. ): string {
  118. [, $principalType, $principalId] = explode('/', $principalUri, 3);
  119. return $this->urlGenerator->getAbsoluteURL(
  120. $this->urlGenerator->linkTo('', 'remote.php') . '/dav/addressbooks/'
  121. . $principalType . '/'
  122. . $principalId . '/'
  123. . $addressBookUri . '/'
  124. . $contactsUri
  125. );
  126. }
  127. protected function getDeepLinkToContactsApp(
  128. string $addressBookUri,
  129. string $contactUid,
  130. ): string {
  131. return $this->urlGenerator->getAbsoluteURL(
  132. $this->urlGenerator->linkToRoute('contacts.contacts.direct', [
  133. 'contact' => $contactUid . '~' . $addressBookUri
  134. ])
  135. );
  136. }
  137. protected function generateSubline(VCard $vCard): string {
  138. $emailAddresses = $vCard->select('EMAIL');
  139. if (!is_array($emailAddresses) || empty($emailAddresses)) {
  140. return '';
  141. }
  142. return (string)$emailAddresses[0];
  143. }
  144. public function getSupportedFilters(): array {
  145. return [
  146. 'term',
  147. 'since',
  148. 'until',
  149. 'person',
  150. 'title-only',
  151. ];
  152. }
  153. public function getAlternateIds(): array {
  154. return [];
  155. }
  156. public function getCustomFilters(): array {
  157. return [
  158. new FilterDefinition('company'),
  159. ];
  160. }
  161. }