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