Plugin.php 21 KB

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