Plugin.php 23 KB

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