This connector allows the integration of the document sharing, calendars and tasks features of Nextcloud in Watcha. It is thus possible to link the resources of these applications to Watcha rooms thanks to Nextcloud groups.
You can not 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-connector/lib/Controller/CalendarController.php

685 lines
24 KiB

<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2021, Watcha <contact@watcha.fr>
*
* @author Charlie Calendre <c-cal@watcha.fr>
*
* @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\Watcha\Controller;
use Psr\Log\LoggerInterface;
use OCA\DAV\CalDAV\CalDavBackend;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\JSONResponse;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\IGroup;
use OCP\IGroupManager;
use OCP\IRequest;
use OCP\IUserManager;
use OCP\Util;
use Sabre\DAV\Exception\NotFound;
use Sabre\Uri;
use OCA\Watcha\Dav;
use OCA\Watcha\Exception\GenericException;
const CALENDAR_ORDER_KEY = "{http://apple.com/ns/ical/}calendar-order";
const DISPLAYNAME_KEY = "{DAV:}displayname";
const OWNER_PRINCIPAL_KEY = "{" . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . "}owner-principal";
const SUPPORTED_CALENDAR_COMPONENT_SET_KEY = "{" . \OCA\DAV\CalDAV\Plugin::NS_CALDAV . "}supported-calendar-component-set";
class CalendarController extends Controller {
/** @var string */
private $userId;
/** @var LoggerInterface */
private $logger;
/** @var CalDavBackend */
private $caldav;
/** @var IDBConnection */
private $connection;
/** @var IUserManager */
private $userManager;
/** @var IGroupManager */
private $groupManager;
/** @var IConfig */
private $config;
public function __construct(
string $AppName,
string $UserId,
IRequest $request,
LoggerInterface $logger,
CalDavBackend $caldav,
IDBConnection $connection,
IUserManager $userManager,
IGroupManager $groupManager,
IConfig $config
) {
parent::__construct($AppName, $request);
$this->userId = $UserId;
$this->logger = $logger;
$this->caldav = $caldav;
$this->connection = $connection;
$this->userManager = $userManager;
$this->groupManager = $groupManager;
$this->config = $config;
}
/**
* @NoAdminRequired
* @NoCSRFRequired
*
* @param string $userId
* @return JSONResponse
*/
public function list(string $userId) {
$principalUri = "principals/users/$userId";
$calendars = $this->caldav->getUsersOwnCalendars($principalUri);
$fmtCalendars = [];
foreach ($calendars as $calendar) {
$fmtCalendars[] = [
"id" => (int) $calendar["id"],
"displayname" => $calendar[DISPLAYNAME_KEY],
"components" => $calendar[SUPPORTED_CALENDAR_COMPONENT_SET_KEY]->getValue()
];
}
usort($fmtCalendars, function (array $a, array $b) {
return strcmp(strtolower($a['displayname']), strtolower($b['displayname']));
});
return new JSONResponse($fmtCalendars);
}
/**
* @NoAdminRequired
* @NoCSRFRequired
*
* @param string $userId
* @param int $calendarId
* @return JSONResponse
*/
public function get(string $userId, int $calendarId) {
$calendar = $this->getFormatedCalendar($calendarId);
try {
$displayName = $this->getDisplayName($userId, $calendarId);
} catch (GenericException $e) {
return new JSONResponse(["message" => $e->getMessage()], $e->getHTTPCode());
}
$calendar["displayname"] = $displayName;
return new JSONResponse($calendar);
}
/**
* @NoAdminRequired
* @NoCSRFRequired
*
* @param string $userId
* @param int $calendarId
* @return JSONResponse
*/
public function reorder(string $userId, int $calendarId) {
$calendar = $this->caldav->getCalendarById($calendarId);
if (is_null($calendar)) {
$this->logger->warning("calendar $calendarId no found, can't reorder it for user $userId");
return;
}
$components = $calendar[SUPPORTED_CALENDAR_COMPONENT_SET_KEY]->getValue();
if (in_array("VTODO", $components)) {
list(, $ownerId) = Uri\split($calendar["principaluri"]);
$calendarUri = $ownerId === $userId ? $calendar["uri"] : $this->computeUriForSharedCalendar($calendar["uri"], $ownerId);
$value = "/calendars/$calendarUri";
$this->config->setUserValue($userId, "tasks", "various_initialRoute", $value);
$this->logger->info("todo list with URI $calendarUri defined as initial route for user $userId");
}
$query = $this->connection->getQueryBuilder();
$fields = ["propertypath", "propertyvalue"];
$query->select($fields)
->from("properties")
->where($query->expr()->eq("userid", $query->createNamedParameter($userId)))
->andWhere($query->expr()->eq("propertyname", $query->createNamedParameter(CALENDAR_ORDER_KEY)))
->orderBy("propertyvalue", "ASC");
$cursor = $query->execute();
$ocProperties = array();
while ($row = $cursor->fetch()) {
$order = (int) $row["propertyvalue"];
$path = (string) $row["propertypath"];
$ocProperties[$path] = [
"order" => $order
];
}
$cursor->closeCursor();
$principalUri = "principals/users/$userId";
$calendars = $this->caldav->getCalendarsForUser($principalUri);
$orderedCalendars = array();
foreach ($calendars as $calendar) {
$owner = $calendar[OWNER_PRINCIPAL_KEY];
$uri = $calendar["uri"];
$path = "calendars/$userId/$uri";
if ($owner !== $principalUri and array_key_exists($path, $ocProperties)) {
$ocProperties[$path]["calendar"] = $calendar;
} else {
$orderedCalendars[] = $calendar;
}
}
foreach ($ocProperties as $propertie) {
$order = $propertie["order"];
$calendar = $propertie["calendar"];
array_splice($orderedCalendars, $order, 0, array($calendar));
}
$user = $this->userManager->get($userId);
$server = Dav::getServerInstance($this->connection, $user);
$i = 1;
foreach ($orderedCalendars as $calendar) {
$uri = $calendar["uri"];
$path = "calendars/$userId/$uri";
$order = (int) $calendar["id"] === $calendarId ? 0 : $i++;
$properties = [CALENDAR_ORDER_KEY => $order];
$server->updateProperties($path, $properties);
}
$this->logger->info("calendar $calendarId moved to top of list for user $userId");
return new JSONResponse((object)[]);
}
/**
* @NoAdminRequired
* @NoCSRFRequired
*
* @param string $mxRoomId
* @param string $displayName
* @param string[] $userIds (optional)
* @return JSONResponse
*/
public function createAndShare(string $mxRoomId, string $displayName, array $userIds = []) {
$userId = $this->userId;
$calendarUri = $this->computeIdFromMxRoomId($mxRoomId);
$calendarId = $this->create($userId, $calendarUri, $displayName);
$userIds[] = $this->userId;
return $this->share($userId, $calendarId, $mxRoomId, $displayName, $userIds);
}
/**
* @NoAdminRequired
* @NoCSRFRequired
*
* @param string $userId
* @param int $calendarId
* @param string $mxRoomId
* @param string $displayName
* @param string[] $userIds (optional)
* @return JSONResponse
*/
public function share(string $userId, int $calendarId, string $mxRoomId, string $displayName, array $userIds = []) {
$ownerId = $this->getUserIdFromCalendarId($calendarId);
if (is_null($ownerId)) {
$message = "calendar $calendarId not found, can't share it";
$this->logger->warning($message);
return new JSONResponse(["message" => $message], Http::STATUS_NOT_FOUND);
}
if ($ownerId !== $userId) {
$message = "calendar $calendarId is not owned by $userId, can't share it";
$this->logger->warning($message);
return new JSONResponse(["message" => $message], Http::STATUS_FORBIDDEN);
}
$groupId = $this->computeIdFromMxRoomId($mxRoomId);
try {
$this->createGroup($groupId, $displayName);
} catch (GenericException $e) {
return new JSONResponse(["message" => $e->getMessage()], $e->getHTTPCode());
}
# workaround for an upstream bug: users must be added to the group before
# sharing the calendar to avoid it appearing empty when its members are listed later
$this->addUsersToGroup($groupId, $userIds, $ownerId);
$add = [
array(
"href" => "principal:principals/groups/$groupId",
"commonName" => null,
"summary" => null,
"readOnly" => false,
)
];
try {
$this->updateShares($calendarId, $add);
} catch (GenericException | NotFound $e) {
return new JSONResponse(["message" => $e->getMessage()], $e->getHTTPCode());
}
$this->renameForGroupMembers($groupId, $calendarId, $displayName);
$calendar = $this->getFormatedCalendar($calendarId);
return new JSONResponse($calendar);
}
/**
* @NoAdminRequired
* @NoCSRFRequired
*
* @param int[] $calendarIds
* @param string $mxRoomId
* @param bool $deleteGroup (optional)
* @return JSONResponse
*/
public function unShare(array $calendarIds, string $mxRoomId, bool $deleteGroup = False) {
$groupId = $this->computeIdFromMxRoomId($mxRoomId);
foreach ($calendarIds as $calendarId) {
if ($this->getUserIdFromCalendarId($calendarId) === $this->userId) {
$this->delete($calendarId);
} else {
$remove = ["principal:principals/groups/$groupId"];
$this->updateShares($calendarId, [], $remove);
}
}
if ($deleteGroup) {
try {
$this->deleteGroup($groupId);
} catch (GenericException $e) {
return new JSONResponse(["message" => $e->getMessage()], $e->getHTTPCode());
}
}
return new JSONResponse((object)[]);
}
/**
* @NoAdminRequired
* @NoCSRFRequired
*
* @param string $userId
* @param string $mxRoomId
* @param int[] $calendarIds
* @param string $displayName
* @return JSONResponse
*/
public function addUser(string $userId, string $mxRoomId, array $calendarIds, string $displayName) {
$groupId = $this->computeIdFromMxRoomId($mxRoomId);
foreach ($calendarIds as $calendarId) {
$ownerId = $this->getUserIdFromCalendarId($calendarId);
$this->addUsersToGroup($groupId, [$userId], $ownerId);
try {
$this->renameForUser($userId, $calendarId, $displayName);
} catch (NotFound $e) {
$this->logger->warning($e->getMessage());
}
}
return new JSONResponse((object)[]);
}
/**
* @NoAdminRequired
* @NoCSRFRequired
*
* @param string $userId
* @param string $mxRoomId
* @return JSONResponse
*/
public function removeUser(string $userId, string $mxRoomId) {
$groupId = $this->computeIdFromMxRoomId($mxRoomId);
$this->removeUserFromGroup($groupId, $userId);
return new JSONResponse((object)[]);
}
/**
* @NoAdminRequired
* @NoCSRFRequired
*
* @param int[] $calendarIds
* @param string $mxRoomId
* @param string $displayName
* @return JSONResponse
*/
public function rename(array $calendarIds, string $mxRoomId, string $displayName) {
$groupId = $this->computeIdFromMxRoomId($mxRoomId);
$this->renameGroup($groupId, $displayName);
foreach ($calendarIds as $calendarId) {
$this->renameForGroupMembers($groupId, $calendarId, $displayName);
}
return new JSONResponse((object)[]);
}
/**
* @param int $calendarId
* @return array
*/
private function getFormatedCalendar(int $calendarId) {
$calendar = $this->caldav->getCalendarById($calendarId);
return [
"id" => (int) $calendar["id"],
"components" => $calendar[SUPPORTED_CALENDAR_COMPONENT_SET_KEY]->getValue(),
"is_personal" => $this->getUserIdFromCalendarId($calendarId) !== $this->userId
];
}
/**
* @param string $userId
* @param int $calendarId
* @return JSONResponse
*/
private function getDisplayName(string $userId, int $calendarId) {
$user = $this->userManager->get($userId);
$server = Dav::getServerInstance($this->connection, $user);
$propertyNames = [DISPLAYNAME_KEY];
$principalUri = "principals/users/$userId";
$calendars = $this->caldav->getCalendarsForUser($principalUri);
foreach ($calendars as $calendar) {
if ((int) $calendar["id"] !== $calendarId) {
continue;
}
$uri = $calendar["uri"];
$path = "calendars/$userId/$uri";
$properties = $server->getProperties($path, $propertyNames);
return $properties[DISPLAYNAME_KEY];
}
$message = "calendar $calendarId not available for user $userId, can't get displayname";
$this->logger->warning($message);
throw new GenericException($message, Http::STATUS_FORBIDDEN);
}
/**
* @param string $userId
* @param string $calendarUri
* @param string $displayName
* @return int
*/
private function create(string $userId, string $calendarUri, string $displayName) {
$server = Dav::getServerInstance();
$path = "calendars/$userId/$calendarUri";
if ($server->tree->nodeExists($path)) {
$this->logger->warning("calendar $path already exists");
return $this->getCalendarIdFromUri($userId, $calendarUri);
}
$principalUri = "principals/users/$userId";
$properties = [DISPLAYNAME_KEY => $displayName];
$calendarId = $this->caldav->createCalendar($principalUri, $calendarUri, $properties);
$this->logger->info("calendar $calendarId ($displayName) created");
return $calendarId;
}
/**
* @param int $calendarId
* @return void
*/
private function delete(int $calendarId) {
if (is_null($this->caldav->getCalendarById($calendarId))) {
$this->logger->warning("calendar $calendarId no found, can't delete it");
return;
}
list($major,) = Util::getVersion();
if ($major > 21) {
$this->caldav->deleteCalendar($calendarId, true);
} else {
$this->caldav->deleteCalendar($calendarId);
}
$this->logger->info("calendar $calendarId deleted");
}
/**
* @param int $calendarId
* @param array $add
* @param array $remove
* @return void
* @throws GenericException
* @throws Sabre\DAV\Exception\NotFound
*/
private function updateShares(int $calendarId, array $add = [], array $remove = []) {
$server = Dav::getServerInstance();
$userId = $this->getUserIdFromCalendarId($calendarId);
if (is_null($userId)) {
if ($add) {
$exception = new GenericException("no calendar $calendarId to add a share to");
$this->logger->error($exception->getMessage(), ["exception" => $exception]);
throw $exception;
}
$this->logger->warning("no calendar $calendarId to remove a share to");
return;
}
$calendar = $this->caldav->getCalendarById($calendarId);
$uri = $calendar["uri"];
$path = "calendars/$userId/$uri";
$node = $server->tree->getNodeForPath($path);
$node->updateShares($add, $remove);
$this->logger->info("share(s) for calendar $calendarId updated (add: {add} | remove: {remove})", [
"add" => json_encode($add, JSON_UNESCAPED_SLASHES), "remove" => json_encode($remove, JSON_UNESCAPED_SLASHES)
]);
}
/**
* @param string $groupId
* @param string $displayName
* @return void
* @throws GenericException
*/
private function createGroup(string $groupId, string $displayName) {
if ($this->groupManager->groupExists($groupId)) {
$this->logger->warning("group $groupId already exists");
return;
}
$group = $this->groupManager->createGroup($groupId);
if (!$group instanceof IGroup) {
$exception = new GenericException("can't create $groupId");
$this->logger->error($exception->getMessage(), ["exception" => $exception]);
throw $exception;
}
$group->setDisplayName($displayName);
$this->logger->info("group $groupId ($displayName) created");
}
/**
* @param string $groupId
* @return void
*/
private function deleteGroup(string $groupId) {
$group = $this->groupManager->get($groupId);
if (is_null($group)) {
$this->logger->warning("group $groupId no found, can't delete it");
return;
}
if (!$group->delete()) {
$exception = new GenericException("can't delete group $groupId");
$this->logger->error($exception->getMessage(), ["exception" => $exception]);
throw $exception;
}
$this->logger->info("group $groupId deleted");
}
/**
* @param string $groupId
* @param string $displayName
* @return void
*/
private function renameGroup(string $groupId, string $displayName) {
$group = $this->groupManager->get($groupId);
if (is_null($group)) {
$this->logger->warning("group $groupId no found, can't rename it");
return;
}
$group->setDisplayName($displayName);
$this->logger->info("group $groupId renamed to $displayName");
}
/**
* @param string $groupId
* @param string[] $userIds
* @param string $ownerId
* @return void
*/
private function addUsersToGroup(string $groupId, array $userIds, string $ownerId) {
$group = $this->groupManager->get($groupId);
if (is_null($group)) {
$this->logger->warning("group $groupId no found, can't add users");
return;
}
foreach ($userIds as $userId) {
# never add calendar owner other than the service account to group
if ($userId === $ownerId and $ownerId !== $this->userId) {
$this->logger->info("user $userId is the calendar owner, addition to group $groupId skipped");
continue;
}
$user = $this->userManager->get($userId);
if (is_null($user)) {
$this->logger->warning("user $userId no found, can't add it to group $groupId");
continue;
}
$group->addUser($user);
$this->logger->info("user $userId added to group $groupId");
}
}
/**
* @param string $groupId
* @param string $userId
* @return void
*/
private function removeUserFromGroup(string $groupId, string $userId) {
$group = $this->groupManager->get($groupId);
if (is_null($group)) {
$this->logger->warning("group $groupId no found, can't remove user");
return;
}
$user = $this->userManager->get($userId);
if (is_null($user)) {
$this->logger->warning("user $userId no found, can't remove it from group $groupId");
return;
}
$group->removeUser($user);
$this->logger->info("user $userId remove from group $groupId");
}
/**
* @param string $groupId
* @param int $calendarId
* @param string $displayName
* @return void
*/
private function renameForGroupMembers(string $groupId, int $calendarId, string $displayName) {
$group = $this->groupManager->get($groupId);
if (is_null($group)) {
$this->logger->warning("group $groupId no found, can't rename calendar $calendarId for its members");
return;
}
$users = $group->getUsers();
foreach ($users as $user) {
$userId = $user->getUID();
try {
$this->renameForUser($userId, $calendarId, $displayName);
} catch (NotFound $e) {
$this->logger->warning($e->getMessage());
}
}
}
/**
* @param string $userId
* @param int $calendarId
* @param string $displayName
* @return void
* @throws GenericException
*/
private function renameForUser(string $userId, int $calendarId, string $displayName) {
$user = $this->userManager->get($userId);
if (is_null($user)) {
$this->logger->warning("user $userId no found, can't rename calendar $calendarId for them");
return;
}
$server = Dav::getServerInstance($this->connection, $user);
$calendar = $this->caldav->getCalendarById($calendarId);
if (is_null($calendar)) {
$this->logger->warning("calendar $calendarId no found, can't rename it for user $userId");
return;
}
list(, $ownerId) = Uri\split($calendar["principaluri"]);
# never rename a personal calendar other than those of the service account
if ($ownerId === $userId and $ownerId !== $this->userId) {
return;
}
$calendarUri = $ownerId === $userId ? $calendar["uri"] : $this->computeUriForSharedCalendar($calendar["uri"], $ownerId);
$path = "calendars/$userId/$calendarUri";
$properties = [DISPLAYNAME_KEY => $displayName];
$result = $server->updateProperties($path, $properties);
if (!in_array($result[DISPLAYNAME_KEY], [200, 204])) {
$exception = new GenericException("can't rename calendar with URI $calendarUri for user $userId");
$this->logger->error($exception->getMessage(), ["exception" => $exception]);
throw $exception;
}
$this->logger->info("calendar $calendarId renamed to $displayName for user $userId");
}
/**
* @param int $calendarId
* @return string|null
*/
private function getUserIdFromCalendarId(int $calendarId) {
$calendar = $this->caldav->getCalendarById($calendarId);
if (is_null($calendar)) {
$this->logger->warning("calendar $calendarId no found, can't parse user ID");
return;
}
$principalUri = $calendar["principaluri"];
list(, $userId) = Uri\split($principalUri);
return $userId;
}
/**
* @param string $userId
* @param string $calendarUri
* @return int|null
*/
private function getCalendarIdFromUri(string $userId, string $calendarUri) {
$principalUri = "principals/users/$userId";
$calendar = $this->caldav->getCalendarByUri($principalUri, $calendarUri);
if (is_null($calendar)) {
$this->logger->warning("calendar $calendarUri owned by $userId no found, can't get ID");
return;
}
return (int) $calendar["id"];
}
/**
* @param string $calendarUri
* @param string $ownerId
* @return string
*/
private function computeUriForSharedCalendar(string $calendarUri, string $ownerId) {
return $calendarUri . "_shared_by_" . $ownerId;
}
/**
* @param string $mxRoomId
* @return string
*/
private function computeIdFromMxRoomId(string $mxRoomId) {
return hash("sha256", $mxRoomId);
}
}