Signed-off-by: Georg Ehrke <developer@georgehrke.com>tags/v20.0.0beta1
@@ -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', |
@@ -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', |
@@ -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 { |
@@ -1669,6 +1669,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. |
@@ -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]; | |||
} | |||
} |
@@ -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'); | |||
} | |||
} |
@@ -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 { | |||
} |
@@ -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 ''; | |||
} | |||
} |
@@ -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 { | |||
} |
@@ -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']); | |||
} |
@@ -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']); | |||
} | |||
} |
@@ -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']); |
@@ -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'], | |||
]; | |||
} | |||
} |
@@ -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'], | |||
]; | |||
} | |||
} |