You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
nextcloud/apps/dav/lib/CalDAV/Status/StatusService.php

234 lines
7.5 KiB
PHP

<?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 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;
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;
}
}
}