ReminderService.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * @copyright Copyright (c) 2019, Thomas Citharel
  5. * @copyright Copyright (c) 2019, Georg Ehrke
  6. *
  7. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  8. * @author Georg Ehrke <oc.list@georgehrke.com>
  9. * @author Roeland Jago Douma <roeland@famdouma.nl>
  10. * @author Thomas Citharel <nextcloud@tcit.fr>
  11. *
  12. * @license GNU AGPL version 3 or any later version
  13. *
  14. * This program is free software: you can redistribute it and/or modify
  15. * it under the terms of the GNU Affero General Public License as
  16. * published by the Free Software Foundation, either version 3 of the
  17. * License, or (at your option) any later version.
  18. *
  19. * This program is distributed in the hope that it will be useful,
  20. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  21. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  22. * GNU Affero General Public License for more details.
  23. *
  24. * You should have received a copy of the GNU Affero General Public License
  25. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  26. *
  27. */
  28. namespace OCA\DAV\CalDAV\Reminder;
  29. use DateTimeImmutable;
  30. use OCA\DAV\CalDAV\CalDavBackend;
  31. use OCP\AppFramework\Utility\ITimeFactory;
  32. use OCP\IGroup;
  33. use OCP\IGroupManager;
  34. use OCP\IUser;
  35. use OCP\IUserManager;
  36. use Sabre\VObject;
  37. use Sabre\VObject\Component\VAlarm;
  38. use Sabre\VObject\Component\VEvent;
  39. use Sabre\VObject\ParseException;
  40. use Sabre\VObject\Recur\EventIterator;
  41. use Sabre\VObject\Recur\NoInstancesException;
  42. class ReminderService {
  43. /** @var Backend */
  44. private $backend;
  45. /** @var NotificationProviderManager */
  46. private $notificationProviderManager;
  47. /** @var IUserManager */
  48. private $userManager;
  49. /** @var IGroupManager */
  50. private $groupManager;
  51. /** @var CalDavBackend */
  52. private $caldavBackend;
  53. /** @var ITimeFactory */
  54. private $timeFactory;
  55. public const REMINDER_TYPE_EMAIL = 'EMAIL';
  56. public const REMINDER_TYPE_DISPLAY = 'DISPLAY';
  57. public const REMINDER_TYPE_AUDIO = 'AUDIO';
  58. /**
  59. * @var String[]
  60. *
  61. * Official RFC5545 reminder types
  62. */
  63. public const REMINDER_TYPES = [
  64. self::REMINDER_TYPE_EMAIL,
  65. self::REMINDER_TYPE_DISPLAY,
  66. self::REMINDER_TYPE_AUDIO
  67. ];
  68. /**
  69. * ReminderService constructor.
  70. *
  71. * @param Backend $backend
  72. * @param NotificationProviderManager $notificationProviderManager
  73. * @param IUserManager $userManager
  74. * @param IGroupManager $groupManager
  75. * @param CalDavBackend $caldavBackend
  76. * @param ITimeFactory $timeFactory
  77. */
  78. public function __construct(Backend $backend,
  79. NotificationProviderManager $notificationProviderManager,
  80. IUserManager $userManager,
  81. IGroupManager $groupManager,
  82. CalDavBackend $caldavBackend,
  83. ITimeFactory $timeFactory) {
  84. $this->backend = $backend;
  85. $this->notificationProviderManager = $notificationProviderManager;
  86. $this->userManager = $userManager;
  87. $this->groupManager = $groupManager;
  88. $this->caldavBackend = $caldavBackend;
  89. $this->timeFactory = $timeFactory;
  90. }
  91. /**
  92. * Process reminders to activate
  93. *
  94. * @throws NotificationProvider\ProviderNotAvailableException
  95. * @throws NotificationTypeDoesNotExistException
  96. */
  97. public function processReminders():void {
  98. $reminders = $this->backend->getRemindersToProcess();
  99. foreach ($reminders as $reminder) {
  100. $calendarData = is_resource($reminder['calendardata'])
  101. ? stream_get_contents($reminder['calendardata'])
  102. : $reminder['calendardata'];
  103. $vcalendar = $this->parseCalendarData($calendarData);
  104. if (!$vcalendar) {
  105. $this->backend->removeReminder($reminder['id']);
  106. continue;
  107. }
  108. $vevent = $this->getVEventByRecurrenceId($vcalendar, $reminder['recurrence_id'], $reminder['is_recurrence_exception']);
  109. if (!$vevent) {
  110. $this->backend->removeReminder($reminder['id']);
  111. continue;
  112. }
  113. if ($this->wasEventCancelled($vevent)) {
  114. $this->deleteOrProcessNext($reminder, $vevent);
  115. continue;
  116. }
  117. if (!$this->notificationProviderManager->hasProvider($reminder['type'])) {
  118. $this->deleteOrProcessNext($reminder, $vevent);
  119. continue;
  120. }
  121. $users = $this->getAllUsersWithWriteAccessToCalendar($reminder['calendar_id']);
  122. $user = $this->getUserFromPrincipalURI($reminder['principaluri']);
  123. if ($user) {
  124. $users[] = $user;
  125. }
  126. $notificationProvider = $this->notificationProviderManager->getProvider($reminder['type']);
  127. $notificationProvider->send($vevent, $reminder['displayname'], $users);
  128. $this->deleteOrProcessNext($reminder, $vevent);
  129. }
  130. }
  131. /**
  132. * @param string $action
  133. * @param array $objectData
  134. * @throws VObject\InvalidDataException
  135. */
  136. public function onTouchCalendarObject(string $action,
  137. array $objectData):void {
  138. // We only support VEvents for now
  139. if (strcasecmp($objectData['component'], 'vevent') !== 0) {
  140. return;
  141. }
  142. switch ($action) {
  143. case '\OCA\DAV\CalDAV\CalDavBackend::createCalendarObject':
  144. $this->onCalendarObjectCreate($objectData);
  145. break;
  146. case '\OCA\DAV\CalDAV\CalDavBackend::updateCalendarObject':
  147. $this->onCalendarObjectEdit($objectData);
  148. break;
  149. case '\OCA\DAV\CalDAV\CalDavBackend::deleteCalendarObject':
  150. $this->onCalendarObjectDelete($objectData);
  151. break;
  152. default:
  153. break;
  154. }
  155. }
  156. /**
  157. * @param array $objectData
  158. */
  159. private function onCalendarObjectCreate(array $objectData):void {
  160. $calendarData = is_resource($objectData['calendardata'])
  161. ? stream_get_contents($objectData['calendardata'])
  162. : $objectData['calendardata'];
  163. /** @var VObject\Component\VCalendar $vcalendar */
  164. $vcalendar = $this->parseCalendarData($calendarData);
  165. if (!$vcalendar) {
  166. return;
  167. }
  168. $vevents = $this->getAllVEventsFromVCalendar($vcalendar);
  169. if (count($vevents) === 0) {
  170. return;
  171. }
  172. $uid = (string) $vevents[0]->UID;
  173. $recurrenceExceptions = $this->getRecurrenceExceptionFromListOfVEvents($vevents);
  174. $masterItem = $this->getMasterItemFromListOfVEvents($vevents);
  175. $now = $this->timeFactory->getDateTime();
  176. $isRecurring = $masterItem ? $this->isRecurring($masterItem) : false;
  177. foreach ($recurrenceExceptions as $recurrenceException) {
  178. $eventHash = $this->getEventHash($recurrenceException);
  179. if (!isset($recurrenceException->VALARM)) {
  180. continue;
  181. }
  182. foreach ($recurrenceException->VALARM as $valarm) {
  183. /** @var VAlarm $valarm */
  184. $alarmHash = $this->getAlarmHash($valarm);
  185. $triggerTime = $valarm->getEffectiveTriggerTime();
  186. $diff = $now->diff($triggerTime);
  187. if ($diff->invert === 1) {
  188. continue;
  189. }
  190. $alarms = $this->getRemindersForVAlarm($valarm, $objectData,
  191. $eventHash, $alarmHash, true, true);
  192. $this->writeRemindersToDatabase($alarms);
  193. }
  194. }
  195. if ($masterItem) {
  196. $processedAlarms = [];
  197. $masterAlarms = [];
  198. $masterHash = $this->getEventHash($masterItem);
  199. if (!isset($masterItem->VALARM)) {
  200. return;
  201. }
  202. foreach ($masterItem->VALARM as $valarm) {
  203. $masterAlarms[] = $this->getAlarmHash($valarm);
  204. }
  205. try {
  206. $iterator = new EventIterator($vevents, $uid);
  207. } catch (NoInstancesException $e) {
  208. // This event is recurring, but it doesn't have a single
  209. // instance. We are skipping this event from the output
  210. // entirely.
  211. return;
  212. }
  213. while ($iterator->valid() && count($processedAlarms) < count($masterAlarms)) {
  214. $event = $iterator->getEventObject();
  215. // Recurrence-exceptions are handled separately, so just ignore them here
  216. if (\in_array($event, $recurrenceExceptions, true)) {
  217. $iterator->next();
  218. continue;
  219. }
  220. foreach ($event->VALARM as $valarm) {
  221. /** @var VAlarm $valarm */
  222. $alarmHash = $this->getAlarmHash($valarm);
  223. if (\in_array($alarmHash, $processedAlarms, true)) {
  224. continue;
  225. }
  226. if (!\in_array((string) $valarm->ACTION, self::REMINDER_TYPES, true)) {
  227. // Action allows x-name, we don't insert reminders
  228. // into the database if they are not standard
  229. $processedAlarms[] = $alarmHash;
  230. continue;
  231. }
  232. $triggerTime = $valarm->getEffectiveTriggerTime();
  233. // If effective trigger time is in the past
  234. // just skip and generate for next event
  235. $diff = $now->diff($triggerTime);
  236. if ($diff->invert === 1) {
  237. // If an absolute alarm is in the past,
  238. // just add it to processedAlarms, so
  239. // we don't extend till eternity
  240. if (!$this->isAlarmRelative($valarm)) {
  241. $processedAlarms[] = $alarmHash;
  242. }
  243. continue;
  244. }
  245. $alarms = $this->getRemindersForVAlarm($valarm, $objectData, $masterHash, $alarmHash, $isRecurring, false);
  246. $this->writeRemindersToDatabase($alarms);
  247. $processedAlarms[] = $alarmHash;
  248. }
  249. $iterator->next();
  250. }
  251. }
  252. }
  253. /**
  254. * @param array $objectData
  255. */
  256. private function onCalendarObjectEdit(array $objectData):void {
  257. // TODO - this can be vastly improved
  258. // - get cached reminders
  259. // - ...
  260. $this->onCalendarObjectDelete($objectData);
  261. $this->onCalendarObjectCreate($objectData);
  262. }
  263. /**
  264. * @param array $objectData
  265. */
  266. private function onCalendarObjectDelete(array $objectData):void {
  267. $this->backend->cleanRemindersForEvent((int) $objectData['id']);
  268. }
  269. /**
  270. * @param VAlarm $valarm
  271. * @param array $objectData
  272. * @param string|null $eventHash
  273. * @param string|null $alarmHash
  274. * @param bool $isRecurring
  275. * @param bool $isRecurrenceException
  276. * @return array
  277. */
  278. private function getRemindersForVAlarm(VAlarm $valarm,
  279. array $objectData,
  280. string $eventHash=null,
  281. string $alarmHash=null,
  282. bool $isRecurring=false,
  283. bool $isRecurrenceException=false):array {
  284. if ($eventHash === null) {
  285. $eventHash = $this->getEventHash($valarm->parent);
  286. }
  287. if ($alarmHash === null) {
  288. $alarmHash = $this->getAlarmHash($valarm);
  289. }
  290. $recurrenceId = $this->getEffectiveRecurrenceIdOfVEvent($valarm->parent);
  291. $isRelative = $this->isAlarmRelative($valarm);
  292. /** @var DateTimeImmutable $notificationDate */
  293. $notificationDate = $valarm->getEffectiveTriggerTime();
  294. $clonedNotificationDate = new \DateTime('now', $notificationDate->getTimezone());
  295. $clonedNotificationDate->setTimestamp($notificationDate->getTimestamp());
  296. $alarms = [];
  297. $alarms[] = [
  298. 'calendar_id' => $objectData['calendarid'],
  299. 'object_id' => $objectData['id'],
  300. 'uid' => (string) $valarm->parent->UID,
  301. 'is_recurring' => $isRecurring,
  302. 'recurrence_id' => $recurrenceId,
  303. 'is_recurrence_exception' => $isRecurrenceException,
  304. 'event_hash' => $eventHash,
  305. 'alarm_hash' => $alarmHash,
  306. 'type' => (string) $valarm->ACTION,
  307. 'is_relative' => $isRelative,
  308. 'notification_date' => $notificationDate->getTimestamp(),
  309. 'is_repeat_based' => false,
  310. ];
  311. $repeat = isset($valarm->REPEAT) ? (int) $valarm->REPEAT->getValue() : 0;
  312. for ($i = 0; $i < $repeat; $i++) {
  313. if ($valarm->DURATION === null) {
  314. continue;
  315. }
  316. $clonedNotificationDate->add($valarm->DURATION->getDateInterval());
  317. $alarms[] = [
  318. 'calendar_id' => $objectData['calendarid'],
  319. 'object_id' => $objectData['id'],
  320. 'uid' => (string) $valarm->parent->UID,
  321. 'is_recurring' => $isRecurring,
  322. 'recurrence_id' => $recurrenceId,
  323. 'is_recurrence_exception' => $isRecurrenceException,
  324. 'event_hash' => $eventHash,
  325. 'alarm_hash' => $alarmHash,
  326. 'type' => (string) $valarm->ACTION,
  327. 'is_relative' => $isRelative,
  328. 'notification_date' => $clonedNotificationDate->getTimestamp(),
  329. 'is_repeat_based' => true,
  330. ];
  331. }
  332. return $alarms;
  333. }
  334. /**
  335. * @param array $reminders
  336. */
  337. private function writeRemindersToDatabase(array $reminders): void {
  338. foreach ($reminders as $reminder) {
  339. $this->backend->insertReminder(
  340. (int) $reminder['calendar_id'],
  341. (int) $reminder['object_id'],
  342. $reminder['uid'],
  343. $reminder['is_recurring'],
  344. (int) $reminder['recurrence_id'],
  345. $reminder['is_recurrence_exception'],
  346. $reminder['event_hash'],
  347. $reminder['alarm_hash'],
  348. $reminder['type'],
  349. $reminder['is_relative'],
  350. (int) $reminder['notification_date'],
  351. $reminder['is_repeat_based']
  352. );
  353. }
  354. }
  355. /**
  356. * @param array $reminder
  357. * @param VEvent $vevent
  358. */
  359. private function deleteOrProcessNext(array $reminder,
  360. VObject\Component\VEvent $vevent):void {
  361. if ($reminder['is_repeat_based'] ||
  362. !$reminder['is_recurring'] ||
  363. !$reminder['is_relative'] ||
  364. $reminder['is_recurrence_exception']) {
  365. $this->backend->removeReminder($reminder['id']);
  366. return;
  367. }
  368. $vevents = $this->getAllVEventsFromVCalendar($vevent->parent);
  369. $recurrenceExceptions = $this->getRecurrenceExceptionFromListOfVEvents($vevents);
  370. $now = $this->timeFactory->getDateTime();
  371. try {
  372. $iterator = new EventIterator($vevents, $reminder['uid']);
  373. } catch (NoInstancesException $e) {
  374. // This event is recurring, but it doesn't have a single
  375. // instance. We are skipping this event from the output
  376. // entirely.
  377. return;
  378. }
  379. while ($iterator->valid()) {
  380. $event = $iterator->getEventObject();
  381. // Recurrence-exceptions are handled separately, so just ignore them here
  382. if (\in_array($event, $recurrenceExceptions, true)) {
  383. $iterator->next();
  384. continue;
  385. }
  386. $recurrenceId = $this->getEffectiveRecurrenceIdOfVEvent($event);
  387. if ($reminder['recurrence_id'] >= $recurrenceId) {
  388. $iterator->next();
  389. continue;
  390. }
  391. foreach ($event->VALARM as $valarm) {
  392. /** @var VAlarm $valarm */
  393. $alarmHash = $this->getAlarmHash($valarm);
  394. if ($alarmHash !== $reminder['alarm_hash']) {
  395. continue;
  396. }
  397. $triggerTime = $valarm->getEffectiveTriggerTime();
  398. // If effective trigger time is in the past
  399. // just skip and generate for next event
  400. $diff = $now->diff($triggerTime);
  401. if ($diff->invert === 1) {
  402. continue;
  403. }
  404. $this->backend->removeReminder($reminder['id']);
  405. $alarms = $this->getRemindersForVAlarm($valarm, [
  406. 'calendarid' => $reminder['calendar_id'],
  407. 'id' => $reminder['object_id'],
  408. ], $reminder['event_hash'], $alarmHash, true, false);
  409. $this->writeRemindersToDatabase($alarms);
  410. // Abort generating reminders after creating one successfully
  411. return;
  412. }
  413. $iterator->next();
  414. }
  415. $this->backend->removeReminder($reminder['id']);
  416. }
  417. /**
  418. * @param int $calendarId
  419. * @return IUser[]
  420. */
  421. private function getAllUsersWithWriteAccessToCalendar(int $calendarId):array {
  422. $shares = $this->caldavBackend->getShares($calendarId);
  423. $users = [];
  424. $userIds = [];
  425. $groups = [];
  426. foreach ($shares as $share) {
  427. // Only consider writable shares
  428. if ($share['readOnly']) {
  429. continue;
  430. }
  431. $principal = explode('/', $share['{http://owncloud.org/ns}principal']);
  432. if ($principal[1] === 'users') {
  433. $user = $this->userManager->get($principal[2]);
  434. if ($user) {
  435. $users[] = $user;
  436. $userIds[] = $principal[2];
  437. }
  438. } elseif ($principal[1] === 'groups') {
  439. $groups[] = $principal[2];
  440. }
  441. }
  442. foreach ($groups as $gid) {
  443. $group = $this->groupManager->get($gid);
  444. if ($group instanceof IGroup) {
  445. foreach ($group->getUsers() as $user) {
  446. if (!\in_array($user->getUID(), $userIds, true)) {
  447. $users[] = $user;
  448. $userIds[] = $user->getUID();
  449. }
  450. }
  451. }
  452. }
  453. return $users;
  454. }
  455. /**
  456. * Gets a hash of the event.
  457. * If the hash changes, we have to update all relative alarms.
  458. *
  459. * @param VEvent $vevent
  460. * @return string
  461. */
  462. private function getEventHash(VEvent $vevent):string {
  463. $properties = [
  464. (string) $vevent->DTSTART->serialize(),
  465. ];
  466. if ($vevent->DTEND) {
  467. $properties[] = (string) $vevent->DTEND->serialize();
  468. }
  469. if ($vevent->DURATION) {
  470. $properties[] = (string) $vevent->DURATION->serialize();
  471. }
  472. if ($vevent->{'RECURRENCE-ID'}) {
  473. $properties[] = (string) $vevent->{'RECURRENCE-ID'}->serialize();
  474. }
  475. if ($vevent->RRULE) {
  476. $properties[] = (string) $vevent->RRULE->serialize();
  477. }
  478. if ($vevent->EXDATE) {
  479. $properties[] = (string) $vevent->EXDATE->serialize();
  480. }
  481. if ($vevent->RDATE) {
  482. $properties[] = (string) $vevent->RDATE->serialize();
  483. }
  484. return md5(implode('::', $properties));
  485. }
  486. /**
  487. * Gets a hash of the alarm.
  488. * If the hash changes, we have to update oc_dav_reminders.
  489. *
  490. * @param VAlarm $valarm
  491. * @return string
  492. */
  493. private function getAlarmHash(VAlarm $valarm):string {
  494. $properties = [
  495. (string) $valarm->ACTION->serialize(),
  496. (string) $valarm->TRIGGER->serialize(),
  497. ];
  498. if ($valarm->DURATION) {
  499. $properties[] = (string) $valarm->DURATION->serialize();
  500. }
  501. if ($valarm->REPEAT) {
  502. $properties[] = (string) $valarm->REPEAT->serialize();
  503. }
  504. return md5(implode('::', $properties));
  505. }
  506. /**
  507. * @param VObject\Component\VCalendar $vcalendar
  508. * @param int $recurrenceId
  509. * @param bool $isRecurrenceException
  510. * @return VEvent|null
  511. */
  512. private function getVEventByRecurrenceId(VObject\Component\VCalendar $vcalendar,
  513. int $recurrenceId,
  514. bool $isRecurrenceException):?VEvent {
  515. $vevents = $this->getAllVEventsFromVCalendar($vcalendar);
  516. if (count($vevents) === 0) {
  517. return null;
  518. }
  519. $uid = (string) $vevents[0]->UID;
  520. $recurrenceExceptions = $this->getRecurrenceExceptionFromListOfVEvents($vevents);
  521. $masterItem = $this->getMasterItemFromListOfVEvents($vevents);
  522. // Handle recurrence-exceptions first, because recurrence-expansion is expensive
  523. if ($isRecurrenceException) {
  524. foreach ($recurrenceExceptions as $recurrenceException) {
  525. if ($this->getEffectiveRecurrenceIdOfVEvent($recurrenceException) === $recurrenceId) {
  526. return $recurrenceException;
  527. }
  528. }
  529. return null;
  530. }
  531. if ($masterItem) {
  532. try {
  533. $iterator = new EventIterator($vevents, $uid);
  534. } catch (NoInstancesException $e) {
  535. // This event is recurring, but it doesn't have a single
  536. // instance. We are skipping this event from the output
  537. // entirely.
  538. return null;
  539. }
  540. while ($iterator->valid()) {
  541. $event = $iterator->getEventObject();
  542. // Recurrence-exceptions are handled separately, so just ignore them here
  543. if (\in_array($event, $recurrenceExceptions, true)) {
  544. $iterator->next();
  545. continue;
  546. }
  547. if ($this->getEffectiveRecurrenceIdOfVEvent($event) === $recurrenceId) {
  548. return $event;
  549. }
  550. $iterator->next();
  551. }
  552. }
  553. return null;
  554. }
  555. /**
  556. * @param VEvent $vevent
  557. * @return string
  558. */
  559. private function getStatusOfEvent(VEvent $vevent):string {
  560. if ($vevent->STATUS) {
  561. return (string) $vevent->STATUS;
  562. }
  563. // Doesn't say so in the standard,
  564. // but we consider events without a status
  565. // to be confirmed
  566. return 'CONFIRMED';
  567. }
  568. /**
  569. * @param VObject\Component\VEvent $vevent
  570. * @return bool
  571. */
  572. private function wasEventCancelled(VObject\Component\VEvent $vevent):bool {
  573. return $this->getStatusOfEvent($vevent) === 'CANCELLED';
  574. }
  575. /**
  576. * @param string $calendarData
  577. * @return VObject\Component\VCalendar|null
  578. */
  579. private function parseCalendarData(string $calendarData):?VObject\Component\VCalendar {
  580. try {
  581. return VObject\Reader::read($calendarData,
  582. VObject\Reader::OPTION_FORGIVING);
  583. } catch (ParseException $ex) {
  584. return null;
  585. }
  586. }
  587. /**
  588. * @param string $principalUri
  589. * @return IUser|null
  590. */
  591. private function getUserFromPrincipalURI(string $principalUri):?IUser {
  592. if (!$principalUri) {
  593. return null;
  594. }
  595. if (stripos($principalUri, 'principals/users/') !== 0) {
  596. return null;
  597. }
  598. $userId = substr($principalUri, 17);
  599. return $this->userManager->get($userId);
  600. }
  601. /**
  602. * @param VObject\Component\VCalendar $vcalendar
  603. * @return VObject\Component\VEvent[]
  604. */
  605. private function getAllVEventsFromVCalendar(VObject\Component\VCalendar $vcalendar):array {
  606. $vevents = [];
  607. foreach ($vcalendar->children() as $child) {
  608. if (!($child instanceof VObject\Component)) {
  609. continue;
  610. }
  611. if ($child->name !== 'VEVENT') {
  612. continue;
  613. }
  614. $vevents[] = $child;
  615. }
  616. return $vevents;
  617. }
  618. /**
  619. * @param array $vevents
  620. * @return VObject\Component\VEvent[]
  621. */
  622. private function getRecurrenceExceptionFromListOfVEvents(array $vevents):array {
  623. return array_values(array_filter($vevents, function (VEvent $vevent) {
  624. return $vevent->{'RECURRENCE-ID'} !== null;
  625. }));
  626. }
  627. /**
  628. * @param array $vevents
  629. * @return VEvent|null
  630. */
  631. private function getMasterItemFromListOfVEvents(array $vevents):?VEvent {
  632. $elements = array_values(array_filter($vevents, function (VEvent $vevent) {
  633. return $vevent->{'RECURRENCE-ID'} === null;
  634. }));
  635. if (count($elements) === 0) {
  636. return null;
  637. }
  638. if (count($elements) > 1) {
  639. throw new \TypeError('Multiple master objects');
  640. }
  641. return $elements[0];
  642. }
  643. /**
  644. * @param VAlarm $valarm
  645. * @return bool
  646. */
  647. private function isAlarmRelative(VAlarm $valarm):bool {
  648. $trigger = $valarm->TRIGGER;
  649. return $trigger instanceof VObject\Property\ICalendar\Duration;
  650. }
  651. /**
  652. * @param VEvent $vevent
  653. * @return int
  654. */
  655. private function getEffectiveRecurrenceIdOfVEvent(VEvent $vevent):int {
  656. if (isset($vevent->{'RECURRENCE-ID'})) {
  657. return $vevent->{'RECURRENCE-ID'}->getDateTime()->getTimestamp();
  658. }
  659. return $vevent->DTSTART->getDateTime()->getTimestamp();
  660. }
  661. /**
  662. * @param VEvent $vevent
  663. * @return bool
  664. */
  665. private function isRecurring(VEvent $vevent):bool {
  666. return isset($vevent->RRULE) || isset($vevent->RDATE);
  667. }
  668. }