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