* * @author Anna Larch * * @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 . * */ namespace OCA\DAV\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 as SchedulePlugin; use OCP\AppFramework\Utility\ITimeFactory; use OCP\Calendar\IManager; 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\VEvent; use Sabre\VObject\Parameter; use Sabre\VObject\Property; 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): ?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($calendarEvents)) { 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); $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 if (count($freeBusyProperties) === 0) { return null; } /** @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) { // Ignore BUSY-UNAVAILABLE, that's for the automation case 'BUSY': case 'BUSY-TENTATIVE': return new Status(IUserStatus::BUSY, IUserStatus::MESSAGE_CALENDAR_BUSY, $this->l10n->t('In a meeting')); default: return null; } } }