diff options
author | John Molakvoæ <skjnldsv@users.noreply.github.com> | 2020-08-04 20:54:27 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-08-04 20:54:27 +0200 |
commit | 72b45f9546208c82e76ddb2ad7995f0485d99b18 (patch) | |
tree | 99c62f3dfe6ec35ebcf44c845a0beaf4aa6b3b87 /apps/dav/lib | |
parent | 7d2f5aff1ab6c0890d9a9ce31f9bb17b317f78c9 (diff) | |
parent | 900617e7d7260804ec89a03ca0201340d7585c8b (diff) | |
download | nextcloud-server-72b45f9546208c82e76ddb2ad7995f0485d99b18.tar.gz nextcloud-server-72b45f9546208c82e76ddb2ad7995f0485d99b18.zip |
Merge pull request #22020 from nextcloud/feature/20918/calendar_search
Add Event and Task Backends for Unified Search
Diffstat (limited to 'apps/dav/lib')
-rw-r--r-- | apps/dav/lib/AppInfo/Application.php | 4 | ||||
-rw-r--r-- | apps/dav/lib/CalDAV/CalDavBackend.php | 118 | ||||
-rw-r--r-- | apps/dav/lib/Search/ACalendarSearchProvider.php | 138 | ||||
-rw-r--r-- | apps/dav/lib/Search/EventsSearchProvider.php | 231 | ||||
-rw-r--r-- | apps/dav/lib/Search/EventsSearchResultEntry.php | 30 | ||||
-rw-r--r-- | apps/dav/lib/Search/TasksSearchProvider.php | 160 | ||||
-rw-r--r-- | apps/dav/lib/Search/TasksSearchResultEntry.php | 30 |
7 files changed, 711 insertions, 0 deletions
diff --git a/apps/dav/lib/AppInfo/Application.php b/apps/dav/lib/AppInfo/Application.php index 6f2f7b29153..1bad3cb1eba 100644 --- a/apps/dav/lib/AppInfo/Application.php +++ b/apps/dav/lib/AppInfo/Application.php @@ -55,6 +55,8 @@ use OCA\DAV\CardDAV\PhotoCache; use OCA\DAV\CardDAV\SyncService; use OCA\DAV\HookManager; use OCA\DAV\Search\ContactsSearchProvider; +use OCA\DAV\Search\EventsSearchProvider; +use OCA\DAV\Search\TasksSearchProvider; use OCP\AppFramework\App; use OCP\AppFramework\Bootstrap\IBootContext; use OCP\AppFramework\Bootstrap\IBootstrap; @@ -102,6 +104,8 @@ class Application extends App implements IBootstrap { * Register Search Providers */ $context->registerSearchProvider(ContactsSearchProvider::class); + $context->registerSearchProvider(EventsSearchProvider::class); + $context->registerSearchProvider(TasksSearchProvider::class); } public function boot(IBootContext $context): void { diff --git a/apps/dav/lib/CalDAV/CalDavBackend.php b/apps/dav/lib/CalDAV/CalDavBackend.php index ddfb0a641e5..5cddf6e84b6 100644 --- a/apps/dav/lib/CalDAV/CalDavBackend.php +++ b/apps/dav/lib/CalDAV/CalDavBackend.php @@ -1670,6 +1670,124 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription } /** + * @param string $principalUri + * @param string $pattern + * @param array $componentTypes + * @param array $searchProperties + * @param array $searchParameters + * @param array $options + * @return array + */ + public function searchPrincipalUri(string $principalUri, + string $pattern, + array $componentTypes, + array $searchProperties, + array $searchParameters, + array $options = []): array { + $escapePattern = !\array_key_exists('escape_like_param', $options) || $options['escape_like_param'] !== false; + + $calendarObjectIdQuery = $this->db->getQueryBuilder(); + $calendarOr = $calendarObjectIdQuery->expr()->orX(); + $searchOr = $calendarObjectIdQuery->expr()->orX(); + + // Fetch calendars and subscription + $calendars = $this->getCalendarsForUser($principalUri); + $subscriptions = $this->getSubscriptionsForUser($principalUri); + foreach ($calendars as $calendar) { + $calendarAnd = $calendarObjectIdQuery->expr()->andX(); + $calendarAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int)$calendar['id']))); + $calendarAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendartype', $calendarObjectIdQuery->createNamedParameter(self::CALENDAR_TYPE_CALENDAR))); + + // If it's shared, limit search to public events + if ($calendar['principaluri'] !== $calendar['{http://owncloud.org/ns}owner-principal']) { + $calendarAnd->add($calendarObjectIdQuery->expr()->eq('co.classification', $calendarObjectIdQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC))); + } + + $calendarOr->add($calendarAnd); + } + foreach ($subscriptions as $subscription) { + $subscriptionAnd = $calendarObjectIdQuery->expr()->andX(); + $subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int)$subscription['id']))); + $subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendartype', $calendarObjectIdQuery->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION))); + + // If it's shared, limit search to public events + if ($subscription['principaluri'] !== $subscription['{http://owncloud.org/ns}owner-principal']) { + $subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('co.classification', $calendarObjectIdQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC))); + } + + $calendarOr->add($subscriptionAnd); + } + + foreach ($searchProperties as $property) { + $propertyAnd = $calendarObjectIdQuery->expr()->andX(); + $propertyAnd->add($calendarObjectIdQuery->expr()->eq('cob.name', $calendarObjectIdQuery->createNamedParameter($property, IQueryBuilder::PARAM_STR))); + $propertyAnd->add($calendarObjectIdQuery->expr()->isNull('cob.parameter')); + + $searchOr->add($propertyAnd); + } + foreach ($searchParameters as $property => $parameter) { + $parameterAnd = $calendarObjectIdQuery->expr()->andX(); + $parameterAnd->add($calendarObjectIdQuery->expr()->eq('cob.name', $calendarObjectIdQuery->createNamedParameter($property, IQueryBuilder::PARAM_STR))); + $parameterAnd->add($calendarObjectIdQuery->expr()->eq('cob.parameter', $calendarObjectIdQuery->createNamedParameter($parameter, IQueryBuilder::PARAM_STR))); + + $searchOr->add($parameterAnd); + } + + if ($calendarOr->count() === 0) { + return []; + } + if ($searchOr->count() === 0) { + return []; + } + + $calendarObjectIdQuery->selectDistinct('cob.objectid') + ->from($this->dbObjectPropertiesTable, 'cob') + ->leftJoin('cob', 'calendarobjects', 'co', $calendarObjectIdQuery->expr()->eq('co.id', 'cob.objectid')) + ->andWhere($calendarObjectIdQuery->expr()->in('co.componenttype', $calendarObjectIdQuery->createNamedParameter($componentTypes, IQueryBuilder::PARAM_STR_ARRAY))) + ->andWhere($calendarOr) + ->andWhere($searchOr); + + if ('' !== $pattern) { + if (!$escapePattern) { + $calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->ilike('cob.value', $calendarObjectIdQuery->createNamedParameter($pattern))); + } else { + $calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->ilike('cob.value', $calendarObjectIdQuery->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%'))); + } + } + + if (isset($options['limit'])) { + $calendarObjectIdQuery->setMaxResults($options['limit']); + } + if (isset($options['offset'])) { + $calendarObjectIdQuery->setFirstResult($options['offset']); + } + + $result = $calendarObjectIdQuery->execute(); + $matches = $result->fetchAll(); + $result->closeCursor(); + $matches = array_map(static function (array $match):int { + return (int) $match['objectid']; + }, $matches); + + $query = $this->db->getQueryBuilder(); + $query->select('calendardata', 'uri', 'calendarid', 'calendartype') + ->from('calendarobjects') + ->where($query->expr()->in('id', $query->createNamedParameter($matches, IQueryBuilder::PARAM_INT_ARRAY))); + + $result = $query->execute(); + $calendarObjects = $result->fetchAll(); + $result->closeCursor(); + + return array_map(function (array $array): array { + $array['calendarid'] = (int)$array['calendarid']; + $array['calendartype'] = (int)$array['calendartype']; + $array['calendardata'] = $this->readBlob($array['calendardata']); + + return $array; + }, $calendarObjects); + } + + /** * Searches through all of a users calendars and calendar objects to find * an object with a specific UID. * diff --git a/apps/dav/lib/Search/ACalendarSearchProvider.php b/apps/dav/lib/Search/ACalendarSearchProvider.php new file mode 100644 index 00000000000..56273fe17e4 --- /dev/null +++ b/apps/dav/lib/Search/ACalendarSearchProvider.php @@ -0,0 +1,138 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2020, Georg Ehrke + * + * @author Georg Ehrke <oc.list@georgehrke.com> + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ +namespace OCA\DAV\Search; + +use OCA\DAV\CalDAV\CalDavBackend; +use OCP\App\IAppManager; +use OCP\IL10N; +use OCP\IURLGenerator; +use OCP\Search\IProvider; +use Sabre\VObject\Component; +use Sabre\VObject\Reader; + +/** + * Class ACalendarSearchProvider + * + * @package OCA\DAV\Search + */ +abstract class ACalendarSearchProvider implements IProvider { + + /** @var IAppManager */ + protected $appManager; + + /** @var IL10N */ + protected $l10n; + + /** @var IURLGenerator */ + protected $urlGenerator; + + /** @var CalDavBackend */ + protected $backend; + + /** + * ACalendarSearchProvider constructor. + * + * @param IAppManager $appManager + * @param IL10N $l10n + * @param IURLGenerator $urlGenerator + * @param CalDavBackend $backend + */ + public function __construct(IAppManager $appManager, + IL10N $l10n, + IURLGenerator $urlGenerator, + CalDavBackend $backend) { + $this->appManager = $appManager; + $this->l10n = $l10n; + $this->urlGenerator = $urlGenerator; + $this->backend = $backend; + } + + /** + * Get an associative array of calendars + * calendarId => calendar + * + * @param string $principalUri + * @return array + */ + protected function getSortedCalendars(string $principalUri): array { + $calendars = $this->backend->getCalendarsForUser($principalUri); + $calendarsById = []; + foreach ($calendars as $calendar) { + $calendarsById[(int) $calendar['id']] = $calendar; + } + + return $calendarsById; + } + + /** + * Get an associative array of subscriptions + * subscriptionId => subscription + * + * @param string $principalUri + * @return array + */ + protected function getSortedSubscriptions(string $principalUri): array { + $subscriptions = $this->backend->getSubscriptionsForUser($principalUri); + $subscriptionsById = []; + foreach ($subscriptions as $subscription) { + $subscriptionsById[(int) $subscription['id']] = $subscription; + } + + return $subscriptionsById; + } + + /** + * Returns the primary VEvent / VJournal / VTodo component + * If it's a component with recurrence-ids, it will return + * the primary component + * + * TODO: It would be a nice enhancement to show recurrence-exceptions + * as individual search-results. + * For now we will just display the primary element of a recurrence-set. + * + * @param string $calendarData + * @param string $componentName + * @return Component + */ + protected function getPrimaryComponent(string $calendarData, string $componentName): Component { + $vCalendar = Reader::read($calendarData, Reader::OPTION_FORGIVING); + + $components = $vCalendar->select($componentName); + if (count($components) === 1) { + return $components[0]; + } + + // If it's a recurrence-set, take the primary element + foreach ($components as $component) { + /** @var Component $component */ + if (!$component->{'RECURRENCE-ID'}) { + return $component; + } + } + + // In case of error, just fallback to the first element in the set + return $components[0]; + } +} diff --git a/apps/dav/lib/Search/EventsSearchProvider.php b/apps/dav/lib/Search/EventsSearchProvider.php new file mode 100644 index 00000000000..43fc4f65dfc --- /dev/null +++ b/apps/dav/lib/Search/EventsSearchProvider.php @@ -0,0 +1,231 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2020, Georg Ehrke + * + * @author Georg Ehrke <oc.list@georgehrke.com> + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ +namespace OCA\DAV\Search; + +use OCA\DAV\CalDAV\CalDavBackend; +use OCP\IUser; +use OCP\Search\ISearchQuery; +use OCP\Search\SearchResult; +use Sabre\VObject\Component; +use Sabre\VObject\DateTimeParser; +use Sabre\VObject\Property; + +/** + * Class EventsSearchProvider + * + * @package OCA\DAV\Search + */ +class EventsSearchProvider extends ACalendarSearchProvider { + + /** + * @var string[] + */ + private static $searchProperties = [ + 'SUMMARY', + 'LOCATION', + 'DESCRIPTION', + 'ATTENDEE', + 'ORGANIZER', + 'CATEGORIES', + ]; + + /** + * @var string[] + */ + private static $searchParameters = [ + 'ATTENDEE' => ['CN'], + 'ORGANIZER' => ['CN'], + ]; + + /** + * @var string + */ + private static $componentType = 'VEVENT'; + + /** + * @inheritDoc + */ + public function getId(): string { + return 'calendar-dav'; + } + + /** + * @inheritDoc + */ + public function getName(): string { + return $this->l10n->t('Events'); + } + + /** + * @inheritDoc + */ + public function search(IUser $user, + ISearchQuery $query): SearchResult { + if (!$this->appManager->isEnabledForUser('calendar', $user)) { + return SearchResult::complete($this->getName(), []); + } + + $principalUri = 'principals/users/' . $user->getUID(); + $calendarsById = $this->getSortedCalendars($principalUri); + $subscriptionsById = $this->getSortedSubscriptions($principalUri); + + $searchResults = $this->backend->searchPrincipalUri( + $principalUri, + $query->getTerm(), + [self::$componentType], + self::$searchProperties, + self::$searchParameters, + [ + 'limit' => $query->getLimit(), + 'offset' => $query->getCursor(), + ] + ); + $formattedResults = \array_map(function (array $eventRow) use ($calendarsById, $subscriptionsById):EventsSearchResultEntry { + $component = $this->getPrimaryComponent($eventRow['calendardata'], self::$componentType); + $title = (string)($component->SUMMARY ?? $this->l10n->t('Untitled event')); + $subline = $this->generateSubline($component); + + if ($eventRow['calendartype'] === CalDavBackend::CALENDAR_TYPE_CALENDAR) { + $calendar = $calendarsById[$eventRow['calendarid']]; + } else { + $calendar = $subscriptionsById[$eventRow['calendarid']]; + } + $resourceUrl = $this->getDeepLinkToCalendarApp($calendar['principaluri'], $calendar['uri'], $eventRow['uri']); + + return new EventsSearchResultEntry('', $title, $subline, $resourceUrl, 'icon-calendar-dark', false); + }, $searchResults); + + return SearchResult::paginated( + $this->getName(), + $formattedResults, + $query->getCursor() + count($formattedResults) + ); + } + + /** + * @param string $principalUri + * @param string $calendarUri + * @param string $calendarObjectUri + * @return string + */ + protected function getDeepLinkToCalendarApp(string $principalUri, + string $calendarUri, + string $calendarObjectUri): string { + $davUrl = $this->getDavUrlForCalendarObject($principalUri, $calendarUri, $calendarObjectUri); + // This route will automatically figure out what recurrence-id to open + return $this->urlGenerator->getAbsoluteURL( + $this->urlGenerator->linkToRoute('calendar.view.index') + . 'edit/' + . base64_encode($davUrl) + ); + } + + /** + * @param string $principalUri + * @param string $calendarUri + * @param string $calendarObjectUri + * @return string + */ + protected function getDavUrlForCalendarObject(string $principalUri, + string $calendarUri, + string $calendarObjectUri): string { + [,, $principalId] = explode('/', $principalUri, 3); + + return $this->urlGenerator->linkTo('', 'remote.php') . '/dav/calendars/' + . $principalId . '/' + . $calendarUri . '/' + . $calendarObjectUri; + } + + /** + * @param Component $eventComponent + * @return string + */ + protected function generateSubline(Component $eventComponent): string { + $dtStart = $eventComponent->DTSTART; + $dtEnd = $this->getDTEndForEvent($eventComponent); + $isAllDayEvent = $dtStart instanceof Property\ICalendar\Date; + $startDateTime = new \DateTime($dtStart->getDateTime()->format(\DateTime::ATOM)); + $endDateTime = new \DateTime($dtEnd->getDateTime()->format(\DateTime::ATOM)); + + if ($isAllDayEvent) { + $endDateTime->modify('-1 day'); + if ($this->isDayEqual($startDateTime, $endDateTime)) { + return $this->l10n->l('date', $startDateTime, ['width' => 'medium']); + } + + $formattedStart = $this->l10n->l('date', $startDateTime, ['width' => 'medium']); + $formattedEnd = $this->l10n->l('date', $endDateTime, ['width' => 'medium']); + return "$formattedStart - $formattedEnd"; + } + + $formattedStartDate = $this->l10n->l('date', $startDateTime, ['width' => 'medium']); + $formattedEndDate = $this->l10n->l('date', $endDateTime, ['width' => 'medium']); + $formattedStartTime = $this->l10n->l('time', $startDateTime, ['width' => 'short']); + $formattedEndTime = $this->l10n->l('time', $endDateTime, ['width' => 'short']); + + if ($this->isDayEqual($startDateTime, $endDateTime)) { + return "$formattedStartDate $formattedStartTime - $formattedEndTime"; + } + + return "$formattedStartDate $formattedStartTime - $formattedEndDate $formattedEndTime"; + } + + /** + * @param Component $eventComponent + * @return Property + */ + protected function getDTEndForEvent(Component $eventComponent):Property { + if (isset($eventComponent->DTEND)) { + $end = $eventComponent->DTEND; + } elseif (isset($eventComponent->DURATION)) { + $isFloating = $eventComponent->DTSTART->isFloating(); + $end = clone $eventComponent->DTSTART; + $endDateTime = $end->getDateTime(); + $endDateTime = $endDateTime->add(DateTimeParser::parse($eventComponent->DURATION->getValue())); + $end->setDateTime($endDateTime, $isFloating); + } elseif (!$eventComponent->DTSTART->hasTime()) { + $isFloating = $eventComponent->DTSTART->isFloating(); + $end = clone $eventComponent->DTSTART; + $endDateTime = $end->getDateTime(); + $endDateTime = $endDateTime->modify('+1 day'); + $end->setDateTime($endDateTime, $isFloating); + } else { + $end = clone $eventComponent->DTSTART; + } + + return $end; + } + + /** + * @param \DateTime $dtStart + * @param \DateTime $dtEnd + * @return bool + */ + protected function isDayEqual(\DateTime $dtStart, + \DateTime $dtEnd) { + return $dtStart->format('Y-m-d') === $dtEnd->format('Y-m-d'); + } +} diff --git a/apps/dav/lib/Search/EventsSearchResultEntry.php b/apps/dav/lib/Search/EventsSearchResultEntry.php new file mode 100644 index 00000000000..f70f10a6e75 --- /dev/null +++ b/apps/dav/lib/Search/EventsSearchResultEntry.php @@ -0,0 +1,30 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2020, Georg Ehrke + * + * @author Georg Ehrke <oc.list@georgehrke.com> + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ +namespace OCA\DAV\Search; + +use OCP\Search\ASearchResultEntry; + +class EventsSearchResultEntry extends ASearchResultEntry { +} diff --git a/apps/dav/lib/Search/TasksSearchProvider.php b/apps/dav/lib/Search/TasksSearchProvider.php new file mode 100644 index 00000000000..eee4694f08f --- /dev/null +++ b/apps/dav/lib/Search/TasksSearchProvider.php @@ -0,0 +1,160 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2020, Georg Ehrke + * + * @author Georg Ehrke <oc.list@georgehrke.com> + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ +namespace OCA\DAV\Search; + +use OCA\DAV\CalDAV\CalDavBackend; +use OCP\IUser; +use OCP\Search\ISearchQuery; +use OCP\Search\SearchResult; +use Sabre\VObject\Component; + +/** + * Class TasksSearchProvider + * + * @package OCA\DAV\Search + */ +class TasksSearchProvider extends ACalendarSearchProvider { + + /** + * @var string[] + */ + private static $searchProperties = [ + 'SUMMARY', + 'DESCRIPTION', + 'CATEGORIES', + ]; + + /** + * @var string[] + */ + private static $searchParameters = []; + + /** + * @var string + */ + private static $componentType = 'VTODO'; + + /** + * @inheritDoc + */ + public function getId(): string { + return 'tasks-dav'; + } + + /** + * @inheritDoc + */ + public function getName(): string { + return $this->l10n->t('Tasks'); + } + + /** + * @inheritDoc + */ + public function search(IUser $user, + ISearchQuery $query): SearchResult { + if (!$this->appManager->isEnabledForUser('tasks', $user)) { + return SearchResult::complete($this->getName(), []); + } + + $principalUri = 'principals/users/' . $user->getUID(); + $calendarsById = $this->getSortedCalendars($principalUri); + $subscriptionsById = $this->getSortedSubscriptions($principalUri); + + $searchResults = $this->backend->searchPrincipalUri( + $principalUri, + $query->getTerm(), + [self::$componentType], + self::$searchProperties, + self::$searchParameters, + [ + 'limit' => $query->getLimit(), + 'offset' => $query->getCursor(), + ] + ); + $formattedResults = \array_map(function (array $taskRow) use ($calendarsById, $subscriptionsById):TasksSearchResultEntry { + $component = $this->getPrimaryComponent($taskRow['calendardata'], self::$componentType); + $title = (string)($component->SUMMARY ?? $this->l10n->t('Untitled task')); + $subline = $this->generateSubline($component); + + if ($taskRow['calendartype'] === CalDavBackend::CALENDAR_TYPE_CALENDAR) { + $calendar = $calendarsById[$taskRow['calendarid']]; + } else { + $calendar = $subscriptionsById[$taskRow['calendarid']]; + } + $resourceUrl = $this->getDeepLinkToTasksApp($calendar['uri'], $taskRow['uri']); + + return new TasksSearchResultEntry('', $title, $subline, $resourceUrl, 'icon-checkmark', false); + }, $searchResults); + + return SearchResult::paginated( + $this->getName(), + $formattedResults, + $query->getCursor() + count($formattedResults) + ); + } + + /** + * @param string $calendarUri + * @param string $taskUri + * @return string + */ + protected function getDeepLinkToTasksApp(string $calendarUri, + string $taskUri): string { + return $this->urlGenerator->getAbsoluteURL( + $this->urlGenerator->linkToRoute('tasks.page.index') + . '#/calendars/' + . $calendarUri + . '/tasks/' + . $taskUri + ); + } + + /** + * @param Component $taskComponent + * @return string + */ + protected function generateSubline(Component $taskComponent): string { + if ($taskComponent->COMPLETED) { + $completedDateTime = new \DateTime($taskComponent->COMPLETED->getDateTime()->format(\DateTime::ATOM)); + $formattedDate = $this->l10n->l('date', $completedDateTime, ['width' => 'medium']); + return $this->l10n->t('Completed on %s', [$formattedDate]); + } + + if ($taskComponent->DUE) { + $dueDateTime = new \DateTime($taskComponent->DUE->getDateTime()->format(\DateTime::ATOM)); + $formattedDate = $this->l10n->l('date', $dueDateTime, ['width' => 'medium']); + + if ($taskComponent->DUE->hasTime()) { + $formattedTime = $this->l10n->l('time', $dueDateTime, ['width' => 'short']); + return $this->l10n->t('Due on %s by %s', [$formattedDate, $formattedTime]); + } + + return $this->l10n->t('Due on %s', [$formattedDate]); + } + + return ''; + } +} diff --git a/apps/dav/lib/Search/TasksSearchResultEntry.php b/apps/dav/lib/Search/TasksSearchResultEntry.php new file mode 100644 index 00000000000..ec58ba80af9 --- /dev/null +++ b/apps/dav/lib/Search/TasksSearchResultEntry.php @@ -0,0 +1,30 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2020, Georg Ehrke + * + * @author Georg Ehrke <oc.list@georgehrke.com> + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ +namespace OCA\DAV\Search; + +use OCP\Search\ASearchResultEntry; + +class TasksSearchResultEntry extends ASearchResultEntry { +} |