EventsSearchProvider.php 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * @copyright Copyright (c) 2020, Georg Ehrke
  5. *
  6. * @author Georg Ehrke <oc.list@georgehrke.com>
  7. * @author Joas Schilling <coding@schilljs.com>
  8. * @author John Molakvoæ <skjnldsv@protonmail.com>
  9. *
  10. * @license GNU AGPL version 3 or any later version
  11. *
  12. * This program is free software: you can redistribute it and/or modify
  13. * it under the terms of the GNU Affero General Public License as
  14. * published by the Free Software Foundation, either version 3 of the
  15. * License, or (at your option) any later version.
  16. *
  17. * This program is distributed in the hope that it will be useful,
  18. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  19. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  20. * GNU Affero General Public License for more details.
  21. *
  22. * You should have received a copy of the GNU Affero General Public License
  23. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  24. *
  25. */
  26. namespace OCA\DAV\Search;
  27. use OCA\DAV\CalDAV\CalDavBackend;
  28. use OCP\IUser;
  29. use OCP\Search\IFilteringProvider;
  30. use OCP\Search\ISearchQuery;
  31. use OCP\Search\SearchResult;
  32. use OCP\Search\SearchResultEntry;
  33. use Sabre\VObject\Component;
  34. use Sabre\VObject\DateTimeParser;
  35. use Sabre\VObject\Property;
  36. use function array_combine;
  37. use function array_fill;
  38. use function array_key_exists;
  39. use function array_map;
  40. /**
  41. * Class EventsSearchProvider
  42. *
  43. * @package OCA\DAV\Search
  44. */
  45. class EventsSearchProvider extends ACalendarSearchProvider implements IFilteringProvider {
  46. /**
  47. * @var string[]
  48. */
  49. private static $searchProperties = [
  50. 'SUMMARY',
  51. 'LOCATION',
  52. 'DESCRIPTION',
  53. 'ATTENDEE',
  54. 'ORGANIZER',
  55. 'CATEGORIES',
  56. ];
  57. /**
  58. * @var string[]
  59. */
  60. private static $searchParameters = [
  61. 'ATTENDEE' => ['CN'],
  62. 'ORGANIZER' => ['CN'],
  63. ];
  64. /**
  65. * @var string
  66. */
  67. private static $componentType = 'VEVENT';
  68. /**
  69. * @inheritDoc
  70. */
  71. public function getId(): string {
  72. return 'calendar';
  73. }
  74. /**
  75. * @inheritDoc
  76. */
  77. public function getName(): string {
  78. return $this->l10n->t('Events');
  79. }
  80. /**
  81. * @inheritDoc
  82. */
  83. public function getOrder(string $route, array $routeParameters): ?int {
  84. if ($this->appManager->isEnabledForUser('calendar')) {
  85. return $route === 'calendar.View.index' ? -1 : 30;
  86. }
  87. return null;
  88. }
  89. /**
  90. * @inheritDoc
  91. */
  92. public function search(
  93. IUser $user,
  94. ISearchQuery $query,
  95. ): SearchResult {
  96. if (!$this->appManager->isEnabledForUser('calendar', $user)) {
  97. return SearchResult::complete($this->getName(), []);
  98. }
  99. $principalUri = 'principals/users/' . $user->getUID();
  100. $calendarsById = $this->getSortedCalendars($principalUri);
  101. $subscriptionsById = $this->getSortedSubscriptions($principalUri);
  102. /** @var string|null $term */
  103. $term = $query->getFilter('term')?->get();
  104. if ($term === null) {
  105. $searchResults = [];
  106. } else {
  107. $searchResults = $this->backend->searchPrincipalUri(
  108. $principalUri,
  109. $term,
  110. [self::$componentType],
  111. self::$searchProperties,
  112. self::$searchParameters,
  113. [
  114. 'limit' => $query->getLimit(),
  115. 'offset' => $query->getCursor(),
  116. 'timerange' => [
  117. 'start' => $query->getFilter('since')?->get(),
  118. 'end' => $query->getFilter('until')?->get(),
  119. ],
  120. ]
  121. );
  122. }
  123. /** @var IUser|null $person */
  124. $person = $query->getFilter('person')?->get();
  125. $personDisplayName = $person?->getDisplayName();
  126. if ($personDisplayName !== null) {
  127. $attendeeSearchResults = $this->backend->searchPrincipalUri(
  128. $principalUri,
  129. $personDisplayName,
  130. [self::$componentType],
  131. ['ATTENDEE'],
  132. self::$searchParameters,
  133. [
  134. 'limit' => $query->getLimit(),
  135. 'offset' => $query->getCursor(),
  136. 'timerange' => [
  137. 'start' => $query->getFilter('since')?->get(),
  138. 'end' => $query->getFilter('until')?->get(),
  139. ],
  140. ],
  141. );
  142. $searchResultIndex = array_combine(
  143. array_map(fn ($event) => $event['calendarid'] . '-' . $event['uri'], $searchResults),
  144. array_fill(0, count($searchResults), null),
  145. );
  146. foreach ($attendeeSearchResults as $attendeeResult) {
  147. if (array_key_exists($attendeeResult['calendarid'] . '-' . $attendeeResult['uri'], $searchResultIndex)) {
  148. // Duplicate
  149. continue;
  150. }
  151. $searchResults[] = $attendeeResult;
  152. }
  153. }
  154. $formattedResults = \array_map(function (array $eventRow) use ($calendarsById, $subscriptionsById): SearchResultEntry {
  155. $component = $this->getPrimaryComponent($eventRow['calendardata'], self::$componentType);
  156. $title = (string)($component->SUMMARY ?? $this->l10n->t('Untitled event'));
  157. $subline = $this->generateSubline($component);
  158. if ($eventRow['calendartype'] === CalDavBackend::CALENDAR_TYPE_CALENDAR) {
  159. $calendar = $calendarsById[$eventRow['calendarid']];
  160. } else {
  161. $calendar = $subscriptionsById[$eventRow['calendarid']];
  162. }
  163. $resourceUrl = $this->getDeepLinkToCalendarApp($calendar['principaluri'], $calendar['uri'], $eventRow['uri']);
  164. return new SearchResultEntry('', $title, $subline, $resourceUrl, 'icon-calendar-dark', false);
  165. }, $searchResults);
  166. return SearchResult::paginated(
  167. $this->getName(),
  168. $formattedResults,
  169. $query->getCursor() + count($formattedResults)
  170. );
  171. }
  172. protected function getDeepLinkToCalendarApp(
  173. string $principalUri,
  174. string $calendarUri,
  175. string $calendarObjectUri,
  176. ): string {
  177. $davUrl = $this->getDavUrlForCalendarObject($principalUri, $calendarUri, $calendarObjectUri);
  178. // This route will automatically figure out what recurrence-id to open
  179. return $this->urlGenerator->getAbsoluteURL(
  180. $this->urlGenerator->linkToRoute('calendar.view.index')
  181. . 'edit/'
  182. . base64_encode($davUrl)
  183. );
  184. }
  185. protected function getDavUrlForCalendarObject(
  186. string $principalUri,
  187. string $calendarUri,
  188. string $calendarObjectUri
  189. ): string {
  190. [,, $principalId] = explode('/', $principalUri, 3);
  191. return $this->urlGenerator->linkTo('', 'remote.php') . '/dav/calendars/'
  192. . $principalId . '/'
  193. . $calendarUri . '/'
  194. . $calendarObjectUri;
  195. }
  196. protected function generateSubline(Component $eventComponent): string {
  197. $dtStart = $eventComponent->DTSTART;
  198. $dtEnd = $this->getDTEndForEvent($eventComponent);
  199. $isAllDayEvent = $dtStart instanceof Property\ICalendar\Date;
  200. $startDateTime = new \DateTime($dtStart->getDateTime()->format(\DateTimeInterface::ATOM));
  201. $endDateTime = new \DateTime($dtEnd->getDateTime()->format(\DateTimeInterface::ATOM));
  202. if ($isAllDayEvent) {
  203. $endDateTime->modify('-1 day');
  204. if ($this->isDayEqual($startDateTime, $endDateTime)) {
  205. return $this->l10n->l('date', $startDateTime, ['width' => 'medium']);
  206. }
  207. $formattedStart = $this->l10n->l('date', $startDateTime, ['width' => 'medium']);
  208. $formattedEnd = $this->l10n->l('date', $endDateTime, ['width' => 'medium']);
  209. return "$formattedStart - $formattedEnd";
  210. }
  211. $formattedStartDate = $this->l10n->l('date', $startDateTime, ['width' => 'medium']);
  212. $formattedEndDate = $this->l10n->l('date', $endDateTime, ['width' => 'medium']);
  213. $formattedStartTime = $this->l10n->l('time', $startDateTime, ['width' => 'short']);
  214. $formattedEndTime = $this->l10n->l('time', $endDateTime, ['width' => 'short']);
  215. if ($this->isDayEqual($startDateTime, $endDateTime)) {
  216. return "$formattedStartDate $formattedStartTime - $formattedEndTime";
  217. }
  218. return "$formattedStartDate $formattedStartTime - $formattedEndDate $formattedEndTime";
  219. }
  220. protected function getDTEndForEvent(Component $eventComponent):Property {
  221. if (isset($eventComponent->DTEND)) {
  222. $end = $eventComponent->DTEND;
  223. } elseif (isset($eventComponent->DURATION)) {
  224. $isFloating = $eventComponent->DTSTART->isFloating();
  225. $end = clone $eventComponent->DTSTART;
  226. $endDateTime = $end->getDateTime();
  227. $endDateTime = $endDateTime->add(DateTimeParser::parse($eventComponent->DURATION->getValue()));
  228. $end->setDateTime($endDateTime, $isFloating);
  229. } elseif (!$eventComponent->DTSTART->hasTime()) {
  230. $isFloating = $eventComponent->DTSTART->isFloating();
  231. $end = clone $eventComponent->DTSTART;
  232. $endDateTime = $end->getDateTime();
  233. $endDateTime = $endDateTime->modify('+1 day');
  234. $end->setDateTime($endDateTime, $isFloating);
  235. } else {
  236. $end = clone $eventComponent->DTSTART;
  237. }
  238. return $end;
  239. }
  240. protected function isDayEqual(
  241. \DateTime $dtStart,
  242. \DateTime $dtEnd,
  243. ): bool {
  244. return $dtStart->format('Y-m-d') === $dtEnd->format('Y-m-d');
  245. }
  246. public function getSupportedFilters(): array {
  247. return [
  248. 'term',
  249. 'person',
  250. 'since',
  251. 'until',
  252. ];
  253. }
  254. public function getAlternateIds(): array {
  255. return [];
  256. }
  257. public function getCustomFilters(): array {
  258. return [];
  259. }
  260. }