Passed
Push — master ( 39c67d...50b380 )
by Roeland
16:13 queued 18s
created
apps/dav/lib/CalDAV/Reminder/ReminderService.php 1 patch
Indentation   +737 added lines, -737 removed lines patch added patch discarded remove patch
@@ -47,741 +47,741 @@
 block discarded – undo
47 47
 
48 48
 class ReminderService {
49 49
 
50
-	/** @var Backend */
51
-	private $backend;
52
-
53
-	/** @var NotificationProviderManager */
54
-	private $notificationProviderManager;
55
-
56
-	/** @var IUserManager */
57
-	private $userManager;
58
-
59
-	/** @var IGroupManager */
60
-	private $groupManager;
61
-
62
-	/** @var CalDavBackend */
63
-	private $caldavBackend;
64
-
65
-	/** @var ITimeFactory */
66
-	private $timeFactory;
67
-
68
-	public const REMINDER_TYPE_EMAIL = 'EMAIL';
69
-	public const REMINDER_TYPE_DISPLAY = 'DISPLAY';
70
-	public const REMINDER_TYPE_AUDIO = 'AUDIO';
71
-
72
-	/**
73
-	 * @var String[]
74
-	 *
75
-	 * Official RFC5545 reminder types
76
-	 */
77
-	public const REMINDER_TYPES = [
78
-		self::REMINDER_TYPE_EMAIL,
79
-		self::REMINDER_TYPE_DISPLAY,
80
-		self::REMINDER_TYPE_AUDIO
81
-	];
82
-
83
-	/**
84
-	 * ReminderService constructor.
85
-	 *
86
-	 * @param Backend $backend
87
-	 * @param NotificationProviderManager $notificationProviderManager
88
-	 * @param IUserManager $userManager
89
-	 * @param IGroupManager $groupManager
90
-	 * @param CalDavBackend $caldavBackend
91
-	 * @param ITimeFactory $timeFactory
92
-	 */
93
-	public function __construct(Backend $backend,
94
-								NotificationProviderManager $notificationProviderManager,
95
-								IUserManager $userManager,
96
-								IGroupManager $groupManager,
97
-								CalDavBackend $caldavBackend,
98
-								ITimeFactory $timeFactory) {
99
-		$this->backend = $backend;
100
-		$this->notificationProviderManager = $notificationProviderManager;
101
-		$this->userManager = $userManager;
102
-		$this->groupManager = $groupManager;
103
-		$this->caldavBackend = $caldavBackend;
104
-		$this->timeFactory = $timeFactory;
105
-	}
106
-
107
-	/**
108
-	 * Process reminders to activate
109
-	 *
110
-	 * @throws NotificationProvider\ProviderNotAvailableException
111
-	 * @throws NotificationTypeDoesNotExistException
112
-	 */
113
-	public function processReminders():void {
114
-		$reminders = $this->backend->getRemindersToProcess();
115
-
116
-		foreach ($reminders as $reminder) {
117
-			$calendarData = is_resource($reminder['calendardata'])
118
-				? stream_get_contents($reminder['calendardata'])
119
-				: $reminder['calendardata'];
120
-
121
-			$vcalendar = $this->parseCalendarData($calendarData);
122
-			if (!$vcalendar) {
123
-				$this->backend->removeReminder($reminder['id']);
124
-				continue;
125
-			}
126
-
127
-			$vevent = $this->getVEventByRecurrenceId($vcalendar, $reminder['recurrence_id'], $reminder['is_recurrence_exception']);
128
-			if (!$vevent) {
129
-				$this->backend->removeReminder($reminder['id']);
130
-				continue;
131
-			}
132
-
133
-			if ($this->wasEventCancelled($vevent)) {
134
-				$this->deleteOrProcessNext($reminder, $vevent);
135
-				continue;
136
-			}
137
-
138
-			if (!$this->notificationProviderManager->hasProvider($reminder['type'])) {
139
-				$this->deleteOrProcessNext($reminder, $vevent);
140
-				continue;
141
-			}
142
-
143
-			$users = $this->getAllUsersWithWriteAccessToCalendar($reminder['calendar_id']);
144
-			$user = $this->getUserFromPrincipalURI($reminder['principaluri']);
145
-			if ($user) {
146
-				$users[] = $user;
147
-			}
148
-
149
-			$notificationProvider = $this->notificationProviderManager->getProvider($reminder['type']);
150
-			$notificationProvider->send($vevent, $reminder['displayname'], $users);
151
-
152
-			$this->deleteOrProcessNext($reminder, $vevent);
153
-		}
154
-	}
155
-
156
-	/**
157
-	 * @param string $action
158
-	 * @param array $objectData
159
-	 * @throws VObject\InvalidDataException
160
-	 */
161
-	public function onTouchCalendarObject(string $action,
162
-										  array $objectData):void {
163
-		// We only support VEvents for now
164
-		if (strcasecmp($objectData['component'], 'vevent') !== 0) {
165
-			return;
166
-		}
167
-
168
-		switch ($action) {
169
-			case '\OCA\DAV\CalDAV\CalDavBackend::createCalendarObject':
170
-				$this->onCalendarObjectCreate($objectData);
171
-				break;
172
-
173
-			case '\OCA\DAV\CalDAV\CalDavBackend::updateCalendarObject':
174
-				$this->onCalendarObjectEdit($objectData);
175
-				break;
176
-
177
-			case '\OCA\DAV\CalDAV\CalDavBackend::deleteCalendarObject':
178
-				$this->onCalendarObjectDelete($objectData);
179
-				break;
180
-
181
-			default:
182
-				break;
183
-		}
184
-	}
185
-
186
-	/**
187
-	 * @param array $objectData
188
-	 */
189
-	private function onCalendarObjectCreate(array $objectData):void {
190
-		$calendarData = is_resource($objectData['calendardata'])
191
-			? stream_get_contents($objectData['calendardata'])
192
-			: $objectData['calendardata'];
193
-
194
-		/** @var VObject\Component\VCalendar $vcalendar */
195
-		$vcalendar = $this->parseCalendarData($calendarData);
196
-		if (!$vcalendar) {
197
-			return;
198
-		}
199
-
200
-		$vevents = $this->getAllVEventsFromVCalendar($vcalendar);
201
-		if (count($vevents) === 0) {
202
-			return;
203
-		}
204
-
205
-		$uid = (string) $vevents[0]->UID;
206
-		$recurrenceExceptions = $this->getRecurrenceExceptionFromListOfVEvents($vevents);
207
-		$masterItem = $this->getMasterItemFromListOfVEvents($vevents);
208
-		$now = $this->timeFactory->getDateTime();
209
-		$isRecurring = $masterItem ? $this->isRecurring($masterItem) : false;
210
-
211
-		foreach ($recurrenceExceptions as $recurrenceException) {
212
-			$eventHash = $this->getEventHash($recurrenceException);
213
-
214
-			if (!isset($recurrenceException->VALARM)) {
215
-				continue;
216
-			}
217
-
218
-			foreach ($recurrenceException->VALARM as $valarm) {
219
-				/** @var VAlarm $valarm */
220
-				$alarmHash = $this->getAlarmHash($valarm);
221
-				$triggerTime = $valarm->getEffectiveTriggerTime();
222
-				$diff = $now->diff($triggerTime);
223
-				if ($diff->invert === 1) {
224
-					continue;
225
-				}
226
-
227
-				$alarms = $this->getRemindersForVAlarm($valarm, $objectData,
228
-					$eventHash, $alarmHash, true, true);
229
-				$this->writeRemindersToDatabase($alarms);
230
-			}
231
-		}
232
-
233
-		if ($masterItem) {
234
-			$processedAlarms = [];
235
-			$masterAlarms = [];
236
-			$masterHash = $this->getEventHash($masterItem);
237
-
238
-			if (!isset($masterItem->VALARM)) {
239
-				return;
240
-			}
241
-
242
-			foreach ($masterItem->VALARM as $valarm) {
243
-				$masterAlarms[] = $this->getAlarmHash($valarm);
244
-			}
245
-
246
-			try {
247
-				$iterator = new EventIterator($vevents, $uid);
248
-			} catch (NoInstancesException $e) {
249
-				// This event is recurring, but it doesn't have a single
250
-				// instance. We are skipping this event from the output
251
-				// entirely.
252
-				return;
253
-			}
254
-
255
-			while ($iterator->valid() && count($processedAlarms) < count($masterAlarms)) {
256
-				$event = $iterator->getEventObject();
257
-
258
-				// Recurrence-exceptions are handled separately, so just ignore them here
259
-				if (\in_array($event, $recurrenceExceptions, true)) {
260
-					$iterator->next();
261
-					continue;
262
-				}
263
-
264
-				foreach ($event->VALARM as $valarm) {
265
-					/** @var VAlarm $valarm */
266
-					$alarmHash = $this->getAlarmHash($valarm);
267
-					if (\in_array($alarmHash, $processedAlarms, true)) {
268
-						continue;
269
-					}
270
-
271
-					if (!\in_array((string) $valarm->ACTION, self::REMINDER_TYPES, true)) {
272
-						// Action allows x-name, we don't insert reminders
273
-						// into the database if they are not standard
274
-						$processedAlarms[] = $alarmHash;
275
-						continue;
276
-					}
277
-
278
-					try {
279
-						$triggerTime = $valarm->getEffectiveTriggerTime();
280
-					} catch (InvalidDataException $e) {
281
-						continue;
282
-					}
283
-
284
-					// If effective trigger time is in the past
285
-					// just skip and generate for next event
286
-					$diff = $now->diff($triggerTime);
287
-					if ($diff->invert === 1) {
288
-						// If an absolute alarm is in the past,
289
-						// just add it to processedAlarms, so
290
-						// we don't extend till eternity
291
-						if (!$this->isAlarmRelative($valarm)) {
292
-							$processedAlarms[] = $alarmHash;
293
-						}
294
-
295
-						continue;
296
-					}
297
-
298
-					$alarms = $this->getRemindersForVAlarm($valarm, $objectData, $masterHash, $alarmHash, $isRecurring, false);
299
-					$this->writeRemindersToDatabase($alarms);
300
-					$processedAlarms[] = $alarmHash;
301
-				}
302
-
303
-				$iterator->next();
304
-			}
305
-		}
306
-	}
307
-
308
-	/**
309
-	 * @param array $objectData
310
-	 */
311
-	private function onCalendarObjectEdit(array $objectData):void {
312
-		// TODO - this can be vastly improved
313
-		//  - get cached reminders
314
-		//  - ...
315
-
316
-		$this->onCalendarObjectDelete($objectData);
317
-		$this->onCalendarObjectCreate($objectData);
318
-	}
319
-
320
-	/**
321
-	 * @param array $objectData
322
-	 */
323
-	private function onCalendarObjectDelete(array $objectData):void {
324
-		$this->backend->cleanRemindersForEvent((int) $objectData['id']);
325
-	}
326
-
327
-	/**
328
-	 * @param VAlarm $valarm
329
-	 * @param array $objectData
330
-	 * @param string|null $eventHash
331
-	 * @param string|null $alarmHash
332
-	 * @param bool $isRecurring
333
-	 * @param bool $isRecurrenceException
334
-	 * @return array
335
-	 */
336
-	private function getRemindersForVAlarm(VAlarm $valarm,
337
-										   array $objectData,
338
-										   string $eventHash = null,
339
-										   string $alarmHash = null,
340
-										   bool $isRecurring = false,
341
-										   bool $isRecurrenceException = false):array {
342
-		if ($eventHash === null) {
343
-			$eventHash = $this->getEventHash($valarm->parent);
344
-		}
345
-		if ($alarmHash === null) {
346
-			$alarmHash = $this->getAlarmHash($valarm);
347
-		}
348
-
349
-		$recurrenceId = $this->getEffectiveRecurrenceIdOfVEvent($valarm->parent);
350
-		$isRelative = $this->isAlarmRelative($valarm);
351
-		/** @var DateTimeImmutable $notificationDate */
352
-		$notificationDate = $valarm->getEffectiveTriggerTime();
353
-		$clonedNotificationDate = new \DateTime('now', $notificationDate->getTimezone());
354
-		$clonedNotificationDate->setTimestamp($notificationDate->getTimestamp());
355
-
356
-		$alarms = [];
357
-
358
-		$alarms[] = [
359
-			'calendar_id' => $objectData['calendarid'],
360
-			'object_id' => $objectData['id'],
361
-			'uid' => (string) $valarm->parent->UID,
362
-			'is_recurring' => $isRecurring,
363
-			'recurrence_id' => $recurrenceId,
364
-			'is_recurrence_exception' => $isRecurrenceException,
365
-			'event_hash' => $eventHash,
366
-			'alarm_hash' => $alarmHash,
367
-			'type' => (string) $valarm->ACTION,
368
-			'is_relative' => $isRelative,
369
-			'notification_date' => $notificationDate->getTimestamp(),
370
-			'is_repeat_based' => false,
371
-		];
372
-
373
-		$repeat = isset($valarm->REPEAT) ? (int) $valarm->REPEAT->getValue() : 0;
374
-		for ($i = 0; $i < $repeat; $i++) {
375
-			if ($valarm->DURATION === null) {
376
-				continue;
377
-			}
378
-
379
-			$clonedNotificationDate->add($valarm->DURATION->getDateInterval());
380
-			$alarms[] = [
381
-				'calendar_id' => $objectData['calendarid'],
382
-				'object_id' => $objectData['id'],
383
-				'uid' => (string) $valarm->parent->UID,
384
-				'is_recurring' => $isRecurring,
385
-				'recurrence_id' => $recurrenceId,
386
-				'is_recurrence_exception' => $isRecurrenceException,
387
-				'event_hash' => $eventHash,
388
-				'alarm_hash' => $alarmHash,
389
-				'type' => (string) $valarm->ACTION,
390
-				'is_relative' => $isRelative,
391
-				'notification_date' => $clonedNotificationDate->getTimestamp(),
392
-				'is_repeat_based' => true,
393
-			];
394
-		}
395
-
396
-		return $alarms;
397
-	}
398
-
399
-	/**
400
-	 * @param array $reminders
401
-	 */
402
-	private function writeRemindersToDatabase(array $reminders): void {
403
-		foreach ($reminders as $reminder) {
404
-			$this->backend->insertReminder(
405
-				(int) $reminder['calendar_id'],
406
-				(int) $reminder['object_id'],
407
-				$reminder['uid'],
408
-				$reminder['is_recurring'],
409
-				(int) $reminder['recurrence_id'],
410
-				$reminder['is_recurrence_exception'],
411
-				$reminder['event_hash'],
412
-				$reminder['alarm_hash'],
413
-				$reminder['type'],
414
-				$reminder['is_relative'],
415
-				(int) $reminder['notification_date'],
416
-				$reminder['is_repeat_based']
417
-			);
418
-		}
419
-	}
420
-
421
-	/**
422
-	 * @param array $reminder
423
-	 * @param VEvent $vevent
424
-	 */
425
-	private function deleteOrProcessNext(array $reminder,
426
-										 VObject\Component\VEvent $vevent):void {
427
-		if ($reminder['is_repeat_based'] ||
428
-			!$reminder['is_recurring'] ||
429
-			!$reminder['is_relative'] ||
430
-			$reminder['is_recurrence_exception']) {
431
-			$this->backend->removeReminder($reminder['id']);
432
-			return;
433
-		}
434
-
435
-		$vevents = $this->getAllVEventsFromVCalendar($vevent->parent);
436
-		$recurrenceExceptions = $this->getRecurrenceExceptionFromListOfVEvents($vevents);
437
-		$now = $this->timeFactory->getDateTime();
438
-
439
-		try {
440
-			$iterator = new EventIterator($vevents, $reminder['uid']);
441
-		} catch (NoInstancesException $e) {
442
-			// This event is recurring, but it doesn't have a single
443
-			// instance. We are skipping this event from the output
444
-			// entirely.
445
-			return;
446
-		}
447
-
448
-		while ($iterator->valid()) {
449
-			$event = $iterator->getEventObject();
450
-
451
-			// Recurrence-exceptions are handled separately, so just ignore them here
452
-			if (\in_array($event, $recurrenceExceptions, true)) {
453
-				$iterator->next();
454
-				continue;
455
-			}
456
-
457
-			$recurrenceId = $this->getEffectiveRecurrenceIdOfVEvent($event);
458
-			if ($reminder['recurrence_id'] >= $recurrenceId) {
459
-				$iterator->next();
460
-				continue;
461
-			}
462
-
463
-			foreach ($event->VALARM as $valarm) {
464
-				/** @var VAlarm $valarm */
465
-				$alarmHash = $this->getAlarmHash($valarm);
466
-				if ($alarmHash !== $reminder['alarm_hash']) {
467
-					continue;
468
-				}
469
-
470
-				$triggerTime = $valarm->getEffectiveTriggerTime();
471
-
472
-				// If effective trigger time is in the past
473
-				// just skip and generate for next event
474
-				$diff = $now->diff($triggerTime);
475
-				if ($diff->invert === 1) {
476
-					continue;
477
-				}
478
-
479
-				$this->backend->removeReminder($reminder['id']);
480
-				$alarms = $this->getRemindersForVAlarm($valarm, [
481
-					'calendarid' => $reminder['calendar_id'],
482
-					'id' => $reminder['object_id'],
483
-				], $reminder['event_hash'], $alarmHash, true, false);
484
-				$this->writeRemindersToDatabase($alarms);
485
-
486
-				// Abort generating reminders after creating one successfully
487
-				return;
488
-			}
489
-
490
-			$iterator->next();
491
-		}
492
-
493
-		$this->backend->removeReminder($reminder['id']);
494
-	}
495
-
496
-	/**
497
-	 * @param int $calendarId
498
-	 * @return IUser[]
499
-	 */
500
-	private function getAllUsersWithWriteAccessToCalendar(int $calendarId):array {
501
-		$shares = $this->caldavBackend->getShares($calendarId);
502
-
503
-		$users = [];
504
-		$userIds = [];
505
-		$groups = [];
506
-		foreach ($shares as $share) {
507
-			// Only consider writable shares
508
-			if ($share['readOnly']) {
509
-				continue;
510
-			}
511
-
512
-			$principal = explode('/', $share['{http://owncloud.org/ns}principal']);
513
-			if ($principal[1] === 'users') {
514
-				$user = $this->userManager->get($principal[2]);
515
-				if ($user) {
516
-					$users[] = $user;
517
-					$userIds[] = $principal[2];
518
-				}
519
-			} elseif ($principal[1] === 'groups') {
520
-				$groups[] = $principal[2];
521
-			}
522
-		}
523
-
524
-		foreach ($groups as $gid) {
525
-			$group = $this->groupManager->get($gid);
526
-			if ($group instanceof IGroup) {
527
-				foreach ($group->getUsers() as $user) {
528
-					if (!\in_array($user->getUID(), $userIds, true)) {
529
-						$users[] = $user;
530
-						$userIds[] = $user->getUID();
531
-					}
532
-				}
533
-			}
534
-		}
535
-
536
-		return $users;
537
-	}
538
-
539
-	/**
540
-	 * Gets a hash of the event.
541
-	 * If the hash changes, we have to update all relative alarms.
542
-	 *
543
-	 * @param VEvent $vevent
544
-	 * @return string
545
-	 */
546
-	private function getEventHash(VEvent $vevent):string {
547
-		$properties = [
548
-			(string) $vevent->DTSTART->serialize(),
549
-		];
550
-
551
-		if ($vevent->DTEND) {
552
-			$properties[] = (string) $vevent->DTEND->serialize();
553
-		}
554
-		if ($vevent->DURATION) {
555
-			$properties[] = (string) $vevent->DURATION->serialize();
556
-		}
557
-		if ($vevent->{'RECURRENCE-ID'}) {
558
-			$properties[] = (string) $vevent->{'RECURRENCE-ID'}->serialize();
559
-		}
560
-		if ($vevent->RRULE) {
561
-			$properties[] = (string) $vevent->RRULE->serialize();
562
-		}
563
-		if ($vevent->EXDATE) {
564
-			$properties[] = (string) $vevent->EXDATE->serialize();
565
-		}
566
-		if ($vevent->RDATE) {
567
-			$properties[] = (string) $vevent->RDATE->serialize();
568
-		}
569
-
570
-		return md5(implode('::', $properties));
571
-	}
572
-
573
-	/**
574
-	 * Gets a hash of the alarm.
575
-	 * If the hash changes, we have to update oc_dav_reminders.
576
-	 *
577
-	 * @param VAlarm $valarm
578
-	 * @return string
579
-	 */
580
-	private function getAlarmHash(VAlarm $valarm):string {
581
-		$properties = [
582
-			(string) $valarm->ACTION->serialize(),
583
-			(string) $valarm->TRIGGER->serialize(),
584
-		];
585
-
586
-		if ($valarm->DURATION) {
587
-			$properties[] = (string) $valarm->DURATION->serialize();
588
-		}
589
-		if ($valarm->REPEAT) {
590
-			$properties[] = (string) $valarm->REPEAT->serialize();
591
-		}
592
-
593
-		return md5(implode('::', $properties));
594
-	}
595
-
596
-	/**
597
-	 * @param VObject\Component\VCalendar $vcalendar
598
-	 * @param int $recurrenceId
599
-	 * @param bool $isRecurrenceException
600
-	 * @return VEvent|null
601
-	 */
602
-	private function getVEventByRecurrenceId(VObject\Component\VCalendar $vcalendar,
603
-											 int $recurrenceId,
604
-											 bool $isRecurrenceException):?VEvent {
605
-		$vevents = $this->getAllVEventsFromVCalendar($vcalendar);
606
-		if (count($vevents) === 0) {
607
-			return null;
608
-		}
609
-
610
-		$uid = (string) $vevents[0]->UID;
611
-		$recurrenceExceptions = $this->getRecurrenceExceptionFromListOfVEvents($vevents);
612
-		$masterItem = $this->getMasterItemFromListOfVEvents($vevents);
613
-
614
-		// Handle recurrence-exceptions first, because recurrence-expansion is expensive
615
-		if ($isRecurrenceException) {
616
-			foreach ($recurrenceExceptions as $recurrenceException) {
617
-				if ($this->getEffectiveRecurrenceIdOfVEvent($recurrenceException) === $recurrenceId) {
618
-					return $recurrenceException;
619
-				}
620
-			}
621
-
622
-			return null;
623
-		}
624
-
625
-		if ($masterItem) {
626
-			try {
627
-				$iterator = new EventIterator($vevents, $uid);
628
-			} catch (NoInstancesException $e) {
629
-				// This event is recurring, but it doesn't have a single
630
-				// instance. We are skipping this event from the output
631
-				// entirely.
632
-				return null;
633
-			}
634
-
635
-			while ($iterator->valid()) {
636
-				$event = $iterator->getEventObject();
637
-
638
-				// Recurrence-exceptions are handled separately, so just ignore them here
639
-				if (\in_array($event, $recurrenceExceptions, true)) {
640
-					$iterator->next();
641
-					continue;
642
-				}
643
-
644
-				if ($this->getEffectiveRecurrenceIdOfVEvent($event) === $recurrenceId) {
645
-					return $event;
646
-				}
647
-
648
-				$iterator->next();
649
-			}
650
-		}
651
-
652
-		return null;
653
-	}
654
-
655
-	/**
656
-	 * @param VEvent $vevent
657
-	 * @return string
658
-	 */
659
-	private function getStatusOfEvent(VEvent $vevent):string {
660
-		if ($vevent->STATUS) {
661
-			return (string) $vevent->STATUS;
662
-		}
663
-
664
-		// Doesn't say so in the standard,
665
-		// but we consider events without a status
666
-		// to be confirmed
667
-		return 'CONFIRMED';
668
-	}
669
-
670
-	/**
671
-	 * @param VObject\Component\VEvent $vevent
672
-	 * @return bool
673
-	 */
674
-	private function wasEventCancelled(VObject\Component\VEvent $vevent):bool {
675
-		return $this->getStatusOfEvent($vevent) === 'CANCELLED';
676
-	}
677
-
678
-	/**
679
-	 * @param string $calendarData
680
-	 * @return VObject\Component\VCalendar|null
681
-	 */
682
-	private function parseCalendarData(string $calendarData):?VObject\Component\VCalendar {
683
-		try {
684
-			return VObject\Reader::read($calendarData,
685
-				VObject\Reader::OPTION_FORGIVING);
686
-		} catch (ParseException $ex) {
687
-			return null;
688
-		}
689
-	}
690
-
691
-	/**
692
-	 * @param string $principalUri
693
-	 * @return IUser|null
694
-	 */
695
-	private function getUserFromPrincipalURI(string $principalUri):?IUser {
696
-		if (!$principalUri) {
697
-			return null;
698
-		}
699
-
700
-		if (stripos($principalUri, 'principals/users/') !== 0) {
701
-			return null;
702
-		}
703
-
704
-		$userId = substr($principalUri, 17);
705
-		return $this->userManager->get($userId);
706
-	}
707
-
708
-	/**
709
-	 * @param VObject\Component\VCalendar $vcalendar
710
-	 * @return VObject\Component\VEvent[]
711
-	 */
712
-	private function getAllVEventsFromVCalendar(VObject\Component\VCalendar $vcalendar):array {
713
-		$vevents = [];
714
-
715
-		foreach ($vcalendar->children() as $child) {
716
-			if (!($child instanceof VObject\Component)) {
717
-				continue;
718
-			}
719
-
720
-			if ($child->name !== 'VEVENT') {
721
-				continue;
722
-			}
723
-
724
-			$vevents[] = $child;
725
-		}
726
-
727
-		return $vevents;
728
-	}
729
-
730
-	/**
731
-	 * @param array $vevents
732
-	 * @return VObject\Component\VEvent[]
733
-	 */
734
-	private function getRecurrenceExceptionFromListOfVEvents(array $vevents):array {
735
-		return array_values(array_filter($vevents, function (VEvent $vevent) {
736
-			return $vevent->{'RECURRENCE-ID'} !== null;
737
-		}));
738
-	}
739
-
740
-	/**
741
-	 * @param array $vevents
742
-	 * @return VEvent|null
743
-	 */
744
-	private function getMasterItemFromListOfVEvents(array $vevents):?VEvent {
745
-		$elements = array_values(array_filter($vevents, function (VEvent $vevent) {
746
-			return $vevent->{'RECURRENCE-ID'} === null;
747
-		}));
748
-
749
-		if (count($elements) === 0) {
750
-			return null;
751
-		}
752
-		if (count($elements) > 1) {
753
-			throw new \TypeError('Multiple master objects');
754
-		}
755
-
756
-		return $elements[0];
757
-	}
758
-
759
-	/**
760
-	 * @param VAlarm $valarm
761
-	 * @return bool
762
-	 */
763
-	private function isAlarmRelative(VAlarm $valarm):bool {
764
-		$trigger = $valarm->TRIGGER;
765
-		return $trigger instanceof VObject\Property\ICalendar\Duration;
766
-	}
767
-
768
-	/**
769
-	 * @param VEvent $vevent
770
-	 * @return int
771
-	 */
772
-	private function getEffectiveRecurrenceIdOfVEvent(VEvent $vevent):int {
773
-		if (isset($vevent->{'RECURRENCE-ID'})) {
774
-			return $vevent->{'RECURRENCE-ID'}->getDateTime()->getTimestamp();
775
-		}
776
-
777
-		return $vevent->DTSTART->getDateTime()->getTimestamp();
778
-	}
779
-
780
-	/**
781
-	 * @param VEvent $vevent
782
-	 * @return bool
783
-	 */
784
-	private function isRecurring(VEvent $vevent):bool {
785
-		return isset($vevent->RRULE) || isset($vevent->RDATE);
786
-	}
50
+    /** @var Backend */
51
+    private $backend;
52
+
53
+    /** @var NotificationProviderManager */
54
+    private $notificationProviderManager;
55
+
56
+    /** @var IUserManager */
57
+    private $userManager;
58
+
59
+    /** @var IGroupManager */
60
+    private $groupManager;
61
+
62
+    /** @var CalDavBackend */
63
+    private $caldavBackend;
64
+
65
+    /** @var ITimeFactory */
66
+    private $timeFactory;
67
+
68
+    public const REMINDER_TYPE_EMAIL = 'EMAIL';
69
+    public const REMINDER_TYPE_DISPLAY = 'DISPLAY';
70
+    public const REMINDER_TYPE_AUDIO = 'AUDIO';
71
+
72
+    /**
73
+     * @var String[]
74
+     *
75
+     * Official RFC5545 reminder types
76
+     */
77
+    public const REMINDER_TYPES = [
78
+        self::REMINDER_TYPE_EMAIL,
79
+        self::REMINDER_TYPE_DISPLAY,
80
+        self::REMINDER_TYPE_AUDIO
81
+    ];
82
+
83
+    /**
84
+     * ReminderService constructor.
85
+     *
86
+     * @param Backend $backend
87
+     * @param NotificationProviderManager $notificationProviderManager
88
+     * @param IUserManager $userManager
89
+     * @param IGroupManager $groupManager
90
+     * @param CalDavBackend $caldavBackend
91
+     * @param ITimeFactory $timeFactory
92
+     */
93
+    public function __construct(Backend $backend,
94
+                                NotificationProviderManager $notificationProviderManager,
95
+                                IUserManager $userManager,
96
+                                IGroupManager $groupManager,
97
+                                CalDavBackend $caldavBackend,
98
+                                ITimeFactory $timeFactory) {
99
+        $this->backend = $backend;
100
+        $this->notificationProviderManager = $notificationProviderManager;
101
+        $this->userManager = $userManager;
102
+        $this->groupManager = $groupManager;
103
+        $this->caldavBackend = $caldavBackend;
104
+        $this->timeFactory = $timeFactory;
105
+    }
106
+
107
+    /**
108
+     * Process reminders to activate
109
+     *
110
+     * @throws NotificationProvider\ProviderNotAvailableException
111
+     * @throws NotificationTypeDoesNotExistException
112
+     */
113
+    public function processReminders():void {
114
+        $reminders = $this->backend->getRemindersToProcess();
115
+
116
+        foreach ($reminders as $reminder) {
117
+            $calendarData = is_resource($reminder['calendardata'])
118
+                ? stream_get_contents($reminder['calendardata'])
119
+                : $reminder['calendardata'];
120
+
121
+            $vcalendar = $this->parseCalendarData($calendarData);
122
+            if (!$vcalendar) {
123
+                $this->backend->removeReminder($reminder['id']);
124
+                continue;
125
+            }
126
+
127
+            $vevent = $this->getVEventByRecurrenceId($vcalendar, $reminder['recurrence_id'], $reminder['is_recurrence_exception']);
128
+            if (!$vevent) {
129
+                $this->backend->removeReminder($reminder['id']);
130
+                continue;
131
+            }
132
+
133
+            if ($this->wasEventCancelled($vevent)) {
134
+                $this->deleteOrProcessNext($reminder, $vevent);
135
+                continue;
136
+            }
137
+
138
+            if (!$this->notificationProviderManager->hasProvider($reminder['type'])) {
139
+                $this->deleteOrProcessNext($reminder, $vevent);
140
+                continue;
141
+            }
142
+
143
+            $users = $this->getAllUsersWithWriteAccessToCalendar($reminder['calendar_id']);
144
+            $user = $this->getUserFromPrincipalURI($reminder['principaluri']);
145
+            if ($user) {
146
+                $users[] = $user;
147
+            }
148
+
149
+            $notificationProvider = $this->notificationProviderManager->getProvider($reminder['type']);
150
+            $notificationProvider->send($vevent, $reminder['displayname'], $users);
151
+
152
+            $this->deleteOrProcessNext($reminder, $vevent);
153
+        }
154
+    }
155
+
156
+    /**
157
+     * @param string $action
158
+     * @param array $objectData
159
+     * @throws VObject\InvalidDataException
160
+     */
161
+    public function onTouchCalendarObject(string $action,
162
+                                            array $objectData):void {
163
+        // We only support VEvents for now
164
+        if (strcasecmp($objectData['component'], 'vevent') !== 0) {
165
+            return;
166
+        }
167
+
168
+        switch ($action) {
169
+            case '\OCA\DAV\CalDAV\CalDavBackend::createCalendarObject':
170
+                $this->onCalendarObjectCreate($objectData);
171
+                break;
172
+
173
+            case '\OCA\DAV\CalDAV\CalDavBackend::updateCalendarObject':
174
+                $this->onCalendarObjectEdit($objectData);
175
+                break;
176
+
177
+            case '\OCA\DAV\CalDAV\CalDavBackend::deleteCalendarObject':
178
+                $this->onCalendarObjectDelete($objectData);
179
+                break;
180
+
181
+            default:
182
+                break;
183
+        }
184
+    }
185
+
186
+    /**
187
+     * @param array $objectData
188
+     */
189
+    private function onCalendarObjectCreate(array $objectData):void {
190
+        $calendarData = is_resource($objectData['calendardata'])
191
+            ? stream_get_contents($objectData['calendardata'])
192
+            : $objectData['calendardata'];
193
+
194
+        /** @var VObject\Component\VCalendar $vcalendar */
195
+        $vcalendar = $this->parseCalendarData($calendarData);
196
+        if (!$vcalendar) {
197
+            return;
198
+        }
199
+
200
+        $vevents = $this->getAllVEventsFromVCalendar($vcalendar);
201
+        if (count($vevents) === 0) {
202
+            return;
203
+        }
204
+
205
+        $uid = (string) $vevents[0]->UID;
206
+        $recurrenceExceptions = $this->getRecurrenceExceptionFromListOfVEvents($vevents);
207
+        $masterItem = $this->getMasterItemFromListOfVEvents($vevents);
208
+        $now = $this->timeFactory->getDateTime();
209
+        $isRecurring = $masterItem ? $this->isRecurring($masterItem) : false;
210
+
211
+        foreach ($recurrenceExceptions as $recurrenceException) {
212
+            $eventHash = $this->getEventHash($recurrenceException);
213
+
214
+            if (!isset($recurrenceException->VALARM)) {
215
+                continue;
216
+            }
217
+
218
+            foreach ($recurrenceException->VALARM as $valarm) {
219
+                /** @var VAlarm $valarm */
220
+                $alarmHash = $this->getAlarmHash($valarm);
221
+                $triggerTime = $valarm->getEffectiveTriggerTime();
222
+                $diff = $now->diff($triggerTime);
223
+                if ($diff->invert === 1) {
224
+                    continue;
225
+                }
226
+
227
+                $alarms = $this->getRemindersForVAlarm($valarm, $objectData,
228
+                    $eventHash, $alarmHash, true, true);
229
+                $this->writeRemindersToDatabase($alarms);
230
+            }
231
+        }
232
+
233
+        if ($masterItem) {
234
+            $processedAlarms = [];
235
+            $masterAlarms = [];
236
+            $masterHash = $this->getEventHash($masterItem);
237
+
238
+            if (!isset($masterItem->VALARM)) {
239
+                return;
240
+            }
241
+
242
+            foreach ($masterItem->VALARM as $valarm) {
243
+                $masterAlarms[] = $this->getAlarmHash($valarm);
244
+            }
245
+
246
+            try {
247
+                $iterator = new EventIterator($vevents, $uid);
248
+            } catch (NoInstancesException $e) {
249
+                // This event is recurring, but it doesn't have a single
250
+                // instance. We are skipping this event from the output
251
+                // entirely.
252
+                return;
253
+            }
254
+
255
+            while ($iterator->valid() && count($processedAlarms) < count($masterAlarms)) {
256
+                $event = $iterator->getEventObject();
257
+
258
+                // Recurrence-exceptions are handled separately, so just ignore them here
259
+                if (\in_array($event, $recurrenceExceptions, true)) {
260
+                    $iterator->next();
261
+                    continue;
262
+                }
263
+
264
+                foreach ($event->VALARM as $valarm) {
265
+                    /** @var VAlarm $valarm */
266
+                    $alarmHash = $this->getAlarmHash($valarm);
267
+                    if (\in_array($alarmHash, $processedAlarms, true)) {
268
+                        continue;
269
+                    }
270
+
271
+                    if (!\in_array((string) $valarm->ACTION, self::REMINDER_TYPES, true)) {
272
+                        // Action allows x-name, we don't insert reminders
273
+                        // into the database if they are not standard
274
+                        $processedAlarms[] = $alarmHash;
275
+                        continue;
276
+                    }
277
+
278
+                    try {
279
+                        $triggerTime = $valarm->getEffectiveTriggerTime();
280
+                    } catch (InvalidDataException $e) {
281
+                        continue;
282
+                    }
283
+
284
+                    // If effective trigger time is in the past
285
+                    // just skip and generate for next event
286
+                    $diff = $now->diff($triggerTime);
287
+                    if ($diff->invert === 1) {
288
+                        // If an absolute alarm is in the past,
289
+                        // just add it to processedAlarms, so
290
+                        // we don't extend till eternity
291
+                        if (!$this->isAlarmRelative($valarm)) {
292
+                            $processedAlarms[] = $alarmHash;
293
+                        }
294
+
295
+                        continue;
296
+                    }
297
+
298
+                    $alarms = $this->getRemindersForVAlarm($valarm, $objectData, $masterHash, $alarmHash, $isRecurring, false);
299
+                    $this->writeRemindersToDatabase($alarms);
300
+                    $processedAlarms[] = $alarmHash;
301
+                }
302
+
303
+                $iterator->next();
304
+            }
305
+        }
306
+    }
307
+
308
+    /**
309
+     * @param array $objectData
310
+     */
311
+    private function onCalendarObjectEdit(array $objectData):void {
312
+        // TODO - this can be vastly improved
313
+        //  - get cached reminders
314
+        //  - ...
315
+
316
+        $this->onCalendarObjectDelete($objectData);
317
+        $this->onCalendarObjectCreate($objectData);
318
+    }
319
+
320
+    /**
321
+     * @param array $objectData
322
+     */
323
+    private function onCalendarObjectDelete(array $objectData):void {
324
+        $this->backend->cleanRemindersForEvent((int) $objectData['id']);
325
+    }
326
+
327
+    /**
328
+     * @param VAlarm $valarm
329
+     * @param array $objectData
330
+     * @param string|null $eventHash
331
+     * @param string|null $alarmHash
332
+     * @param bool $isRecurring
333
+     * @param bool $isRecurrenceException
334
+     * @return array
335
+     */
336
+    private function getRemindersForVAlarm(VAlarm $valarm,
337
+                                            array $objectData,
338
+                                            string $eventHash = null,
339
+                                            string $alarmHash = null,
340
+                                            bool $isRecurring = false,
341
+                                            bool $isRecurrenceException = false):array {
342
+        if ($eventHash === null) {
343
+            $eventHash = $this->getEventHash($valarm->parent);
344
+        }
345
+        if ($alarmHash === null) {
346
+            $alarmHash = $this->getAlarmHash($valarm);
347
+        }
348
+
349
+        $recurrenceId = $this->getEffectiveRecurrenceIdOfVEvent($valarm->parent);
350
+        $isRelative = $this->isAlarmRelative($valarm);
351
+        /** @var DateTimeImmutable $notificationDate */
352
+        $notificationDate = $valarm->getEffectiveTriggerTime();
353
+        $clonedNotificationDate = new \DateTime('now', $notificationDate->getTimezone());
354
+        $clonedNotificationDate->setTimestamp($notificationDate->getTimestamp());
355
+
356
+        $alarms = [];
357
+
358
+        $alarms[] = [
359
+            'calendar_id' => $objectData['calendarid'],
360
+            'object_id' => $objectData['id'],
361
+            'uid' => (string) $valarm->parent->UID,
362
+            'is_recurring' => $isRecurring,
363
+            'recurrence_id' => $recurrenceId,
364
+            'is_recurrence_exception' => $isRecurrenceException,
365
+            'event_hash' => $eventHash,
366
+            'alarm_hash' => $alarmHash,
367
+            'type' => (string) $valarm->ACTION,
368
+            'is_relative' => $isRelative,
369
+            'notification_date' => $notificationDate->getTimestamp(),
370
+            'is_repeat_based' => false,
371
+        ];
372
+
373
+        $repeat = isset($valarm->REPEAT) ? (int) $valarm->REPEAT->getValue() : 0;
374
+        for ($i = 0; $i < $repeat; $i++) {
375
+            if ($valarm->DURATION === null) {
376
+                continue;
377
+            }
378
+
379
+            $clonedNotificationDate->add($valarm->DURATION->getDateInterval());
380
+            $alarms[] = [
381
+                'calendar_id' => $objectData['calendarid'],
382
+                'object_id' => $objectData['id'],
383
+                'uid' => (string) $valarm->parent->UID,
384
+                'is_recurring' => $isRecurring,
385
+                'recurrence_id' => $recurrenceId,
386
+                'is_recurrence_exception' => $isRecurrenceException,
387
+                'event_hash' => $eventHash,
388
+                'alarm_hash' => $alarmHash,
389
+                'type' => (string) $valarm->ACTION,
390
+                'is_relative' => $isRelative,
391
+                'notification_date' => $clonedNotificationDate->getTimestamp(),
392
+                'is_repeat_based' => true,
393
+            ];
394
+        }
395
+
396
+        return $alarms;
397
+    }
398
+
399
+    /**
400
+     * @param array $reminders
401
+     */
402
+    private function writeRemindersToDatabase(array $reminders): void {
403
+        foreach ($reminders as $reminder) {
404
+            $this->backend->insertReminder(
405
+                (int) $reminder['calendar_id'],
406
+                (int) $reminder['object_id'],
407
+                $reminder['uid'],
408
+                $reminder['is_recurring'],
409
+                (int) $reminder['recurrence_id'],
410
+                $reminder['is_recurrence_exception'],
411
+                $reminder['event_hash'],
412
+                $reminder['alarm_hash'],
413
+                $reminder['type'],
414
+                $reminder['is_relative'],
415
+                (int) $reminder['notification_date'],
416
+                $reminder['is_repeat_based']
417
+            );
418
+        }
419
+    }
420
+
421
+    /**
422
+     * @param array $reminder
423
+     * @param VEvent $vevent
424
+     */
425
+    private function deleteOrProcessNext(array $reminder,
426
+                                            VObject\Component\VEvent $vevent):void {
427
+        if ($reminder['is_repeat_based'] ||
428
+            !$reminder['is_recurring'] ||
429
+            !$reminder['is_relative'] ||
430
+            $reminder['is_recurrence_exception']) {
431
+            $this->backend->removeReminder($reminder['id']);
432
+            return;
433
+        }
434
+
435
+        $vevents = $this->getAllVEventsFromVCalendar($vevent->parent);
436
+        $recurrenceExceptions = $this->getRecurrenceExceptionFromListOfVEvents($vevents);
437
+        $now = $this->timeFactory->getDateTime();
438
+
439
+        try {
440
+            $iterator = new EventIterator($vevents, $reminder['uid']);
441
+        } catch (NoInstancesException $e) {
442
+            // This event is recurring, but it doesn't have a single
443
+            // instance. We are skipping this event from the output
444
+            // entirely.
445
+            return;
446
+        }
447
+
448
+        while ($iterator->valid()) {
449
+            $event = $iterator->getEventObject();
450
+
451
+            // Recurrence-exceptions are handled separately, so just ignore them here
452
+            if (\in_array($event, $recurrenceExceptions, true)) {
453
+                $iterator->next();
454
+                continue;
455
+            }
456
+
457
+            $recurrenceId = $this->getEffectiveRecurrenceIdOfVEvent($event);
458
+            if ($reminder['recurrence_id'] >= $recurrenceId) {
459
+                $iterator->next();
460
+                continue;
461
+            }
462
+
463
+            foreach ($event->VALARM as $valarm) {
464
+                /** @var VAlarm $valarm */
465
+                $alarmHash = $this->getAlarmHash($valarm);
466
+                if ($alarmHash !== $reminder['alarm_hash']) {
467
+                    continue;
468
+                }
469
+
470
+                $triggerTime = $valarm->getEffectiveTriggerTime();
471
+
472
+                // If effective trigger time is in the past
473
+                // just skip and generate for next event
474
+                $diff = $now->diff($triggerTime);
475
+                if ($diff->invert === 1) {
476
+                    continue;
477
+                }
478
+
479
+                $this->backend->removeReminder($reminder['id']);
480
+                $alarms = $this->getRemindersForVAlarm($valarm, [
481
+                    'calendarid' => $reminder['calendar_id'],
482
+                    'id' => $reminder['object_id'],
483
+                ], $reminder['event_hash'], $alarmHash, true, false);
484
+                $this->writeRemindersToDatabase($alarms);
485
+
486
+                // Abort generating reminders after creating one successfully
487
+                return;
488
+            }
489
+
490
+            $iterator->next();
491
+        }
492
+
493
+        $this->backend->removeReminder($reminder['id']);
494
+    }
495
+
496
+    /**
497
+     * @param int $calendarId
498
+     * @return IUser[]
499
+     */
500
+    private function getAllUsersWithWriteAccessToCalendar(int $calendarId):array {
501
+        $shares = $this->caldavBackend->getShares($calendarId);
502
+
503
+        $users = [];
504
+        $userIds = [];
505
+        $groups = [];
506
+        foreach ($shares as $share) {
507
+            // Only consider writable shares
508
+            if ($share['readOnly']) {
509
+                continue;
510
+            }
511
+
512
+            $principal = explode('/', $share['{http://owncloud.org/ns}principal']);
513
+            if ($principal[1] === 'users') {
514
+                $user = $this->userManager->get($principal[2]);
515
+                if ($user) {
516
+                    $users[] = $user;
517
+                    $userIds[] = $principal[2];
518
+                }
519
+            } elseif ($principal[1] === 'groups') {
520
+                $groups[] = $principal[2];
521
+            }
522
+        }
523
+
524
+        foreach ($groups as $gid) {
525
+            $group = $this->groupManager->get($gid);
526
+            if ($group instanceof IGroup) {
527
+                foreach ($group->getUsers() as $user) {
528
+                    if (!\in_array($user->getUID(), $userIds, true)) {
529
+                        $users[] = $user;
530
+                        $userIds[] = $user->getUID();
531
+                    }
532
+                }
533
+            }
534
+        }
535
+
536
+        return $users;
537
+    }
538
+
539
+    /**
540
+     * Gets a hash of the event.
541
+     * If the hash changes, we have to update all relative alarms.
542
+     *
543
+     * @param VEvent $vevent
544
+     * @return string
545
+     */
546
+    private function getEventHash(VEvent $vevent):string {
547
+        $properties = [
548
+            (string) $vevent->DTSTART->serialize(),
549
+        ];
550
+
551
+        if ($vevent->DTEND) {
552
+            $properties[] = (string) $vevent->DTEND->serialize();
553
+        }
554
+        if ($vevent->DURATION) {
555
+            $properties[] = (string) $vevent->DURATION->serialize();
556
+        }
557
+        if ($vevent->{'RECURRENCE-ID'}) {
558
+            $properties[] = (string) $vevent->{'RECURRENCE-ID'}->serialize();
559
+        }
560
+        if ($vevent->RRULE) {
561
+            $properties[] = (string) $vevent->RRULE->serialize();
562
+        }
563
+        if ($vevent->EXDATE) {
564
+            $properties[] = (string) $vevent->EXDATE->serialize();
565
+        }
566
+        if ($vevent->RDATE) {
567
+            $properties[] = (string) $vevent->RDATE->serialize();
568
+        }
569
+
570
+        return md5(implode('::', $properties));
571
+    }
572
+
573
+    /**
574
+     * Gets a hash of the alarm.
575
+     * If the hash changes, we have to update oc_dav_reminders.
576
+     *
577
+     * @param VAlarm $valarm
578
+     * @return string
579
+     */
580
+    private function getAlarmHash(VAlarm $valarm):string {
581
+        $properties = [
582
+            (string) $valarm->ACTION->serialize(),
583
+            (string) $valarm->TRIGGER->serialize(),
584
+        ];
585
+
586
+        if ($valarm->DURATION) {
587
+            $properties[] = (string) $valarm->DURATION->serialize();
588
+        }
589
+        if ($valarm->REPEAT) {
590
+            $properties[] = (string) $valarm->REPEAT->serialize();
591
+        }
592
+
593
+        return md5(implode('::', $properties));
594
+    }
595
+
596
+    /**
597
+     * @param VObject\Component\VCalendar $vcalendar
598
+     * @param int $recurrenceId
599
+     * @param bool $isRecurrenceException
600
+     * @return VEvent|null
601
+     */
602
+    private function getVEventByRecurrenceId(VObject\Component\VCalendar $vcalendar,
603
+                                                int $recurrenceId,
604
+                                                bool $isRecurrenceException):?VEvent {
605
+        $vevents = $this->getAllVEventsFromVCalendar($vcalendar);
606
+        if (count($vevents) === 0) {
607
+            return null;
608
+        }
609
+
610
+        $uid = (string) $vevents[0]->UID;
611
+        $recurrenceExceptions = $this->getRecurrenceExceptionFromListOfVEvents($vevents);
612
+        $masterItem = $this->getMasterItemFromListOfVEvents($vevents);
613
+
614
+        // Handle recurrence-exceptions first, because recurrence-expansion is expensive
615
+        if ($isRecurrenceException) {
616
+            foreach ($recurrenceExceptions as $recurrenceException) {
617
+                if ($this->getEffectiveRecurrenceIdOfVEvent($recurrenceException) === $recurrenceId) {
618
+                    return $recurrenceException;
619
+                }
620
+            }
621
+
622
+            return null;
623
+        }
624
+
625
+        if ($masterItem) {
626
+            try {
627
+                $iterator = new EventIterator($vevents, $uid);
628
+            } catch (NoInstancesException $e) {
629
+                // This event is recurring, but it doesn't have a single
630
+                // instance. We are skipping this event from the output
631
+                // entirely.
632
+                return null;
633
+            }
634
+
635
+            while ($iterator->valid()) {
636
+                $event = $iterator->getEventObject();
637
+
638
+                // Recurrence-exceptions are handled separately, so just ignore them here
639
+                if (\in_array($event, $recurrenceExceptions, true)) {
640
+                    $iterator->next();
641
+                    continue;
642
+                }
643
+
644
+                if ($this->getEffectiveRecurrenceIdOfVEvent($event) === $recurrenceId) {
645
+                    return $event;
646
+                }
647
+
648
+                $iterator->next();
649
+            }
650
+        }
651
+
652
+        return null;
653
+    }
654
+
655
+    /**
656
+     * @param VEvent $vevent
657
+     * @return string
658
+     */
659
+    private function getStatusOfEvent(VEvent $vevent):string {
660
+        if ($vevent->STATUS) {
661
+            return (string) $vevent->STATUS;
662
+        }
663
+
664
+        // Doesn't say so in the standard,
665
+        // but we consider events without a status
666
+        // to be confirmed
667
+        return 'CONFIRMED';
668
+    }
669
+
670
+    /**
671
+     * @param VObject\Component\VEvent $vevent
672
+     * @return bool
673
+     */
674
+    private function wasEventCancelled(VObject\Component\VEvent $vevent):bool {
675
+        return $this->getStatusOfEvent($vevent) === 'CANCELLED';
676
+    }
677
+
678
+    /**
679
+     * @param string $calendarData
680
+     * @return VObject\Component\VCalendar|null
681
+     */
682
+    private function parseCalendarData(string $calendarData):?VObject\Component\VCalendar {
683
+        try {
684
+            return VObject\Reader::read($calendarData,
685
+                VObject\Reader::OPTION_FORGIVING);
686
+        } catch (ParseException $ex) {
687
+            return null;
688
+        }
689
+    }
690
+
691
+    /**
692
+     * @param string $principalUri
693
+     * @return IUser|null
694
+     */
695
+    private function getUserFromPrincipalURI(string $principalUri):?IUser {
696
+        if (!$principalUri) {
697
+            return null;
698
+        }
699
+
700
+        if (stripos($principalUri, 'principals/users/') !== 0) {
701
+            return null;
702
+        }
703
+
704
+        $userId = substr($principalUri, 17);
705
+        return $this->userManager->get($userId);
706
+    }
707
+
708
+    /**
709
+     * @param VObject\Component\VCalendar $vcalendar
710
+     * @return VObject\Component\VEvent[]
711
+     */
712
+    private function getAllVEventsFromVCalendar(VObject\Component\VCalendar $vcalendar):array {
713
+        $vevents = [];
714
+
715
+        foreach ($vcalendar->children() as $child) {
716
+            if (!($child instanceof VObject\Component)) {
717
+                continue;
718
+            }
719
+
720
+            if ($child->name !== 'VEVENT') {
721
+                continue;
722
+            }
723
+
724
+            $vevents[] = $child;
725
+        }
726
+
727
+        return $vevents;
728
+    }
729
+
730
+    /**
731
+     * @param array $vevents
732
+     * @return VObject\Component\VEvent[]
733
+     */
734
+    private function getRecurrenceExceptionFromListOfVEvents(array $vevents):array {
735
+        return array_values(array_filter($vevents, function (VEvent $vevent) {
736
+            return $vevent->{'RECURRENCE-ID'} !== null;
737
+        }));
738
+    }
739
+
740
+    /**
741
+     * @param array $vevents
742
+     * @return VEvent|null
743
+     */
744
+    private function getMasterItemFromListOfVEvents(array $vevents):?VEvent {
745
+        $elements = array_values(array_filter($vevents, function (VEvent $vevent) {
746
+            return $vevent->{'RECURRENCE-ID'} === null;
747
+        }));
748
+
749
+        if (count($elements) === 0) {
750
+            return null;
751
+        }
752
+        if (count($elements) > 1) {
753
+            throw new \TypeError('Multiple master objects');
754
+        }
755
+
756
+        return $elements[0];
757
+    }
758
+
759
+    /**
760
+     * @param VAlarm $valarm
761
+     * @return bool
762
+     */
763
+    private function isAlarmRelative(VAlarm $valarm):bool {
764
+        $trigger = $valarm->TRIGGER;
765
+        return $trigger instanceof VObject\Property\ICalendar\Duration;
766
+    }
767
+
768
+    /**
769
+     * @param VEvent $vevent
770
+     * @return int
771
+     */
772
+    private function getEffectiveRecurrenceIdOfVEvent(VEvent $vevent):int {
773
+        if (isset($vevent->{'RECURRENCE-ID'})) {
774
+            return $vevent->{'RECURRENCE-ID'}->getDateTime()->getTimestamp();
775
+        }
776
+
777
+        return $vevent->DTSTART->getDateTime()->getTimestamp();
778
+    }
779
+
780
+    /**
781
+     * @param VEvent $vevent
782
+     * @return bool
783
+     */
784
+    private function isRecurring(VEvent $vevent):bool {
785
+        return isset($vevent->RRULE) || isset($vevent->RDATE);
786
+    }
787 787
 }
Please login to merge, or discard this patch.