EventsSearchProvider.php 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  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\CalDAV\CalDavBackend;
  9. use OCP\IUser;
  10. use OCP\Search\IFilteringProvider;
  11. use OCP\Search\ISearchQuery;
  12. use OCP\Search\SearchResult;
  13. use OCP\Search\SearchResultEntry;
  14. use Sabre\VObject\Component;
  15. use Sabre\VObject\DateTimeParser;
  16. use Sabre\VObject\Property;
  17. use function array_combine;
  18. use function array_fill;
  19. use function array_key_exists;
  20. use function array_map;
  21. /**
  22. * Class EventsSearchProvider
  23. *
  24. * @package OCA\DAV\Search
  25. */
  26. class EventsSearchProvider extends ACalendarSearchProvider implements IFilteringProvider {
  27. /**
  28. * @var string[]
  29. */
  30. private static $searchProperties = [
  31. 'SUMMARY',
  32. 'LOCATION',
  33. 'DESCRIPTION',
  34. 'ATTENDEE',
  35. 'ORGANIZER',
  36. 'CATEGORIES',
  37. ];
  38. /**
  39. * @var string[]
  40. */
  41. private static $searchParameters = [
  42. 'ATTENDEE' => ['CN'],
  43. 'ORGANIZER' => ['CN'],
  44. ];
  45. /**
  46. * @var string
  47. */
  48. private static $componentType = 'VEVENT';
  49. /**
  50. * @inheritDoc
  51. */
  52. public function getId(): string {
  53. return 'calendar';
  54. }
  55. /**
  56. * @inheritDoc
  57. */
  58. public function getName(): string {
  59. return $this->l10n->t('Events');
  60. }
  61. /**
  62. * @inheritDoc
  63. */
  64. public function getOrder(string $route, array $routeParameters): ?int {
  65. if ($this->appManager->isEnabledForUser('calendar')) {
  66. return $route === 'calendar.View.index' ? -1 : 30;
  67. }
  68. return null;
  69. }
  70. /**
  71. * @inheritDoc
  72. */
  73. public function search(
  74. IUser $user,
  75. ISearchQuery $query,
  76. ): SearchResult {
  77. if (!$this->appManager->isEnabledForUser('calendar', $user)) {
  78. return SearchResult::complete($this->getName(), []);
  79. }
  80. $principalUri = 'principals/users/' . $user->getUID();
  81. $calendarsById = $this->getSortedCalendars($principalUri);
  82. $subscriptionsById = $this->getSortedSubscriptions($principalUri);
  83. /** @var string|null $term */
  84. $term = $query->getFilter('term')?->get();
  85. if ($term === null) {
  86. $searchResults = [];
  87. } else {
  88. $searchResults = $this->backend->searchPrincipalUri(
  89. $principalUri,
  90. $term,
  91. [self::$componentType],
  92. self::$searchProperties,
  93. self::$searchParameters,
  94. [
  95. 'limit' => $query->getLimit(),
  96. 'offset' => $query->getCursor(),
  97. 'timerange' => [
  98. 'start' => $query->getFilter('since')?->get(),
  99. 'end' => $query->getFilter('until')?->get(),
  100. ],
  101. ]
  102. );
  103. }
  104. /** @var IUser|null $person */
  105. $person = $query->getFilter('person')?->get();
  106. $personDisplayName = $person?->getDisplayName();
  107. if ($personDisplayName !== null) {
  108. $attendeeSearchResults = $this->backend->searchPrincipalUri(
  109. $principalUri,
  110. $personDisplayName,
  111. [self::$componentType],
  112. ['ATTENDEE'],
  113. self::$searchParameters,
  114. [
  115. 'limit' => $query->getLimit(),
  116. 'offset' => $query->getCursor(),
  117. 'timerange' => [
  118. 'start' => $query->getFilter('since')?->get(),
  119. 'end' => $query->getFilter('until')?->get(),
  120. ],
  121. ],
  122. );
  123. $searchResultIndex = array_combine(
  124. array_map(fn ($event) => $event['calendarid'] . '-' . $event['uri'], $searchResults),
  125. array_fill(0, count($searchResults), null),
  126. );
  127. foreach ($attendeeSearchResults as $attendeeResult) {
  128. if (array_key_exists($attendeeResult['calendarid'] . '-' . $attendeeResult['uri'], $searchResultIndex)) {
  129. // Duplicate
  130. continue;
  131. }
  132. $searchResults[] = $attendeeResult;
  133. }
  134. }
  135. $formattedResults = \array_map(function (array $eventRow) use ($calendarsById, $subscriptionsById): SearchResultEntry {
  136. $component = $this->getPrimaryComponent($eventRow['calendardata'], self::$componentType);
  137. $title = (string)($component->SUMMARY ?? $this->l10n->t('Untitled event'));
  138. $subline = $this->generateSubline($component);
  139. if ($eventRow['calendartype'] === CalDavBackend::CALENDAR_TYPE_CALENDAR) {
  140. $calendar = $calendarsById[$eventRow['calendarid']];
  141. } else {
  142. $calendar = $subscriptionsById[$eventRow['calendarid']];
  143. }
  144. $resourceUrl = $this->getDeepLinkToCalendarApp($calendar['principaluri'], $calendar['uri'], $eventRow['uri']);
  145. return new SearchResultEntry('', $title, $subline, $resourceUrl, 'icon-calendar-dark', false);
  146. }, $searchResults);
  147. return SearchResult::paginated(
  148. $this->getName(),
  149. $formattedResults,
  150. $query->getCursor() + count($formattedResults)
  151. );
  152. }
  153. protected function getDeepLinkToCalendarApp(
  154. string $principalUri,
  155. string $calendarUri,
  156. string $calendarObjectUri,
  157. ): string {
  158. $davUrl = $this->getDavUrlForCalendarObject($principalUri, $calendarUri, $calendarObjectUri);
  159. // This route will automatically figure out what recurrence-id to open
  160. return $this->urlGenerator->getAbsoluteURL(
  161. $this->urlGenerator->linkToRoute('calendar.view.index')
  162. . 'edit/'
  163. . base64_encode($davUrl)
  164. );
  165. }
  166. protected function getDavUrlForCalendarObject(
  167. string $principalUri,
  168. string $calendarUri,
  169. string $calendarObjectUri
  170. ): string {
  171. [,, $principalId] = explode('/', $principalUri, 3);
  172. return $this->urlGenerator->linkTo('', 'remote.php') . '/dav/calendars/'
  173. . $principalId . '/'
  174. . $calendarUri . '/'
  175. . $calendarObjectUri;
  176. }
  177. protected function generateSubline(Component $eventComponent): string {
  178. $dtStart = $eventComponent->DTSTART;
  179. $dtEnd = $this->getDTEndForEvent($eventComponent);
  180. $isAllDayEvent = $dtStart instanceof Property\ICalendar\Date;
  181. $startDateTime = new \DateTime($dtStart->getDateTime()->format(\DateTimeInterface::ATOM));
  182. $endDateTime = new \DateTime($dtEnd->getDateTime()->format(\DateTimeInterface::ATOM));
  183. if ($isAllDayEvent) {
  184. $endDateTime->modify('-1 day');
  185. if ($this->isDayEqual($startDateTime, $endDateTime)) {
  186. return $this->l10n->l('date', $startDateTime, ['width' => 'medium']);
  187. }
  188. $formattedStart = $this->l10n->l('date', $startDateTime, ['width' => 'medium']);
  189. $formattedEnd = $this->l10n->l('date', $endDateTime, ['width' => 'medium']);
  190. return "$formattedStart - $formattedEnd";
  191. }
  192. $formattedStartDate = $this->l10n->l('date', $startDateTime, ['width' => 'medium']);
  193. $formattedEndDate = $this->l10n->l('date', $endDateTime, ['width' => 'medium']);
  194. $formattedStartTime = $this->l10n->l('time', $startDateTime, ['width' => 'short']);
  195. $formattedEndTime = $this->l10n->l('time', $endDateTime, ['width' => 'short']);
  196. if ($this->isDayEqual($startDateTime, $endDateTime)) {
  197. return "$formattedStartDate $formattedStartTime - $formattedEndTime";
  198. }
  199. return "$formattedStartDate $formattedStartTime - $formattedEndDate $formattedEndTime";
  200. }
  201. protected function getDTEndForEvent(Component $eventComponent):Property {
  202. if (isset($eventComponent->DTEND)) {
  203. $end = $eventComponent->DTEND;
  204. } elseif (isset($eventComponent->DURATION)) {
  205. $isFloating = $eventComponent->DTSTART->isFloating();
  206. $end = clone $eventComponent->DTSTART;
  207. $endDateTime = $end->getDateTime();
  208. $endDateTime = $endDateTime->add(DateTimeParser::parse($eventComponent->DURATION->getValue()));
  209. $end->setDateTime($endDateTime, $isFloating);
  210. } elseif (!$eventComponent->DTSTART->hasTime()) {
  211. $isFloating = $eventComponent->DTSTART->isFloating();
  212. $end = clone $eventComponent->DTSTART;
  213. $endDateTime = $end->getDateTime();
  214. $endDateTime = $endDateTime->modify('+1 day');
  215. $end->setDateTime($endDateTime, $isFloating);
  216. } else {
  217. $end = clone $eventComponent->DTSTART;
  218. }
  219. return $end;
  220. }
  221. protected function isDayEqual(
  222. \DateTime $dtStart,
  223. \DateTime $dtEnd,
  224. ): bool {
  225. return $dtStart->format('Y-m-d') === $dtEnd->format('Y-m-d');
  226. }
  227. public function getSupportedFilters(): array {
  228. return [
  229. 'term',
  230. 'person',
  231. 'since',
  232. 'until',
  233. ];
  234. }
  235. public function getAlternateIds(): array {
  236. return [];
  237. }
  238. public function getCustomFilters(): array {
  239. return [];
  240. }
  241. }