Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/server.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJohn Molakvoæ <skjnldsv@users.noreply.github.com>2020-08-04 21:54:27 +0300
committerGitHub <noreply@github.com>2020-08-04 21:54:27 +0300
commit72b45f9546208c82e76ddb2ad7995f0485d99b18 (patch)
tree99c62f3dfe6ec35ebcf44c845a0beaf4aa6b3b87
parent7d2f5aff1ab6c0890d9a9ce31f9bb17b317f78c9 (diff)
parent900617e7d7260804ec89a03ca0201340d7585c8b (diff)
Merge pull request #22020 from nextcloud/feature/20918/calendar_search
Add Event and Task Backends for Unified Search
-rw-r--r--apps/dav/composer/composer/autoload_classmap.php5
-rw-r--r--apps/dav/composer/composer/autoload_static.php5
-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
-rw-r--r--apps/dav/tests/unit/CalDAV/AbstractCalDavBackend.php9
-rw-r--r--apps/dav/tests/unit/CalDAV/CalDavBackendTest.php298
-rw-r--r--apps/dav/tests/unit/Search/ContactsSearchProviderTest.php2
-rw-r--r--apps/dav/tests/unit/Search/EventsSearchProviderTest.php473
-rw-r--r--apps/dav/tests/unit/Search/TasksSearchProviderTest.php344
14 files changed, 1834 insertions, 13 deletions
diff --git a/apps/dav/composer/composer/autoload_classmap.php b/apps/dav/composer/composer/autoload_classmap.php
index bd63dee13b7..081f334a4f9 100644
--- a/apps/dav/composer/composer/autoload_classmap.php
+++ b/apps/dav/composer/composer/autoload_classmap.php
@@ -210,8 +210,13 @@ return array(
'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningNode' => $baseDir . '/../lib/Provisioning/Apple/AppleProvisioningNode.php',
'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningPlugin' => $baseDir . '/../lib/Provisioning/Apple/AppleProvisioningPlugin.php',
'OCA\\DAV\\RootCollection' => $baseDir . '/../lib/RootCollection.php',
+ 'OCA\\DAV\\Search\\ACalendarSearchProvider' => $baseDir . '/../lib/Search/ACalendarSearchProvider.php',
'OCA\\DAV\\Search\\ContactsSearchProvider' => $baseDir . '/../lib/Search/ContactsSearchProvider.php',
'OCA\\DAV\\Search\\ContactsSearchResultEntry' => $baseDir . '/../lib/Search/ContactsSearchResultEntry.php',
+ 'OCA\\DAV\\Search\\EventsSearchProvider' => $baseDir . '/../lib/Search/EventsSearchProvider.php',
+ 'OCA\\DAV\\Search\\EventsSearchResultEntry' => $baseDir . '/../lib/Search/EventsSearchResultEntry.php',
+ 'OCA\\DAV\\Search\\TasksSearchProvider' => $baseDir . '/../lib/Search/TasksSearchProvider.php',
+ 'OCA\\DAV\\Search\\TasksSearchResultEntry' => $baseDir . '/../lib/Search/TasksSearchResultEntry.php',
'OCA\\DAV\\Server' => $baseDir . '/../lib/Server.php',
'OCA\\DAV\\Settings\\CalDAVSettings' => $baseDir . '/../lib/Settings/CalDAVSettings.php',
'OCA\\DAV\\Storage\\PublicOwnerWrapper' => $baseDir . '/../lib/Storage/PublicOwnerWrapper.php',
diff --git a/apps/dav/composer/composer/autoload_static.php b/apps/dav/composer/composer/autoload_static.php
index a664c86f5fd..3bfdb3b8628 100644
--- a/apps/dav/composer/composer/autoload_static.php
+++ b/apps/dav/composer/composer/autoload_static.php
@@ -225,8 +225,13 @@ class ComposerStaticInitDAV
'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningNode' => __DIR__ . '/..' . '/../lib/Provisioning/Apple/AppleProvisioningNode.php',
'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningPlugin' => __DIR__ . '/..' . '/../lib/Provisioning/Apple/AppleProvisioningPlugin.php',
'OCA\\DAV\\RootCollection' => __DIR__ . '/..' . '/../lib/RootCollection.php',
+ 'OCA\\DAV\\Search\\ACalendarSearchProvider' => __DIR__ . '/..' . '/../lib/Search/ACalendarSearchProvider.php',
'OCA\\DAV\\Search\\ContactsSearchProvider' => __DIR__ . '/..' . '/../lib/Search/ContactsSearchProvider.php',
'OCA\\DAV\\Search\\ContactsSearchResultEntry' => __DIR__ . '/..' . '/../lib/Search/ContactsSearchResultEntry.php',
+ 'OCA\\DAV\\Search\\EventsSearchProvider' => __DIR__ . '/..' . '/../lib/Search/EventsSearchProvider.php',
+ 'OCA\\DAV\\Search\\EventsSearchResultEntry' => __DIR__ . '/..' . '/../lib/Search/EventsSearchResultEntry.php',
+ 'OCA\\DAV\\Search\\TasksSearchProvider' => __DIR__ . '/..' . '/../lib/Search/TasksSearchProvider.php',
+ 'OCA\\DAV\\Search\\TasksSearchResultEntry' => __DIR__ . '/..' . '/../lib/Search/TasksSearchResultEntry.php',
'OCA\\DAV\\Server' => __DIR__ . '/..' . '/../lib/Server.php',
'OCA\\DAV\\Settings\\CalDAVSettings' => __DIR__ . '/..' . '/../lib/Settings/CalDAVSettings.php',
'OCA\\DAV\\Storage\\PublicOwnerWrapper' => __DIR__ . '/..' . '/../lib/Storage/PublicOwnerWrapper.php',
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 {
+}
diff --git a/apps/dav/tests/unit/CalDAV/AbstractCalDavBackend.php b/apps/dav/tests/unit/CalDAV/AbstractCalDavBackend.php
index 4c6c8549055..79da92148af 100644
--- a/apps/dav/tests/unit/CalDAV/AbstractCalDavBackend.php
+++ b/apps/dav/tests/unit/CalDAV/AbstractCalDavBackend.php
@@ -121,7 +121,12 @@ abstract class AbstractCalDavBackend extends TestCase {
$this->principal->expects($this->any())->method('getGroupMembership')
->withAnyParameters()
->willReturn([self::UNIT_TEST_GROUP, self::UNIT_TEST_GROUP2]);
- $calendars = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER);
+ $this->cleanupForPrincipal(self::UNIT_TEST_USER);
+ $this->cleanupForPrincipal(self::UNIT_TEST_USER1);
+ }
+
+ private function cleanupForPrincipal($principal): void {
+ $calendars = $this->backend->getCalendarsForUser($principal);
foreach ($calendars as $calendar) {
$this->dispatcher->expects($this->at(0))
->method('dispatch')
@@ -129,7 +134,7 @@ abstract class AbstractCalDavBackend extends TestCase {
$this->backend->deleteCalendar($calendar['id']);
}
- $subscriptions = $this->backend->getSubscriptionsForUser(self::UNIT_TEST_USER);
+ $subscriptions = $this->backend->getSubscriptionsForUser($principal);
foreach ($subscriptions as $subscription) {
$this->backend->deleteSubscription($subscription['id']);
}
diff --git a/apps/dav/tests/unit/CalDAV/CalDavBackendTest.php b/apps/dav/tests/unit/CalDAV/CalDavBackendTest.php
index bd6a8856d51..4e343340f25 100644
--- a/apps/dav/tests/unit/CalDAV/CalDavBackendTest.php
+++ b/apps/dav/tests/unit/CalDAV/CalDavBackendTest.php
@@ -605,21 +605,21 @@ DTSTART;TZID=Europe/Warsaw:20170325T150000
DTEND;TZID=Europe/Warsaw:20170325T160000
TRANSP:OPAQUE
DESCRIPTION:Magiczna treść uzyskana za pomocą magicznego proszku.\n\nę
- żźćńłóÓŻŹĆŁĘ€śśśŚŚ\n \,\,))))))))\;\,\n
+ żźćńłóÓŻŹĆŁĘ€śśśŚŚ\n \,\,))))))))\;\,\n
__))))))))))))))\,\n \\|/ -\\(((((''''((((((((.\n -*-==///
- ///(('' . `))))))\,\n /|\\ ))| o \;-. '(((((
- \,(\,\n ( `| / ) \;))))'
+ ///(('' . `))))))\,\n /|\\ ))| o \;-. '(((((
+ \,(\,\n ( `| / ) \;))))'
\,_))^\;(~\n | | | \,))((((_ _____-
-----~~~-. %\,\;(\;(>'\;'~\n o_)\; \; )))(((` ~---
~ `:: \\ %%~~)(v\;(`('~\n \; ''''````
- `: `:::|\\\,__\,%% )\;`'\; ~\n | _
- ) / `:|`----' `-'\n ______/\\/~ |
+ `: `:::|\\\,__\,%% )\;`'\; ~\n | _
+ ) / `:|`----' `-'\n ______/\\/~ |
/ /\n /~\;\;.____/\;\;' / ___--\
- ,-( `\;\;\;/\n / // _\;______\;'------~~~~~ /\;\;/\\ /\n
- // | | / \; \\\;\;\,\\\n (<_ | \;
- /'\,/-----' _>\n \\_| ||_
- //~\;~~~~~~~~~\n `\\_| (\,~~ -Tua Xiong\n
- \\~\\\n
+ ,-( `\;\;\;/\n / // _\;______\;'------~~~~~ /\;\;/\\ /\n
+ // | | / \; \\\;\;\,\\\n (<_ | \;
+ /'\,/-----' _>\n \\_| ||_
+ //~\;~~~~~~~~~\n `\\_| (\,~~ -Tua Xiong\n
+ \\~\\\n
~~\n\n
SEQUENCE:1
X-MOZ-GENERATION:1
@@ -999,4 +999,282 @@ EOD;
$this->assertEquals($calendarInfoUser['id'], $calendarInfoUser1['id']);
$this->assertEquals($calendarInfoUser['uri'], $calendarInfoUser1['uri']);
}
+
+ public function testSearchPrincipal(): void {
+ $myPublic = <<<EOD
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//dmfs.org//mimedir.icalendar//EN
+BEGIN:VTIMEZONE
+TZID:Europe/Berlin
+X-LIC-LOCATION:Europe/Berlin
+BEGIN:DAYLIGHT
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+TZNAME:CEST
+DTSTART:19700329T020000
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+TZNAME:CET
+DTSTART:19701025T030000
+RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+DTSTART;TZID=Europe/Berlin:20160419T130000
+SUMMARY:My Test (public)
+CLASS:PUBLIC
+TRANSP:OPAQUE
+STATUS:CONFIRMED
+DTEND;TZID=Europe/Berlin:20160419T140000
+LAST-MODIFIED:20160419T074202Z
+DTSTAMP:20160419T074202Z
+CREATED:20160419T074202Z
+UID:2e468c48-7860-492e-bc52-92fa0daeeccf.1461051722310-1
+END:VEVENT
+END:VCALENDAR
+EOD;
+ $myPrivate = <<<EOD
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//dmfs.org//mimedir.icalendar//EN
+BEGIN:VTIMEZONE
+TZID:Europe/Berlin
+X-LIC-LOCATION:Europe/Berlin
+BEGIN:DAYLIGHT
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+TZNAME:CEST
+DTSTART:19700329T020000
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+TZNAME:CET
+DTSTART:19701025T030000
+RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+DTSTART;TZID=Europe/Berlin:20160419T130000
+SUMMARY:My Test (private)
+CLASS:PRIVATE
+TRANSP:OPAQUE
+STATUS:CONFIRMED
+DTEND;TZID=Europe/Berlin:20160419T140000
+LAST-MODIFIED:20160419T074202Z
+DTSTAMP:20160419T074202Z
+CREATED:20160419T074202Z
+UID:2e468c48-7860-492e-bc52-92fa0daeeccf.1461051722310-2
+END:VEVENT
+END:VCALENDAR
+EOD;
+ $myConfidential = <<<EOD
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//dmfs.org//mimedir.icalendar//EN
+BEGIN:VTIMEZONE
+TZID:Europe/Berlin
+X-LIC-LOCATION:Europe/Berlin
+BEGIN:DAYLIGHT
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+TZNAME:CEST
+DTSTART:19700329T020000
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+TZNAME:CET
+DTSTART:19701025T030000
+RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+DTSTART;TZID=Europe/Berlin:20160419T130000
+SUMMARY:My Test (confidential)
+CLASS:CONFIDENTIAL
+TRANSP:OPAQUE
+STATUS:CONFIRMED
+DTEND;TZID=Europe/Berlin:20160419T140000
+LAST-MODIFIED:20160419T074202Z
+DTSTAMP:20160419T074202Z
+CREATED:20160419T074202Z
+UID:2e468c48-7860-492e-bc52-92fa0daeeccf.1461051722310-3
+END:VEVENT
+END:VCALENDAR
+EOD;
+
+ $sharerPublic = <<<EOD
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//dmfs.org//mimedir.icalendar//EN
+BEGIN:VTIMEZONE
+TZID:Europe/Berlin
+X-LIC-LOCATION:Europe/Berlin
+BEGIN:DAYLIGHT
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+TZNAME:CEST
+DTSTART:19700329T020000
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+TZNAME:CET
+DTSTART:19701025T030000
+RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+DTSTART;TZID=Europe/Berlin:20160419T130000
+SUMMARY:Sharer Test (public)
+CLASS:PUBLIC
+TRANSP:OPAQUE
+STATUS:CONFIRMED
+DTEND;TZID=Europe/Berlin:20160419T140000
+LAST-MODIFIED:20160419T074202Z
+DTSTAMP:20160419T074202Z
+CREATED:20160419T074202Z
+UID:2e468c48-7860-492e-bc52-92fa0daeeccf.1461051722310-4
+END:VEVENT
+END:VCALENDAR
+EOD;
+ $sharerPrivate = <<<EOD
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//dmfs.org//mimedir.icalendar//EN
+BEGIN:VTIMEZONE
+TZID:Europe/Berlin
+X-LIC-LOCATION:Europe/Berlin
+BEGIN:DAYLIGHT
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+TZNAME:CEST
+DTSTART:19700329T020000
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+TZNAME:CET
+DTSTART:19701025T030000
+RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+DTSTART;TZID=Europe/Berlin:20160419T130000
+SUMMARY:Sharer Test (private)
+CLASS:PRIVATE
+TRANSP:OPAQUE
+STATUS:CONFIRMED
+DTEND;TZID=Europe/Berlin:20160419T140000
+LAST-MODIFIED:20160419T074202Z
+DTSTAMP:20160419T074202Z
+CREATED:20160419T074202Z
+UID:2e468c48-7860-492e-bc52-92fa0daeeccf.1461051722310-5
+END:VEVENT
+END:VCALENDAR
+EOD;
+ $sharerConfidential = <<<EOD
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//dmfs.org//mimedir.icalendar//EN
+BEGIN:VTIMEZONE
+TZID:Europe/Berlin
+X-LIC-LOCATION:Europe/Berlin
+BEGIN:DAYLIGHT
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+TZNAME:CEST
+DTSTART:19700329T020000
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+TZNAME:CET
+DTSTART:19701025T030000
+RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+DTSTART;TZID=Europe/Berlin:20160419T130000
+SUMMARY:Sharer Test (confidential)
+CLASS:CONFIDENTIAL
+TRANSP:OPAQUE
+STATUS:CONFIRMED
+DTEND;TZID=Europe/Berlin:20160419T140000
+LAST-MODIFIED:20160419T074202Z
+DTSTAMP:20160419T074202Z
+CREATED:20160419T074202Z
+UID:2e468c48-7860-492e-bc52-92fa0daeeccf.1461051722310-6
+END:VEVENT
+END:VCALENDAR
+EOD;
+
+ $l10n = $this->createMock(IL10N::class);
+ $l10n
+ ->expects($this->any())
+ ->method('t')
+ ->willReturnCallback(function ($text, $parameters = []) {
+ return vsprintf($text, $parameters);
+ });
+ $config = $this->createMock(IConfig::class);
+ $this->userManager->expects($this->any())
+ ->method('userExists')
+ ->willReturn(true);
+ $this->groupManager->expects($this->any())
+ ->method('groupExists')
+ ->willReturn(true);
+
+ $me = self::UNIT_TEST_USER;
+ $sharer = self::UNIT_TEST_USER1;
+ $this->backend->createCalendar($me, 'calendar-uri-me', []);
+ $this->backend->createCalendar($sharer, 'calendar-uri-sharer', []);
+
+ $myCalendars = $this->backend->getCalendarsForUser($me);
+ $this->assertCount(1, $myCalendars);
+
+ $sharerCalendars = $this->backend->getCalendarsForUser($sharer);
+ $this->assertCount(1, $sharerCalendars);
+ $sharerCalendar = new Calendar($this->backend, $sharerCalendars[0], $l10n, $config);
+ $this->backend->updateShares($sharerCalendar, [
+ [
+ 'href' => 'principal:' . $me,
+ 'readOnly' => false,
+ ],
+ ], []);
+
+ $this->assertCount(2, $this->backend->getCalendarsForUser($me));
+
+ $this->backend->createCalendarObject($myCalendars[0]['id'], 'event0.ics', $myPublic);
+ $this->backend->createCalendarObject($myCalendars[0]['id'], 'event1.ics', $myPrivate);
+ $this->backend->createCalendarObject($myCalendars[0]['id'], 'event2.ics', $myConfidential);
+
+ $this->backend->createCalendarObject($sharerCalendars[0]['id'], 'event3.ics', $sharerPublic);
+ $this->backend->createCalendarObject($sharerCalendars[0]['id'], 'event4.ics', $sharerPrivate);
+ $this->backend->createCalendarObject($sharerCalendars[0]['id'], 'event5.ics', $sharerConfidential);
+
+ $mySearchResults = $this->backend->searchPrincipalUri($me, 'Test', ['VEVENT'], ['SUMMARY'], []);
+ $sharerSearchResults = $this->backend->searchPrincipalUri($sharer, 'Test', ['VEVENT'], ['SUMMARY'], []);
+
+ $this->assertCount(4, $mySearchResults);
+ $this->assertCount(3, $sharerSearchResults);
+
+ $this->assertEquals($myPublic, $mySearchResults[0]['calendardata']);
+ $this->assertEquals($myPrivate, $mySearchResults[1]['calendardata']);
+ $this->assertEquals($myConfidential, $mySearchResults[2]['calendardata']);
+ $this->assertEquals($sharerPublic, $mySearchResults[3]['calendardata']);
+
+ $this->assertEquals($sharerPublic, $sharerSearchResults[0]['calendardata']);
+ $this->assertEquals($sharerPrivate, $sharerSearchResults[1]['calendardata']);
+ $this->assertEquals($sharerConfidential, $sharerSearchResults[2]['calendardata']);
+ }
}
diff --git a/apps/dav/tests/unit/Search/ContactsSearchProviderTest.php b/apps/dav/tests/unit/Search/ContactsSearchProviderTest.php
index d89d8dd1690..e0cfe3245ba 100644
--- a/apps/dav/tests/unit/Search/ContactsSearchProviderTest.php
+++ b/apps/dav/tests/unit/Search/ContactsSearchProviderTest.php
@@ -224,7 +224,7 @@ class ContactsSearchProviderTest extends TestCase {
$this->assertEquals('icon-contacts-dark', $result0Data['iconClass']);
$this->assertTrue($result0Data['rounded']);
- $this->assertInstanceOf(ContactsSearchResultEntry::class, $result0);
+ $this->assertInstanceOf(ContactsSearchResultEntry::class, $result1);
$this->assertEquals('absolute-thumbnail-url?photo', $result1Data['thumbnailUrl']);
$this->assertEquals('FN of Test2', $result1Data['title']);
$this->assertEquals('subline', $result1Data['subline']);
diff --git a/apps/dav/tests/unit/Search/EventsSearchProviderTest.php b/apps/dav/tests/unit/Search/EventsSearchProviderTest.php
new file mode 100644
index 00000000000..f0d6329db5c
--- /dev/null
+++ b/apps/dav/tests/unit/Search/EventsSearchProviderTest.php
@@ -0,0 +1,473 @@
+<?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\Tests\unit\Search;
+
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCA\DAV\Search\EventsSearchProvider;
+use OCA\DAV\Search\EventsSearchResultEntry;
+use OCP\App\IAppManager;
+use OCP\IL10N;
+use OCP\IURLGenerator;
+use OCP\IUser;
+use OCP\Search\ISearchQuery;
+use OCP\Search\SearchResult;
+use Sabre\VObject\Reader;
+use Test\TestCase;
+
+class EventsSearchProviderTest extends TestCase {
+
+ /** @var IAppManager|\PHPUnit\Framework\MockObject\MockObject */
+ private $appManager;
+
+ /** @var IL10N|\PHPUnit\Framework\MockObject\MockObject */
+ private $l10n;
+
+ /** @var IURLGenerator|\PHPUnit\Framework\MockObject\MockObject */
+ private $urlGenerator;
+
+ /** @var CalDavBackend|\PHPUnit\Framework\MockObject\MockObject */
+ private $backend;
+
+ /** @var EventsSearchProvider */
+ private $provider;
+
+ // NO SUMMARY
+ private $vEvent0 = 'BEGIN:VCALENDAR'.PHP_EOL.
+ 'VERSION:2.0'.PHP_EOL.
+ 'PRODID:-//Apple Inc.//Mac OS X 10.11.6//EN'.PHP_EOL.
+ 'CALSCALE:GREGORIAN'.PHP_EOL.
+ 'BEGIN:VEVENT'.PHP_EOL.
+ 'CREATED:20161004T144433Z'.PHP_EOL.
+ 'UID:85560E76-1B0D-47E1-A735-21625767FCA4'.PHP_EOL.
+ 'DTEND;VALUE=DATE:20161008'.PHP_EOL.
+ 'TRANSP:TRANSPARENT'.PHP_EOL.
+ 'DTSTART;VALUE=DATE:20161005'.PHP_EOL.
+ 'DTSTAMP:20161004T144437Z'.PHP_EOL.
+ 'SEQUENCE:0'.PHP_EOL.
+ 'END:VEVENT'.PHP_EOL.
+ 'END:VCALENDAR';
+
+ // TIMED SAME DAY
+ private $vEvent1 = 'BEGIN:VCALENDAR'.PHP_EOL.
+ 'VERSION:2.0'.PHP_EOL.
+ 'PRODID:-//Tests//'.PHP_EOL.
+ 'CALSCALE:GREGORIAN'.PHP_EOL.
+ 'BEGIN:VTIMEZONE'.PHP_EOL.
+ 'TZID:Europe/Berlin'.PHP_EOL.
+ 'BEGIN:DAYLIGHT'.PHP_EOL.
+ 'TZOFFSETFROM:+0100'.PHP_EOL.
+ 'RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU'.PHP_EOL.
+ 'DTSTART:19810329T020000'.PHP_EOL.
+ 'TZNAME:GMT+2'.PHP_EOL.
+ 'TZOFFSETTO:+0200'.PHP_EOL.
+ 'END:DAYLIGHT'.PHP_EOL.
+ 'BEGIN:STANDARD'.PHP_EOL.
+ 'TZOFFSETFROM:+0200'.PHP_EOL.
+ 'RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU'.PHP_EOL.
+ 'DTSTART:19961027T030000'.PHP_EOL.
+ 'TZNAME:GMT+1'.PHP_EOL.
+ 'TZOFFSETTO:+0100'.PHP_EOL.
+ 'END:STANDARD'.PHP_EOL.
+ 'END:VTIMEZONE'.PHP_EOL.
+ 'BEGIN:VEVENT'.PHP_EOL.
+ 'CREATED:20160809T163629Z'.PHP_EOL.
+ 'UID:0AD16F58-01B3-463B-A215-FD09FC729A02'.PHP_EOL.
+ 'DTEND;TZID=Europe/Berlin:20160816T100000'.PHP_EOL.
+ 'TRANSP:OPAQUE'.PHP_EOL.
+ 'SUMMARY:Test Europe Berlin'.PHP_EOL.
+ 'DTSTART;TZID=Europe/Berlin:20160816T090000'.PHP_EOL.
+ 'DTSTAMP:20160809T163632Z'.PHP_EOL.
+ 'SEQUENCE:0'.PHP_EOL.
+ 'END:VEVENT'.PHP_EOL.
+ 'END:VCALENDAR';
+
+ // TIMED DIFFERENT DAY
+ private $vEvent2 = 'BEGIN:VCALENDAR'.PHP_EOL.
+ 'VERSION:2.0'.PHP_EOL.
+ 'PRODID:-//Tests//'.PHP_EOL.
+ 'CALSCALE:GREGORIAN'.PHP_EOL.
+ 'BEGIN:VTIMEZONE'.PHP_EOL.
+ 'TZID:Europe/Berlin'.PHP_EOL.
+ 'BEGIN:DAYLIGHT'.PHP_EOL.
+ 'TZOFFSETFROM:+0100'.PHP_EOL.
+ 'RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU'.PHP_EOL.
+ 'DTSTART:19810329T020000'.PHP_EOL.
+ 'TZNAME:GMT+2'.PHP_EOL.
+ 'TZOFFSETTO:+0200'.PHP_EOL.
+ 'END:DAYLIGHT'.PHP_EOL.
+ 'BEGIN:STANDARD'.PHP_EOL.
+ 'TZOFFSETFROM:+0200'.PHP_EOL.
+ 'RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU'.PHP_EOL.
+ 'DTSTART:19961027T030000'.PHP_EOL.
+ 'TZNAME:GMT+1'.PHP_EOL.
+ 'TZOFFSETTO:+0100'.PHP_EOL.
+ 'END:STANDARD'.PHP_EOL.
+ 'END:VTIMEZONE'.PHP_EOL.
+ 'BEGIN:VEVENT'.PHP_EOL.
+ 'CREATED:20160809T163629Z'.PHP_EOL.
+ 'UID:0AD16F58-01B3-463B-A215-FD09FC729A02'.PHP_EOL.
+ 'DTEND;TZID=Europe/Berlin:20160817T100000'.PHP_EOL.
+ 'TRANSP:OPAQUE'.PHP_EOL.
+ 'SUMMARY:Test Europe Berlin'.PHP_EOL.
+ 'DTSTART;TZID=Europe/Berlin:20160816T090000'.PHP_EOL.
+ 'DTSTAMP:20160809T163632Z'.PHP_EOL.
+ 'SEQUENCE:0'.PHP_EOL.
+ 'END:VEVENT'.PHP_EOL.
+ 'END:VCALENDAR';
+
+ // ALL-DAY ONE-DAY
+ private $vEvent3 = 'BEGIN:VCALENDAR'.PHP_EOL.
+ 'VERSION:2.0'.PHP_EOL.
+ 'PRODID:-//Apple Inc.//Mac OS X 10.11.6//EN'.PHP_EOL.
+ 'CALSCALE:GREGORIAN'.PHP_EOL.
+ 'BEGIN:VEVENT'.PHP_EOL.
+ 'CREATED:20161004T144433Z'.PHP_EOL.
+ 'UID:85560E76-1B0D-47E1-A735-21625767FCA4'.PHP_EOL.
+ 'DTEND;VALUE=DATE:20161006'.PHP_EOL.
+ 'TRANSP:TRANSPARENT'.PHP_EOL.
+ 'DTSTART;VALUE=DATE:20161005'.PHP_EOL.
+ 'DTSTAMP:20161004T144437Z'.PHP_EOL.
+ 'SEQUENCE:0'.PHP_EOL.
+ 'END:VEVENT'.PHP_EOL.
+ 'END:VCALENDAR';
+
+ // ALL-DAY MULTIPLE DAYS
+ private $vEvent4 = 'BEGIN:VCALENDAR'.PHP_EOL.
+ 'VERSION:2.0'.PHP_EOL.
+ 'PRODID:-//Apple Inc.//Mac OS X 10.11.6//EN'.PHP_EOL.
+ 'CALSCALE:GREGORIAN'.PHP_EOL.
+ 'BEGIN:VEVENT'.PHP_EOL.
+ 'CREATED:20161004T144433Z'.PHP_EOL.
+ 'UID:85560E76-1B0D-47E1-A735-21625767FCA4'.PHP_EOL.
+ 'DTEND;VALUE=DATE:20161008'.PHP_EOL.
+ 'TRANSP:TRANSPARENT'.PHP_EOL.
+ 'DTSTART;VALUE=DATE:20161005'.PHP_EOL.
+ 'DTSTAMP:20161004T144437Z'.PHP_EOL.
+ 'SEQUENCE:0'.PHP_EOL.
+ 'END:VEVENT'.PHP_EOL.
+ 'END:VCALENDAR';
+
+ // DURATION
+ private $vEvent5 = 'BEGIN:VCALENDAR'.PHP_EOL.
+ 'VERSION:2.0'.PHP_EOL.
+ 'PRODID:-//Apple Inc.//Mac OS X 10.11.6//EN'.PHP_EOL.
+ 'CALSCALE:GREGORIAN'.PHP_EOL.
+ 'BEGIN:VEVENT'.PHP_EOL.
+ 'CREATED:20161004T144433Z'.PHP_EOL.
+ 'UID:85560E76-1B0D-47E1-A735-21625767FCA4'.PHP_EOL.
+ 'DURATION:P5D'.PHP_EOL.
+ 'TRANSP:TRANSPARENT'.PHP_EOL.
+ 'DTSTART;VALUE=DATE:20161005'.PHP_EOL.
+ 'DTSTAMP:20161004T144437Z'.PHP_EOL.
+ 'SEQUENCE:0'.PHP_EOL.
+ 'END:VEVENT'.PHP_EOL.
+ 'END:VCALENDAR';
+
+ // NO DTEND - DATE
+ private $vEvent6 = 'BEGIN:VCALENDAR'.PHP_EOL.
+ 'VERSION:2.0'.PHP_EOL.
+ 'PRODID:-//Apple Inc.//Mac OS X 10.11.6//EN'.PHP_EOL.
+ 'CALSCALE:GREGORIAN'.PHP_EOL.
+ 'BEGIN:VEVENT'.PHP_EOL.
+ 'CREATED:20161004T144433Z'.PHP_EOL.
+ 'UID:85560E76-1B0D-47E1-A735-21625767FCA4'.PHP_EOL.
+ 'TRANSP:TRANSPARENT'.PHP_EOL.
+ 'DTSTART;VALUE=DATE:20161005'.PHP_EOL.
+ 'DTSTAMP:20161004T144437Z'.PHP_EOL.
+ 'SEQUENCE:0'.PHP_EOL.
+ 'END:VEVENT'.PHP_EOL.
+ 'END:VCALENDAR';
+
+ // NO DTEND - DATE-TIME
+ private $vEvent7 = 'BEGIN:VCALENDAR'.PHP_EOL.
+ 'VERSION:2.0'.PHP_EOL.
+ 'PRODID:-//Tests//'.PHP_EOL.
+ 'CALSCALE:GREGORIAN'.PHP_EOL.
+ 'BEGIN:VTIMEZONE'.PHP_EOL.
+ 'TZID:Europe/Berlin'.PHP_EOL.
+ 'BEGIN:DAYLIGHT'.PHP_EOL.
+ 'TZOFFSETFROM:+0100'.PHP_EOL.
+ 'RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU'.PHP_EOL.
+ 'DTSTART:19810329T020000'.PHP_EOL.
+ 'TZNAME:GMT+2'.PHP_EOL.
+ 'TZOFFSETTO:+0200'.PHP_EOL.
+ 'END:DAYLIGHT'.PHP_EOL.
+ 'BEGIN:STANDARD'.PHP_EOL.
+ 'TZOFFSETFROM:+0200'.PHP_EOL.
+ 'RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU'.PHP_EOL.
+ 'DTSTART:19961027T030000'.PHP_EOL.
+ 'TZNAME:GMT+1'.PHP_EOL.
+ 'TZOFFSETTO:+0100'.PHP_EOL.
+ 'END:STANDARD'.PHP_EOL.
+ 'END:VTIMEZONE'.PHP_EOL.
+ 'BEGIN:VEVENT'.PHP_EOL.
+ 'CREATED:20160809T163629Z'.PHP_EOL.
+ 'UID:0AD16F58-01B3-463B-A215-FD09FC729A02'.PHP_EOL.
+ 'TRANSP:OPAQUE'.PHP_EOL.
+ 'SUMMARY:Test Europe Berlin'.PHP_EOL.
+ 'DTSTART;TZID=Europe/Berlin:20160816T090000'.PHP_EOL.
+ 'DTSTAMP:20160809T163632Z'.PHP_EOL.
+ 'SEQUENCE:0'.PHP_EOL.
+ 'END:VEVENT'.PHP_EOL.
+ 'END:VCALENDAR';
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->appManager = $this->createMock(IAppManager::class);
+ $this->l10n = $this->createMock(IL10N::class);
+ $this->urlGenerator = $this->createMock(IURLGenerator::class);
+ $this->backend = $this->createMock(CalDavBackend::class);
+
+ $this->provider = new EventsSearchProvider(
+ $this->appManager,
+ $this->l10n,
+ $this->urlGenerator,
+ $this->backend
+ );
+ }
+
+ public function testGetId(): void {
+ $this->assertEquals('calendar-dav', $this->provider->getId());
+ }
+
+ public function testGetName(): void {
+ $this->l10n->expects($this->exactly(1))
+ ->method('t')
+ ->with('Events')
+ ->willReturnArgument(0);
+
+ $this->assertEquals('Events', $this->provider->getName());
+ }
+
+ public function testSearchAppDisabled(): void {
+ $user = $this->createMock(IUser::class);
+ $query = $this->createMock(ISearchQuery::class);
+ $this->appManager->expects($this->once())
+ ->method('isEnabledForUser')
+ ->with('calendar', $user)
+ ->willReturn(false);
+ $this->l10n->expects($this->exactly(1))
+ ->method('t')
+ ->willReturnArgument(0);
+ $this->backend->expects($this->never())
+ ->method('getCalendarsForUser');
+ $this->backend->expects($this->never())
+ ->method('getSubscriptionsForUser');
+ $this->backend->expects($this->never())
+ ->method('searchPrincipalUri');
+
+ $actual = $this->provider->search($user, $query);
+ $data = $actual->jsonSerialize();
+ $this->assertInstanceOf(SearchResult::class, $actual);
+ $this->assertEquals('Events', $data['name']);
+ $this->assertEmpty($data['entries']);
+ $this->assertFalse($data['isPaginated']);
+ $this->assertNull($data['cursor']);
+ }
+
+ public function testSearch(): void {
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn('john.doe');
+ $query = $this->createMock(ISearchQuery::class);
+ $query->method('getTerm')->willReturn('search term');
+ $query->method('getLimit')->willReturn(5);
+ $query->method('getCursor')->willReturn(20);
+ $this->appManager->expects($this->once())
+ ->method('isEnabledForUser')
+ ->with('calendar', $user)
+ ->willReturn(true);
+ $this->l10n->method('t')->willReturnArgument(0);
+
+ $this->backend->expects($this->once())
+ ->method('getCalendarsForUser')
+ ->with('principals/users/john.doe')
+ ->willReturn([
+ [
+ 'id' => 99,
+ 'principaluri' => 'principals/users/john.doe',
+ 'uri' => 'calendar-uri-99',
+ ], [
+ 'id' => 123,
+ 'principaluri' => 'principals/users/john.doe',
+ 'uri' => 'calendar-uri-123',
+ ]
+ ]);
+ $this->backend->expects($this->once())
+ ->method('getSubscriptionsForUser')
+ ->with('principals/users/john.doe')
+ ->willReturn([
+ [
+ 'id' => 1337,
+ 'principaluri' => 'principals/users/john.doe',
+ 'uri' => 'subscription-uri-1337',
+ ]
+ ]);
+ $this->backend->expects($this->once())
+ ->method('searchPrincipalUri')
+ ->with('principals/users/john.doe', 'search term', ['VEVENT'],
+ ['SUMMARY', 'LOCATION', 'DESCRIPTION', 'ATTENDEE', 'ORGANIZER', 'CATEGORIES'],
+ ['ATTENDEE' => ['CN'], 'ORGANIZER' => ['CN']],
+ ['limit' => 5, 'offset' => 20])
+ ->willReturn([
+ [
+ 'calendarid' => 99,
+ 'calendartype' => CalDavBackend::CALENDAR_TYPE_CALENDAR,
+ 'uri' => 'event0.ics',
+ 'calendardata' => $this->vEvent0,
+ ],
+ [
+ 'calendarid' => 123,
+ 'calendartype' => CalDavBackend::CALENDAR_TYPE_CALENDAR,
+ 'uri' => 'event1.ics',
+ 'calendardata' => $this->vEvent1,
+ ],
+ [
+ 'calendarid' => 1337,
+ 'calendartype' => CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION,
+ 'uri' => 'event2.ics',
+ 'calendardata' => $this->vEvent2,
+ ]
+ ]);
+
+ $provider = $this->getMockBuilder(EventsSearchProvider::class)
+ ->setConstructorArgs([
+ $this->appManager,
+ $this->l10n,
+ $this->urlGenerator,
+ $this->backend,
+ ])
+ ->setMethods([
+ 'getDeepLinkToCalendarApp',
+ 'generateSubline',
+ ])
+ ->getMock();
+
+ $provider->expects($this->exactly(3))
+ ->method('generateSubline')
+ ->willReturn('subline');
+ $provider->expects($this->exactly(3))
+ ->method('getDeepLinkToCalendarApp')
+ ->withConsecutive(
+ ['principals/users/john.doe', 'calendar-uri-99', 'event0.ics'],
+ ['principals/users/john.doe', 'calendar-uri-123', 'event1.ics'],
+ ['principals/users/john.doe', 'subscription-uri-1337', 'event2.ics']
+ )
+ ->willReturn('deep-link-to-calendar');
+
+ $actual = $provider->search($user, $query);
+ $data = $actual->jsonSerialize();
+ $this->assertInstanceOf(SearchResult::class, $actual);
+ $this->assertEquals('Events', $data['name']);
+ $this->assertCount(3, $data['entries']);
+ $this->assertTrue($data['isPaginated']);
+ $this->assertEquals(23, $data['cursor']);
+
+ $result0 = $data['entries'][0];
+ $result0Data = $result0->jsonSerialize();
+ $result1 = $data['entries'][1];
+ $result1Data = $result1->jsonSerialize();
+ $result2 = $data['entries'][2];
+ $result2Data = $result2->jsonSerialize();
+
+ $this->assertInstanceOf(EventsSearchResultEntry::class, $result0);
+ $this->assertEmpty($result0Data['thumbnailUrl']);
+ $this->assertEquals('Untitled event', $result0Data['title']);
+ $this->assertEquals('subline', $result0Data['subline']);
+ $this->assertEquals('deep-link-to-calendar', $result0Data['resourceUrl']);
+ $this->assertEquals('icon-calendar-dark', $result0Data['iconClass']);
+ $this->assertFalse($result0Data['rounded']);
+
+ $this->assertInstanceOf(EventsSearchResultEntry::class, $result1);
+ $this->assertEmpty($result1Data['thumbnailUrl']);
+ $this->assertEquals('Test Europe Berlin', $result1Data['title']);
+ $this->assertEquals('subline', $result1Data['subline']);
+ $this->assertEquals('deep-link-to-calendar', $result1Data['resourceUrl']);
+ $this->assertEquals('icon-calendar-dark', $result1Data['iconClass']);
+ $this->assertFalse($result1Data['rounded']);
+
+ $this->assertInstanceOf(EventsSearchResultEntry::class, $result2);
+ $this->assertEmpty($result2Data['thumbnailUrl']);
+ $this->assertEquals('Test Europe Berlin', $result2Data['title']);
+ $this->assertEquals('subline', $result2Data['subline']);
+ $this->assertEquals('deep-link-to-calendar', $result2Data['resourceUrl']);
+ $this->assertEquals('icon-calendar-dark', $result2Data['iconClass']);
+ $this->assertFalse($result2Data['rounded']);
+ }
+
+ public function testGetDeepLinkToCalendarApp(): void {
+ $this->urlGenerator->expects($this->at(0))
+ ->method('linkTo')
+ ->with('', 'remote.php')
+ ->willReturn('link-to-remote.php');
+ $this->urlGenerator->expects($this->at(1))
+ ->method('linkToRoute')
+ ->with('calendar.view.index')
+ ->willReturn('link-to-route-calendar/');
+ $this->urlGenerator->expects($this->at(2))
+ ->method('getAbsoluteURL')
+ ->with('link-to-route-calendar/edit/bGluay10by1yZW1vdGUucGhwL2Rhdi9jYWxlbmRhcnMvam9obi5kb2UvZm9vL2Jhci5pY3M=')
+ ->willReturn('absolute-url-to-route');
+
+ $actual = self::invokePrivate($this->provider, 'getDeepLinkToCalendarApp', ['principals/users/john.doe', 'foo', 'bar.ics']);
+
+ $this->assertEquals('absolute-url-to-route', $actual);
+ }
+
+ /**
+ * @param string $ics
+ * @param string $expectedSubline
+ *
+ * @dataProvider generateSublineDataProvider
+ */
+ public function testGenerateSubline(string $ics, string $expectedSubline): void {
+ $vCalendar = Reader::read($ics, Reader::OPTION_FORGIVING);
+ $eventComponent = $vCalendar->VEVENT;
+
+ $this->l10n->method('l')
+ ->willReturnCallback(static function (string $type, \DateTime $date, $_):string {
+ if ($type === 'time') {
+ return $date->format('H:i');
+ }
+
+ return $date->format('m-d');
+ });
+
+ $actual = self::invokePrivate($this->provider, 'generateSubline', [$eventComponent]);
+ $this->assertEquals($expectedSubline, $actual);
+ }
+
+ public function generateSublineDataProvider(): array {
+ return [
+ [$this->vEvent1, '08-16 09:00 - 10:00'],
+ [$this->vEvent2, '08-16 09:00 - 08-17 10:00'],
+ [$this->vEvent3, '10-05'],
+ [$this->vEvent4, '10-05 - 10-07'],
+ [$this->vEvent5, '10-05 - 10-09'],
+ [$this->vEvent6, '10-05'],
+ [$this->vEvent7, '08-16 09:00 - 09:00'],
+ ];
+ }
+}
diff --git a/apps/dav/tests/unit/Search/TasksSearchProviderTest.php b/apps/dav/tests/unit/Search/TasksSearchProviderTest.php
new file mode 100644
index 00000000000..30f57270e95
--- /dev/null
+++ b/apps/dav/tests/unit/Search/TasksSearchProviderTest.php
@@ -0,0 +1,344 @@
+<?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\Tests\unit\Search;
+
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCA\DAV\Search\TasksSearchProvider;
+use OCA\DAV\Search\TasksSearchResultEntry;
+use OCP\App\IAppManager;
+use OCP\IL10N;
+use OCP\IURLGenerator;
+use OCP\IUser;
+use OCP\Search\ISearchQuery;
+use OCP\Search\SearchResult;
+use Sabre\VObject\Reader;
+use Test\TestCase;
+
+class TasksSearchProviderTest extends TestCase {
+
+ /** @var IAppManager|\PHPUnit\Framework\MockObject\MockObject */
+ private $appManager;
+
+ /** @var IL10N|\PHPUnit\Framework\MockObject\MockObject */
+ private $l10n;
+
+ /** @var IURLGenerator|\PHPUnit\Framework\MockObject\MockObject */
+ private $urlGenerator;
+
+ /** @var CalDavBackend|\PHPUnit\Framework\MockObject\MockObject */
+ private $backend;
+
+ /** @var TasksSearchProvider */
+ private $provider;
+
+ // NO DUE NOR COMPLETED NOR SUMMARY
+ private $vTodo0 = 'BEGIN:VCALENDAR'.PHP_EOL.
+ 'PRODID:TEST'.PHP_EOL.
+ 'VERSION:2.0'.PHP_EOL.
+ 'BEGIN:VTODO'.PHP_EOL.
+ 'UID:20070313T123432Z-456553@example.com'.PHP_EOL.
+ 'DTSTAMP:20070313T123432Z'.PHP_EOL.
+ 'STATUS:NEEDS-ACTION'.PHP_EOL.
+ 'END:VTODO'.PHP_EOL.
+ 'END:VCALENDAR';
+
+ // DUE AND COMPLETED
+ private $vTodo1 = 'BEGIN:VCALENDAR'.PHP_EOL.
+ 'PRODID:TEST'.PHP_EOL.
+ 'VERSION:2.0'.PHP_EOL.
+ 'BEGIN:VTODO'.PHP_EOL.
+ 'UID:20070313T123432Z-456553@example.com'.PHP_EOL.
+ 'DTSTAMP:20070313T123432Z'.PHP_EOL.
+ 'COMPLETED:20070707T100000Z'.PHP_EOL.
+ 'DUE;VALUE=DATE:20070501'.PHP_EOL.
+ 'SUMMARY:Task title'.PHP_EOL.
+ 'STATUS:NEEDS-ACTION'.PHP_EOL.
+ 'END:VTODO'.PHP_EOL.
+ 'END:VCALENDAR';
+
+ // COMPLETED ONLY
+ private $vTodo2 = 'BEGIN:VCALENDAR'.PHP_EOL.
+ 'PRODID:TEST'.PHP_EOL.
+ 'VERSION:2.0'.PHP_EOL.
+ 'BEGIN:VTODO'.PHP_EOL.
+ 'UID:20070313T123432Z-456553@example.com'.PHP_EOL.
+ 'DTSTAMP:20070313T123432Z'.PHP_EOL.
+ 'COMPLETED:20070707T100000Z'.PHP_EOL.
+ 'SUMMARY:Task title'.PHP_EOL.
+ 'STATUS:NEEDS-ACTION'.PHP_EOL.
+ 'END:VTODO'.PHP_EOL.
+ 'END:VCALENDAR';
+
+ // DUE DATE
+ private $vTodo3 = 'BEGIN:VCALENDAR'.PHP_EOL.
+ 'PRODID:TEST'.PHP_EOL.
+ 'VERSION:2.0'.PHP_EOL.
+ 'BEGIN:VTODO'.PHP_EOL.
+ 'UID:20070313T123432Z-456553@example.com'.PHP_EOL.
+ 'DTSTAMP:20070313T123432Z'.PHP_EOL.
+ 'DUE;VALUE=DATE:20070501'.PHP_EOL.
+ 'SUMMARY:Task title'.PHP_EOL.
+ 'STATUS:NEEDS-ACTION'.PHP_EOL.
+ 'END:VTODO'.PHP_EOL.
+ 'END:VCALENDAR';
+
+ // DUE DATETIME
+ private $vTodo4 = 'BEGIN:VCALENDAR'.PHP_EOL.
+ 'PRODID:TEST'.PHP_EOL.
+ 'VERSION:2.0'.PHP_EOL.
+ 'BEGIN:VTODO'.PHP_EOL.
+ 'UID:20070313T123432Z-456553@example.com'.PHP_EOL.
+ 'DTSTAMP:20070313T123432Z'.PHP_EOL.
+ 'DUE:20070709T130000Z'.PHP_EOL.
+ 'SUMMARY:Task title'.PHP_EOL.
+ 'STATUS:NEEDS-ACTION'.PHP_EOL.
+ 'END:VTODO'.PHP_EOL.
+ 'END:VCALENDAR';
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->appManager = $this->createMock(IAppManager::class);
+ $this->l10n = $this->createMock(IL10N::class);
+ $this->urlGenerator = $this->createMock(IURLGenerator::class);
+ $this->backend = $this->createMock(CalDavBackend::class);
+
+ $this->provider = new TasksSearchProvider(
+ $this->appManager,
+ $this->l10n,
+ $this->urlGenerator,
+ $this->backend
+ );
+ }
+
+ public function testGetId(): void {
+ $this->assertEquals('tasks-dav', $this->provider->getId());
+ }
+
+ public function testGetName(): void {
+ $this->l10n->expects($this->exactly(1))
+ ->method('t')
+ ->with('Tasks')
+ ->willReturnArgument(0);
+
+ $this->assertEquals('Tasks', $this->provider->getName());
+ }
+
+ public function testSearchAppDisabled(): void {
+ $user = $this->createMock(IUser::class);
+ $query = $this->createMock(ISearchQuery::class);
+ $this->appManager->expects($this->once())
+ ->method('isEnabledForUser')
+ ->with('tasks', $user)
+ ->willReturn(false);
+ $this->l10n->expects($this->exactly(1))
+ ->method('t')
+ ->willReturnArgument(0);
+ $this->backend->expects($this->never())
+ ->method('getCalendarsForUser');
+ $this->backend->expects($this->never())
+ ->method('getSubscriptionsForUser');
+ $this->backend->expects($this->never())
+ ->method('searchPrincipalUri');
+
+ $actual = $this->provider->search($user, $query);
+ $data = $actual->jsonSerialize();
+ $this->assertInstanceOf(SearchResult::class, $actual);
+ $this->assertEquals('Tasks', $data['name']);
+ $this->assertEmpty($data['entries']);
+ $this->assertFalse($data['isPaginated']);
+ $this->assertNull($data['cursor']);
+ }
+
+ public function testSearch(): void {
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn('john.doe');
+ $query = $this->createMock(ISearchQuery::class);
+ $query->method('getTerm')->willReturn('search term');
+ $query->method('getLimit')->willReturn(5);
+ $query->method('getCursor')->willReturn(20);
+ $this->appManager->expects($this->once())
+ ->method('isEnabledForUser')
+ ->with('tasks', $user)
+ ->willReturn(true);
+ $this->l10n->method('t')->willReturnArgument(0);
+
+ $this->backend->expects($this->once())
+ ->method('getCalendarsForUser')
+ ->with('principals/users/john.doe')
+ ->willReturn([
+ [
+ 'id' => 99,
+ 'principaluri' => 'principals/users/john.doe',
+ 'uri' => 'calendar-uri-99',
+ ], [
+ 'id' => 123,
+ 'principaluri' => 'principals/users/john.doe',
+ 'uri' => 'calendar-uri-123',
+ ]
+ ]);
+ $this->backend->expects($this->once())
+ ->method('getSubscriptionsForUser')
+ ->with('principals/users/john.doe')
+ ->willReturn([
+ [
+ 'id' => 1337,
+ 'principaluri' => 'principals/users/john.doe',
+ 'uri' => 'subscription-uri-1337',
+ ]
+ ]);
+ $this->backend->expects($this->once())
+ ->method('searchPrincipalUri')
+ ->with('principals/users/john.doe', 'search term', ['VTODO'],
+ ['SUMMARY', 'DESCRIPTION', 'CATEGORIES'],
+ [],
+ ['limit' => 5, 'offset' => 20])
+ ->willReturn([
+ [
+ 'calendarid' => 99,
+ 'calendartype' => CalDavBackend::CALENDAR_TYPE_CALENDAR,
+ 'uri' => 'todo0.ics',
+ 'calendardata' => $this->vTodo0,
+ ],
+ [
+ 'calendarid' => 123,
+ 'calendartype' => CalDavBackend::CALENDAR_TYPE_CALENDAR,
+ 'uri' => 'todo1.ics',
+ 'calendardata' => $this->vTodo1,
+ ],
+ [
+ 'calendarid' => 1337,
+ 'calendartype' => CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION,
+ 'uri' => 'todo2.ics',
+ 'calendardata' => $this->vTodo2,
+ ]
+ ]);
+
+ $provider = $this->getMockBuilder(TasksSearchProvider::class)
+ ->setConstructorArgs([
+ $this->appManager,
+ $this->l10n,
+ $this->urlGenerator,
+ $this->backend,
+ ])
+ ->setMethods([
+ 'getDeepLinkToTasksApp',
+ 'generateSubline',
+ ])
+ ->getMock();
+
+ $provider->expects($this->exactly(3))
+ ->method('generateSubline')
+ ->willReturn('subline');
+ $provider->expects($this->exactly(3))
+ ->method('getDeepLinkToTasksApp')
+ ->withConsecutive(
+ ['calendar-uri-99', 'todo0.ics'],
+ ['calendar-uri-123', 'todo1.ics'],
+ ['subscription-uri-1337', 'todo2.ics']
+ )
+ ->willReturn('deep-link-to-tasks');
+
+ $actual = $provider->search($user, $query);
+ $data = $actual->jsonSerialize();
+ $this->assertInstanceOf(SearchResult::class, $actual);
+ $this->assertEquals('Tasks', $data['name']);
+ $this->assertCount(3, $data['entries']);
+ $this->assertTrue($data['isPaginated']);
+ $this->assertEquals(23, $data['cursor']);
+
+ $result0 = $data['entries'][0];
+ $result0Data = $result0->jsonSerialize();
+ $result1 = $data['entries'][1];
+ $result1Data = $result1->jsonSerialize();
+ $result2 = $data['entries'][2];
+ $result2Data = $result2->jsonSerialize();
+
+ $this->assertInstanceOf(TasksSearchResultEntry::class, $result0);
+ $this->assertEmpty($result0Data['thumbnailUrl']);
+ $this->assertEquals('Untitled task', $result0Data['title']);
+ $this->assertEquals('subline', $result0Data['subline']);
+ $this->assertEquals('deep-link-to-tasks', $result0Data['resourceUrl']);
+ $this->assertEquals('icon-checkmark', $result0Data['iconClass']);
+ $this->assertFalse($result0Data['rounded']);
+
+ $this->assertInstanceOf(TasksSearchResultEntry::class, $result1);
+ $this->assertEmpty($result1Data['thumbnailUrl']);
+ $this->assertEquals('Task title', $result1Data['title']);
+ $this->assertEquals('subline', $result1Data['subline']);
+ $this->assertEquals('deep-link-to-tasks', $result1Data['resourceUrl']);
+ $this->assertEquals('icon-checkmark', $result1Data['iconClass']);
+ $this->assertFalse($result1Data['rounded']);
+
+ $this->assertInstanceOf(TasksSearchResultEntry::class, $result2);
+ $this->assertEmpty($result2Data['thumbnailUrl']);
+ $this->assertEquals('Task title', $result2Data['title']);
+ $this->assertEquals('subline', $result2Data['subline']);
+ $this->assertEquals('deep-link-to-tasks', $result2Data['resourceUrl']);
+ $this->assertEquals('icon-checkmark', $result2Data['iconClass']);
+ $this->assertFalse($result2Data['rounded']);
+ }
+
+ public function testGetDeepLinkToTasksApp(): void {
+ $this->urlGenerator->expects($this->once())
+ ->method('linkToRoute')
+ ->with('tasks.page.index')
+ ->willReturn('link-to-route-tasks.index');
+ $this->urlGenerator->expects($this->once())
+ ->method('getAbsoluteURL')
+ ->with('link-to-route-tasks.index#/calendars/uri-john.doe/tasks/task-uri.ics')
+ ->willReturn('absolute-url-link-to-route-tasks.index#/calendars/uri-john.doe/tasks/task-uri.ics');
+
+ $actual = self::invokePrivate($this->provider, 'getDeepLinkToTasksApp', ['uri-john.doe', 'task-uri.ics']);
+ $this->assertEquals('absolute-url-link-to-route-tasks.index#/calendars/uri-john.doe/tasks/task-uri.ics', $actual);
+ }
+
+ /**
+ * @param string $ics
+ * @param string $expectedSubline
+ *
+ * @dataProvider generateSublineDataProvider
+ */
+ public function testGenerateSubline(string $ics, string $expectedSubline): void {
+ $vCalendar = Reader::read($ics, Reader::OPTION_FORGIVING);
+ $taskComponent = $vCalendar->VTODO;
+
+ $this->l10n->method('t')->willReturnArgument(0);
+ $this->l10n->method('l')->willReturnArgument('');
+
+ $actual = self::invokePrivate($this->provider, 'generateSubline', [$taskComponent]);
+ $this->assertEquals($expectedSubline, $actual);
+ }
+
+ public function generateSublineDataProvider(): array {
+ return [
+ [$this->vTodo0, ''],
+ [$this->vTodo1, 'Completed on %s'],
+ [$this->vTodo2, 'Completed on %s'],
+ [$this->vTodo3, 'Due on %s'],
+ [$this->vTodo4, 'Due on %s by %s'],
+ ];
+ }
+}