ContactsSearchProvider.php 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  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. $result = new SearchResultEntry($thumbnailUrl, $title, $subline, $resourceUrl, 'icon-contacts-dark', true);
  99. $result->addAttribute("displayName", $title);
  100. $result->addAttribute("email", $subline);
  101. $result->addAttribute("phoneNumber", (string)$vCard->TEL);
  102. return $result;
  103. }, $searchResults);
  104. return SearchResult::paginated(
  105. $this->getName(),
  106. $formattedResults,
  107. $query->getCursor() + count($formattedResults)
  108. );
  109. }
  110. private function getPersonDisplayName(?IFilter $person): ?string {
  111. $user = $person?->get();
  112. if ($user instanceof IUser) {
  113. return $user->getDisplayName();
  114. }
  115. return null;
  116. }
  117. protected function getDavUrlForContact(
  118. string $principalUri,
  119. string $addressBookUri,
  120. string $contactsUri,
  121. ): string {
  122. [, $principalType, $principalId] = explode('/', $principalUri, 3);
  123. return $this->urlGenerator->getAbsoluteURL(
  124. $this->urlGenerator->linkTo('', 'remote.php') . '/dav/addressbooks/'
  125. . $principalType . '/'
  126. . $principalId . '/'
  127. . $addressBookUri . '/'
  128. . $contactsUri
  129. );
  130. }
  131. protected function getDeepLinkToContactsApp(
  132. string $addressBookUri,
  133. string $contactUid,
  134. ): string {
  135. return $this->urlGenerator->getAbsoluteURL(
  136. $this->urlGenerator->linkToRoute('contacts.contacts.direct', [
  137. 'contact' => $contactUid . '~' . $addressBookUri
  138. ])
  139. );
  140. }
  141. protected function generateSubline(VCard $vCard): string {
  142. $emailAddresses = $vCard->select('EMAIL');
  143. if (!is_array($emailAddresses) || empty($emailAddresses)) {
  144. return '';
  145. }
  146. return (string)$emailAddresses[0];
  147. }
  148. public function getSupportedFilters(): array {
  149. return [
  150. 'term',
  151. 'since',
  152. 'until',
  153. 'person',
  154. 'title-only',
  155. ];
  156. }
  157. public function getAlternateIds(): array {
  158. return [];
  159. }
  160. public function getCustomFilters(): array {
  161. return [
  162. new FilterDefinition('company'),
  163. ];
  164. }
  165. }