Calendar.php 12 KB

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