EventsSearchProvider.php 7.9 KB

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