ReminderService.php 20 KB

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