Calendar.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  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. $this->caldavBackend->updateShares($this, [], [
  221. $principal
  222. ]);
  223. return;
  224. }
  225. // Remember when a user deleted their birthday calendar
  226. // in order to not regenerate it on the next contacts change
  227. if ($this->getName() === BirthdayService::BIRTHDAY_CALENDAR_URI) {
  228. $principalURI = $this->getPrincipalURI();
  229. $userId = substr($principalURI, 17);
  230. $this->config->setUserValue($userId, 'dav', 'generateBirthdayCalendar', 'no');
  231. }
  232. $this->caldavBackend->deleteCalendar(
  233. $this->calendarInfo['id'],
  234. !$this->useTrashbin
  235. );
  236. }
  237. public function propPatch(PropPatch $propPatch) {
  238. // parent::propPatch will only update calendars table
  239. // if calendar is shared, changes have to be made to the properties table
  240. if (!$this->isShared()) {
  241. parent::propPatch($propPatch);
  242. }
  243. }
  244. public function getChild($name) {
  245. $obj = $this->caldavBackend->getCalendarObject($this->calendarInfo['id'], $name);
  246. if (!$obj) {
  247. throw new NotFound('Calendar object not found');
  248. }
  249. if ($obj['classification'] === CalDavBackend::CLASSIFICATION_PRIVATE && $this->isShared()) {
  250. throw new NotFound('Calendar object not found');
  251. }
  252. $obj['acl'] = $this->getChildACL();
  253. return new CalendarObject($this->caldavBackend, $this->l10n, $this->calendarInfo, $obj);
  254. }
  255. public function getChildren() {
  256. $objs = $this->caldavBackend->getCalendarObjects($this->calendarInfo['id']);
  257. $children = [];
  258. foreach ($objs as $obj) {
  259. if ($obj['classification'] === CalDavBackend::CLASSIFICATION_PRIVATE && $this->isShared()) {
  260. continue;
  261. }
  262. $obj['acl'] = $this->getChildACL();
  263. $children[] = new CalendarObject($this->caldavBackend, $this->l10n, $this->calendarInfo, $obj);
  264. }
  265. return $children;
  266. }
  267. public function getMultipleChildren(array $paths) {
  268. $objs = $this->caldavBackend->getMultipleCalendarObjects($this->calendarInfo['id'], $paths);
  269. $children = [];
  270. foreach ($objs as $obj) {
  271. if ($obj['classification'] === CalDavBackend::CLASSIFICATION_PRIVATE && $this->isShared()) {
  272. continue;
  273. }
  274. $obj['acl'] = $this->getChildACL();
  275. $children[] = new CalendarObject($this->caldavBackend, $this->l10n, $this->calendarInfo, $obj);
  276. }
  277. return $children;
  278. }
  279. public function childExists($name) {
  280. $obj = $this->caldavBackend->getCalendarObject($this->calendarInfo['id'], $name);
  281. if (!$obj) {
  282. return false;
  283. }
  284. if ($obj['classification'] === CalDavBackend::CLASSIFICATION_PRIVATE && $this->isShared()) {
  285. return false;
  286. }
  287. return true;
  288. }
  289. public function calendarQuery(array $filters) {
  290. $uris = $this->caldavBackend->calendarQuery($this->calendarInfo['id'], $filters);
  291. if ($this->isShared()) {
  292. return array_filter($uris, function ($uri) {
  293. return $this->childExists($uri);
  294. });
  295. }
  296. return $uris;
  297. }
  298. /**
  299. * @param boolean $value
  300. * @return string|null
  301. */
  302. public function setPublishStatus($value) {
  303. $publicUri = $this->caldavBackend->setPublishStatus($value, $this);
  304. $this->calendarInfo['publicuri'] = $publicUri;
  305. return $publicUri;
  306. }
  307. /**
  308. * @return mixed $value
  309. */
  310. public function getPublishStatus() {
  311. return $this->caldavBackend->getPublishStatus($this);
  312. }
  313. public function canWrite() {
  314. if ($this->getName() === BirthdayService::BIRTHDAY_CALENDAR_URI) {
  315. return false;
  316. }
  317. if (isset($this->calendarInfo['{http://owncloud.org/ns}read-only'])) {
  318. return !$this->calendarInfo['{http://owncloud.org/ns}read-only'];
  319. }
  320. return true;
  321. }
  322. private function isPublic() {
  323. return isset($this->calendarInfo['{http://owncloud.org/ns}public']);
  324. }
  325. public function isShared() {
  326. if (!isset($this->calendarInfo['{http://owncloud.org/ns}owner-principal'])) {
  327. return false;
  328. }
  329. return $this->calendarInfo['{http://owncloud.org/ns}owner-principal'] !== $this->calendarInfo['principaluri'];
  330. }
  331. public function isSubscription() {
  332. return isset($this->calendarInfo['{http://calendarserver.org/ns/}source']);
  333. }
  334. public function isDeleted(): bool {
  335. if (!isset($this->calendarInfo[TrashbinPlugin::PROPERTY_DELETED_AT])) {
  336. return false;
  337. }
  338. return $this->calendarInfo[TrashbinPlugin::PROPERTY_DELETED_AT] !== null;
  339. }
  340. /**
  341. * @inheritDoc
  342. */
  343. public function getChanges($syncToken, $syncLevel, $limit = null) {
  344. if (!$syncToken && $limit) {
  345. throw new UnsupportedLimitOnInitialSyncException();
  346. }
  347. return parent::getChanges($syncToken, $syncLevel, $limit);
  348. }
  349. /**
  350. * @inheritDoc
  351. */
  352. public function restore(): void {
  353. $this->caldavBackend->restoreCalendar((int) $this->calendarInfo['id']);
  354. }
  355. public function disableTrashbin(): void {
  356. $this->useTrashbin = false;
  357. }
  358. /**
  359. * @inheritDoc
  360. */
  361. public function moveInto($targetName, $sourcePath, INode $sourceNode) {
  362. if (!($sourceNode instanceof CalendarObject)) {
  363. return false;
  364. }
  365. try {
  366. return $this->caldavBackend->moveCalendarObject($sourceNode->getCalendarId(), (int)$this->calendarInfo['id'], $sourceNode->getId(), $sourceNode->getOwner(), $this->getOwner());
  367. } catch (Exception $e) {
  368. $this->logger->error('Could not move calendar object: ' . $e->getMessage(), ['exception' => $e]);
  369. return false;
  370. }
  371. }
  372. }