Calendar.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  1. <?php
  2. /**
  3. * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
  4. * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
  5. * SPDX-License-Identifier: AGPL-3.0-only
  6. */
  7. namespace OCA\DAV\CalDAV;
  8. use DateTimeImmutable;
  9. use DateTimeInterface;
  10. use OCA\DAV\CalDAV\Trashbin\Plugin as TrashbinPlugin;
  11. use OCA\DAV\DAV\Sharing\IShareable;
  12. use OCA\DAV\Exception\UnsupportedLimitOnInitialSyncException;
  13. use OCP\DB\Exception;
  14. use OCP\IConfig;
  15. use OCP\IL10N;
  16. use Psr\Log\LoggerInterface;
  17. use Sabre\CalDAV\Backend\BackendInterface;
  18. use Sabre\DAV\Exception\Forbidden;
  19. use Sabre\DAV\Exception\NotFound;
  20. use Sabre\DAV\IMoveTarget;
  21. use Sabre\DAV\INode;
  22. use Sabre\DAV\PropPatch;
  23. /**
  24. * Class Calendar
  25. *
  26. * @package OCA\DAV\CalDAV
  27. * @property CalDavBackend $caldavBackend
  28. */
  29. class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable, IMoveTarget {
  30. protected IL10N $l10n;
  31. private bool $useTrashbin = true;
  32. public function __construct(
  33. BackendInterface $caldavBackend,
  34. $calendarInfo,
  35. IL10N $l10n,
  36. private IConfig $config,
  37. private LoggerInterface $logger,
  38. ) {
  39. // Convert deletion date to ISO8601 string
  40. if (isset($calendarInfo[TrashbinPlugin::PROPERTY_DELETED_AT])) {
  41. $calendarInfo[TrashbinPlugin::PROPERTY_DELETED_AT] = (new DateTimeImmutable())
  42. ->setTimestamp($calendarInfo[TrashbinPlugin::PROPERTY_DELETED_AT])
  43. ->format(DateTimeInterface::ATOM);
  44. }
  45. parent::__construct($caldavBackend, $calendarInfo);
  46. if ($this->getName() === BirthdayService::BIRTHDAY_CALENDAR_URI && strcasecmp($this->calendarInfo['{DAV:}displayname'], 'Contact birthdays') === 0) {
  47. $this->calendarInfo['{DAV:}displayname'] = $l10n->t('Contact birthdays');
  48. }
  49. if ($this->getName() === CalDavBackend::PERSONAL_CALENDAR_URI &&
  50. $this->calendarInfo['{DAV:}displayname'] === CalDavBackend::PERSONAL_CALENDAR_NAME) {
  51. $this->calendarInfo['{DAV:}displayname'] = $l10n->t('Personal');
  52. }
  53. $this->l10n = $l10n;
  54. }
  55. /**
  56. * {@inheritdoc}
  57. * @throws Forbidden
  58. */
  59. public function updateShares(array $add, array $remove): void {
  60. if ($this->isShared()) {
  61. throw new Forbidden();
  62. }
  63. $this->caldavBackend->updateShares($this, $add, $remove);
  64. }
  65. /**
  66. * Returns the list of people whom this resource is shared with.
  67. *
  68. * Every element in this array should have the following properties:
  69. * * href - Often a mailto: address
  70. * * commonName - Optional, for example a first + last name
  71. * * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants.
  72. * * readOnly - boolean
  73. * * summary - Optional, a description for the share
  74. *
  75. * @return list<array{href: string, commonName: string, status: int, readOnly: bool, '{http://owncloud.org/ns}principal': string, '{http://owncloud.org/ns}group-share': bool}>
  76. */
  77. public function getShares(): array {
  78. if ($this->isShared()) {
  79. return [];
  80. }
  81. return $this->caldavBackend->getShares($this->getResourceId());
  82. }
  83. public function getResourceId(): int {
  84. return $this->calendarInfo['id'];
  85. }
  86. /**
  87. * @return string
  88. */
  89. public function getPrincipalURI() {
  90. return $this->calendarInfo['principaluri'];
  91. }
  92. /**
  93. * @param int $resourceId
  94. * @param list<array{privilege: string, principal: string, protected: bool}> $acl
  95. * @return list<array{privilege: string, principal: ?string, protected: bool}>
  96. */
  97. public function getACL() {
  98. $acl = [
  99. [
  100. 'privilege' => '{DAV:}read',
  101. 'principal' => $this->getOwner(),
  102. 'protected' => true,
  103. ],
  104. [
  105. 'privilege' => '{DAV:}read',
  106. 'principal' => $this->getOwner() . '/calendar-proxy-write',
  107. 'protected' => true,
  108. ],
  109. [
  110. 'privilege' => '{DAV:}read',
  111. 'principal' => $this->getOwner() . '/calendar-proxy-read',
  112. 'protected' => true,
  113. ],
  114. ];
  115. if ($this->getName() !== BirthdayService::BIRTHDAY_CALENDAR_URI) {
  116. $acl[] = [
  117. 'privilege' => '{DAV:}write',
  118. 'principal' => $this->getOwner(),
  119. 'protected' => true,
  120. ];
  121. $acl[] = [
  122. 'privilege' => '{DAV:}write',
  123. 'principal' => $this->getOwner() . '/calendar-proxy-write',
  124. 'protected' => true,
  125. ];
  126. } else {
  127. $acl[] = [
  128. 'privilege' => '{DAV:}write-properties',
  129. 'principal' => $this->getOwner(),
  130. 'protected' => true,
  131. ];
  132. $acl[] = [
  133. 'privilege' => '{DAV:}write-properties',
  134. 'principal' => $this->getOwner() . '/calendar-proxy-write',
  135. 'protected' => true,
  136. ];
  137. }
  138. $acl[] = [
  139. 'privilege' => '{DAV:}write-properties',
  140. 'principal' => $this->getOwner() . '/calendar-proxy-read',
  141. 'protected' => true,
  142. ];
  143. if (!$this->isShared()) {
  144. return $acl;
  145. }
  146. if ($this->getOwner() !== parent::getOwner()) {
  147. $acl[] = [
  148. 'privilege' => '{DAV:}read',
  149. 'principal' => parent::getOwner(),
  150. 'protected' => true,
  151. ];
  152. if ($this->canWrite()) {
  153. $acl[] = [
  154. 'privilege' => '{DAV:}write',
  155. 'principal' => parent::getOwner(),
  156. 'protected' => true,
  157. ];
  158. } else {
  159. $acl[] = [
  160. 'privilege' => '{DAV:}write-properties',
  161. 'principal' => parent::getOwner(),
  162. 'protected' => true,
  163. ];
  164. }
  165. }
  166. if ($this->isPublic()) {
  167. $acl[] = [
  168. 'privilege' => '{DAV:}read',
  169. 'principal' => 'principals/system/public',
  170. 'protected' => true,
  171. ];
  172. }
  173. $acl = $this->caldavBackend->applyShareAcl($this->getResourceId(), $acl);
  174. $allowedPrincipals = [
  175. $this->getOwner(),
  176. $this->getOwner() . '/calendar-proxy-read',
  177. $this->getOwner() . '/calendar-proxy-write',
  178. parent::getOwner(),
  179. 'principals/system/public'
  180. ];
  181. /** @var list<array{privilege: string, principal: string, protected: bool}> $acl */
  182. $acl = array_filter($acl, function (array $rule) use ($allowedPrincipals): bool {
  183. return \in_array($rule['principal'], $allowedPrincipals, true);
  184. });
  185. return $acl;
  186. }
  187. public function getChildACL() {
  188. return $this->getACL();
  189. }
  190. public function getOwner(): ?string {
  191. if (isset($this->calendarInfo['{http://owncloud.org/ns}owner-principal'])) {
  192. return $this->calendarInfo['{http://owncloud.org/ns}owner-principal'];
  193. }
  194. return parent::getOwner();
  195. }
  196. public function delete() {
  197. if (isset($this->calendarInfo['{http://owncloud.org/ns}owner-principal']) &&
  198. $this->calendarInfo['{http://owncloud.org/ns}owner-principal'] !== $this->calendarInfo['principaluri']) {
  199. $principal = 'principal:' . parent::getOwner();
  200. $this->caldavBackend->updateShares($this, [], [
  201. $principal
  202. ]);
  203. return;
  204. }
  205. // Remember when a user deleted their birthday calendar
  206. // in order to not regenerate it on the next contacts change
  207. if ($this->getName() === BirthdayService::BIRTHDAY_CALENDAR_URI) {
  208. $principalURI = $this->getPrincipalURI();
  209. $userId = substr($principalURI, 17);
  210. $this->config->setUserValue($userId, 'dav', 'generateBirthdayCalendar', 'no');
  211. }
  212. $this->caldavBackend->deleteCalendar(
  213. $this->calendarInfo['id'],
  214. !$this->useTrashbin
  215. );
  216. }
  217. public function propPatch(PropPatch $propPatch) {
  218. // parent::propPatch will only update calendars table
  219. // if calendar is shared, changes have to be made to the properties table
  220. if (!$this->isShared()) {
  221. parent::propPatch($propPatch);
  222. }
  223. }
  224. public function getChild($name) {
  225. $obj = $this->caldavBackend->getCalendarObject($this->calendarInfo['id'], $name);
  226. if (!$obj) {
  227. throw new NotFound('Calendar object not found');
  228. }
  229. if ($obj['classification'] === CalDavBackend::CLASSIFICATION_PRIVATE && $this->isShared()) {
  230. throw new NotFound('Calendar object not found');
  231. }
  232. $obj['acl'] = $this->getChildACL();
  233. return new CalendarObject($this->caldavBackend, $this->l10n, $this->calendarInfo, $obj);
  234. }
  235. public function getChildren() {
  236. $objs = $this->caldavBackend->getCalendarObjects($this->calendarInfo['id']);
  237. $children = [];
  238. foreach ($objs as $obj) {
  239. if ($obj['classification'] === CalDavBackend::CLASSIFICATION_PRIVATE && $this->isShared()) {
  240. continue;
  241. }
  242. $obj['acl'] = $this->getChildACL();
  243. $children[] = new CalendarObject($this->caldavBackend, $this->l10n, $this->calendarInfo, $obj);
  244. }
  245. return $children;
  246. }
  247. public function getMultipleChildren(array $paths) {
  248. $objs = $this->caldavBackend->getMultipleCalendarObjects($this->calendarInfo['id'], $paths);
  249. $children = [];
  250. foreach ($objs as $obj) {
  251. if ($obj['classification'] === CalDavBackend::CLASSIFICATION_PRIVATE && $this->isShared()) {
  252. continue;
  253. }
  254. $obj['acl'] = $this->getChildACL();
  255. $children[] = new CalendarObject($this->caldavBackend, $this->l10n, $this->calendarInfo, $obj);
  256. }
  257. return $children;
  258. }
  259. public function childExists($name) {
  260. $obj = $this->caldavBackend->getCalendarObject($this->calendarInfo['id'], $name);
  261. if (!$obj) {
  262. return false;
  263. }
  264. if ($obj['classification'] === CalDavBackend::CLASSIFICATION_PRIVATE && $this->isShared()) {
  265. return false;
  266. }
  267. return true;
  268. }
  269. public function calendarQuery(array $filters) {
  270. $uris = $this->caldavBackend->calendarQuery($this->calendarInfo['id'], $filters);
  271. if ($this->isShared()) {
  272. return array_filter($uris, function ($uri) {
  273. return $this->childExists($uri);
  274. });
  275. }
  276. return $uris;
  277. }
  278. /**
  279. * @param boolean $value
  280. * @return string|null
  281. */
  282. public function setPublishStatus($value) {
  283. $publicUri = $this->caldavBackend->setPublishStatus($value, $this);
  284. $this->calendarInfo['publicuri'] = $publicUri;
  285. return $publicUri;
  286. }
  287. /**
  288. * @return mixed $value
  289. */
  290. public function getPublishStatus() {
  291. return $this->caldavBackend->getPublishStatus($this);
  292. }
  293. public function canWrite() {
  294. if ($this->getName() === BirthdayService::BIRTHDAY_CALENDAR_URI) {
  295. return false;
  296. }
  297. if (isset($this->calendarInfo['{http://owncloud.org/ns}read-only'])) {
  298. return !$this->calendarInfo['{http://owncloud.org/ns}read-only'];
  299. }
  300. return true;
  301. }
  302. private function isPublic() {
  303. return isset($this->calendarInfo['{http://owncloud.org/ns}public']);
  304. }
  305. public function isShared() {
  306. if (!isset($this->calendarInfo['{http://owncloud.org/ns}owner-principal'])) {
  307. return false;
  308. }
  309. return $this->calendarInfo['{http://owncloud.org/ns}owner-principal'] !== $this->calendarInfo['principaluri'];
  310. }
  311. public function isSubscription() {
  312. return isset($this->calendarInfo['{http://calendarserver.org/ns/}source']);
  313. }
  314. public function isDeleted(): bool {
  315. if (!isset($this->calendarInfo[TrashbinPlugin::PROPERTY_DELETED_AT])) {
  316. return false;
  317. }
  318. return $this->calendarInfo[TrashbinPlugin::PROPERTY_DELETED_AT] !== null;
  319. }
  320. /**
  321. * @inheritDoc
  322. */
  323. public function getChanges($syncToken, $syncLevel, $limit = null) {
  324. if (!$syncToken && $limit) {
  325. throw new UnsupportedLimitOnInitialSyncException();
  326. }
  327. return parent::getChanges($syncToken, $syncLevel, $limit);
  328. }
  329. /**
  330. * @inheritDoc
  331. */
  332. public function restore(): void {
  333. $this->caldavBackend->restoreCalendar((int)$this->calendarInfo['id']);
  334. }
  335. public function disableTrashbin(): void {
  336. $this->useTrashbin = false;
  337. }
  338. /**
  339. * @inheritDoc
  340. */
  341. public function moveInto($targetName, $sourcePath, INode $sourceNode) {
  342. if (!($sourceNode instanceof CalendarObject)) {
  343. return false;
  344. }
  345. try {
  346. return $this->caldavBackend->moveCalendarObject($sourceNode->getCalendarId(), (int)$this->calendarInfo['id'], $sourceNode->getId(), $sourceNode->getOwner(), $this->getOwner());
  347. } catch (Exception $e) {
  348. $this->logger->error('Could not move calendar object: ' . $e->getMessage(), ['exception' => $e]);
  349. return false;
  350. }
  351. }
  352. }