You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ReminderService.php 21KB

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