summaryrefslogtreecommitdiffstats
path: root/apps/dav/lib
diff options
context:
space:
mode:
authorJohn Molakvoæ <skjnldsv@users.noreply.github.com>2020-08-04 20:54:27 +0200
committerGitHub <noreply@github.com>2020-08-04 20:54:27 +0200
commit72b45f9546208c82e76ddb2ad7995f0485d99b18 (patch)
tree99c62f3dfe6ec35ebcf44c845a0beaf4aa6b3b87 /apps/dav/lib
parent7d2f5aff1ab6c0890d9a9ce31f9bb17b317f78c9 (diff)
parent900617e7d7260804ec89a03ca0201340d7585c8b (diff)
downloadnextcloud-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.php4
-rw-r--r--apps/dav/lib/CalDAV/CalDavBackend.php118
-rw-r--r--apps/dav/lib/Search/ACalendarSearchProvider.php138
-rw-r--r--apps/dav/lib/Search/EventsSearchProvider.php231
-rw-r--r--apps/dav/lib/Search/EventsSearchResultEntry.php30
-rw-r--r--apps/dav/lib/Search/TasksSearchProvider.php160
-rw-r--r--apps/dav/lib/Search/TasksSearchResultEntry.php30
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 {
+}