diff options
author | Anna Larch <anna@nextcloud.com> | 2023-09-25 14:47:02 +0200 |
---|---|---|
committer | Anna Larch <anna@nextcloud.com> | 2023-11-09 16:20:19 +0100 |
commit | f14a4f8fd73c71e76a9747ac51e657030f5bb835 (patch) | |
tree | 6d5ec8e5365a72b83bfd270ce56bcbaa06703b8a /apps | |
parent | 1aa24c024e207b54df3867f5f7ccd67625ac0492 (diff) | |
download | nextcloud-server-f14a4f8fd73c71e76a9747ac51e657030f5bb835.tar.gz nextcloud-server-f14a4f8fd73c71e76a9747ac51e657030f5bb835.zip |
feat(user status): automate user status for events
and automatically set a user status to free or busy depending on their calendar
transparency, event status and availability settings
Signed-off-by: Anna Larch <anna@nextcloud.com>
Diffstat (limited to 'apps')
-rw-r--r-- | apps/dav/composer/composer/autoload_classmap.php | 3 | ||||
-rw-r--r-- | apps/dav/composer/composer/autoload_static.php | 3 | ||||
-rw-r--r-- | apps/dav/lib/CalDAV/CalendarImpl.php | 27 | ||||
-rw-r--r-- | apps/dav/lib/CalDAV/FreeBusy/FreeBusyGenerator.php | 44 | ||||
-rw-r--r-- | apps/dav/lib/CalDAV/Schedule/Plugin.php | 2 | ||||
-rw-r--r-- | apps/dav/lib/CalDAV/Status/Status.php | 57 | ||||
-rw-r--r-- | apps/dav/lib/CalDAV/Status/StatusService.php | 236 | ||||
-rw-r--r-- | apps/dav/tests/unit/CalDAV/Status/StatusServiceTest.php | 1508 | ||||
-rw-r--r-- | apps/user_status/lib/Db/UserStatusMapper.php | 20 | ||||
-rw-r--r-- | apps/user_status/lib/Listener/UserLiveStatusListener.php | 2 | ||||
-rw-r--r-- | apps/user_status/lib/Service/PredefinedStatusService.php | 2 | ||||
-rw-r--r-- | apps/user_status/lib/Service/StatusService.php | 140 | ||||
-rw-r--r-- | apps/user_status/src/mixins/OnlineStatusMixin.js | 2 | ||||
-rw-r--r-- | apps/user_status/tests/Unit/Service/StatusServiceTest.php | 345 |
14 files changed, 2326 insertions, 65 deletions
diff --git a/apps/dav/composer/composer/autoload_classmap.php b/apps/dav/composer/composer/autoload_classmap.php index e0c3e20dc6b..396b94601bb 100644 --- a/apps/dav/composer/composer/autoload_classmap.php +++ b/apps/dav/composer/composer/autoload_classmap.php @@ -55,6 +55,7 @@ return array( 'OCA\\DAV\\CalDAV\\CalendarProvider' => $baseDir . '/../lib/CalDAV/CalendarProvider.php', 'OCA\\DAV\\CalDAV\\CalendarRoot' => $baseDir . '/../lib/CalDAV/CalendarRoot.php', 'OCA\\DAV\\CalDAV\\EventComparisonService' => $baseDir . '/../lib/CalDAV/EventComparisonService.php', + 'OCA\\DAV\\CalDAV\\FreeBusy\\FreeBusyGenerator' => $baseDir . '/../lib/CalDAV/FreeBusy/FreeBusyGenerator.php', 'OCA\\DAV\\CalDAV\\ICSExportPlugin\\ICSExportPlugin' => $baseDir . '/../lib/CalDAV/ICSExportPlugin/ICSExportPlugin.php', 'OCA\\DAV\\CalDAV\\IRestorable' => $baseDir . '/../lib/CalDAV/IRestorable.php', 'OCA\\DAV\\CalDAV\\Integration\\ExternalCalendar' => $baseDir . '/../lib/CalDAV/Integration/ExternalCalendar.php', @@ -97,6 +98,8 @@ return array( 'OCA\\DAV\\CalDAV\\Search\\Xml\\Filter\\PropFilter' => $baseDir . '/../lib/CalDAV/Search/Xml/Filter/PropFilter.php', 'OCA\\DAV\\CalDAV\\Search\\Xml\\Filter\\SearchTermFilter' => $baseDir . '/../lib/CalDAV/Search/Xml/Filter/SearchTermFilter.php', 'OCA\\DAV\\CalDAV\\Search\\Xml\\Request\\CalendarSearchReport' => $baseDir . '/../lib/CalDAV/Search/Xml/Request/CalendarSearchReport.php', + 'OCA\\DAV\\CalDAV\\Status\\Status' => $baseDir . '/../lib/CalDAV/Status/Status.php', + 'OCA\\DAV\\CalDAV\\Status\\StatusService' => $baseDir . '/../lib/CalDAV/Status/StatusService.php', 'OCA\\DAV\\CalDAV\\Trashbin\\DeletedCalendarObject' => $baseDir . '/../lib/CalDAV/Trashbin/DeletedCalendarObject.php', 'OCA\\DAV\\CalDAV\\Trashbin\\DeletedCalendarObjectsCollection' => $baseDir . '/../lib/CalDAV/Trashbin/DeletedCalendarObjectsCollection.php', 'OCA\\DAV\\CalDAV\\Trashbin\\Plugin' => $baseDir . '/../lib/CalDAV/Trashbin/Plugin.php', diff --git a/apps/dav/composer/composer/autoload_static.php b/apps/dav/composer/composer/autoload_static.php index 9292731af98..f02d39bd11b 100644 --- a/apps/dav/composer/composer/autoload_static.php +++ b/apps/dav/composer/composer/autoload_static.php @@ -70,6 +70,7 @@ class ComposerStaticInitDAV 'OCA\\DAV\\CalDAV\\CalendarProvider' => __DIR__ . '/..' . '/../lib/CalDAV/CalendarProvider.php', 'OCA\\DAV\\CalDAV\\CalendarRoot' => __DIR__ . '/..' . '/../lib/CalDAV/CalendarRoot.php', 'OCA\\DAV\\CalDAV\\EventComparisonService' => __DIR__ . '/..' . '/../lib/CalDAV/EventComparisonService.php', + 'OCA\\DAV\\CalDAV\\FreeBusy\\FreeBusyGenerator' => __DIR__ . '/..' . '/../lib/CalDAV/FreeBusy/FreeBusyGenerator.php', 'OCA\\DAV\\CalDAV\\ICSExportPlugin\\ICSExportPlugin' => __DIR__ . '/..' . '/../lib/CalDAV/ICSExportPlugin/ICSExportPlugin.php', 'OCA\\DAV\\CalDAV\\IRestorable' => __DIR__ . '/..' . '/../lib/CalDAV/IRestorable.php', 'OCA\\DAV\\CalDAV\\Integration\\ExternalCalendar' => __DIR__ . '/..' . '/../lib/CalDAV/Integration/ExternalCalendar.php', @@ -112,6 +113,8 @@ class ComposerStaticInitDAV 'OCA\\DAV\\CalDAV\\Search\\Xml\\Filter\\PropFilter' => __DIR__ . '/..' . '/../lib/CalDAV/Search/Xml/Filter/PropFilter.php', 'OCA\\DAV\\CalDAV\\Search\\Xml\\Filter\\SearchTermFilter' => __DIR__ . '/..' . '/../lib/CalDAV/Search/Xml/Filter/SearchTermFilter.php', 'OCA\\DAV\\CalDAV\\Search\\Xml\\Request\\CalendarSearchReport' => __DIR__ . '/..' . '/../lib/CalDAV/Search/Xml/Request/CalendarSearchReport.php', + 'OCA\\DAV\\CalDAV\\Status\\Status' => __DIR__ . '/..' . '/../lib/CalDAV/Status/Status.php', + 'OCA\\DAV\\CalDAV\\Status\\StatusService' => __DIR__ . '/..' . '/../lib/CalDAV/Status/StatusService.php', 'OCA\\DAV\\CalDAV\\Trashbin\\DeletedCalendarObject' => __DIR__ . '/..' . '/../lib/CalDAV/Trashbin/DeletedCalendarObject.php', 'OCA\\DAV\\CalDAV\\Trashbin\\DeletedCalendarObjectsCollection' => __DIR__ . '/..' . '/../lib/CalDAV/Trashbin/DeletedCalendarObjectsCollection.php', 'OCA\\DAV\\CalDAV\\Trashbin\\Plugin' => __DIR__ . '/..' . '/../lib/CalDAV/Trashbin/Plugin.php', diff --git a/apps/dav/lib/CalDAV/CalendarImpl.php b/apps/dav/lib/CalDAV/CalendarImpl.php index cc57aa36469..de20c9ac3ae 100644 --- a/apps/dav/lib/CalDAV/CalendarImpl.php +++ b/apps/dav/lib/CalDAV/CalendarImpl.php @@ -33,11 +33,15 @@ use OCA\DAV\CalDAV\InvitationResponse\InvitationResponseServer; use OCP\Calendar\Exceptions\CalendarException; use OCP\Calendar\ICreateFromString; use OCP\Calendar\IHandleImipMessage; +use OCP\Calendar\ISchedulingInformation; use OCP\Constants; +use Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp; use Sabre\DAV\Exception\Conflict; use Sabre\VObject\Component\VCalendar; use Sabre\VObject\Component\VEvent; +use Sabre\VObject\Component\VTimeZone; use Sabre\VObject\ITip\Message; +use Sabre\VObject\Property; use Sabre\VObject\Reader; use function Sabre\Uri\split as uriSplit; @@ -86,6 +90,29 @@ class CalendarImpl implements ICreateFromString, IHandleImipMessage { return $this->calendarInfo['{http://apple.com/ns/ical/}calendar-color']; } + public function getSchedulingTransparency(): ?ScheduleCalendarTransp { + return $this->calendarInfo['{' . \OCA\DAV\CalDAV\Schedule\Plugin::NS_CALDAV . '}schedule-calendar-transp']; + } + + public function getSchedulingTimezone(): ?VTimeZone { + $tzProp = '{' . \OCA\DAV\CalDAV\Schedule\Plugin::NS_CALDAV . '}calendar-timezone'; + if (!isset($this->calendarInfo[$tzProp])) { + return null; + } + // This property contains a VCALENDAR with a single VTIMEZONE + /** @var string $timezoneProp */ + $timezoneProp = $this->calendarInfo[$tzProp]; + /** @var VCalendar $vobj */ + $vobj = Reader::read($timezoneProp); + $components = $vobj->getComponents(); + if(empty($components)) { + return null; + } + /** @var VTimeZone $vtimezone */ + $vtimezone = $components[0]; + return $vtimezone; + } + /** * @param string $pattern which should match within the $searchProperties * @param array $searchProperties defines the properties within the query pattern should match diff --git a/apps/dav/lib/CalDAV/FreeBusy/FreeBusyGenerator.php b/apps/dav/lib/CalDAV/FreeBusy/FreeBusyGenerator.php new file mode 100644 index 00000000000..29daca4e092 --- /dev/null +++ b/apps/dav/lib/CalDAV/FreeBusy/FreeBusyGenerator.php @@ -0,0 +1,44 @@ +<?php +declare(strict_types=1); +/* + * * + * * + * * @copyright 2023 Anna Larch <anna.larch@gmx.net> + * * + * * @author Anna Larch <anna.larch@gmx.net> + * * + * * This library is free software; you can redistribute it and/or + * * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * * License as published by the Free Software Foundation; either + * * version 3 of the License, or any later version. + * * + * * This library 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 along with this library. If not, see <http://www.gnu.org/licenses/>. + * * + * + */ + +namespace OCA\DAV\CalDAV\FreeBusy; + +use DateTimeInterface; +use DateTimeZone; +use Sabre\VObject\Component\VCalendar; + +/** + * @psalm-suppress PropertyNotSetInConstructor + */ +class FreeBusyGenerator extends \Sabre\VObject\FreeBusyGenerator { + + public function __construct() { + parent::__construct(); + } + + public function getVCalendar(): VCalendar { + return new VCalendar(); + } +} diff --git a/apps/dav/lib/CalDAV/Schedule/Plugin.php b/apps/dav/lib/CalDAV/Schedule/Plugin.php index 2845ccdf6c2..16acc72d988 100644 --- a/apps/dav/lib/CalDAV/Schedule/Plugin.php +++ b/apps/dav/lib/CalDAV/Schedule/Plugin.php @@ -36,6 +36,7 @@ use OCA\DAV\CalDAV\CalendarHome; use OCP\IConfig; use Psr\Log\LoggerInterface; use Sabre\CalDAV\ICalendar; +use Sabre\CalDAV\Schedule\IOutbox; use Sabre\DAV\INode; use Sabre\DAV\IProperties; use Sabre\DAV\PropFind; @@ -44,6 +45,7 @@ use Sabre\DAV\Xml\Property\LocalHref; use Sabre\DAVACL\IPrincipal; use Sabre\HTTP\RequestInterface; use Sabre\HTTP\ResponseInterface; +use Sabre\VObject; use Sabre\VObject\Component; use Sabre\VObject\Component\VCalendar; use Sabre\VObject\Component\VEvent; diff --git a/apps/dav/lib/CalDAV/Status/Status.php b/apps/dav/lib/CalDAV/Status/Status.php new file mode 100644 index 00000000000..8857d0f14e7 --- /dev/null +++ b/apps/dav/lib/CalDAV/Status/Status.php @@ -0,0 +1,57 @@ +<?php +/* + * * + * * Dav App + * * + * * @copyright 2023 Anna Larch <anna.larch@gmx.net> + * * + * * @author Anna Larch <anna.larch@gmx.net> + * * + * * This library is free software; you can redistribute it and/or + * * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * * License as published by the Free Software Foundation; either + * * version 3 of the License, or any later version. + * * + * * This library 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 along with this library. If not, see <http://www.gnu.org/licenses/>. + * * + * + */ + +namespace OCA\DAV\CalDAV\Status; + +class Status { + + public function __construct(private string $status = '', private ?string $message = null, private ?string $customMessage = null){} + + public function getStatus(): string { + return $this->status; + } + + public function setStatus(string $status): void { + $this->status = $status; + } + + public function getMessage(): ?string { + return $this->message; + } + + public function setMessage(?string $message): void { + $this->message = $message; + } + + public function getCustomMessage(): ?string { + return $this->customMessage; + } + + public function setCustomMessage(?string $customMessage): void { + $this->customMessage = $customMessage; + } + + +} diff --git a/apps/dav/lib/CalDAV/Status/StatusService.php b/apps/dav/lib/CalDAV/Status/StatusService.php new file mode 100644 index 00000000000..92554f800c3 --- /dev/null +++ b/apps/dav/lib/CalDAV/Status/StatusService.php @@ -0,0 +1,236 @@ +<?php +/* + * * + * * Dav App + * * + * * @copyright 2023 Anna Larch <anna.larch@gmx.net> + * * + * * @author Anna Larch <anna.larch@gmx.net> + * * + * * This library is free software; you can redistribute it and/or + * * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * * License as published by the Free Software Foundation; either + * * version 3 of the License, or any later version. + * * + * * This library 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 along with this library. If not, see <http://www.gnu.org/licenses/>. + * * + * + */ + +declare(strict_types=1); + +/** + * @copyright 2023 Anna Larch <anna.larch@gmx.net> + * + * @author Anna Larch <anna.larch@gmx.net> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ +namespace OCA\DAV\CalDAV\Status; + +use DateTimeZone; +use OC\Calendar\CalendarQuery; +use OCA\DAV\CalDAV\CalendarImpl; +use OCA\DAV\CalDAV\FreeBusy\FreeBusyGenerator; +use OCA\DAV\CalDAV\InvitationResponse\InvitationResponseServer; +use OCA\DAV\CalDAV\IUser; +use OCA\DAV\CalDAV\Schedule\Plugin as SchedulePlugin; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\Calendar\IManager; +use OCP\Calendar\ISchedulingInformation; +use OCP\IL10N; +use OCP\IUser as User; +use OCP\UserStatus\IUserStatus; +use Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp; +use Sabre\DAV\Exception\NotAuthenticated; +use Sabre\DAVACL\Exception\NeedPrivileges; +use Sabre\DAVACL\Plugin as AclPlugin; +use Sabre\VObject\Component; +use Sabre\VObject\Component\VCalendar; +use Sabre\VObject\Component\VEvent; +use Sabre\VObject\Parameter; +use Sabre\VObject\Property; +use Sabre\VObject\Reader; + +class StatusService { + public function __construct(private ITimeFactory $timeFactory, + private IManager $calendarManager, + private InvitationResponseServer $server, + private IL10N $l10n, + private FreeBusyGenerator $generator){} + + public function processCalendarAvailability(User $user, ?string $availability): ?Status { + $userId = $user->getUID(); + $email = $user->getEMailAddress(); + if($email === null) { + return null; + } + + $server = $this->server->getServer(); + + /** @var SchedulePlugin $schedulingPlugin */ + $schedulingPlugin = $server->getPlugin('caldav-schedule'); + $caldavNS = '{'.$schedulingPlugin::NS_CALDAV.'}'; + + /** @var AclPlugin $aclPlugin */ + $aclPlugin = $server->getPlugin('acl'); + if ('mailto:' === substr($email, 0, 7)) { + $email = substr($email, 7); + } + + $result = $aclPlugin->principalSearch( + ['{http://sabredav.org/ns}email-address' => $email], + [ + '{DAV:}principal-URL', + $caldavNS.'calendar-home-set', + $caldavNS.'schedule-inbox-URL', + '{http://sabredav.org/ns}email-address', + ] + ); + + if (!count($result) || !isset($result[0][200][$caldavNS.'schedule-inbox-URL'])) { + return null; + } + + $inboxUrl = $result[0][200][$caldavNS.'schedule-inbox-URL']->getHref(); + + // Do we have permission? + try { + $aclPlugin->checkPrivileges($inboxUrl, $caldavNS.'schedule-query-freebusy'); + } catch (NeedPrivileges | NotAuthenticated $exception) { + return null; + } + + $now = $this->timeFactory->now(); + $calendarTimeZone = $now->getTimezone(); + $calendars = $this->calendarManager->getCalendarsForPrincipal('principals/users/' . $userId); + if(empty($calendars)) { + return null; + } + + $query = $this->calendarManager->newQuery('principals/users/' . $userId); + foreach ($calendars as $calendarObject) { + // We can only work with a calendar if it exposes its scheduling information + if (!$calendarObject instanceof CalendarImpl) { + continue; + } + + $sct = $calendarObject->getSchedulingTransparency(); + if ($sct !== null && ScheduleCalendarTransp::TRANSPARENT == strtolower($sct->getValue())) { + // If a calendar is marked as 'transparent', it means we must + // ignore it for free-busy purposes. + continue; + } + + /** @var Component\VTimeZone|null $ctz */ + $ctz = $calendarObject->getSchedulingTimezone(); + if ($ctz !== null) { + $calendarTimeZone = $ctz->getTimeZone(); + } + $query->addSearchCalendar($calendarObject->getUri()); + } + + $calendarEvents = []; + $dtStart = $now; + $dtEnd = \DateTimeImmutable::createFromMutable($this->timeFactory->getDateTime('+10 minutes')); + + // Only query the calendars when there's any to search + if($query instanceof CalendarQuery && !empty($query->getCalendarUris())) { + // Query the next hour + $query->setTimerangeStart($dtStart); + $query->setTimerangeEnd($dtEnd); + $calendarEvents = $this->calendarManager->searchForPrincipal($query); + } + + // @todo we can cache that + if(empty($availability) && empty($calendarEvents)) { + // No availability settings and no calendar events, we can stop here + return null; + } + + $calendar = $this->generator->getVCalendar(); + foreach ($calendarEvents as $calendarEvent) { + $vEvent = new VEvent($calendar, 'VEVENT'); + foreach($calendarEvent['objects'] as $component) { + foreach ($component as $key => $value) { + $vEvent->add($key, $value[0]); + } + } + $calendar->add($vEvent); + } + + $calendar->METHOD = 'REQUEST'; + + $this->generator->setObjects($calendar); + $this->generator->setTimeRange($dtStart, $dtEnd); + $this->generator->setTimeZone($calendarTimeZone); + + if (!empty($availability)) { + $this->generator->setVAvailability( + Reader::read( + $availability + ) + ); + } + // Generate the intersection of VAVILABILITY and all VEVENTS in all calendars + $result = $this->generator->getResult(); + + if (!isset($result->VFREEBUSY)) { + return null; + } + + /** @var Component $freeBusyComponent */ + $freeBusyComponent = $result->VFREEBUSY; + $freeBusyProperties = $freeBusyComponent->select('FREEBUSY'); + // If there is no FreeBusy property, the time-range is empty and available + // so set the status to online as otherwise we will never recover from a BUSY status + if (count($freeBusyProperties) === 0) { + return new Status(IUserStatus::ONLINE); + } + + /** @var Property $freeBusyProperty */ + $freeBusyProperty = $freeBusyProperties[0]; + if (!$freeBusyProperty->offsetExists('FBTYPE')) { + // If there is no FBTYPE, it means it's busy from a regular event + return new Status(IUserStatus::BUSY, IUserStatus::MESSAGE_CALENDAR_BUSY); + } + + // If we can't deal with the FBTYPE (custom properties are a possibility) + // we should ignore it and leave the current status + $fbTypeParameter = $freeBusyProperty->offsetGet('FBTYPE'); + if (!($fbTypeParameter instanceof Parameter)) { + return null; + } + $fbType = $fbTypeParameter->getValue(); + switch ($fbType) { + case 'BUSY': + return new Status(IUserStatus::BUSY, IUserStatus::MESSAGE_CALENDAR_BUSY, $this->l10n->t('In a meeting')); + case 'BUSY-UNAVAILABLE': + return new Status(IUserStatus::AWAY, IUserStatus::MESSAGE_AVAILABILITY); + case 'BUSY-TENTATIVE': + return new Status(IUserStatus::AWAY, IUserStatus::MESSAGE_CALENDAR_BUSY_TENTATIVE); + default: + return null; + } + } +} diff --git a/apps/dav/tests/unit/CalDAV/Status/StatusServiceTest.php b/apps/dav/tests/unit/CalDAV/Status/StatusServiceTest.php new file mode 100644 index 00000000000..450eac9ee25 --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/Status/StatusServiceTest.php @@ -0,0 +1,1508 @@ +<?php +/** + * @copyright 2023 Anna Larch <anna.larch@gmx.net> + * + * @author Anna Larch <anna.larch@gmx.net> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ +namespace OCA\DAV\Tests\unit\CalDAV\Status; + +use OC\Calendar\CalendarQuery; +use OCA\DAV\CalDAV\CalendarImpl; +use OCA\DAV\CalDAV\FreeBusy\FreeBusyGenerator; +use OCA\DAV\CalDAV\InvitationResponse\InvitationResponseServer; +use OCA\DAV\CalDAV\Schedule\Plugin; +use OCA\DAV\CalDAV\Status\Status; +use OCA\DAV\CalDAV\Status\StatusService; +use OCA\DAV\Connector\Sabre\Server; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\Calendar\IManager; +use OCP\IL10N; +use OCP\IUser; +use OCP\UserStatus\IUserStatus; +use PHPUnit\Framework\MockObject\MockObject; +use Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp; +use Sabre\DAV\Exception\NotAuthenticated; +use Sabre\DAV\Xml\Property\LocalHref; +use Sabre\DAVACL\Exception\NeedPrivileges; +use Sabre\VObject\Component\VCalendar; +use Sabre\VObject\Component\VTimeZone; +use Sabre\VObject\Document; +use Sabre\VObject\Reader; +use Test\TestCase; + +class StatusServiceTest extends TestCase { + private ITimeFactory|MockObject $timeFactory; + private IManager|MockObject $calendarManager; + private InvitationResponseServer|MockObject $server; + private IL10N|MockObject $l10n; + private FreeBusyGenerator|MockObject $generator; + + protected function setUp(): void { + parent::setUp(); + + $this->timeFactory = $this->createMock(ITimeFactory::class); + $this->calendarManager = $this->createMock(IManager::class); + $this->server = $this->createMock(InvitationResponseServer::class); + $this->l10n = $this->createMock(IL10N::class); + $this->generator = $this->createMock(FreeBusyGenerator::class); + + $this->service = new StatusService($this->timeFactory, + $this->calendarManager, + $this->server, + $this->l10n, + $this->generator); + } + + public function testNoEmail(): void { + $user = $this->createConfiguredMock(IUser::class, [ + 'getUID' => 'admin', + 'getEMailAddress' => null, + ]); + $availability = ''; + + $user->expects(self::once()) + ->method('getUID') + ->willReturn('admin'); + $user->expects(self::once()) + ->method('getEMailAddress') + ->willReturn(null); + $this->server->expects(self::never()) + ->method('getServer'); + $this->timeFactory->expects(self::never()) + ->method('now'); + $this->timeFactory->expects(self::never()) + ->method('getDateTime'); + $this->calendarManager->expects(self::never()) + ->method('getCalendarsForPrincipal'); + $this->calendarManager->expects(self::never()) + ->method('newQuery'); + $this->calendarManager->expects(self::never()) + ->method('searchForPrincipal'); + $this->generator->expects(self::never()) + ->method('getVCalendar'); + $this->generator->expects(self::never()) + ->method('setObjects'); + $this->generator->expects(self::never()) + ->method('setTimeRange'); + $this->generator->expects(self::never()) + ->method('setTimeZone'); + $this->generator->expects(self::never()) + ->method('setVAvailability'); + $this->generator->expects(self::never()) + ->method('getResult'); + + $status = $this->service->processCalendarAvailability($user, $availability); + $this->assertNull($status); + } + + public function testNoAcl(): void { + $user = $this->createConfiguredMock(IUser::class, [ + 'getUID' => 'admin', + 'getEMailAddress' => 'test@test.com', + ]); + $availability = ''; + $server = $this->createMock(Server::class); + $schedulingPlugin = $this->createMock(Plugin::class); + $aclPlugin = $this->createMock(\Sabre\DAVACL\Plugin::class); + + $user->expects(self::once()) + ->method('getUID') + ->willReturn('admin'); + $user->expects(self::once()) + ->method('getEMailAddress') + ->willReturn('test@test.com'); + $this->server->expects(self::once()) + ->method('getServer') + ->willReturn($server); + $server->expects(self::exactly(2)) + ->method('getPlugin') + ->withConsecutive( + ['caldav-schedule'], + ['acl'], + )->willReturnOnConsecutiveCalls($schedulingPlugin, $aclPlugin); + $aclPlugin->expects(self::once()) + ->method('principalSearch') + ->with([ '{http://sabredav.org/ns}email-address' => 'test@test.com']) + ->willReturn([]); + $aclPlugin->expects(self::never()) + ->method('checkPrivileges'); + $this->timeFactory->expects(self::never()) + ->method('now'); + $this->timeFactory->expects(self::never()) + ->method('getDateTime'); + $this->calendarManager->expects(self::never()) + ->method('getCalendarsForPrincipal'); + $this->calendarManager->expects(self::never()) + ->method('newQuery'); + $this->calendarManager->expects(self::never()) + ->method('searchForPrincipal'); + $this->generator->expects(self::never()) + ->method('getVCalendar'); + $this->generator->expects(self::never()) + ->method('setObjects'); + $this->generator->expects(self::never()) + ->method('setTimeRange'); + $this->generator->expects(self::never()) + ->method('setTimeZone'); + $this->generator->expects(self::never()) + ->method('setVAvailability'); + $this->generator->expects(self::never()) + ->method('getResult'); + + $status = $this->service->processCalendarAvailability($user, $availability); + $this->assertNull($status); + } + + public function testNoInbox(): void { + $user = $this->createConfiguredMock(IUser::class, [ + 'getUID' => 'admin', + 'getEMailAddress' => 'test@test.com', + ]); + $availability = ''; + $server = $this->createMock(Server::class); + $schedulingPlugin = $this->createMock(Plugin::class); + $aclPlugin = $this->createMock(\Sabre\DAVACL\Plugin::class); + + $user->expects(self::once()) + ->method('getUID') + ->willReturn('admin'); + $user->expects(self::once()) + ->method('getEMailAddress') + ->willReturn('test@test.com'); + $this->server->expects(self::once()) + ->method('getServer') + ->willReturn($server); + $server->expects(self::exactly(2)) + ->method('getPlugin') + ->withConsecutive( + ['caldav-schedule'], + ['acl'], + )->willReturnOnConsecutiveCalls($schedulingPlugin, $aclPlugin); + $aclPlugin->expects(self::once()) + ->method('principalSearch') + ->with([ '{http://sabredav.org/ns}email-address' => 'test@test.com']) + ->willReturn([]); + $aclPlugin->expects(self::never()) + ->method('checkPrivileges'); + $this->timeFactory->expects(self::never()) + ->method('now'); + $this->timeFactory->expects(self::never()) + ->method('getDateTime'); + $this->calendarManager->expects(self::never()) + ->method('getCalendarsForPrincipal'); + $this->calendarManager->expects(self::never()) + ->method('newQuery'); + $this->calendarManager->expects(self::never()) + ->method('searchForPrincipal'); + $this->generator->expects(self::never()) + ->method('getVCalendar'); + $this->generator->expects(self::never()) + ->method('setObjects'); + $this->generator->expects(self::never()) + ->method('setTimeRange'); + $this->generator->expects(self::never()) + ->method('setTimeZone'); + $this->generator->expects(self::never()) + ->method('setVAvailability'); + $this->generator->expects(self::never()) + ->method('getResult'); + + $status = $this->service->processCalendarAvailability($user, $availability); + $this->assertNull($status); + } + + public function testNoPrivilegesAcl(): void { + $user = $this->createConfiguredMock(IUser::class, [ + 'getUID' => 'admin', + 'getEMailAddress' => 'test@test.com', + ]); + $availability = ''; + $server = $this->createMock(Server::class); + $schedulingPlugin = $this->createMock(Plugin::class); + $aclPlugin = $this->createMock(\Sabre\DAVACL\Plugin::class); + $principal = 'principals/users/admin'; + $calendarHome = $this->createMock(LocalHref::class); + $acl = [[200 => ['{urn:ietf:params:xml:ns:caldav}schedule-inbox-URL' => $calendarHome]]]; + + $user->expects(self::once()) + ->method('getUID') + ->willReturn('admin'); + $user->expects(self::once()) + ->method('getEMailAddress') + ->willReturn('test@test.com'); + $this->server->expects(self::once()) + ->method('getServer') + ->willReturn($server); + $server->expects(self::exactly(2)) + ->method('getPlugin') + ->withConsecutive( + ['caldav-schedule'], + ['acl'], + )->willReturnOnConsecutiveCalls($schedulingPlugin, $aclPlugin); + $aclPlugin->expects(self::once()) + ->method('principalSearch') + ->with([ '{http://sabredav.org/ns}email-address' => 'test@test.com']) + ->willReturn($acl); + $calendarHome->expects(self::once()) + ->method('getHref') + ->willReturn('calendars/admin/inbox/'); + $aclPlugin->expects(self::once()) + ->method('checkPrivileges') + ->willThrowException(new NeedPrivileges($principal, ['{DAV:}all'])); + $this->timeFactory->expects(self::never()) + ->method('now'); + $this->timeFactory->expects(self::never()) + ->method('getDateTime'); + $this->calendarManager->expects(self::never()) + ->method('getCalendarsForPrincipal'); + $this->calendarManager->expects(self::never()) + ->method('newQuery'); + $this->calendarManager->expects(self::never()) + ->method('searchForPrincipal'); + $this->generator->expects(self::never()) + ->method('getVCalendar'); + $this->generator->expects(self::never()) + ->method('setObjects'); + $this->generator->expects(self::never()) + ->method('setTimeRange'); + $this->generator->expects(self::never()) + ->method('setTimeZone'); + $this->generator->expects(self::never()) + ->method('setVAvailability'); + $this->generator->expects(self::never()) + ->method('getResult'); + + $status = $this->service->processCalendarAvailability($user, $availability); + $this->assertNull($status); + } + + public function testNotAuthenticated(): void { + $user = $this->createConfiguredMock(IUser::class, [ + 'getUID' => 'admin', + 'getEMailAddress' => 'test@test.com', + ]); + $availability = ''; + $server = $this->createMock(Server::class); + $schedulingPlugin = $this->createMock(Plugin::class); + $aclPlugin = $this->createMock(\Sabre\DAVACL\Plugin::class); + $calendarHome = $this->createMock(LocalHref::class); + $acl = [[200 => ['{urn:ietf:params:xml:ns:caldav}schedule-inbox-URL' => $calendarHome]]]; + + $user->expects(self::once()) + ->method('getUID') + ->willReturn('admin'); + $user->expects(self::once()) + ->method('getEMailAddress') + ->willReturn('test@test.com'); + $this->server->expects(self::once()) + ->method('getServer') + ->willReturn($server); + $server->expects(self::exactly(2)) + ->method('getPlugin') + ->withConsecutive( + ['caldav-schedule'], + ['acl'], + )->willReturnOnConsecutiveCalls($schedulingPlugin, $aclPlugin); + $aclPlugin->expects(self::once()) + ->method('principalSearch') + ->with([ '{http://sabredav.org/ns}email-address' => 'test@test.com']) + ->willReturn($acl); + $calendarHome->expects(self::once()) + ->method('getHref') + ->willReturn('calendars/admin/inbox/'); + $aclPlugin->expects(self::once()) + ->method('checkPrivileges') + ->willThrowException(new NotAuthenticated()); + $this->timeFactory->expects(self::never()) + ->method('now'); + $this->timeFactory->expects(self::never()) + ->method('getDateTime'); + $this->calendarManager->expects(self::never()) + ->method('getCalendarsForPrincipal'); + $this->calendarManager->expects(self::never()) + ->method('newQuery'); + $this->calendarManager->expects(self::never()) + ->method('searchForPrincipal'); + $this->generator->expects(self::never()) + ->method('getVCalendar'); + $this->generator->expects(self::never()) + ->method('setObjects'); + $this->generator->expects(self::never()) + ->method('setTimeRange'); + $this->generator->expects(self::never()) + ->method('setTimeZone'); + $this->generator->expects(self::never()) + ->method('setVAvailability'); + $this->generator->expects(self::never()) + ->method('getResult'); + + $status = $this->service->processCalendarAvailability($user, $availability); + $this->assertNull($status); + } + + public function testNoCalendars(): void { + $user = $this->createConfiguredMock(IUser::class, [ + 'getUID' => 'admin', + 'getEMailAddress' => 'test@test.com', + ]); + $availability = ''; + $server = $this->createMock(Server::class); + $schedulingPlugin = $this->createMock(Plugin::class); + $aclPlugin = $this->createMock(\Sabre\DAVACL\Plugin::class); + $calendarHome = $this->createMock(LocalHref::class); + $acl = [[200 => ['{urn:ietf:params:xml:ns:caldav}schedule-inbox-URL' => $calendarHome]]]; + $now = new \DateTimeImmutable('1970-1-1', new \DateTimeZone('UTC')); + $principal = 'principals/users/admin'; + + $user->expects(self::once()) + ->method('getUID') + ->willReturn('admin'); + $user->expects(self::once()) + ->method('getEMailAddress') + ->willReturn('test@test.com'); + $this->server->expects(self::once()) + ->method('getServer') + ->willReturn($server); + $server->expects(self::exactly(2)) + ->method('getPlugin') + ->withConsecutive( + ['caldav-schedule'], + ['acl'], + )->willReturnOnConsecutiveCalls($schedulingPlugin, $aclPlugin); + $aclPlugin->expects(self::once()) + ->method('principalSearch') + ->with([ '{http://sabredav.org/ns}email-address' => 'test@test.com']) + ->willReturn($acl); + $calendarHome->expects(self::once()) + ->method('getHref') + ->willReturn('calendars/admin/inbox/'); + $aclPlugin->expects(self::once()) + ->method('checkPrivileges') + ->willReturn(true); + $this->timeFactory->expects(self::once()) + ->method('now') + ->willReturn($now); + $this->calendarManager->expects(self::once()) + ->method('getCalendarsForPrincipal') + ->with($principal) + ->willReturn([]); + $this->timeFactory->expects(self::never()) + ->method('getDateTime'); + $this->calendarManager->expects(self::never()) + ->method('newQuery'); + $this->calendarManager->expects(self::never()) + ->method('searchForPrincipal'); + $this->generator->expects(self::never()) + ->method('getVCalendar'); + $this->generator->expects(self::never()) + ->method('setObjects'); + $this->generator->expects(self::never()) + ->method('setTimeRange'); + $this->generator->expects(self::never()) + ->method('setTimeZone'); + $this->generator->expects(self::never()) + ->method('setVAvailability'); + $this->generator->expects(self::never()) + ->method('getResult'); + + $status = $this->service->processCalendarAvailability($user, $availability); + $this->assertNull($status); + } + + public function testEmptyAvailabilityAndNoSearchCalendars(): void { + $user = $this->createConfiguredMock(IUser::class, [ + 'getUID' => 'admin', + 'getEMailAddress' => 'test@test.com', + ]); + $availability = ''; + $server = $this->createMock(Server::class); + $schedulingPlugin = $this->createMock(Plugin::class); + $aclPlugin = $this->createMock(\Sabre\DAVACL\Plugin::class); + $calendarHome = $this->createMock(LocalHref::class); + $now = new \DateTimeImmutable('1970-1-1', new \DateTimeZone('UTC')); + $inTenMinutes = new \DateTime('1970-1-1 01:00'); + $acl = [[200 => ['{urn:ietf:params:xml:ns:caldav}schedule-inbox-URL' => $calendarHome]]]; + $principal = 'principals/users/admin'; + $calendar = $this->createMock(CalendarImpl::class); + $query = $this->createMock(CalendarQuery::class); + + $user->expects(self::once()) + ->method('getUID') + ->willReturn('admin'); + $user->expects(self::once()) + ->method('getEMailAddress') + ->willReturn('test@test.com'); + $this->server->expects(self::once()) + ->method('getServer') + ->willReturn($server); + $server->expects(self::exactly(2)) + ->method('getPlugin') + ->withConsecutive( + ['caldav-schedule'], + ['acl'], + )->willReturnOnConsecutiveCalls($schedulingPlugin, $aclPlugin); + $aclPlugin->expects(self::once()) + ->method('principalSearch') + ->with([ '{http://sabredav.org/ns}email-address' => 'test@test.com']) + ->willReturn($acl); + $calendarHome->expects(self::once()) + ->method('getHref') + ->willReturn('calendars/admin/inbox/'); + $aclPlugin->expects(self::once()) + ->method('checkPrivileges') + ->willReturn(true); + $this->timeFactory->expects(self::once()) + ->method('now') + ->willReturn($now); + $this->calendarManager->expects(self::once()) + ->method('getCalendarsForPrincipal') + ->with($principal) + ->willReturn([$calendar]); + $this->calendarManager->expects(self::once()) + ->method('newQuery') + ->with($principal) + ->willReturn($query); + $calendar->expects(self::once()) + ->method('getSchedulingTransparency') + ->willReturn(new ScheduleCalendarTransp('transparent')); + $this->timeFactory->expects(self::once()) + ->method('getDateTime') + ->with('+10 minutes') + ->willReturn($inTenMinutes); + $this->calendarManager->expects(self::never()) + ->method('searchForPrincipal'); + $this->generator->expects(self::never()) + ->method('getVCalendar'); + $this->generator->expects(self::never()) + ->method('setObjects'); + $this->generator->expects(self::never()) + ->method('setTimeRange'); + $this->generator->expects(self::never()) + ->method('setTimeZone'); + $this->generator->expects(self::never()) + ->method('setVAvailability'); + $this->generator->expects(self::never()) + ->method('getResult'); + + $status = $this->service->processCalendarAvailability($user, $availability); + $this->assertNull($status); + } + + public function testEmptyAvailabilityAndSearchCalendarsNoResults(): void { + $user = $this->createConfiguredMock(IUser::class, [ + 'getUID' => 'admin', + 'getEMailAddress' => 'test@test.com', + ]); + $availability = ''; + $server = $this->createMock(Server::class); + $schedulingPlugin = $this->createMock(Plugin::class); + $aclPlugin = $this->createMock(\Sabre\DAVACL\Plugin::class); + $calendarHome = $this->createMock(LocalHref::class); + $acl = [[200 => ['{urn:ietf:params:xml:ns:caldav}schedule-inbox-URL' => $calendarHome]]]; + $now = new \DateTimeImmutable('1970-1-1 00:00', new \DateTimeZone('UTC')); + $inTenMinutes = new \DateTime('1970-1-1 01:00'); + $immutableInTenMinutes = \DateTimeImmutable::createFromMutable($inTenMinutes); + $principal = 'principals/users/admin'; + $query = $this->createMock(CalendarQuery::class); + $timezone = new \DateTimeZone('UTC'); + $timezoneObj = $this->createMock(VTimeZone::class); + $calendar = $this->createMock(CalendarImpl::class); + + $user->expects(self::once()) + ->method('getUID') + ->willReturn('admin'); + $user->expects(self::once()) + ->method('getEMailAddress') + ->willReturn('test@test.com'); + $this->server->expects(self::once()) + ->method('getServer') + ->willReturn($server); + $server->expects(self::exactly(2)) + ->method('getPlugin') + ->withConsecutive( + ['caldav-schedule'], + ['acl'], + )->willReturnOnConsecutiveCalls($schedulingPlugin, $aclPlugin); + $aclPlugin->expects(self::once()) + ->method('principalSearch') + ->with(['{http://sabredav.org/ns}email-address' => 'test@test.com']) + ->willReturn($acl); + $calendarHome->expects(self::once()) + ->method('getHref') + ->willReturn('calendars/admin/inbox/'); + $aclPlugin->expects(self::once()) + ->method('checkPrivileges') + ->willReturn(true); + $this->timeFactory->expects(self::once()) + ->method('now') + ->willReturn($now); + $this->calendarManager->expects(self::once()) + ->method('getCalendarsForPrincipal') + ->with($principal) + ->willReturn([$calendar]); + $this->calendarManager->expects(self::once()) + ->method('newQuery') + ->with($principal) + ->willReturn($query); + $calendar->expects(self::once()) + ->method('getSchedulingTransparency') + ->willReturn(new ScheduleCalendarTransp('opaque')); + $calendar->expects(self::once()) + ->method('getSchedulingTimezone') + ->willReturn($timezoneObj); + $timezoneObj->expects(self::once()) + ->method('getTimeZone') + ->willReturn($timezone); + $calendar->expects(self::once()) + ->method('getUri'); + $query->expects(self::once()) + ->method('addSearchCalendar'); + $query->expects(self::once()) + ->method('getCalendarUris') + ->willReturn([$calendar]); + $this->timeFactory->expects(self::once()) + ->method('getDateTime') + ->with('+10 minutes') + ->willReturn($inTenMinutes); + $query->expects(self::once()) + ->method('setTimerangeStart') + ->with($now); + $query->expects(self::once()) + ->method('setTimerangeEnd') + ->with($immutableInTenMinutes); + $this->calendarManager->expects(self::once()) + ->method('searchForPrincipal') + ->with($query) + ->willReturn([]); + $this->generator->expects(self::never()) + ->method('getVCalendar'); + $this->generator->expects(self::never()) + ->method('setObjects'); + $this->generator->expects(self::never()) + ->method('setTimeRange'); + $this->generator->expects(self::never()) + ->method('setTimeZone'); + $this->generator->expects(self::never()) + ->method('setVAvailability'); + $this->generator->expects(self::never()) + ->method('getResult'); + + $status = $this->service->processCalendarAvailability($user, $availability); + $this->assertNull($status); + } + + public function testAvailabilityAndSearchCalendarsNoResults(): void { + $user = $this->createConfiguredMock(IUser::class, [ + 'getUID' => 'admin', + 'getEMailAddress' => 'test@test.com', + ]); + $server = $this->createMock(Server::class); + $schedulingPlugin = $this->createMock(Plugin::class); + $aclPlugin = $this->createMock(\Sabre\DAVACL\Plugin::class); + $calendarHome = $this->createMock(LocalHref::class); + $acl = [[200 => ['{urn:ietf:params:xml:ns:caldav}schedule-inbox-URL' => $calendarHome]]]; + $now = new \DateTimeImmutable('1970-1-1 00:00', new \DateTimeZone('UTC')); + $inTenMinutes = new \DateTime('1970-1-1 01:00'); + $immutableInTenMinutes = \DateTimeImmutable::createFromMutable($inTenMinutes); + $principal = 'principals/users/admin'; + $query = $this->createMock(CalendarQuery::class); + $timezone = new \DateTimeZone('UTC'); + $timezoneObj = $this->createMock(VTimeZone::class); + $calendar = $this->createMock(CalendarImpl::class); + $vCalendar = $this->createMock(VCalendar::class); + $availability = $this->getVAvailability(); + $result = Reader::read('BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject 4.5.3//EN + CALSCALE:GREGORIAN +METHOD:REQUEST +END:VCALENDAR'); + + $user->expects(self::once()) + ->method('getUID') + ->willReturn('admin'); + $user->expects(self::once()) + ->method('getEMailAddress') + ->willReturn('test@test.com'); + $this->server->expects(self::once()) + ->method('getServer') + ->willReturn($server); + $server->expects(self::exactly(2)) + ->method('getPlugin') + ->withConsecutive( + ['caldav-schedule'], + ['acl'], + )->willReturnOnConsecutiveCalls($schedulingPlugin, $aclPlugin); + $aclPlugin->expects(self::once()) + ->method('principalSearch') + ->with(['{http://sabredav.org/ns}email-address' => 'test@test.com']) + ->willReturn($acl); + $calendarHome->expects(self::once()) + ->method('getHref') + ->willReturn('calendars/admin/inbox/'); + $aclPlugin->expects(self::once()) + ->method('checkPrivileges') + ->willReturn(true); + $this->timeFactory->expects(self::once()) + ->method('now') + ->willReturn($now); + $this->calendarManager->expects(self::once()) + ->method('getCalendarsForPrincipal') + ->with($principal) + ->willReturn([$calendar]); + $this->calendarManager->expects(self::once()) + ->method('newQuery') + ->with($principal) + ->willReturn($query); + $calendar->expects(self::once()) + ->method('getSchedulingTransparency') + ->willReturn(new ScheduleCalendarTransp('opaque')); + $calendar->expects(self::once()) + ->method('getSchedulingTimezone') + ->willReturn($timezoneObj); + $timezoneObj->expects(self::once()) + ->method('getTimeZone') + ->willReturn($timezone); + $calendar->expects(self::once()) + ->method('getUri'); + $query->expects(self::once()) + ->method('addSearchCalendar'); + $query->expects(self::once()) + ->method('getCalendarUris') + ->willReturn([$calendar]); + $this->timeFactory->expects(self::once()) + ->method('getDateTime') + ->with('+10 minutes') + ->willReturn($inTenMinutes); + $query->expects(self::once()) + ->method('setTimerangeStart') + ->with($now); + $query->expects(self::once()) + ->method('setTimerangeEnd') + ->with($immutableInTenMinutes); + $this->calendarManager->expects(self::once()) + ->method('searchForPrincipal') + ->with($query) + ->willReturn([]); + $this->generator->expects(self::once()) + ->method('getVCalendar') + ->willReturn($vCalendar); + $vCalendar->expects(self::never()) + ->method('add'); + $this->generator->expects(self::once()) + ->method('setObjects') + ->with($vCalendar); + $this->generator->expects(self::once()) + ->method('setTimeRange') + ->with($now, $immutableInTenMinutes); + $this->generator->expects(self::once()) + ->method('setTimeZone') + ->with($timezone); + $this->generator->expects(self::once()) + ->method('setVAvailability') + ->with($availability); + $this->generator->expects(self::once()) + ->method('getResult') + ->willReturn($result); + + $status = $this->service->processCalendarAvailability($user, $availability->serialize()); + $this->assertNull($status); + } + + public function testAvailabilityAndSearchCalendarsStatusOnline(): void { + $user = $this->createConfiguredMock(IUser::class, [ + 'getUID' => 'admin', + 'getEMailAddress' => 'test@test.com', + ]); + $server = $this->createMock(Server::class); + $schedulingPlugin = $this->createMock(Plugin::class); + $aclPlugin = $this->createMock(\Sabre\DAVACL\Plugin::class); + $calendarHome = $this->createMock(LocalHref::class); + $acl = [[200 => ['{urn:ietf:params:xml:ns:caldav}schedule-inbox-URL' => $calendarHome]]]; + $now = new \DateTimeImmutable('1970-1-1 00:00', new \DateTimeZone('UTC')); + $inTenMinutes = new \DateTime('1970-1-1 01:00'); + $immutableInTenMinutes = \DateTimeImmutable::createFromMutable($inTenMinutes); + $principal = 'principals/users/admin'; + $query = $this->createMock(CalendarQuery::class); + $timezone = new \DateTimeZone('UTC'); + $timezoneObj = $this->createMock(VTimeZone::class); + $calendar = $this->createMock(CalendarImpl::class); + $vCalendar = $this->createMock(VCalendar::class); + $availability = $this->getVAvailability(); + $result = Reader::read('BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject 4.5.3//EN + CALSCALE:GREGORIAN +METHOD:REQUEST +BEGIN:VFREEBUSY +DTSTART:19700101T000000Z +DTEND:19700101T003600Z +DTSTAMP:19700101T000200Z +END:VFREEBUSY +END:VCALENDAR'); + + $user->expects(self::once()) + ->method('getUID') + ->willReturn('admin'); + $user->expects(self::once()) + ->method('getEMailAddress') + ->willReturn('test@test.com'); + $this->server->expects(self::once()) + ->method('getServer') + ->willReturn($server); + $server->expects(self::exactly(2)) + ->method('getPlugin') + ->withConsecutive( + ['caldav-schedule'], + ['acl'], + )->willReturnOnConsecutiveCalls($schedulingPlugin, $aclPlugin); + $aclPlugin->expects(self::once()) + ->method('principalSearch') + ->with(['{http://sabredav.org/ns}email-address' => 'test@test.com']) + ->willReturn($acl); + $calendarHome->expects(self::once()) + ->method('getHref') + ->willReturn('calendars/admin/inbox/'); + $aclPlugin->expects(self::once()) + ->method('checkPrivileges') + ->willReturn(true); + $this->timeFactory->expects(self::once()) + ->method('now') + ->willReturn($now); + $this->calendarManager->expects(self::once()) + ->method('getCalendarsForPrincipal') + ->with($principal) + ->willReturn([$calendar]); + $this->calendarManager->expects(self::once()) + ->method('newQuery') + ->with($principal) + ->willReturn($query); + $calendar->expects(self::once()) + ->method('getSchedulingTransparency') + ->willReturn(new ScheduleCalendarTransp('opaque')); + $calendar->expects(self::once()) + ->method('getSchedulingTimezone') + ->willReturn($timezoneObj); + $timezoneObj->expects(self::once()) + ->method('getTimeZone') + ->willReturn($timezone); + $calendar->expects(self::once()) + ->method('getUri'); + $query->expects(self::once()) + ->method('addSearchCalendar'); + $query->expects(self::once()) + ->method('getCalendarUris') + ->willReturn([$calendar]); + $this->timeFactory->expects(self::once()) + ->method('getDateTime') + ->with('+10 minutes') + ->willReturn($inTenMinutes); + $query->expects(self::once()) + ->method('setTimerangeStart') + ->with($now); + $query->expects(self::once()) + ->method('setTimerangeEnd') + ->with($immutableInTenMinutes); + $this->calendarManager->expects(self::once()) + ->method('searchForPrincipal') + ->with($query) + ->willReturn([]); + $this->generator->expects(self::once()) + ->method('getVCalendar') + ->willReturn($vCalendar); + $vCalendar->expects(self::never()) + ->method('add'); + $this->generator->expects(self::once()) + ->method('setObjects') + ->with($vCalendar); + $this->generator->expects(self::once()) + ->method('setTimeRange') + ->with($now, $immutableInTenMinutes); + $this->generator->expects(self::once()) + ->method('setTimeZone') + ->with($timezone); + $this->generator->expects(self::once()) + ->method('setVAvailability') + ->with($availability); + $this->generator->expects(self::once()) + ->method('getResult') + ->willReturn($result); + + $status = $this->service->processCalendarAvailability($user, $availability->serialize()); + $this->assertEquals(new Status(IUserStatus::ONLINE), $status); + } + + public function testAvailabilityAndSearchCalendarsStatusBusyNoFBType(): void { + $user = $this->createConfiguredMock(IUser::class, [ + 'getUID' => 'admin', + 'getEMailAddress' => 'test@test.com', + ]); + $server = $this->createMock(Server::class); + $schedulingPlugin = $this->createMock(Plugin::class); + $aclPlugin = $this->createMock(\Sabre\DAVACL\Plugin::class); + $calendarHome = $this->createMock(LocalHref::class); + $acl = [[200 => ['{urn:ietf:params:xml:ns:caldav}schedule-inbox-URL' => $calendarHome]]]; + $now = new \DateTimeImmutable('1970-1-1 00:00', new \DateTimeZone('UTC')); + $inTenMinutes = new \DateTime('1970-1-1 01:00'); + $immutableInTenMinutes = \DateTimeImmutable::createFromMutable($inTenMinutes); + $principal = 'principals/users/admin'; + $query = $this->createMock(CalendarQuery::class); + $timezone = new \DateTimeZone('UTC'); + $timezoneObj = $this->createMock(VTimeZone::class); + $calendar = $this->createMock(CalendarImpl::class); + $vCalendar = $this->createMock(VCalendar::class); + $availability = $this->getVAvailability(); + $result = Reader::read('BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject 4.5.3//EN + CALSCALE:GREGORIAN +METHOD:REQUEST +BEGIN:VFREEBUSY +DTSTART:19700101T000000Z +DTEND:19700101T003600Z +DTSTAMP:19700101T000200Z +FREEBUSY:19700101T000000Z/19700101T003600Z +END:VFREEBUSY +END:VCALENDAR'); + + $user->expects(self::once()) + ->method('getUID') + ->willReturn('admin'); + $user->expects(self::once()) + ->method('getEMailAddress') + ->willReturn('test@test.com'); + $this->server->expects(self::once()) + ->method('getServer') + ->willReturn($server); + $server->expects(self::exactly(2)) + ->method('getPlugin') + ->withConsecutive( + ['caldav-schedule'], + ['acl'], + )->willReturnOnConsecutiveCalls($schedulingPlugin, $aclPlugin); + $aclPlugin->expects(self::once()) + ->method('principalSearch') + ->with(['{http://sabredav.org/ns}email-address' => 'test@test.com']) + ->willReturn($acl); + $calendarHome->expects(self::once()) + ->method('getHref') + ->willReturn('calendars/admin/inbox/'); + $aclPlugin->expects(self::once()) + ->method('checkPrivileges') + ->willReturn(true); + $this->timeFactory->expects(self::once()) + ->method('now') + ->willReturn($now); + $this->calendarManager->expects(self::once()) + ->method('getCalendarsForPrincipal') + ->with($principal) + ->willReturn([$calendar]); + $this->calendarManager->expects(self::once()) + ->method('newQuery') + ->with($principal) + ->willReturn($query); + $calendar->expects(self::once()) + ->method('getSchedulingTransparency') + ->willReturn(new ScheduleCalendarTransp('opaque')); + $calendar->expects(self::once()) + ->method('getSchedulingTimezone') + ->willReturn($timezoneObj); + $timezoneObj->expects(self::once()) + ->method('getTimeZone') + ->willReturn($timezone); + $calendar->expects(self::once()) + ->method('getUri'); + $query->expects(self::once()) + ->method('addSearchCalendar'); + $query->expects(self::once()) + ->method('getCalendarUris') + ->willReturn([$calendar]); + $this->timeFactory->expects(self::once()) + ->method('getDateTime') + ->with('+10 minutes') + ->willReturn($inTenMinutes); + $query->expects(self::once()) + ->method('setTimerangeStart') + ->with($now); + $query->expects(self::once()) + ->method('setTimerangeEnd') + ->with($immutableInTenMinutes); + $this->calendarManager->expects(self::once()) + ->method('searchForPrincipal') + ->with($query) + ->willReturn([]); + $this->generator->expects(self::once()) + ->method('getVCalendar') + ->willReturn($vCalendar); + $vCalendar->expects(self::never()) + ->method('add'); + $this->generator->expects(self::once()) + ->method('setObjects') + ->with($vCalendar); + $this->generator->expects(self::once()) + ->method('setTimeRange') + ->with($now, $immutableInTenMinutes); + $this->generator->expects(self::once()) + ->method('setTimeZone') + ->with($timezone); + $this->generator->expects(self::once()) + ->method('setVAvailability') + ->with($availability); + $this->generator->expects(self::once()) + ->method('getResult') + ->willReturn($result); + + $status = $this->service->processCalendarAvailability($user, $availability->serialize()); + $this->assertEquals(new Status(IUserStatus::BUSY, IUserStatus::MESSAGE_CALENDAR_BUSY), $status); + } + + public function testAvailabilityAndSearchCalendarsStatusBusy(): void { + $user = $this->createConfiguredMock(IUser::class, [ + 'getUID' => 'admin', + 'getEMailAddress' => 'test@test.com', + ]); + $server = $this->createMock(Server::class); + $schedulingPlugin = $this->createMock(Plugin::class); + $aclPlugin = $this->createMock(\Sabre\DAVACL\Plugin::class); + $calendarHome = $this->createMock(LocalHref::class); + $acl = [[200 => ['{urn:ietf:params:xml:ns:caldav}schedule-inbox-URL' => $calendarHome]]]; + $now = new \DateTimeImmutable('1970-1-1 00:00', new \DateTimeZone('UTC')); + $inTenMinutes = new \DateTime('1970-1-1 01:00'); + $immutableInTenMinutes = \DateTimeImmutable::createFromMutable($inTenMinutes); + $principal = 'principals/users/admin'; + $query = $this->createMock(CalendarQuery::class); + $timezone = new \DateTimeZone('UTC'); + $timezoneObj = $this->createMock(VTimeZone::class); + $calendar = $this->createMock(CalendarImpl::class); + $vCalendar = $this->createMock(VCalendar::class); + $availability = $this->getVAvailability(); + $result = Reader::read('BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject 4.5.3//EN + CALSCALE:GREGORIAN +METHOD:REQUEST +BEGIN:VFREEBUSY +DTSTART:19700101T000000Z +DTEND:19700101T003600Z +DTSTAMP:19700101T000200Z +FREEBUSY;FBTYPE=BUSY:19700101T000000Z/19700101T003600Z +END:VFREEBUSY +END:VCALENDAR'); + + $user->expects(self::once()) + ->method('getUID') + ->willReturn('admin'); + $user->expects(self::once()) + ->method('getEMailAddress') + ->willReturn('test@test.com'); + $this->server->expects(self::once()) + ->method('getServer') + ->willReturn($server); + $server->expects(self::exactly(2)) + ->method('getPlugin') + ->withConsecutive( + ['caldav-schedule'], + ['acl'], + )->willReturnOnConsecutiveCalls($schedulingPlugin, $aclPlugin); + $aclPlugin->expects(self::once()) + ->method('principalSearch') + ->with(['{http://sabredav.org/ns}email-address' => 'test@test.com']) + ->willReturn($acl); + $calendarHome->expects(self::once()) + ->method('getHref') + ->willReturn('calendars/admin/inbox/'); + $aclPlugin->expects(self::once()) + ->method('checkPrivileges') + ->willReturn(true); + $this->timeFactory->expects(self::once()) + ->method('now') + ->willReturn($now); + $this->calendarManager->expects(self::once()) + ->method('getCalendarsForPrincipal') + ->with($principal) + ->willReturn([$calendar]); + $this->calendarManager->expects(self::once()) + ->method('newQuery') + ->with($principal) + ->willReturn($query); + $calendar->expects(self::once()) + ->method('getSchedulingTransparency') + ->willReturn(new ScheduleCalendarTransp('opaque')); + $calendar->expects(self::once()) + ->method('getSchedulingTimezone') + ->willReturn($timezoneObj); + $timezoneObj->expects(self::once()) + ->method('getTimeZone') + ->willReturn($timezone); + $calendar->expects(self::once()) + ->method('getUri'); + $query->expects(self::once()) + ->method('addSearchCalendar'); + $query->expects(self::once()) + ->method('getCalendarUris') + ->willReturn([$calendar]); + $this->timeFactory->expects(self::once()) + ->method('getDateTime') + ->with('+10 minutes') + ->willReturn($inTenMinutes); + $query->expects(self::once()) + ->method('setTimerangeStart') + ->with($now); + $query->expects(self::once()) + ->method('setTimerangeEnd') + ->with($immutableInTenMinutes); + $this->calendarManager->expects(self::once()) + ->method('searchForPrincipal') + ->with($query) + ->willReturn([]); + $this->generator->expects(self::once()) + ->method('getVCalendar') + ->willReturn($vCalendar); + $vCalendar->expects(self::never()) + ->method('add'); + $this->generator->expects(self::once()) + ->method('setObjects') + ->with($vCalendar); + $this->generator->expects(self::once()) + ->method('setTimeRange') + ->with($now, $immutableInTenMinutes); + $this->generator->expects(self::once()) + ->method('setTimeZone') + ->with($timezone); + $this->generator->expects(self::once()) + ->method('setVAvailability') + ->with($availability); + $this->generator->expects(self::once()) + ->method('getResult') + ->willReturn($result); + $this->l10n->expects(self::once()) + ->method('t') + ->with('In a meeting') + ->willReturn('In a meeting'); + + $status = $this->service->processCalendarAvailability($user, $availability->serialize()); + $this->assertEquals(new Status(IUserStatus::BUSY, IUserStatus::MESSAGE_CALENDAR_BUSY, 'In a meeting'), $status); + } + + public function testAvailabilityAndSearchCalendarsStatusBusyUnavailable(): void { + $user = $this->createConfiguredMock(IUser::class, [ + 'getUID' => 'admin', + 'getEMailAddress' => 'test@test.com', + ]); + $server = $this->createMock(Server::class); + $schedulingPlugin = $this->createMock(Plugin::class); + $aclPlugin = $this->createMock(\Sabre\DAVACL\Plugin::class); + $calendarHome = $this->createMock(LocalHref::class); + $acl = [[200 => ['{urn:ietf:params:xml:ns:caldav}schedule-inbox-URL' => $calendarHome]]]; + $now = new \DateTimeImmutable('1970-1-1 00:00', new \DateTimeZone('UTC')); + $inTenMinutes = new \DateTime('1970-1-1 01:00'); + $immutableInTenMinutes = \DateTimeImmutable::createFromMutable($inTenMinutes); + $principal = 'principals/users/admin'; + $query = $this->createMock(CalendarQuery::class); + $timezone = new \DateTimeZone('UTC'); + $timezoneObj = $this->createMock(VTimeZone::class); + $calendar = $this->createMock(CalendarImpl::class); + $vCalendar = $this->createMock(VCalendar::class); + $availability = $this->getVAvailability(); + $result = Reader::read('BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject 4.5.3//EN + CALSCALE:GREGORIAN +METHOD:REQUEST +BEGIN:VFREEBUSY +DTSTART:19700101T000000Z +DTEND:19700101T003600Z +DTSTAMP:19700101T000200Z +FREEBUSY;FBTYPE=BUSY-UNAVAILABLE:19700101T000000Z/19700101T003600Z +END:VFREEBUSY +END:VCALENDAR'); + + $user->expects(self::once()) + ->method('getUID') + ->willReturn('admin'); + $user->expects(self::once()) + ->method('getEMailAddress') + ->willReturn('test@test.com'); + $this->server->expects(self::once()) + ->method('getServer') + ->willReturn($server); + $server->expects(self::exactly(2)) + ->method('getPlugin') + ->withConsecutive( + ['caldav-schedule'], + ['acl'], + )->willReturnOnConsecutiveCalls($schedulingPlugin, $aclPlugin); + $aclPlugin->expects(self::once()) + ->method('principalSearch') + ->with(['{http://sabredav.org/ns}email-address' => 'test@test.com']) + ->willReturn($acl); + $calendarHome->expects(self::once()) + ->method('getHref') + ->willReturn('calendars/admin/inbox/'); + $aclPlugin->expects(self::once()) + ->method('checkPrivileges') + ->willReturn(true); + $this->timeFactory->expects(self::once()) + ->method('now') + ->willReturn($now); + $this->calendarManager->expects(self::once()) + ->method('getCalendarsForPrincipal') + ->with($principal) + ->willReturn([$calendar]); + $this->calendarManager->expects(self::once()) + ->method('newQuery') + ->with($principal) + ->willReturn($query); + $calendar->expects(self::once()) + ->method('getSchedulingTransparency') + ->willReturn(new ScheduleCalendarTransp('opaque')); + $calendar->expects(self::once()) + ->method('getSchedulingTimezone') + ->willReturn($timezoneObj); + $timezoneObj->expects(self::once()) + ->method('getTimeZone') + ->willReturn($timezone); + $calendar->expects(self::once()) + ->method('getUri'); + $query->expects(self::once()) + ->method('addSearchCalendar'); + $query->expects(self::once()) + ->method('getCalendarUris') + ->willReturn([$calendar]); + $this->timeFactory->expects(self::once()) + ->method('getDateTime') + ->with('+10 minutes') + ->willReturn($inTenMinutes); + $query->expects(self::once()) + ->method('setTimerangeStart') + ->with($now); + $query->expects(self::once()) + ->method('setTimerangeEnd') + ->with($immutableInTenMinutes); + $this->calendarManager->expects(self::once()) + ->method('searchForPrincipal') + ->with($query) + ->willReturn([]); + $this->generator->expects(self::once()) + ->method('getVCalendar') + ->willReturn($vCalendar); + $vCalendar->expects(self::never()) + ->method('add'); + $this->generator->expects(self::once()) + ->method('setObjects') + ->with($vCalendar); + $this->generator->expects(self::once()) + ->method('setTimeRange') + ->with($now, $immutableInTenMinutes); + $this->generator->expects(self::once()) + ->method('setTimeZone') + ->with($timezone); + $this->generator->expects(self::once()) + ->method('setVAvailability') + ->with($availability); + $this->generator->expects(self::once()) + ->method('getResult') + ->willReturn($result); + $this->l10n->expects(self::never()) + ->method('t'); + $status = $this->service->processCalendarAvailability($user, $availability->serialize()); + $this->assertEquals(new Status(IUserStatus::AWAY, IUserStatus::MESSAGE_AVAILABILITY), $status); + } + + public function testAvailabilityAndSearchCalendarsStatusBusyTentative(): void { + $user = $this->createConfiguredMock(IUser::class, [ + 'getUID' => 'admin', + 'getEMailAddress' => 'test@test.com', + ]); + $server = $this->createMock(Server::class); + $schedulingPlugin = $this->createMock(Plugin::class); + $aclPlugin = $this->createMock(\Sabre\DAVACL\Plugin::class); + $calendarHome = $this->createMock(LocalHref::class); + $acl = [[200 => ['{urn:ietf:params:xml:ns:caldav}schedule-inbox-URL' => $calendarHome]]]; + $now = new \DateTimeImmutable('1970-1-1 00:00', new \DateTimeZone('UTC')); + $inTenMinutes = new \DateTime('1970-1-1 01:00'); + $immutableInTenMinutes = \DateTimeImmutable::createFromMutable($inTenMinutes); + $principal = 'principals/users/admin'; + $query = $this->createMock(CalendarQuery::class); + $timezone = new \DateTimeZone('UTC'); + $timezoneObj = $this->createMock(VTimeZone::class); + $calendar = $this->createMock(CalendarImpl::class); + $vCalendar = $this->createMock(VCalendar::class); + $availability = $this->getVAvailability(); + $result = Reader::read('BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject 4.5.3//EN + CALSCALE:GREGORIAN +METHOD:REQUEST +BEGIN:VFREEBUSY +DTSTART:19700101T000000Z +DTEND:19700101T003600Z +DTSTAMP:19700101T000200Z +FREEBUSY;FBTYPE=BUSY-TENTATIVE:19700101T000000Z/19700101T003600Z +END:VFREEBUSY +END:VCALENDAR'); + + $user->expects(self::once()) + ->method('getUID') + ->willReturn('admin'); + $user->expects(self::once()) + ->method('getEMailAddress') + ->willReturn('test@test.com'); + $this->server->expects(self::once()) + ->method('getServer') + ->willReturn($server); + $server->expects(self::exactly(2)) + ->method('getPlugin') + ->withConsecutive( + ['caldav-schedule'], + ['acl'], + )->willReturnOnConsecutiveCalls($schedulingPlugin, $aclPlugin); + $aclPlugin->expects(self::once()) + ->method('principalSearch') + ->with(['{http://sabredav.org/ns}email-address' => 'test@test.com']) + ->willReturn($acl); + $calendarHome->expects(self::once()) + ->method('getHref') + ->willReturn('calendars/admin/inbox/'); + $aclPlugin->expects(self::once()) + ->method('checkPrivileges') + ->willReturn(true); + $this->timeFactory->expects(self::once()) + ->method('now') + ->willReturn($now); + $this->calendarManager->expects(self::once()) + ->method('getCalendarsForPrincipal') + ->with($principal) + ->willReturn([$calendar]); + $this->calendarManager->expects(self::once()) + ->method('newQuery') + ->with($principal) + ->willReturn($query); + $calendar->expects(self::once()) + ->method('getSchedulingTransparency') + ->willReturn(new ScheduleCalendarTransp('opaque')); + $calendar->expects(self::once()) + ->method('getSchedulingTimezone') + ->willReturn($timezoneObj); + $timezoneObj->expects(self::once()) + ->method('getTimeZone') + ->willReturn($timezone); + $calendar->expects(self::once()) + ->method('getUri'); + $query->expects(self::once()) + ->method('addSearchCalendar'); + $query->expects(self::once()) + ->method('getCalendarUris') + ->willReturn([$calendar]); + $this->timeFactory->expects(self::once()) + ->method('getDateTime') + ->with('+10 minutes') + ->willReturn($inTenMinutes); + $query->expects(self::once()) + ->method('setTimerangeStart') + ->with($now); + $query->expects(self::once()) + ->method('setTimerangeEnd') + ->with($immutableInTenMinutes); + $this->calendarManager->expects(self::once()) + ->method('searchForPrincipal') + ->with($query) + ->willReturn([]); + $this->generator->expects(self::once()) + ->method('getVCalendar') + ->willReturn($vCalendar); + $vCalendar->expects(self::never()) + ->method('add'); + $this->generator->expects(self::once()) + ->method('setObjects') + ->with($vCalendar); + $this->generator->expects(self::once()) + ->method('setTimeRange') + ->with($now, $immutableInTenMinutes); + $this->generator->expects(self::once()) + ->method('setTimeZone') + ->with($timezone); + $this->generator->expects(self::once()) + ->method('setVAvailability') + ->with($availability); + $this->generator->expects(self::once()) + ->method('getResult') + ->willReturn($result); + $this->l10n->expects(self::never()) + ->method('t'); + $status = $this->service->processCalendarAvailability($user, $availability->serialize()); + $this->assertEquals(new Status(IUserStatus::AWAY, IUserStatus::MESSAGE_CALENDAR_BUSY_TENTATIVE), $status); + } + + public function testAvailabilityAndSearchCalendarsStatusBusyUnknownProperty(): void { + $user = $this->createConfiguredMock(IUser::class, [ + 'getUID' => 'admin', + 'getEMailAddress' => 'test@test.com', + ]); + $server = $this->createMock(Server::class); + $schedulingPlugin = $this->createMock(Plugin::class); + $aclPlugin = $this->createMock(\Sabre\DAVACL\Plugin::class); + $calendarHome = $this->createMock(LocalHref::class); + $acl = [[200 => ['{urn:ietf:params:xml:ns:caldav}schedule-inbox-URL' => $calendarHome]]]; + $now = new \DateTimeImmutable('1970-1-1 00:00', new \DateTimeZone('UTC')); + $inTenMinutes = new \DateTime('1970-1-1 01:00'); + $immutableInTenMinutes = \DateTimeImmutable::createFromMutable($inTenMinutes); + $principal = 'principals/users/admin'; + $query = $this->createMock(CalendarQuery::class); + $timezone = new \DateTimeZone('UTC'); + $timezoneObj = $this->createMock(VTimeZone::class); + $calendar = $this->createMock(CalendarImpl::class); + $vCalendar = $this->createMock(VCalendar::class); + $availability = $this->getVAvailability(); + $result = Reader::read('BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject 4.5.3//EN + CALSCALE:GREGORIAN +METHOD:REQUEST +BEGIN:VFREEBUSY +DTSTART:19700101T000000Z +DTEND:19700101T003600Z +DTSTAMP:19700101T000200Z +FREEBUSY;FBTYPE=X-MEETING:19700101T000000Z/19700101T003600Z +END:VFREEBUSY +END:VCALENDAR'); + + $user->expects(self::once()) + ->method('getUID') + ->willReturn('admin'); + $user->expects(self::once()) + ->method('getEMailAddress') + ->willReturn('test@test.com'); + $this->server->expects(self::once()) + ->method('getServer') + ->willReturn($server); + $server->expects(self::exactly(2)) + ->method('getPlugin') + ->withConsecutive( + ['caldav-schedule'], + ['acl'], + )->willReturnOnConsecutiveCalls($schedulingPlugin, $aclPlugin); + $aclPlugin->expects(self::once()) + ->method('principalSearch') + ->with(['{http://sabredav.org/ns}email-address' => 'test@test.com']) + ->willReturn($acl); + $calendarHome->expects(self::once()) + ->method('getHref') + ->willReturn('calendars/admin/inbox/'); + $aclPlugin->expects(self::once()) + ->method('checkPrivileges') + ->willReturn(true); + $this->timeFactory->expects(self::once()) + ->method('now') + ->willReturn($now); + $this->calendarManager->expects(self::once()) + ->method('getCalendarsForPrincipal') + ->with($principal) + ->willReturn([$calendar]); + $this->calendarManager->expects(self::once()) + ->method('newQuery') + ->with($principal) + ->willReturn($query); + $calendar->expects(self::once()) + ->method('getSchedulingTransparency') + ->willReturn(new ScheduleCalendarTransp('opaque')); + $calendar->expects(self::once()) + ->method('getSchedulingTimezone') + ->willReturn($timezoneObj); + $timezoneObj->expects(self::once()) + ->method('getTimeZone') + ->willReturn($timezone); + $calendar->expects(self::once()) + ->method('getUri'); + $query->expects(self::once()) + ->method('addSearchCalendar'); + $query->expects(self::once()) + ->method('getCalendarUris') + ->willReturn([$calendar]); + $this->timeFactory->expects(self::once()) + ->method('getDateTime') + ->with('+10 minutes') + ->willReturn($inTenMinutes); + $query->expects(self::once()) + ->method('setTimerangeStart') + ->with($now); + $query->expects(self::once()) + ->method('setTimerangeEnd') + ->with($immutableInTenMinutes); + $this->calendarManager->expects(self::once()) + ->method('searchForPrincipal') + ->with($query) + ->willReturn([]); + $this->generator->expects(self::once()) + ->method('getVCalendar') + ->willReturn($vCalendar); + $vCalendar->expects(self::never()) + ->method('add'); + $this->generator->expects(self::once()) + ->method('setObjects') + ->with($vCalendar); + $this->generator->expects(self::once()) + ->method('setTimeRange') + ->with($now, $immutableInTenMinutes); + $this->generator->expects(self::once()) + ->method('setTimeZone') + ->with($timezone); + $this->generator->expects(self::once()) + ->method('setVAvailability') + ->with($availability); + $this->generator->expects(self::once()) + ->method('getResult') + ->willReturn($result); + $this->l10n->expects(self::never()) + ->method('t'); + $status = $this->service->processCalendarAvailability($user, $availability->serialize()); + $this->assertNull($status); + } + + private function getVAvailability(): Document { + return Reader::read('BEGIN:VCALENDAR +PRODID:Nextcloud DAV app +BEGIN:VTIMEZONE +TZID:Europe/Vienna +BEGIN:STANDARD +TZNAME:CET +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +DTSTART:19701025T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:CEST +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +DTSTART:19700329T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +END:DAYLIGHT +END:VTIMEZONE +BEGIN:VAVAILABILITY +BEGIN:AVAILABLE +DTSTART;TZID=Europe/Vienna:20231025T000000 +DTEND;TZID=Europe/Vienna:20231025T235900 +UID:d866782e-e003-4906-9ece-303f270a2c6b +RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR,SA,SU +END:AVAILABLE +END:VAVAILABILITY +END:VCALENDAR'); + } +} diff --git a/apps/user_status/lib/Db/UserStatusMapper.php b/apps/user_status/lib/Db/UserStatusMapper.php index 3621c1bfa96..c8ffcca5ad9 100644 --- a/apps/user_status/lib/Db/UserStatusMapper.php +++ b/apps/user_status/lib/Db/UserStatusMapper.php @@ -26,6 +26,7 @@ declare(strict_types=1); namespace OCA\UserStatus\Db; +use Sabre\CalDAV\Schedule\Plugin; use OCP\AppFramework\Db\QBMapper; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; @@ -210,4 +211,23 @@ class UserStatusMapper extends QBMapper { $qb->executeStatement(); } + + public function getAvailabilityFromPropertiesTable(string $userId): ?string { + $propertyPath = 'calendars/' . $userId . '/inbox'; + $propertyName = '{' . Plugin::NS_CALDAV . '}calendar-availability'; + + $query = $this->db->getQueryBuilder(); + $query->select('propertyvalue') + ->from('properties') + ->where($query->expr()->eq('userid', $query->createNamedParameter($userId))) + ->andWhere($query->expr()->eq('propertypath', $query->createNamedParameter($propertyPath))) + ->andWhere($query->expr()->eq('propertyname', $query->createNamedParameter($propertyName))) + ->setMaxResults(1); + + $result = $query->executeQuery(); + $property = $result->fetchOne(); + $result->closeCursor(); + + return ($property === false ? null : $property); + } } diff --git a/apps/user_status/lib/Listener/UserLiveStatusListener.php b/apps/user_status/lib/Listener/UserLiveStatusListener.php index 3e05efa7118..687e01fc3a7 100644 --- a/apps/user_status/lib/Listener/UserLiveStatusListener.php +++ b/apps/user_status/lib/Listener/UserLiveStatusListener.php @@ -74,7 +74,7 @@ class UserLiveStatusListener implements IEventListener { $userStatus->setIsUserDefined(false); } - // If the status is user-defined and one of the persistent statuses, we + // If the status is user-defined and one of the persistent status, we // will not override it. if ($userStatus->getIsUserDefined() && \in_array($userStatus->getStatus(), StatusService::PERSISTENT_STATUSES, true)) { diff --git a/apps/user_status/lib/Service/PredefinedStatusService.php b/apps/user_status/lib/Service/PredefinedStatusService.php index 7d2f985c168..516680ba683 100644 --- a/apps/user_status/lib/Service/PredefinedStatusService.php +++ b/apps/user_status/lib/Service/PredefinedStatusService.php @@ -202,6 +202,8 @@ class PredefinedStatusService { self::REMOTE_WORK, IUserStatus::MESSAGE_CALL, IUserStatus::MESSAGE_AVAILABILITY, + IUserStatus::MESSAGE_CALENDAR_BUSY, + IUserStatus::MESSAGE_CALENDAR_BUSY_TENTATIVE, ], true); } } diff --git a/apps/user_status/lib/Service/StatusService.php b/apps/user_status/lib/Service/StatusService.php index f9ae769a5a9..003c9aa849a 100644 --- a/apps/user_status/lib/Service/StatusService.php +++ b/apps/user_status/lib/Service/StatusService.php @@ -7,6 +7,7 @@ declare(strict_types=1); * * @author Georg Ehrke <oc.list@georgehrke.com> * @author Joas Schilling <coding@schilljs.com> + * @author Anna Larch <anna.larch@gmx.net> * * @license GNU AGPL version 3 or any later version * @@ -26,6 +27,8 @@ declare(strict_types=1); */ namespace OCA\UserStatus\Service; +use OCA\DAV\CalDAV\Status\Status as CalendarStatus; +use OCA\DAV\CalDAV\Status\StatusService as CalendarStatusService; use OCA\UserStatus\Db\UserStatus; use OCA\UserStatus\Db\UserStatusMapper; use OCA\UserStatus\Exception\InvalidClearAtException; @@ -35,10 +38,13 @@ use OCA\UserStatus\Exception\InvalidStatusTypeException; use OCA\UserStatus\Exception\StatusMessageTooLongException; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Utility\ITimeFactory; +use OCP\Calendar\ISchedulingInformation; use OCP\DB\Exception; use OCP\IConfig; use OCP\IEmojiHelper; +use OCP\IUserManager; use OCP\UserStatus\IUserStatus; +use function in_array; /** * Class StatusService @@ -46,26 +52,9 @@ use OCP\UserStatus\IUserStatus; * @package OCA\UserStatus\Service */ class StatusService { - - /** @var UserStatusMapper */ - private $mapper; - - /** @var ITimeFactory */ - private $timeFactory; - - /** @var PredefinedStatusService */ - private $predefinedStatusService; - - private IEmojiHelper $emojiHelper; - - /** @var bool */ - private $shareeEnumeration; - - /** @var bool */ - private $shareeEnumerationInGroupOnly; - - /** @var bool */ - private $shareeEnumerationPhone; + private bool $shareeEnumeration; + private bool $shareeEnumerationInGroupOnly; + private bool $shareeEnumerationPhone; /** * List of priorities ordered by their priority @@ -74,6 +63,7 @@ class StatusService { IUserStatus::ONLINE, IUserStatus::AWAY, IUserStatus::DND, + IUserStatus::BUSY, IUserStatus::INVISIBLE, IUserStatus::OFFLINE, ]; @@ -84,6 +74,7 @@ class StatusService { */ public const PERSISTENT_STATUSES = [ IUserStatus::AWAY, + IUserStatus::BUSY, IUserStatus::DND, IUserStatus::INVISIBLE, ]; @@ -94,18 +85,16 @@ class StatusService { /** @var int */ public const MAXIMUM_MESSAGE_LENGTH = 80; - public function __construct(UserStatusMapper $mapper, - ITimeFactory $timeFactory, - PredefinedStatusService $defaultStatusService, - IEmojiHelper $emojiHelper, - IConfig $config) { - $this->mapper = $mapper; - $this->timeFactory = $timeFactory; - $this->predefinedStatusService = $defaultStatusService; - $this->emojiHelper = $emojiHelper; - $this->shareeEnumeration = $config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes'; - $this->shareeEnumerationInGroupOnly = $this->shareeEnumeration && $config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes'; - $this->shareeEnumerationPhone = $this->shareeEnumeration && $config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_phone', 'no') === 'yes'; + public function __construct(private UserStatusMapper $mapper, + private ITimeFactory $timeFactory, + private PredefinedStatusService $predefinedStatusService, + private IEmojiHelper $emojiHelper, + private IConfig $config, + private IUserManager $userManager, + private CalendarStatusService $calendarStatusService) { + $this->shareeEnumeration = $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes'; + $this->shareeEnumerationInGroupOnly = $this->shareeEnumeration && $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes'; + $this->shareeEnumerationPhone = $this->shareeEnumeration && $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_phone', 'no') === 'yes'; } /** @@ -149,8 +138,37 @@ class StatusService { * @return UserStatus * @throws DoesNotExistException */ - public function findByUserId(string $userId):UserStatus { - return $this->processStatus($this->mapper->findByUserId($userId)); + public function findByUserId(string $userId): UserStatus { + $userStatus = $this->mapper->findByUserId($userId); + // If the status is user-defined and one of the persistent status, we + // will not override it. + if ($userStatus->getIsUserDefined() && \in_array($userStatus->getStatus(), StatusService::PERSISTENT_STATUSES, true)) { + return $this->processStatus($userStatus); + } + + $calendarStatus = $this->getCalendarStatus($userId); + // We found no status from the calendar, proceed with the existing status + if($calendarStatus === null) { + return $this->processStatus($userStatus); + } + + // if we have the same status result for the calendar and the current status, + // and a custom message to boot, we leave the existing status alone + // as to not overwrite a custom message / emoji + if($userStatus->getIsUserDefined() + && $calendarStatus->getStatus() === $userStatus->getStatus() + && !empty($userStatus->getCustomMessage())) { + return $this->processStatus($userStatus); + } + + // If the new status is null, there's already an identical status in place + $newUserStatus = $this->setUserStatus($userId, + $calendarStatus->getStatus(), + $calendarStatus->getMessage() ?? IUserStatus::MESSAGE_AVAILABILITY, + true, + $calendarStatus->getCustomMessage() ?? ''); + + return $newUserStatus === null ? $this->processStatus($userStatus) : $this->processStatus($newUserStatus); } /** @@ -183,9 +201,12 @@ class StatusService { } // Check if status-type is valid - if (!\in_array($status, self::PRIORITY_ORDERED_STATUSES, true)) { + if (!in_array($status, self::PRIORITY_ORDERED_STATUSES, true)) { throw new InvalidStatusTypeException('Status-type "' . $status . '" is not supported'); } + + + if ($statusTimestamp === null) { $statusTimestamp = $this->timeFactory->getTime(); } @@ -255,11 +276,12 @@ class StatusService { * @throws InvalidMessageIdException */ public function setUserStatus(string $userId, - string $status, - string $messageId, - bool $createBackup): void { + string $status, + string $messageId, + bool $createBackup, + string $customMessage = null): ?UserStatus { // Check if status-type is valid - if (!\in_array($status, self::PRIORITY_ORDERED_STATUSES, true)) { + if (!in_array($status, self::PRIORITY_ORDERED_STATUSES, true)) { throw new InvalidStatusTypeException('Status-type "' . $status . '" is not supported'); } @@ -269,7 +291,7 @@ class StatusService { if ($createBackup) { if ($this->backupCurrentStatus($userId) === false) { - return; // Already a status set automatically => abort. + return null; // Already a status set automatically => abort. } // If we just created the backup @@ -290,15 +312,14 @@ class StatusService { $userStatus->setIsBackup(false); $userStatus->setMessageId($messageId); $userStatus->setCustomIcon(null); - $userStatus->setCustomMessage(null); + $userStatus->setCustomMessage($customMessage); $userStatus->setClearAt(null); $userStatus->setStatusMessageTimestamp($this->timeFactory->now()->getTimestamp()); if ($userStatus->getId() !== null) { - $this->mapper->update($userStatus); - return; + return $this->mapper->update($userStatus); } - $this->mapper->insert($userStatus); + return $this->mapper->insert($userStatus); } /** @@ -561,4 +582,35 @@ class StatusService { // For users that matched restore the previous status $this->mapper->restoreBackupStatuses($restoreIds); } + + /** + * Calculate a users' status according to their availabilit settings and their calendar + * events + * + * There are 4 predefined types of FBTYPE - 'FREE', 'BUSY', 'BUSY-UNAVAILABLE', 'BUSY-TENTATIVE', + * but 'X-' properties are possible + * + * @link https://icalendar.org/iCalendar-RFC-5545/3-2-9-free-busy-time-type.html + * + * The status will be changed for types + * - 'BUSY' + * - 'BUSY-UNAVAILABLE' (ex.: when a VAVILABILITY setting is in effect) + * - 'BUSY-TENTATIVE' (ex.: an event has been accepted tentatively) + * and all FREEBUSY components without a type (implicitly a 'BUSY' status) + * + * 'X-' properties are not handled for now + * + * @param string $userId + * @return CalendarStatus|null + */ + public function getCalendarStatus(string $userId): ?CalendarStatus { + $user = $this->userManager->get($userId); + if ($user === null) { + return null; + } + + $availability = $this->mapper->getAvailabilityFromPropertiesTable($userId); + + return $this->calendarStatusService->processCalendarAvailability($user, $availability); + } } diff --git a/apps/user_status/src/mixins/OnlineStatusMixin.js b/apps/user_status/src/mixins/OnlineStatusMixin.js index d1e3a9111fa..e9746669f09 100644 --- a/apps/user_status/src/mixins/OnlineStatusMixin.js +++ b/apps/user_status/src/mixins/OnlineStatusMixin.js @@ -52,6 +52,7 @@ export default { return this.$t('user_status', 'Online') case 'away': + case 'busy': return this.$t('user_status', 'Away') case 'dnd': @@ -79,6 +80,7 @@ export default { return 'icon-user-status-online' case 'away': + case 'busy': return 'icon-user-status-away' case 'dnd': diff --git a/apps/user_status/tests/Unit/Service/StatusServiceTest.php b/apps/user_status/tests/Unit/Service/StatusServiceTest.php index 413cecce595..bd150cd4258 100644 --- a/apps/user_status/tests/Unit/Service/StatusServiceTest.php +++ b/apps/user_status/tests/Unit/Service/StatusServiceTest.php @@ -28,6 +28,9 @@ namespace OCA\UserStatus\Tests\Service; use Doctrine\DBAL\Exception\UniqueConstraintViolationException; use OC\DB\Exceptions\DbalException; +use OC\User\User; +use OCA\DAV\CalDAV\Status\Status; +use OCA\DAV\CalDAV\Status\StatusService as CalendarStatusService; use OCA\UserStatus\Db\UserStatus; use OCA\UserStatus\Db\UserStatusMapper; use OCA\UserStatus\Exception\InvalidClearAtException; @@ -40,30 +43,40 @@ use OCA\UserStatus\Service\StatusService; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Utility\ITimeFactory; use OCP\DB\Exception; +use OCP\EventDispatcher\IEventDispatcher; use OCP\IConfig; use OCP\IEmojiHelper; +use OCP\IUser; +use OCP\IUserBackend; +use OCP\IUserManager; use OCP\UserStatus\IUserStatus; +use PHPUnit\Framework\MockObject\MockObject; use Test\TestCase; class StatusServiceTest extends TestCase { - /** @var UserStatusMapper|\PHPUnit\Framework\MockObject\MockObject */ + /** @var UserStatusMapper|MockObject */ private $mapper; - /** @var ITimeFactory|\PHPUnit\Framework\MockObject\MockObject */ + /** @var ITimeFactory|MockObject */ private $timeFactory; - /** @var PredefinedStatusService|\PHPUnit\Framework\MockObject\MockObject */ + /** @var PredefinedStatusService|MockObject */ private $predefinedStatusService; - /** @var IEmojiHelper|\PHPUnit\Framework\MockObject\MockObject */ + /** @var IEmojiHelper|MockObject */ private $emojiHelper; - /** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */ + /** @var IConfig|MockObject */ private $config; - /** @var StatusService */ - private $service; + /** @var IUserManager|MockObject */ + private $userManager; + + /** @var CalendarStatusService|MockObject */ + private $calendarStatusService; + + private StatusService $service; protected function setUp(): void { parent::setUp(); @@ -72,6 +85,8 @@ class StatusServiceTest extends TestCase { $this->timeFactory = $this->createMock(ITimeFactory::class); $this->predefinedStatusService = $this->createMock(PredefinedStatusService::class); $this->emojiHelper = $this->createMock(IEmojiHelper::class); + $this->userManager = $this->createMock(IUserManager::class); + $this->calendarStatusService = $this->createMock(CalendarStatusService::class); $this->config = $this->createMock(IConfig::class); @@ -85,7 +100,10 @@ class StatusServiceTest extends TestCase { $this->timeFactory, $this->predefinedStatusService, $this->emojiHelper, - $this->config); + $this->config, + $this->userManager, + $this->calendarStatusService, + ); } public function testFindAll(): void { @@ -139,7 +157,10 @@ class StatusServiceTest extends TestCase { $this->timeFactory, $this->predefinedStatusService, $this->emojiHelper, - $this->config); + $this->config, + $this->userManager, + $this->calendarStatusService, + ); $this->assertEquals([], $this->service->findAllRecentStatusChanges(20, 50)); @@ -156,21 +177,14 @@ class StatusServiceTest extends TestCase { $this->timeFactory, $this->predefinedStatusService, $this->emojiHelper, - $this->config); + $this->config, + $this->userManager, + $this->calendarStatusService, + ); $this->assertEquals([], $this->service->findAllRecentStatusChanges(20, 50)); } - public function testFindByUserId(): void { - $status = $this->createMock(UserStatus::class); - $this->mapper->expects($this->once()) - ->method('findByUserId') - ->with('john.doe') - ->willReturn($status); - - $this->assertEquals($status, $this->service->findByUserId('john.doe')); - } - public function testFindByUserIdDoesNotExist(): void { $this->mapper->expects($this->once()) ->method('findByUserId') @@ -825,4 +839,295 @@ class StatusServiceTest extends TestCase { $this->service->revertMultipleUserStatus(['john', 'nobackup', 'backuponly', 'nobackupanddnd'], 'call'); } + + public function testCalendarAvailabilityNoUser(): void { + $userId = 'admin'; + + $this->userManager->expects(self::once()) + ->method('get') + ->with($userId) + ->willReturn(null); + $this->mapper->expects(self::never()) + ->method('getAvailabilityFromPropertiesTable'); + $this->calendarStatusService->expects(self::never()) + ->method('processCalendarAvailability'); + + $this->service->getCalendarStatus($userId); + } + + public function testCalendarAvailabilityNoVavailablility(): void { + $user = $this->createConfiguredMock(IUser::class, [ + 'getUID' => 'admin', + 'getEMailAddress' => 'test@test.com', + ]); + + $this->userManager->expects(self::once()) + ->method('get') + ->with($user->getUID()) + ->willReturn($user); + $this->mapper->expects(self::once()) + ->method('getAvailabilityFromPropertiesTable') + ->willReturn(''); + $this->calendarStatusService->expects(self::once()) + ->method('processCalendarAvailability') + ->with($user, '') + ->willReturn(null); + + $this->service->getCalendarStatus($user->getUID()); + } + + public function testCalendarAvailabilityVavailablilityAvailable(): void { + $user = $this->createConfiguredMock(IUser::class, [ + 'getUID' => 'admin', + 'getEMailAddress' => 'test@test.com', + ]); + + $vavailability = <<<EOF +BEGIN:VCALENDAR +PRODID:Nextcloud DAV app +BEGIN:VTIMEZONE +TZID:Europe/Vienna +BEGIN:STANDARD +TZNAME:CET +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +DTSTART:19701025T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:CEST +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +DTSTART:19700329T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +END:DAYLIGHT +END:VTIMEZONE +BEGIN:VAVAILABILITY +BEGIN:AVAILABLE +DTSTART;TZID=Europe/Vienna:20231025T000000 +DTEND;TZID=Europe/Vienna:20231025T235900 +UID:d866782e-e003-4906-9ece-303f270a2c6b +RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR,SA,SU +END:AVAILABLE +END:VAVAILABILITY +END:VCALENDAR +EOF; + $status = new Status(IUserStatus::AWAY); + $this->userManager->expects(self::once()) + ->method('get') + ->with($user->getUID()) + ->willReturn($user); + $this->mapper->expects(self::once()) + ->method('getAvailabilityFromPropertiesTable') + ->willReturn($vavailability); + $this->calendarStatusService->expects(self::once()) + ->method('processCalendarAvailability') + ->with($user, $vavailability) + ->willReturn($status); + + $this->service->getCalendarStatus($user->getUID()); + } + + public function testCalendarAvailabilityVavailablilityUpdate(): void { + $user = $this->createConfiguredMock(IUser::class, [ + 'getUID' => 'admin', + 'getEMailAddress' => 'test@test.com', + ]); + $calDavStatus = new Status(IUserStatus::BUSY, 'meeting', 'In a meeting'); + $vavailability = <<<EOF +BEGIN:VCALENDAR +PRODID:Nextcloud DAV app +BEGIN:VTIMEZONE +TZID:Europe/Vienna +BEGIN:STANDARD +TZNAME:CET +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +DTSTART:19701025T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:CEST +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +DTSTART:19700329T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +END:DAYLIGHT +END:VTIMEZONE +BEGIN:VAVAILABILITY +BEGIN:AVAILABLE +DTSTART;TZID=Europe/Vienna:20231025T000000 +DTEND;TZID=Europe/Vienna:20231025T235900 +UID:d866782e-e003-4906-9ece-303f270a2c6b +RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR,SA,SU +END:AVAILABLE +END:VAVAILABILITY +END:VCALENDAR +EOF; + $this->userManager->expects(self::once()) + ->method('get') + ->with($user->getUID()) + ->willReturn($user); + $this->mapper->expects(self::once()) + ->method('getAvailabilityFromPropertiesTable') + ->willReturn($vavailability); + $this->calendarStatusService->expects(self::once()) + ->method('processCalendarAvailability') + ->with($user, $vavailability) + ->willReturn($calDavStatus); + + $this->service->getCalendarStatus($user->getUID()); + } + + public function testFindByUserIdUserDefinedAndPersistent(): void { + $status = new UserStatus(); + $status->setIsUserDefined(true); + $status->setStatus(IUserStatus::DND); + + $this->mapper->expects($this->once()) + ->method('findByUserId') + ->with('admin') + ->willReturn($status); + $this->mapper->expects(self::never()) + ->method('getAvailabilityFromPropertiesTable'); + $this->calendarStatusService->expects(self::never()) + ->method('processCalendarAvailability'); + + $this->assertEquals($status, $this->service->findByUserId('admin')); + } + + public function testFindByUserIdUserDefinedNoCalStatus(): void { + $user = $this->createConfiguredMock(IUser::class, [ + 'getUID' => 'admin', + 'getEMailAddress' => 'test@test.com', + ]); + $status = new UserStatus(); + $status->setIsUserDefined(true); + $status->setStatus(IUserStatus::ONLINE); + + $this->mapper->expects($this->once()) + ->method('findByUserId') + ->with($user->getUID()) + ->willReturn($status); + $this->userManager->expects(self::once()) + ->method('get') + ->willReturn($user); + $this->mapper->expects(self::once()) + ->method('getAvailabilityFromPropertiesTable') + ->willReturn(''); + $this->calendarStatusService->expects(self::once()) + ->method('processCalendarAvailability') + ->with($user, '') + ->willReturn(null); + + $this->assertEquals($status, $this->service->findByUserId('admin')); + } + + public function testFindByUserIdUserDefinedCalStatusIdentical(): void { + $user = $this->createConfiguredMock(IUser::class, [ + 'getUID' => 'admin', + 'getEMailAddress' => 'test@test.com', + ]); + $calDavStatus = new Status(IUserStatus::ONLINE); + $userStatus = new UserStatus(); + $userStatus->setStatus(IUserStatus::ONLINE); + $userStatus->setIsUserDefined(true); + $userStatus->setCustomMessage('Test'); + + $this->mapper->expects(self::once()) + ->method('findByUserId') + ->with($user->getUID()) + ->willReturn($userStatus); + $this->userManager->expects(self::once()) + ->method('get') + ->willReturn($user); + $this->mapper->expects(self::once()) + ->method('getAvailabilityFromPropertiesTable') + ->willReturn(''); + $this->calendarStatusService->expects(self::once()) + ->method('processCalendarAvailability') + ->with($user, '') + ->willReturn($calDavStatus); + + $this->assertEquals($userStatus, $this->service->findByUserId('admin')); + } + + public function testFindByUserIdUserDefinedCalStatusUpdate(): void { + $user = $this->createConfiguredMock(IUser::class, [ + 'getUID' => 'admin', + 'getEMailAddress' => 'test@test.com', + ]); + $calDavStatus = new Status(IUserStatus::BUSY, 'meeting', 'In a meeting'); + + $oldStatus = new UserStatus(); + $oldStatus->setId(42); + $oldStatus->setUserId($user->getUID()); + $oldStatus->setStatus(IUserStatus::ONLINE); + $oldStatus->setStatusTimestamp(0); + $oldStatus->setIsUserDefined(true); + + $expected = new UserStatus(); + $expected->setUserId($user->getUID()); + $expected->setStatus(IUserStatus::BUSY); + $expected->setStatusTimestamp(0); + $expected->setIsUserDefined(true); + $expected->setIsBackup(false); + + $this->mapper->expects(self::once()) + ->method('findByUserId') + ->with($user->getUID()) + ->willReturn($oldStatus); + $this->userManager->expects(self::once()) + ->method('get') + ->willReturn($user); + $this->mapper->expects(self::once()) + ->method('getAvailabilityFromPropertiesTable') + ->willReturn(''); + $this->mapper->expects(self::once()) + ->method('createBackupStatus') + ->with($user->getUID()) + ->willReturn(true); + $this->calendarStatusService->expects(self::once()) + ->method('processCalendarAvailability') + ->with($user, '') + ->willReturn($calDavStatus); + $this->predefinedStatusService->expects(self::once()) + ->method('isValidId') + ->with($calDavStatus->getMessage()) + ->willReturn(true); + $this->mapper->expects(self::once()) + ->method('insert') + ->willReturn($expected); + + $actual = $this->service->findByUserId('admin'); + $this->assertEquals($expected->getStatus(), $actual->getStatus()); + $this->assertEquals($expected->getCustomMessage(), $actual->getCustomMessage()); + } + + public function testFindByUserIdSystemDefined(): void { + $user = $this->createConfiguredMock(IUser::class, [ + 'getUID' => 'admin', + 'getEMailAddress' => 'test@test.com', + ]); + $status = new UserStatus(); + $status->setIsUserDefined(false); + $status->setStatus(IUserStatus::ONLINE); + + $this->mapper->expects($this->once()) + ->method('findByUserId') + ->with($user->getUID()) + ->willReturn($status); + $this->userManager->expects(self::once()) + ->method('get') + ->willReturn($user); + $this->mapper->expects(self::once()) + ->method('getAvailabilityFromPropertiesTable') + ->willReturn(''); + $this->calendarStatusService->expects(self::once()) + ->method('processCalendarAvailability') + ->with($user, '') + ->willReturn(null); + + $this->assertEquals($status, $this->service->findByUserId('admin')); + } } |