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