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