Plugin.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2016, Roeland Jago Douma <roeland@famdouma.nl>
  4. * @copyright Copyright (c) 2016, Joas Schilling <coding@schilljs.com>
  5. *
  6. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  7. * @author Daniel Kesselberg <mail@danielkesselberg.de>
  8. * @author Georg Ehrke <oc.list@georgehrke.com>
  9. * @author Joas Schilling <coding@schilljs.com>
  10. * @author Roeland Jago Douma <roeland@famdouma.nl>
  11. * @author Thomas Citharel <nextcloud@tcit.fr>
  12. * @author Richard Steinmetz <richard@steinmetz.cloud>
  13. *
  14. * @license GNU AGPL version 3 or any later version
  15. *
  16. * This program is free software: you can redistribute it and/or modify
  17. * it under the terms of the GNU Affero General Public License as
  18. * published by the Free Software Foundation, either version 3 of the
  19. * License, or (at your option) any later version.
  20. *
  21. * This program is distributed in the hope that it will be useful,
  22. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  23. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  24. * GNU Affero General Public License for more details.
  25. *
  26. * You should have received a copy of the GNU Affero General Public License
  27. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  28. *
  29. */
  30. namespace OCA\DAV\CalDAV\Schedule;
  31. use DateTimeZone;
  32. use OCA\DAV\CalDAV\CalDavBackend;
  33. use OCA\DAV\CalDAV\Calendar;
  34. use OCA\DAV\CalDAV\CalendarHome;
  35. use OCA\DAV\CalDAV\DefaultCalendarValidator;
  36. use OCP\IConfig;
  37. use Psr\Log\LoggerInterface;
  38. use Sabre\CalDAV\ICalendar;
  39. use Sabre\CalDAV\ICalendarObject;
  40. use Sabre\CalDAV\Schedule\ISchedulingObject;
  41. use Sabre\DAV\Exception as DavException;
  42. use Sabre\DAV\INode;
  43. use Sabre\DAV\IProperties;
  44. use Sabre\DAV\PropFind;
  45. use Sabre\DAV\Server;
  46. use Sabre\DAV\Xml\Property\LocalHref;
  47. use Sabre\DAVACL\IACL;
  48. use Sabre\DAVACL\IPrincipal;
  49. use Sabre\HTTP\RequestInterface;
  50. use Sabre\HTTP\ResponseInterface;
  51. use Sabre\VObject\Component;
  52. use Sabre\VObject\Component\VCalendar;
  53. use Sabre\VObject\Component\VEvent;
  54. use Sabre\VObject\DateTimeParser;
  55. use Sabre\VObject\FreeBusyGenerator;
  56. use Sabre\VObject\ITip;
  57. use Sabre\VObject\ITip\SameOrganizerForAllComponentsException;
  58. use Sabre\VObject\Parameter;
  59. use Sabre\VObject\Property;
  60. use Sabre\VObject\Reader;
  61. use function Sabre\Uri\split;
  62. class Plugin extends \Sabre\CalDAV\Schedule\Plugin {
  63. /**
  64. * @var IConfig
  65. */
  66. private $config;
  67. /** @var ITip\Message[] */
  68. private $schedulingResponses = [];
  69. /** @var string|null */
  70. private $pathOfCalendarObjectChange = null;
  71. public const CALENDAR_USER_TYPE = '{' . self::NS_CALDAV . '}calendar-user-type';
  72. public const SCHEDULE_DEFAULT_CALENDAR_URL = '{' . Plugin::NS_CALDAV . '}schedule-default-calendar-URL';
  73. private LoggerInterface $logger;
  74. private DefaultCalendarValidator $defaultCalendarValidator;
  75. /**
  76. * @param IConfig $config
  77. */
  78. public function __construct(IConfig $config, LoggerInterface $logger, DefaultCalendarValidator $defaultCalendarValidator) {
  79. $this->config = $config;
  80. $this->logger = $logger;
  81. $this->defaultCalendarValidator = $defaultCalendarValidator;
  82. }
  83. /**
  84. * Initializes the plugin
  85. *
  86. * @param Server $server
  87. * @return void
  88. */
  89. public function initialize(Server $server) {
  90. parent::initialize($server);
  91. $server->on('propFind', [$this, 'propFindDefaultCalendarUrl'], 90);
  92. $server->on('afterWriteContent', [$this, 'dispatchSchedulingResponses']);
  93. $server->on('afterCreateFile', [$this, 'dispatchSchedulingResponses']);
  94. // We allow mutating the default calendar URL through the CustomPropertiesBackend
  95. // (oc_properties table)
  96. $server->protectedProperties = array_filter(
  97. $server->protectedProperties,
  98. static fn (string $property) => $property !== self::SCHEDULE_DEFAULT_CALENDAR_URL,
  99. );
  100. }
  101. /**
  102. * Allow manual setting of the object change URL
  103. * to support public write
  104. *
  105. * @param string $path
  106. */
  107. public function setPathOfCalendarObjectChange(string $path): void {
  108. $this->pathOfCalendarObjectChange = $path;
  109. }
  110. /**
  111. * This method handler is invoked during fetching of properties.
  112. *
  113. * We use this event to add calendar-auto-schedule-specific properties.
  114. *
  115. * @param PropFind $propFind
  116. * @param INode $node
  117. * @return void
  118. */
  119. public function propFind(PropFind $propFind, INode $node) {
  120. if ($node instanceof IPrincipal) {
  121. // overwrite Sabre/Dav's implementation
  122. $propFind->handle(self::CALENDAR_USER_TYPE, function () use ($node) {
  123. if ($node instanceof IProperties) {
  124. $props = $node->getProperties([self::CALENDAR_USER_TYPE]);
  125. if (isset($props[self::CALENDAR_USER_TYPE])) {
  126. return $props[self::CALENDAR_USER_TYPE];
  127. }
  128. }
  129. return 'INDIVIDUAL';
  130. });
  131. }
  132. parent::propFind($propFind, $node);
  133. }
  134. /**
  135. * Returns a list of addresses that are associated with a principal.
  136. *
  137. * @param string $principal
  138. * @return array
  139. */
  140. protected function getAddressesForPrincipal($principal) {
  141. $result = parent::getAddressesForPrincipal($principal);
  142. if ($result === null) {
  143. $result = [];
  144. }
  145. // iterate through items and html decode values
  146. foreach ($result as $key => $value) {
  147. $result[$key] = urldecode($value);
  148. }
  149. return $result;
  150. }
  151. /**
  152. * @param RequestInterface $request
  153. * @param ResponseInterface $response
  154. * @param VCalendar $vCal
  155. * @param mixed $calendarPath
  156. * @param mixed $modified
  157. * @param mixed $isNew
  158. */
  159. public function calendarObjectChange(RequestInterface $request, ResponseInterface $response, VCalendar $vCal, $calendarPath, &$modified, $isNew) {
  160. // Save the first path we get as a calendar-object-change request
  161. if (!$this->pathOfCalendarObjectChange) {
  162. $this->pathOfCalendarObjectChange = $request->getPath();
  163. }
  164. try {
  165. parent::calendarObjectChange($request, $response, $vCal, $calendarPath, $modified, $isNew);
  166. } catch (SameOrganizerForAllComponentsException $e) {
  167. $this->handleSameOrganizerException($e, $vCal, $calendarPath);
  168. }
  169. }
  170. /**
  171. * @inheritDoc
  172. */
  173. public function beforeUnbind($path): void {
  174. try {
  175. parent::beforeUnbind($path);
  176. } catch (SameOrganizerForAllComponentsException $e) {
  177. $node = $this->server->tree->getNodeForPath($path);
  178. if (!$node instanceof ICalendarObject || $node instanceof ISchedulingObject) {
  179. throw $e;
  180. }
  181. /** @var VCalendar $vCal */
  182. $vCal = Reader::read($node->get());
  183. $this->handleSameOrganizerException($e, $vCal, $path);
  184. }
  185. }
  186. /**
  187. * @inheritDoc
  188. */
  189. public function scheduleLocalDelivery(ITip\Message $iTipMessage):void {
  190. /** @var VEvent|null $vevent */
  191. $vevent = $iTipMessage->message->VEVENT ?? null;
  192. // Strip VALARMs from incoming VEVENT
  193. if ($vevent && isset($vevent->VALARM)) {
  194. $vevent->remove('VALARM');
  195. }
  196. parent::scheduleLocalDelivery($iTipMessage);
  197. // We only care when the message was successfully delivered locally
  198. // Log all possible codes returned from the parent method that mean something went wrong
  199. // 3.7, 3.8, 5.0, 5.2
  200. if ($iTipMessage->scheduleStatus !== '1.2;Message delivered locally') {
  201. $this->logger->debug('Message not delivered locally with status: ' . $iTipMessage->scheduleStatus);
  202. return;
  203. }
  204. // We only care about request. reply and cancel are properly handled
  205. // by parent::scheduleLocalDelivery already
  206. if (strcasecmp($iTipMessage->method, 'REQUEST') !== 0) {
  207. return;
  208. }
  209. // If parent::scheduleLocalDelivery set scheduleStatus to 1.2,
  210. // it means that it was successfully delivered locally.
  211. // Meaning that the ACL plugin is loaded and that a principal
  212. // exists for the given recipient id, no need to double check
  213. /** @var \Sabre\DAVACL\Plugin $aclPlugin */
  214. $aclPlugin = $this->server->getPlugin('acl');
  215. $principalUri = $aclPlugin->getPrincipalByUri($iTipMessage->recipient);
  216. $calendarUserType = $this->getCalendarUserTypeForPrincipal($principalUri);
  217. if (strcasecmp($calendarUserType, 'ROOM') !== 0 && strcasecmp($calendarUserType, 'RESOURCE') !== 0) {
  218. $this->logger->debug('Calendar user type is room or resource, not processing further');
  219. return;
  220. }
  221. $attendee = $this->getCurrentAttendee($iTipMessage);
  222. if (!$attendee) {
  223. $this->logger->debug('No attendee set for scheduling message');
  224. return;
  225. }
  226. // We only respond when a response was actually requested
  227. $rsvp = $this->getAttendeeRSVP($attendee);
  228. if (!$rsvp) {
  229. $this->logger->debug('No RSVP requested for attendee ' . $attendee->getValue());
  230. return;
  231. }
  232. if (!$vevent) {
  233. $this->logger->debug('No VEVENT set to process on scheduling message');
  234. return;
  235. }
  236. // We don't support autoresponses for recurrencing events for now
  237. if (isset($vevent->RRULE) || isset($vevent->RDATE)) {
  238. $this->logger->debug('VEVENT is a recurring event, autoresponding not supported');
  239. return;
  240. }
  241. $dtstart = $vevent->DTSTART;
  242. $dtend = $this->getDTEndFromVEvent($vevent);
  243. $uid = $vevent->UID->getValue();
  244. $sequence = isset($vevent->SEQUENCE) ? $vevent->SEQUENCE->getValue() : 0;
  245. $recurrenceId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->serialize() : '';
  246. $message = <<<EOF
  247. BEGIN:VCALENDAR
  248. PRODID:-//Nextcloud/Nextcloud CalDAV Server//EN
  249. METHOD:REPLY
  250. VERSION:2.0
  251. BEGIN:VEVENT
  252. ATTENDEE;PARTSTAT=%s:%s
  253. ORGANIZER:%s
  254. UID:%s
  255. SEQUENCE:%s
  256. REQUEST-STATUS:2.0;Success
  257. %sEND:VEVENT
  258. END:VCALENDAR
  259. EOF;
  260. if ($this->isAvailableAtTime($attendee->getValue(), $dtstart->getDateTime(), $dtend->getDateTime(), $uid)) {
  261. $partStat = 'ACCEPTED';
  262. } else {
  263. $partStat = 'DECLINED';
  264. }
  265. $vObject = Reader::read(vsprintf($message, [
  266. $partStat,
  267. $iTipMessage->recipient,
  268. $iTipMessage->sender,
  269. $uid,
  270. $sequence,
  271. $recurrenceId
  272. ]));
  273. $responseITipMessage = new ITip\Message();
  274. $responseITipMessage->uid = $uid;
  275. $responseITipMessage->component = 'VEVENT';
  276. $responseITipMessage->method = 'REPLY';
  277. $responseITipMessage->sequence = $sequence;
  278. $responseITipMessage->sender = $iTipMessage->recipient;
  279. $responseITipMessage->recipient = $iTipMessage->sender;
  280. $responseITipMessage->message = $vObject;
  281. // We can't dispatch them now already, because the organizers calendar-object
  282. // was not yet created. Hence Sabre/DAV won't find a calendar-object, when we
  283. // send our reply.
  284. $this->schedulingResponses[] = $responseITipMessage;
  285. }
  286. /**
  287. * @param string $uri
  288. */
  289. public function dispatchSchedulingResponses(string $uri):void {
  290. if ($uri !== $this->pathOfCalendarObjectChange) {
  291. return;
  292. }
  293. foreach ($this->schedulingResponses as $schedulingResponse) {
  294. $this->scheduleLocalDelivery($schedulingResponse);
  295. }
  296. }
  297. /**
  298. * Always use the personal calendar as target for scheduled events
  299. *
  300. * @param PropFind $propFind
  301. * @param INode $node
  302. * @return void
  303. */
  304. public function propFindDefaultCalendarUrl(PropFind $propFind, INode $node) {
  305. if ($node instanceof IPrincipal) {
  306. $propFind->handle(self::SCHEDULE_DEFAULT_CALENDAR_URL, function () use ($node) {
  307. /** @var \OCA\DAV\CalDAV\Plugin $caldavPlugin */
  308. $caldavPlugin = $this->server->getPlugin('caldav');
  309. $principalUrl = $node->getPrincipalUrl();
  310. $calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl);
  311. if (!$calendarHomePath) {
  312. return null;
  313. }
  314. $isResourceOrRoom = str_starts_with($principalUrl, 'principals/calendar-resources') ||
  315. str_starts_with($principalUrl, 'principals/calendar-rooms');
  316. if (str_starts_with($principalUrl, 'principals/users')) {
  317. [, $userId] = split($principalUrl);
  318. $uri = $this->config->getUserValue($userId, 'dav', 'defaultCalendar', CalDavBackend::PERSONAL_CALENDAR_URI);
  319. $displayName = CalDavBackend::PERSONAL_CALENDAR_NAME;
  320. } elseif ($isResourceOrRoom) {
  321. $uri = CalDavBackend::RESOURCE_BOOKING_CALENDAR_URI;
  322. $displayName = CalDavBackend::RESOURCE_BOOKING_CALENDAR_NAME;
  323. } else {
  324. // How did we end up here?
  325. // TODO - throw exception or just ignore?
  326. return null;
  327. }
  328. /** @var CalendarHome $calendarHome */
  329. $calendarHome = $this->server->tree->getNodeForPath($calendarHomePath);
  330. $currentCalendarDeleted = false;
  331. if (!$calendarHome->childExists($uri) || $currentCalendarDeleted = $this->isCalendarDeleted($calendarHome, $uri)) {
  332. // If the default calendar doesn't exist
  333. if ($isResourceOrRoom) {
  334. // Resources or rooms can't be in the trashbin, so we're fine
  335. $this->createCalendar($calendarHome, $principalUrl, $uri, $displayName);
  336. } else {
  337. // And we're not handling scheduling on resource/room booking
  338. $userCalendars = [];
  339. /**
  340. * If the default calendar of the user isn't set and the
  341. * fallback doesn't match any of the user's calendar
  342. * try to find the first "personal" calendar we can write to
  343. * instead of creating a new one.
  344. * A appropriate personal calendar to receive invites:
  345. * - isn't a calendar subscription
  346. * - user can write to it (no virtual/3rd-party calendars)
  347. * - calendar isn't a share
  348. * - calendar supports VEVENTs
  349. */
  350. foreach ($calendarHome->getChildren() as $node) {
  351. if (!($node instanceof Calendar)) {
  352. continue;
  353. }
  354. try {
  355. $this->defaultCalendarValidator->validateScheduleDefaultCalendar($node);
  356. } catch (DavException $e) {
  357. continue;
  358. }
  359. $userCalendars[] = $node;
  360. }
  361. if (count($userCalendars) > 0) {
  362. // Calendar backend returns calendar by calendarorder property
  363. $uri = $userCalendars[0]->getName();
  364. } else {
  365. // Otherwise if we have really nothing, create a new calendar
  366. if ($currentCalendarDeleted) {
  367. // If the calendar exists but is deleted, we need to purge it first
  368. // This may cause some issues in a non synchronous database setup
  369. $calendar = $this->getCalendar($calendarHome, $uri);
  370. if ($calendar instanceof Calendar) {
  371. $calendar->disableTrashbin();
  372. $calendar->delete();
  373. }
  374. }
  375. $this->createCalendar($calendarHome, $principalUrl, $uri, $displayName);
  376. }
  377. }
  378. }
  379. $result = $this->server->getPropertiesForPath($calendarHomePath . '/' . $uri, [], 1);
  380. if (empty($result)) {
  381. return null;
  382. }
  383. return new LocalHref($result[0]['href']);
  384. });
  385. }
  386. }
  387. /**
  388. * Returns a list of addresses that are associated with a principal.
  389. *
  390. * @param string $principal
  391. * @return string|null
  392. */
  393. protected function getCalendarUserTypeForPrincipal($principal):?string {
  394. $calendarUserType = '{' . self::NS_CALDAV . '}calendar-user-type';
  395. $properties = $this->server->getProperties(
  396. $principal,
  397. [$calendarUserType]
  398. );
  399. // If we can't find this information, we'll stop processing
  400. if (!isset($properties[$calendarUserType])) {
  401. return null;
  402. }
  403. return $properties[$calendarUserType];
  404. }
  405. /**
  406. * @param ITip\Message $iTipMessage
  407. * @return null|Property
  408. */
  409. private function getCurrentAttendee(ITip\Message $iTipMessage):?Property {
  410. /** @var VEvent $vevent */
  411. $vevent = $iTipMessage->message->VEVENT;
  412. $attendees = $vevent->select('ATTENDEE');
  413. foreach ($attendees as $attendee) {
  414. /** @var Property $attendee */
  415. if (strcasecmp($attendee->getValue(), $iTipMessage->recipient) === 0) {
  416. return $attendee;
  417. }
  418. }
  419. return null;
  420. }
  421. /**
  422. * @param Property|null $attendee
  423. * @return bool
  424. */
  425. private function getAttendeeRSVP(?Property $attendee = null):bool {
  426. if ($attendee !== null) {
  427. $rsvp = $attendee->offsetGet('RSVP');
  428. if (($rsvp instanceof Parameter) && (strcasecmp($rsvp->getValue(), 'TRUE') === 0)) {
  429. return true;
  430. }
  431. }
  432. // RFC 5545 3.2.17: default RSVP is false
  433. return false;
  434. }
  435. /**
  436. * @param VEvent $vevent
  437. * @return Property\ICalendar\DateTime
  438. */
  439. private function getDTEndFromVEvent(VEvent $vevent):Property\ICalendar\DateTime {
  440. if (isset($vevent->DTEND)) {
  441. return $vevent->DTEND;
  442. }
  443. if (isset($vevent->DURATION)) {
  444. $isFloating = $vevent->DTSTART->isFloating();
  445. /** @var Property\ICalendar\DateTime $end */
  446. $end = clone $vevent->DTSTART;
  447. $endDateTime = $end->getDateTime();
  448. $endDateTime = $endDateTime->add(DateTimeParser::parse($vevent->DURATION->getValue()));
  449. $end->setDateTime($endDateTime, $isFloating);
  450. return $end;
  451. }
  452. if (!$vevent->DTSTART->hasTime()) {
  453. $isFloating = $vevent->DTSTART->isFloating();
  454. /** @var Property\ICalendar\DateTime $end */
  455. $end = clone $vevent->DTSTART;
  456. $endDateTime = $end->getDateTime();
  457. $endDateTime = $endDateTime->modify('+1 day');
  458. $end->setDateTime($endDateTime, $isFloating);
  459. return $end;
  460. }
  461. return clone $vevent->DTSTART;
  462. }
  463. /**
  464. * @param string $email
  465. * @param \DateTimeInterface $start
  466. * @param \DateTimeInterface $end
  467. * @param string $ignoreUID
  468. * @return bool
  469. */
  470. private function isAvailableAtTime(string $email, \DateTimeInterface $start, \DateTimeInterface $end, string $ignoreUID):bool {
  471. // This method is heavily inspired by Sabre\CalDAV\Schedule\Plugin::scheduleLocalDelivery
  472. // and Sabre\CalDAV\Schedule\Plugin::getFreeBusyForEmail
  473. $aclPlugin = $this->server->getPlugin('acl');
  474. $this->server->removeListener('propFind', [$aclPlugin, 'propFind']);
  475. $result = $aclPlugin->principalSearch(
  476. ['{http://sabredav.org/ns}email-address' => $this->stripOffMailTo($email)],
  477. [
  478. '{DAV:}principal-URL',
  479. '{' . self::NS_CALDAV . '}calendar-home-set',
  480. '{' . self::NS_CALDAV . '}schedule-inbox-URL',
  481. '{http://sabredav.org/ns}email-address',
  482. ]
  483. );
  484. $this->server->on('propFind', [$aclPlugin, 'propFind'], 20);
  485. // Grabbing the calendar list
  486. $objects = [];
  487. $calendarTimeZone = new DateTimeZone('UTC');
  488. $homePath = $result[0][200]['{' . self::NS_CALDAV . '}calendar-home-set']->getHref();
  489. foreach ($this->server->tree->getNodeForPath($homePath)->getChildren() as $node) {
  490. if (!$node instanceof ICalendar) {
  491. continue;
  492. }
  493. // Getting the list of object uris within the time-range
  494. $urls = $node->calendarQuery([
  495. 'name' => 'VCALENDAR',
  496. 'comp-filters' => [
  497. [
  498. 'name' => 'VEVENT',
  499. 'is-not-defined' => false,
  500. 'time-range' => [
  501. 'start' => $start,
  502. 'end' => $end,
  503. ],
  504. 'comp-filters' => [],
  505. 'prop-filters' => [],
  506. ],
  507. [
  508. 'name' => 'VEVENT',
  509. 'is-not-defined' => false,
  510. 'time-range' => null,
  511. 'comp-filters' => [],
  512. 'prop-filters' => [
  513. [
  514. 'name' => 'UID',
  515. 'is-not-defined' => false,
  516. 'time-range' => null,
  517. 'text-match' => [
  518. 'value' => $ignoreUID,
  519. 'negate-condition' => true,
  520. 'collation' => 'i;octet',
  521. ],
  522. 'param-filters' => [],
  523. ],
  524. ]
  525. ],
  526. ],
  527. 'prop-filters' => [],
  528. 'is-not-defined' => false,
  529. 'time-range' => null,
  530. ]);
  531. foreach ($urls as $url) {
  532. $objects[] = $node->getChild($url)->get();
  533. }
  534. }
  535. $inboxProps = $this->server->getProperties(
  536. $result[0][200]['{' . self::NS_CALDAV . '}schedule-inbox-URL']->getHref(),
  537. ['{' . self::NS_CALDAV . '}calendar-availability']
  538. );
  539. $vcalendar = new VCalendar();
  540. $vcalendar->METHOD = 'REPLY';
  541. $generator = new FreeBusyGenerator();
  542. $generator->setObjects($objects);
  543. $generator->setTimeRange($start, $end);
  544. $generator->setBaseObject($vcalendar);
  545. $generator->setTimeZone($calendarTimeZone);
  546. if (isset($inboxProps['{' . self::NS_CALDAV . '}calendar-availability'])) {
  547. $generator->setVAvailability(
  548. Reader::read(
  549. $inboxProps['{' . self::NS_CALDAV . '}calendar-availability']
  550. )
  551. );
  552. }
  553. $result = $generator->getResult();
  554. if (!isset($result->VFREEBUSY)) {
  555. return false;
  556. }
  557. /** @var Component $freeBusyComponent */
  558. $freeBusyComponent = $result->VFREEBUSY;
  559. $freeBusyProperties = $freeBusyComponent->select('FREEBUSY');
  560. // If there is no Free-busy property at all, the time-range is empty and available
  561. if (count($freeBusyProperties) === 0) {
  562. return true;
  563. }
  564. // If more than one Free-Busy property was returned, it means that an event
  565. // starts or ends inside this time-range, so it's not available and we return false
  566. if (count($freeBusyProperties) > 1) {
  567. return false;
  568. }
  569. /** @var Property $freeBusyProperty */
  570. $freeBusyProperty = $freeBusyProperties[0];
  571. if (!$freeBusyProperty->offsetExists('FBTYPE')) {
  572. // If there is no FBTYPE, it means it's busy
  573. return false;
  574. }
  575. $fbTypeParameter = $freeBusyProperty->offsetGet('FBTYPE');
  576. if (!($fbTypeParameter instanceof Parameter)) {
  577. return false;
  578. }
  579. return (strcasecmp($fbTypeParameter->getValue(), 'FREE') === 0);
  580. }
  581. /**
  582. * @param string $email
  583. * @return string
  584. */
  585. private function stripOffMailTo(string $email): string {
  586. if (stripos($email, 'mailto:') === 0) {
  587. return substr($email, 7);
  588. }
  589. return $email;
  590. }
  591. private function getCalendar(CalendarHome $calendarHome, string $uri): INode {
  592. return $calendarHome->getChild($uri);
  593. }
  594. private function isCalendarDeleted(CalendarHome $calendarHome, string $uri): bool {
  595. $calendar = $this->getCalendar($calendarHome, $uri);
  596. return $calendar instanceof Calendar && $calendar->isDeleted();
  597. }
  598. private function createCalendar(CalendarHome $calendarHome, string $principalUri, string $uri, string $displayName): void {
  599. $calendarHome->getCalDAVBackend()->createCalendar($principalUri, $uri, [
  600. '{DAV:}displayname' => $displayName,
  601. ]);
  602. }
  603. /**
  604. * Try to handle the given exception gracefully or throw it if necessary.
  605. *
  606. * @throws SameOrganizerForAllComponentsException If the exception should not be ignored
  607. */
  608. private function handleSameOrganizerException(
  609. SameOrganizerForAllComponentsException $e,
  610. VCalendar $vCal,
  611. string $calendarPath,
  612. ): void {
  613. // This is very hacky! However, we want to allow saving events with multiple
  614. // organizers. Those events are not RFC compliant, but sometimes imported from major
  615. // external calendar services (e.g. Google). If the current user is not an organizer of
  616. // the event we ignore the exception as no scheduling messages will be sent anyway.
  617. // It would be cleaner to patch Sabre to validate organizers *after* checking if
  618. // scheduling messages are necessary. Currently, organizers are validated first and
  619. // afterwards the broker checks if messages should be scheduled. So the code will throw
  620. // even if the organizers are not relevant. This is to ensure compliance with RFCs but
  621. // a bit too strict for real world usage.
  622. if (!isset($vCal->VEVENT)) {
  623. throw $e;
  624. }
  625. $calendarNode = $this->server->tree->getNodeForPath($calendarPath);
  626. if (!($calendarNode instanceof IACL)) {
  627. // Should always be an instance of IACL but just to be sure
  628. throw $e;
  629. }
  630. $addresses = $this->getAddressesForPrincipal($calendarNode->getOwner());
  631. foreach ($vCal->VEVENT as $vevent) {
  632. if (in_array($vevent->ORGANIZER->getNormalizedValue(), $addresses, true)) {
  633. // User is an organizer => throw the exception
  634. throw $e;
  635. }
  636. }
  637. }
  638. }