* * @author Charlie Calendre * * @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\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); } }