Passed
Push — master ( 9c2d70...6ef7ba )
by Roeland
10:28
created
apps/dav/lib/CalDAV/Reminder/INotificationProvider.php 1 patch
Indentation   +11 added lines, -11 removed lines patch added patch discarded remove patch
@@ -33,15 +33,15 @@
 block discarded – undo
33 33
  */
34 34
 interface INotificationProvider {
35 35
 
36
-	/**
37
-	 * Send notification
38
-	 *
39
-	 * @param VEvent $vevent
40
-	 * @param string $calendarDisplayName
41
-	 * @param IUser[] $users
42
-	 * @return void
43
-	 */
44
-	public function send(VEvent $vevent,
45
-						 string $calendarDisplayName,
46
-						 array $users=[]): void;
36
+    /**
37
+     * Send notification
38
+     *
39
+     * @param VEvent $vevent
40
+     * @param string $calendarDisplayName
41
+     * @param IUser[] $users
42
+     * @return void
43
+     */
44
+    public function send(VEvent $vevent,
45
+                            string $calendarDisplayName,
46
+                            array $users=[]): void;
47 47
 }
48 48
\ No newline at end of file
Please login to merge, or discard this patch.
apps/dav/lib/CalDAV/Reminder/ReminderService.php 1 patch
Indentation   +718 added lines, -718 removed lines patch added patch discarded remove patch
@@ -40,722 +40,722 @@
 block discarded – undo
40 40
 
41 41
 class ReminderService {
42 42
 
43
-	/** @var Backend */
44
-	private $backend;
45
-
46
-	/** @var NotificationProviderManager */
47
-	private $notificationProviderManager;
48
-
49
-	/** @var IUserManager */
50
-	private $userManager;
51
-
52
-	/** @var IGroupManager */
53
-	private $groupManager;
54
-
55
-	/** @var CalDavBackend */
56
-	private $caldavBackend;
57
-
58
-	/** @var ITimeFactory */
59
-	private $timeFactory;
60
-
61
-	public const REMINDER_TYPE_EMAIL = 'EMAIL';
62
-	public const REMINDER_TYPE_DISPLAY = 'DISPLAY';
63
-	public const REMINDER_TYPE_AUDIO = 'AUDIO';
64
-
65
-	/**
66
-	 * @var String[]
67
-	 *
68
-	 * Official RFC5545 reminder types
69
-	 */
70
-	public const REMINDER_TYPES = [
71
-		self::REMINDER_TYPE_EMAIL,
72
-		self::REMINDER_TYPE_DISPLAY,
73
-		self::REMINDER_TYPE_AUDIO
74
-	];
75
-
76
-	/**
77
-	 * ReminderService constructor.
78
-	 *
79
-	 * @param Backend $backend
80
-	 * @param NotificationProviderManager $notificationProviderManager
81
-	 * @param IUserManager $userManager
82
-	 * @param IGroupManager $groupManager
83
-	 * @param CalDavBackend $caldavBackend
84
-	 * @param ITimeFactory $timeFactory
85
-	 */
86
-	public function __construct(Backend $backend,
87
-								NotificationProviderManager $notificationProviderManager,
88
-								IUserManager $userManager,
89
-								IGroupManager $groupManager,
90
-								CalDavBackend $caldavBackend,
91
-								ITimeFactory $timeFactory) {
92
-		$this->backend = $backend;
93
-		$this->notificationProviderManager = $notificationProviderManager;
94
-		$this->userManager = $userManager;
95
-		$this->groupManager = $groupManager;
96
-		$this->caldavBackend = $caldavBackend;
97
-		$this->timeFactory = $timeFactory;
98
-	}
99
-
100
-	/**
101
-	 * Process reminders to activate
102
-	 *
103
-	 * @throws NotificationProvider\ProviderNotAvailableException
104
-	 * @throws NotificationTypeDoesNotExistException
105
-	 */
106
-	public function processReminders():void {
107
-		$reminders = $this->backend->getRemindersToProcess();
108
-
109
-		foreach($reminders as $reminder) {
110
-			$vcalendar = $this->parseCalendarData($reminder['calendardata']);
111
-			if (!$vcalendar) {
112
-				$this->backend->removeReminder($reminder['id']);
113
-				continue;
114
-			}
115
-
116
-			$vevent = $this->getVEventByRecurrenceId($vcalendar, $reminder['recurrence_id'], $reminder['is_recurrence_exception']);
117
-			if (!$vevent) {
118
-				$this->backend->removeReminder($reminder['id']);
119
-				continue;
120
-			}
121
-
122
-			if ($this->wasEventCancelled($vevent)) {
123
-				$this->deleteOrProcessNext($reminder, $vevent);
124
-				continue;
125
-			}
126
-
127
-			if (!$this->notificationProviderManager->hasProvider($reminder['type'])) {
128
-				$this->deleteOrProcessNext($reminder, $vevent);
129
-				continue;
130
-			}
131
-
132
-			$users = $this->getAllUsersWithWriteAccessToCalendar($reminder['calendar_id']);
133
-			$user = $this->getUserFromPrincipalURI($reminder['principaluri']);
134
-			if ($user) {
135
-				$users[] = $user;
136
-			}
137
-
138
-			$notificationProvider = $this->notificationProviderManager->getProvider($reminder['type']);
139
-			$notificationProvider->send($vevent, $reminder['displayname'], $users);
140
-
141
-			$this->deleteOrProcessNext($reminder, $vevent);
142
-		}
143
-	}
144
-
145
-	/**
146
-	 * @param string $action
147
-	 * @param array $objectData
148
-	 * @throws VObject\InvalidDataException
149
-	 */
150
-	public function onTouchCalendarObject(string $action,
151
-										  array $objectData):void {
152
-		// We only support VEvents for now
153
-		if (strcasecmp($objectData['component'], 'vevent') !== 0) {
154
-			return;
155
-		}
156
-
157
-		switch($action) {
158
-			case '\OCA\DAV\CalDAV\CalDavBackend::createCalendarObject':
159
-				$this->onCalendarObjectCreate($objectData);
160
-				break;
161
-
162
-			case '\OCA\DAV\CalDAV\CalDavBackend::updateCalendarObject':
163
-				$this->onCalendarObjectEdit($objectData);
164
-				break;
165
-
166
-			case '\OCA\DAV\CalDAV\CalDavBackend::deleteCalendarObject':
167
-				$this->onCalendarObjectDelete($objectData);
168
-				break;
169
-
170
-			default:
171
-				break;
172
-		}
173
-	}
174
-
175
-	/**
176
-	 * @param array $objectData
177
-	 */
178
-	private function onCalendarObjectCreate(array $objectData):void {
179
-		/** @var VObject\Component\VCalendar $vcalendar */
180
-		$vcalendar = $this->parseCalendarData($objectData['calendardata']);
181
-		if (!$vcalendar) {
182
-			return;
183
-		}
184
-
185
-		$vevents = $this->getAllVEventsFromVCalendar($vcalendar);
186
-		if (count($vevents) === 0) {
187
-			return;
188
-		}
189
-
190
-		$uid = (string) $vevents[0]->UID;
191
-		$recurrenceExceptions = $this->getRecurrenceExceptionFromListOfVEvents($vevents);
192
-		$masterItem = $this->getMasterItemFromListOfVEvents($vevents);
193
-		$now = $this->timeFactory->getDateTime();
194
-		$isRecurring = $masterItem ? $this->isRecurring($masterItem) : false;
195
-
196
-		foreach($recurrenceExceptions as $recurrenceException) {
197
-			$eventHash = $this->getEventHash($recurrenceException);
198
-
199
-			foreach($recurrenceException->VALARM as $valarm) {
200
-				/** @var VAlarm $valarm */
201
-				$alarmHash = $this->getAlarmHash($valarm);
202
-				$triggerTime = $valarm->getEffectiveTriggerTime();
203
-				$diff = $now->diff($triggerTime);
204
-				if ($diff->invert === 1) {
205
-					continue;
206
-				}
207
-
208
-				$alarms = $this->getRemindersForVAlarm($valarm, $objectData,
209
-					$eventHash, $alarmHash, true, true);
210
-				$this->writeRemindersToDatabase($alarms);
211
-			}
212
-		}
213
-
214
-		if ($masterItem) {
215
-			$processedAlarms = [];
216
-			$masterAlarms = [];
217
-			$masterHash = $this->getEventHash($masterItem);
218
-
219
-			foreach($masterItem->VALARM as $valarm) {
220
-				$masterAlarms[] = $this->getAlarmHash($valarm);
221
-			}
222
-
223
-			try {
224
-				$iterator = new EventIterator($vevents, $uid);
225
-			} catch (NoInstancesException $e) {
226
-				// This event is recurring, but it doesn't have a single
227
-				// instance. We are skipping this event from the output
228
-				// entirely.
229
-				return;
230
-			}
231
-
232
-			while($iterator->valid() && count($processedAlarms) < count($masterAlarms)) {
233
-				$event = $iterator->getEventObject();
234
-
235
-				// Recurrence-exceptions are handled separately, so just ignore them here
236
-				if (\in_array($event, $recurrenceExceptions, true)) {
237
-					$iterator->next();
238
-					continue;
239
-				}
240
-
241
-				foreach($event->VALARM as $valarm) {
242
-					/** @var VAlarm $valarm */
243
-					$alarmHash = $this->getAlarmHash($valarm);
244
-					if (\in_array($alarmHash, $processedAlarms, true)) {
245
-						continue;
246
-					}
247
-
248
-					if (!\in_array((string) $valarm->ACTION, self::REMINDER_TYPES, true)) {
249
-						// Action allows x-name, we don't insert reminders
250
-						// into the database if they are not standard
251
-						$processedAlarms[] = $alarmHash;
252
-						continue;
253
-					}
254
-
255
-					$triggerTime = $valarm->getEffectiveTriggerTime();
256
-
257
-					// If effective trigger time is in the past
258
-					// just skip and generate for next event
259
-					$diff = $now->diff($triggerTime);
260
-					if ($diff->invert === 1) {
261
-						// If an absolute alarm is in the past,
262
-						// just add it to processedAlarms, so
263
-						// we don't extend till eternity
264
-						if (!$this->isAlarmRelative($valarm)) {
265
-							$processedAlarms[] = $alarmHash;
266
-						}
267
-
268
-						continue;
269
-					}
270
-
271
-					$alarms = $this->getRemindersForVAlarm($valarm, $objectData, $masterHash, $alarmHash, $isRecurring, false);
272
-					$this->writeRemindersToDatabase($alarms);
273
-					$processedAlarms[] = $alarmHash;
274
-				}
275
-
276
-				$iterator->next();
277
-			}
278
-		}
279
-	}
280
-
281
-	/**
282
-	 * @param array $objectData
283
-	 */
284
-	private function onCalendarObjectEdit(array $objectData):void {
285
-		// TODO - this can be vastly improved
286
-		//  - get cached reminders
287
-		//  - ...
288
-
289
-		$this->onCalendarObjectDelete($objectData);
290
-		$this->onCalendarObjectCreate($objectData);
291
-	}
292
-
293
-	/**
294
-	 * @param array $objectData
295
-	 */
296
-	private function onCalendarObjectDelete(array $objectData):void {
297
-		$this->backend->cleanRemindersForEvent((int) $objectData['id']);
298
-	}
299
-
300
-	/**
301
-	 * @param VAlarm $valarm
302
-	 * @param array $objectData
303
-	 * @param string|null $eventHash
304
-	 * @param string|null $alarmHash
305
-	 * @param bool $isRecurring
306
-	 * @param bool $isRecurrenceException
307
-	 * @return array
308
-	 */
309
-	private function getRemindersForVAlarm(VAlarm $valarm,
310
-										   array $objectData,
311
-										   string $eventHash=null,
312
-										   string $alarmHash=null,
313
-										   bool $isRecurring=false,
314
-										   bool $isRecurrenceException=false):array {
315
-		if ($eventHash === null) {
316
-			$eventHash = $this->getEventHash($valarm->parent);
317
-		}
318
-		if ($alarmHash === null) {
319
-			$alarmHash = $this->getAlarmHash($valarm);
320
-		}
321
-
322
-		$recurrenceId = $this->getEffectiveRecurrenceIdOfVEvent($valarm->parent);
323
-		$isRelative = $this->isAlarmRelative($valarm);
324
-		/** @var DateTimeImmutable $notificationDate */
325
-		$notificationDate = $valarm->getEffectiveTriggerTime();
326
-		$clonedNotificationDate = new \DateTime('now', $notificationDate->getTimezone());
327
-		$clonedNotificationDate->setTimestamp($notificationDate->getTimestamp());
328
-
329
-		$alarms = [];
330
-
331
-		$alarms[] = [
332
-			'calendar_id' => $objectData['calendarid'],
333
-			'object_id' => $objectData['id'],
334
-			'uid' => (string) $valarm->parent->UID,
335
-			'is_recurring' => $isRecurring,
336
-			'recurrence_id' => $recurrenceId,
337
-			'is_recurrence_exception' => $isRecurrenceException,
338
-			'event_hash' => $eventHash,
339
-			'alarm_hash' => $alarmHash,
340
-			'type' => (string) $valarm->ACTION,
341
-			'is_relative' => $isRelative,
342
-			'notification_date' => $notificationDate->getTimestamp(),
343
-			'is_repeat_based' => false,
344
-		];
345
-
346
-		$repeat = isset($valarm->REPEAT) ? (int) $valarm->REPEAT->getValue() : 0;
347
-		for($i = 0; $i < $repeat; $i++) {
348
-			if ($valarm->DURATION === null) {
349
-				continue;
350
-			}
351
-
352
-			$clonedNotificationDate->add($valarm->DURATION->getDateInterval());
353
-			$alarms[] = [
354
-				'calendar_id' => $objectData['calendarid'],
355
-				'object_id' => $objectData['id'],
356
-				'uid' => (string) $valarm->parent->UID,
357
-				'is_recurring' => $isRecurring,
358
-				'recurrence_id' => $recurrenceId,
359
-				'is_recurrence_exception' => $isRecurrenceException,
360
-				'event_hash' => $eventHash,
361
-				'alarm_hash' => $alarmHash,
362
-				'type' => (string) $valarm->ACTION,
363
-				'is_relative' => $isRelative,
364
-				'notification_date' => $clonedNotificationDate->getTimestamp(),
365
-				'is_repeat_based' => true,
366
-			];
367
-		}
368
-
369
-		return $alarms;
370
-	}
371
-
372
-	/**
373
-	 * @param array $reminders
374
-	 */
375
-	private function writeRemindersToDatabase(array $reminders): void {
376
-		foreach($reminders as $reminder) {
377
-			$this->backend->insertReminder(
378
-				(int) $reminder['calendar_id'],
379
-				(int) $reminder['object_id'],
380
-				$reminder['uid'],
381
-				$reminder['is_recurring'],
382
-				(int) $reminder['recurrence_id'],
383
-				$reminder['is_recurrence_exception'],
384
-				$reminder['event_hash'],
385
-				$reminder['alarm_hash'],
386
-				$reminder['type'],
387
-				$reminder['is_relative'],
388
-				(int) $reminder['notification_date'],
389
-				$reminder['is_repeat_based']
390
-			);
391
-		}
392
-	}
393
-
394
-	/**
395
-	 * @param array $reminder
396
-	 * @param VEvent $vevent
397
-	 */
398
-	private function deleteOrProcessNext(array $reminder,
399
-										 VObject\Component\VEvent $vevent):void {
400
-		if ($reminder['is_repeat_based'] ||
401
-			!$reminder['is_recurring'] ||
402
-			!$reminder['is_relative'] ||
403
-			$reminder['is_recurrence_exception']) {
404
-
405
-			$this->backend->removeReminder($reminder['id']);
406
-			return;
407
-		}
408
-
409
-		$vevents = $this->getAllVEventsFromVCalendar($vevent->parent);
410
-		$recurrenceExceptions = $this->getRecurrenceExceptionFromListOfVEvents($vevents);
411
-		$now = $this->timeFactory->getDateTime();
412
-
413
-		try {
414
-			$iterator = new EventIterator($vevents, $reminder['uid']);
415
-		} catch (NoInstancesException $e) {
416
-			// This event is recurring, but it doesn't have a single
417
-			// instance. We are skipping this event from the output
418
-			// entirely.
419
-			return;
420
-		}
421
-
422
-		while($iterator->valid()) {
423
-			$event = $iterator->getEventObject();
424
-
425
-			// Recurrence-exceptions are handled separately, so just ignore them here
426
-			if (\in_array($event, $recurrenceExceptions, true)) {
427
-				$iterator->next();
428
-				continue;
429
-			}
430
-
431
-			$recurrenceId = $this->getEffectiveRecurrenceIdOfVEvent($event);
432
-			if ($reminder['recurrence_id'] >= $recurrenceId) {
433
-				$iterator->next();
434
-				continue;
435
-			}
436
-
437
-			foreach($event->VALARM as $valarm) {
438
-				/** @var VAlarm $valarm */
439
-				$alarmHash = $this->getAlarmHash($valarm);
440
-				if ($alarmHash !== $reminder['alarm_hash']) {
441
-					continue;
442
-				}
443
-
444
-				$triggerTime = $valarm->getEffectiveTriggerTime();
445
-
446
-				// If effective trigger time is in the past
447
-				// just skip and generate for next event
448
-				$diff = $now->diff($triggerTime);
449
-				if ($diff->invert === 1) {
450
-					continue;
451
-				}
452
-
453
-				$this->backend->removeReminder($reminder['id']);
454
-				$alarms = $this->getRemindersForVAlarm($valarm, [
455
-					'calendarid' => $reminder['calendar_id'],
456
-					'id' => $reminder['object_id'],
457
-				], $reminder['event_hash'], $alarmHash, true, false);
458
-				$this->writeRemindersToDatabase($alarms);
459
-
460
-				// Abort generating reminders after creating one successfully
461
-				return;
462
-			}
463
-
464
-			$iterator->next();
465
-		}
466
-
467
-		$this->backend->removeReminder($reminder['id']);
468
-	}
469
-
470
-	/**
471
-	 * @param int $calendarId
472
-	 * @return IUser[]
473
-	 */
474
-	private function getAllUsersWithWriteAccessToCalendar(int $calendarId):array {
475
-		$shares = $this->caldavBackend->getShares($calendarId);
476
-
477
-		$users = [];
478
-		$userIds = [];
479
-		$groups = [];
480
-		foreach ($shares as $share) {
481
-			// Only consider writable shares
482
-			if ($share['readOnly']) {
483
-				continue;
484
-			}
485
-
486
-			$principal = explode('/', $share['{http://owncloud.org/ns}principal']);
487
-			if ($principal[1] === 'users') {
488
-				$user = $this->userManager->get($principal[2]);
489
-				if ($user) {
490
-					$users[] = $user;
491
-					$userIds[] = $principal[2];
492
-				}
493
-			} else if ($principal[1] === 'groups') {
494
-				$groups[] = $principal[2];
495
-			}
496
-		}
497
-
498
-		foreach ($groups as $gid) {
499
-			$group = $this->groupManager->get($gid);
500
-			if ($group instanceof IGroup) {
501
-				foreach ($group->getUsers() as $user) {
502
-					if (!\in_array($user->getUID(), $userIds, true)) {
503
-						$users[] = $user;
504
-						$userIds[] = $user->getUID();
505
-					}
506
-				}
507
-			}
508
-		}
509
-
510
-		return $users;
511
-	}
512
-
513
-	/**
514
-	 * Gets a hash of the event.
515
-	 * If the hash changes, we have to update all relative alarms.
516
-	 *
517
-	 * @param VEvent $vevent
518
-	 * @return string
519
-	 */
520
-	private function getEventHash(VEvent $vevent):string {
521
-		$properties = [
522
-			(string) $vevent->DTSTART->serialize(),
523
-		];
524
-
525
-		if ($vevent->DTEND) {
526
-			$properties[] = (string) $vevent->DTEND->serialize();
527
-		}
528
-		if ($vevent->DURATION) {
529
-			$properties[] = (string) $vevent->DURATION->serialize();
530
-		}
531
-		if ($vevent->{'RECURRENCE-ID'}) {
532
-			$properties[] = (string) $vevent->{'RECURRENCE-ID'}->serialize();
533
-		}
534
-		if ($vevent->RRULE) {
535
-			$properties[] = (string) $vevent->RRULE->serialize();
536
-		}
537
-		if ($vevent->EXDATE) {
538
-			$properties[] = (string) $vevent->EXDATE->serialize();
539
-		}
540
-		if ($vevent->RDATE) {
541
-			$properties[] = (string) $vevent->RDATE->serialize();
542
-		}
543
-
544
-		return md5(implode('::', $properties));
545
-	}
546
-
547
-	/**
548
-	 * Gets a hash of the alarm.
549
-	 * If the hash changes, we have to update oc_dav_reminders.
550
-	 *
551
-	 * @param VAlarm $valarm
552
-	 * @return string
553
-	 */
554
-	private function getAlarmHash(VAlarm $valarm):string {
555
-		$properties = [
556
-			(string) $valarm->ACTION->serialize(),
557
-			(string) $valarm->TRIGGER->serialize(),
558
-		];
559
-
560
-		if ($valarm->DURATION) {
561
-			$properties[] = (string) $valarm->DURATION->serialize();
562
-		}
563
-		if ($valarm->REPEAT) {
564
-			$properties[] = (string) $valarm->REPEAT->serialize();
565
-		}
566
-
567
-		return md5(implode('::', $properties));
568
-	}
569
-
570
-	/**
571
-	 * @param VObject\Component\VCalendar $vcalendar
572
-	 * @param int $recurrenceId
573
-	 * @param bool $isRecurrenceException
574
-	 * @return VEvent|null
575
-	 */
576
-	private function getVEventByRecurrenceId(VObject\Component\VCalendar $vcalendar,
577
-											 int $recurrenceId,
578
-											 bool $isRecurrenceException):?VEvent {
579
-		$vevents = $this->getAllVEventsFromVCalendar($vcalendar);
580
-		if (count($vevents) === 0) {
581
-			return null;
582
-		}
583
-
584
-		$uid = (string) $vevents[0]->UID;
585
-		$recurrenceExceptions = $this->getRecurrenceExceptionFromListOfVEvents($vevents);
586
-		$masterItem = $this->getMasterItemFromListOfVEvents($vevents);
587
-
588
-		// Handle recurrence-exceptions first, because recurrence-expansion is expensive
589
-		if ($isRecurrenceException) {
590
-			foreach($recurrenceExceptions as $recurrenceException) {
591
-				if ($this->getEffectiveRecurrenceIdOfVEvent($recurrenceException) === $recurrenceId) {
592
-					return $recurrenceException;
593
-				}
594
-			}
595
-
596
-			return null;
597
-		}
598
-
599
-		if ($masterItem) {
600
-			try {
601
-				$iterator = new EventIterator($vevents, $uid);
602
-			} catch (NoInstancesException $e) {
603
-				// This event is recurring, but it doesn't have a single
604
-				// instance. We are skipping this event from the output
605
-				// entirely.
606
-				return null;
607
-			}
608
-
609
-			while ($iterator->valid()) {
610
-				$event = $iterator->getEventObject();
611
-
612
-				// Recurrence-exceptions are handled separately, so just ignore them here
613
-				if (\in_array($event, $recurrenceExceptions, true)) {
614
-					$iterator->next();
615
-					continue;
616
-				}
617
-
618
-				if ($this->getEffectiveRecurrenceIdOfVEvent($event) === $recurrenceId) {
619
-					return $event;
620
-				}
621
-
622
-				$iterator->next();
623
-			}
624
-		}
625
-
626
-		return null;
627
-	}
628
-
629
-	/**
630
-	 * @param VEvent $vevent
631
-	 * @return string
632
-	 */
633
-	private function getStatusOfEvent(VEvent $vevent):string {
634
-		if ($vevent->STATUS) {
635
-			return (string) $vevent->STATUS;
636
-		}
637
-
638
-		// Doesn't say so in the standard,
639
-		// but we consider events without a status
640
-		// to be confirmed
641
-		return 'CONFIRMED';
642
-	}
643
-
644
-	/**
645
-	 * @param VObject\Component\VEvent $vevent
646
-	 * @return bool
647
-	 */
648
-	private function wasEventCancelled(VObject\Component\VEvent $vevent):bool {
649
-		return $this->getStatusOfEvent($vevent) === 'CANCELLED';
650
-	}
651
-
652
-	/**
653
-	 * @param string $calendarData
654
-	 * @return VObject\Component\VCalendar|null
655
-	 */
656
-	private function parseCalendarData(string $calendarData):?VObject\Component\VCalendar {
657
-		try {
658
-			return VObject\Reader::read($calendarData,
659
-				VObject\Reader::OPTION_FORGIVING);
660
-		} catch(ParseException $ex) {
661
-			return null;
662
-		}
663
-	}
664
-
665
-	/**
666
-	 * @param string $principalUri
667
-	 * @return IUser|null
668
-	 */
669
-	private function getUserFromPrincipalURI(string $principalUri):?IUser {
670
-		if (!$principalUri) {
671
-			return null;
672
-		}
673
-
674
-		if (stripos($principalUri, 'principals/users/') !== 0) {
675
-			return null;
676
-		}
677
-
678
-		$userId = substr($principalUri, 17);
679
-		return $this->userManager->get($userId);
680
-	}
681
-
682
-	/**
683
-	 * @param VObject\Component\VCalendar $vcalendar
684
-	 * @return VObject\Component\VEvent[]
685
-	 */
686
-	private function getAllVEventsFromVCalendar(VObject\Component\VCalendar $vcalendar):array {
687
-		$vevents = [];
688
-
689
-		foreach($vcalendar->children() as $child) {
690
-			if (!($child instanceof VObject\Component)) {
691
-				continue;
692
-			}
693
-
694
-			if ($child->name !== 'VEVENT') {
695
-				continue;
696
-			}
697
-
698
-			$vevents[] = $child;
699
-		}
700
-
701
-		return $vevents;
702
-	}
703
-
704
-	/**
705
-	 * @param array $vevents
706
-	 * @return VObject\Component\VEvent[]
707
-	 */
708
-	private function getRecurrenceExceptionFromListOfVEvents(array $vevents):array {
709
-		return array_values(array_filter($vevents, function(VEvent $vevent) {
710
-			return $vevent->{'RECURRENCE-ID'} !== null;
711
-		}));
712
-	}
713
-
714
-	/**
715
-	 * @param array $vevents
716
-	 * @return VEvent|null
717
-	 */
718
-	private function getMasterItemFromListOfVEvents(array $vevents):?VEvent {
719
-		$elements = array_values(array_filter($vevents, function(VEvent $vevent) {
720
-			return $vevent->{'RECURRENCE-ID'} === null;
721
-		}));
722
-
723
-		if (count($elements) === 0) {
724
-			return null;
725
-		}
726
-		if (count($elements) > 1) {
727
-			throw new \TypeError('Multiple master objects');
728
-		}
729
-
730
-		return $elements[0];
731
-	}
732
-
733
-	/**
734
-	 * @param VAlarm $valarm
735
-	 * @return bool
736
-	 */
737
-	private function isAlarmRelative(VAlarm $valarm):bool {
738
-		$trigger = $valarm->TRIGGER;
739
-		return $trigger instanceof VObject\Property\ICalendar\Duration;
740
-	}
741
-
742
-	/**
743
-	 * @param VEvent $vevent
744
-	 * @return int
745
-	 */
746
-	private function getEffectiveRecurrenceIdOfVEvent(VEvent $vevent):int {
747
-		if (isset($vevent->{'RECURRENCE-ID'})) {
748
-			return $vevent->{'RECURRENCE-ID'}->getDateTime()->getTimestamp();
749
-		}
750
-
751
-		return $vevent->DTSTART->getDateTime()->getTimestamp();
752
-	}
753
-
754
-	/**
755
-	 * @param VEvent $vevent
756
-	 * @return bool
757
-	 */
758
-	private function isRecurring(VEvent $vevent):bool {
759
-		return isset($vevent->RRULE) || isset($vevent->RDATE);
760
-	}
43
+    /** @var Backend */
44
+    private $backend;
45
+
46
+    /** @var NotificationProviderManager */
47
+    private $notificationProviderManager;
48
+
49
+    /** @var IUserManager */
50
+    private $userManager;
51
+
52
+    /** @var IGroupManager */
53
+    private $groupManager;
54
+
55
+    /** @var CalDavBackend */
56
+    private $caldavBackend;
57
+
58
+    /** @var ITimeFactory */
59
+    private $timeFactory;
60
+
61
+    public const REMINDER_TYPE_EMAIL = 'EMAIL';
62
+    public const REMINDER_TYPE_DISPLAY = 'DISPLAY';
63
+    public const REMINDER_TYPE_AUDIO = 'AUDIO';
64
+
65
+    /**
66
+     * @var String[]
67
+     *
68
+     * Official RFC5545 reminder types
69
+     */
70
+    public const REMINDER_TYPES = [
71
+        self::REMINDER_TYPE_EMAIL,
72
+        self::REMINDER_TYPE_DISPLAY,
73
+        self::REMINDER_TYPE_AUDIO
74
+    ];
75
+
76
+    /**
77
+     * ReminderService constructor.
78
+     *
79
+     * @param Backend $backend
80
+     * @param NotificationProviderManager $notificationProviderManager
81
+     * @param IUserManager $userManager
82
+     * @param IGroupManager $groupManager
83
+     * @param CalDavBackend $caldavBackend
84
+     * @param ITimeFactory $timeFactory
85
+     */
86
+    public function __construct(Backend $backend,
87
+                                NotificationProviderManager $notificationProviderManager,
88
+                                IUserManager $userManager,
89
+                                IGroupManager $groupManager,
90
+                                CalDavBackend $caldavBackend,
91
+                                ITimeFactory $timeFactory) {
92
+        $this->backend = $backend;
93
+        $this->notificationProviderManager = $notificationProviderManager;
94
+        $this->userManager = $userManager;
95
+        $this->groupManager = $groupManager;
96
+        $this->caldavBackend = $caldavBackend;
97
+        $this->timeFactory = $timeFactory;
98
+    }
99
+
100
+    /**
101
+     * Process reminders to activate
102
+     *
103
+     * @throws NotificationProvider\ProviderNotAvailableException
104
+     * @throws NotificationTypeDoesNotExistException
105
+     */
106
+    public function processReminders():void {
107
+        $reminders = $this->backend->getRemindersToProcess();
108
+
109
+        foreach($reminders as $reminder) {
110
+            $vcalendar = $this->parseCalendarData($reminder['calendardata']);
111
+            if (!$vcalendar) {
112
+                $this->backend->removeReminder($reminder['id']);
113
+                continue;
114
+            }
115
+
116
+            $vevent = $this->getVEventByRecurrenceId($vcalendar, $reminder['recurrence_id'], $reminder['is_recurrence_exception']);
117
+            if (!$vevent) {
118
+                $this->backend->removeReminder($reminder['id']);
119
+                continue;
120
+            }
121
+
122
+            if ($this->wasEventCancelled($vevent)) {
123
+                $this->deleteOrProcessNext($reminder, $vevent);
124
+                continue;
125
+            }
126
+
127
+            if (!$this->notificationProviderManager->hasProvider($reminder['type'])) {
128
+                $this->deleteOrProcessNext($reminder, $vevent);
129
+                continue;
130
+            }
131
+
132
+            $users = $this->getAllUsersWithWriteAccessToCalendar($reminder['calendar_id']);
133
+            $user = $this->getUserFromPrincipalURI($reminder['principaluri']);
134
+            if ($user) {
135
+                $users[] = $user;
136
+            }
137
+
138
+            $notificationProvider = $this->notificationProviderManager->getProvider($reminder['type']);
139
+            $notificationProvider->send($vevent, $reminder['displayname'], $users);
140
+
141
+            $this->deleteOrProcessNext($reminder, $vevent);
142
+        }
143
+    }
144
+
145
+    /**
146
+     * @param string $action
147
+     * @param array $objectData
148
+     * @throws VObject\InvalidDataException
149
+     */
150
+    public function onTouchCalendarObject(string $action,
151
+                                            array $objectData):void {
152
+        // We only support VEvents for now
153
+        if (strcasecmp($objectData['component'], 'vevent') !== 0) {
154
+            return;
155
+        }
156
+
157
+        switch($action) {
158
+            case '\OCA\DAV\CalDAV\CalDavBackend::createCalendarObject':
159
+                $this->onCalendarObjectCreate($objectData);
160
+                break;
161
+
162
+            case '\OCA\DAV\CalDAV\CalDavBackend::updateCalendarObject':
163
+                $this->onCalendarObjectEdit($objectData);
164
+                break;
165
+
166
+            case '\OCA\DAV\CalDAV\CalDavBackend::deleteCalendarObject':
167
+                $this->onCalendarObjectDelete($objectData);
168
+                break;
169
+
170
+            default:
171
+                break;
172
+        }
173
+    }
174
+
175
+    /**
176
+     * @param array $objectData
177
+     */
178
+    private function onCalendarObjectCreate(array $objectData):void {
179
+        /** @var VObject\Component\VCalendar $vcalendar */
180
+        $vcalendar = $this->parseCalendarData($objectData['calendardata']);
181
+        if (!$vcalendar) {
182
+            return;
183
+        }
184
+
185
+        $vevents = $this->getAllVEventsFromVCalendar($vcalendar);
186
+        if (count($vevents) === 0) {
187
+            return;
188
+        }
189
+
190
+        $uid = (string) $vevents[0]->UID;
191
+        $recurrenceExceptions = $this->getRecurrenceExceptionFromListOfVEvents($vevents);
192
+        $masterItem = $this->getMasterItemFromListOfVEvents($vevents);
193
+        $now = $this->timeFactory->getDateTime();
194
+        $isRecurring = $masterItem ? $this->isRecurring($masterItem) : false;
195
+
196
+        foreach($recurrenceExceptions as $recurrenceException) {
197
+            $eventHash = $this->getEventHash($recurrenceException);
198
+
199
+            foreach($recurrenceException->VALARM as $valarm) {
200
+                /** @var VAlarm $valarm */
201
+                $alarmHash = $this->getAlarmHash($valarm);
202
+                $triggerTime = $valarm->getEffectiveTriggerTime();
203
+                $diff = $now->diff($triggerTime);
204
+                if ($diff->invert === 1) {
205
+                    continue;
206
+                }
207
+
208
+                $alarms = $this->getRemindersForVAlarm($valarm, $objectData,
209
+                    $eventHash, $alarmHash, true, true);
210
+                $this->writeRemindersToDatabase($alarms);
211
+            }
212
+        }
213
+
214
+        if ($masterItem) {
215
+            $processedAlarms = [];
216
+            $masterAlarms = [];
217
+            $masterHash = $this->getEventHash($masterItem);
218
+
219
+            foreach($masterItem->VALARM as $valarm) {
220
+                $masterAlarms[] = $this->getAlarmHash($valarm);
221
+            }
222
+
223
+            try {
224
+                $iterator = new EventIterator($vevents, $uid);
225
+            } catch (NoInstancesException $e) {
226
+                // This event is recurring, but it doesn't have a single
227
+                // instance. We are skipping this event from the output
228
+                // entirely.
229
+                return;
230
+            }
231
+
232
+            while($iterator->valid() && count($processedAlarms) < count($masterAlarms)) {
233
+                $event = $iterator->getEventObject();
234
+
235
+                // Recurrence-exceptions are handled separately, so just ignore them here
236
+                if (\in_array($event, $recurrenceExceptions, true)) {
237
+                    $iterator->next();
238
+                    continue;
239
+                }
240
+
241
+                foreach($event->VALARM as $valarm) {
242
+                    /** @var VAlarm $valarm */
243
+                    $alarmHash = $this->getAlarmHash($valarm);
244
+                    if (\in_array($alarmHash, $processedAlarms, true)) {
245
+                        continue;
246
+                    }
247
+
248
+                    if (!\in_array((string) $valarm->ACTION, self::REMINDER_TYPES, true)) {
249
+                        // Action allows x-name, we don't insert reminders
250
+                        // into the database if they are not standard
251
+                        $processedAlarms[] = $alarmHash;
252
+                        continue;
253
+                    }
254
+
255
+                    $triggerTime = $valarm->getEffectiveTriggerTime();
256
+
257
+                    // If effective trigger time is in the past
258
+                    // just skip and generate for next event
259
+                    $diff = $now->diff($triggerTime);
260
+                    if ($diff->invert === 1) {
261
+                        // If an absolute alarm is in the past,
262
+                        // just add it to processedAlarms, so
263
+                        // we don't extend till eternity
264
+                        if (!$this->isAlarmRelative($valarm)) {
265
+                            $processedAlarms[] = $alarmHash;
266
+                        }
267
+
268
+                        continue;
269
+                    }
270
+
271
+                    $alarms = $this->getRemindersForVAlarm($valarm, $objectData, $masterHash, $alarmHash, $isRecurring, false);
272
+                    $this->writeRemindersToDatabase($alarms);
273
+                    $processedAlarms[] = $alarmHash;
274
+                }
275
+
276
+                $iterator->next();
277
+            }
278
+        }
279
+    }
280
+
281
+    /**
282
+     * @param array $objectData
283
+     */
284
+    private function onCalendarObjectEdit(array $objectData):void {
285
+        // TODO - this can be vastly improved
286
+        //  - get cached reminders
287
+        //  - ...
288
+
289
+        $this->onCalendarObjectDelete($objectData);
290
+        $this->onCalendarObjectCreate($objectData);
291
+    }
292
+
293
+    /**
294
+     * @param array $objectData
295
+     */
296
+    private function onCalendarObjectDelete(array $objectData):void {
297
+        $this->backend->cleanRemindersForEvent((int) $objectData['id']);
298
+    }
299
+
300
+    /**
301
+     * @param VAlarm $valarm
302
+     * @param array $objectData
303
+     * @param string|null $eventHash
304
+     * @param string|null $alarmHash
305
+     * @param bool $isRecurring
306
+     * @param bool $isRecurrenceException
307
+     * @return array
308
+     */
309
+    private function getRemindersForVAlarm(VAlarm $valarm,
310
+                                            array $objectData,
311
+                                            string $eventHash=null,
312
+                                            string $alarmHash=null,
313
+                                            bool $isRecurring=false,
314
+                                            bool $isRecurrenceException=false):array {
315
+        if ($eventHash === null) {
316
+            $eventHash = $this->getEventHash($valarm->parent);
317
+        }
318
+        if ($alarmHash === null) {
319
+            $alarmHash = $this->getAlarmHash($valarm);
320
+        }
321
+
322
+        $recurrenceId = $this->getEffectiveRecurrenceIdOfVEvent($valarm->parent);
323
+        $isRelative = $this->isAlarmRelative($valarm);
324
+        /** @var DateTimeImmutable $notificationDate */
325
+        $notificationDate = $valarm->getEffectiveTriggerTime();
326
+        $clonedNotificationDate = new \DateTime('now', $notificationDate->getTimezone());
327
+        $clonedNotificationDate->setTimestamp($notificationDate->getTimestamp());
328
+
329
+        $alarms = [];
330
+
331
+        $alarms[] = [
332
+            'calendar_id' => $objectData['calendarid'],
333
+            'object_id' => $objectData['id'],
334
+            'uid' => (string) $valarm->parent->UID,
335
+            'is_recurring' => $isRecurring,
336
+            'recurrence_id' => $recurrenceId,
337
+            'is_recurrence_exception' => $isRecurrenceException,
338
+            'event_hash' => $eventHash,
339
+            'alarm_hash' => $alarmHash,
340
+            'type' => (string) $valarm->ACTION,
341
+            'is_relative' => $isRelative,
342
+            'notification_date' => $notificationDate->getTimestamp(),
343
+            'is_repeat_based' => false,
344
+        ];
345
+
346
+        $repeat = isset($valarm->REPEAT) ? (int) $valarm->REPEAT->getValue() : 0;
347
+        for($i = 0; $i < $repeat; $i++) {
348
+            if ($valarm->DURATION === null) {
349
+                continue;
350
+            }
351
+
352
+            $clonedNotificationDate->add($valarm->DURATION->getDateInterval());
353
+            $alarms[] = [
354
+                'calendar_id' => $objectData['calendarid'],
355
+                'object_id' => $objectData['id'],
356
+                'uid' => (string) $valarm->parent->UID,
357
+                'is_recurring' => $isRecurring,
358
+                'recurrence_id' => $recurrenceId,
359
+                'is_recurrence_exception' => $isRecurrenceException,
360
+                'event_hash' => $eventHash,
361
+                'alarm_hash' => $alarmHash,
362
+                'type' => (string) $valarm->ACTION,
363
+                'is_relative' => $isRelative,
364
+                'notification_date' => $clonedNotificationDate->getTimestamp(),
365
+                'is_repeat_based' => true,
366
+            ];
367
+        }
368
+
369
+        return $alarms;
370
+    }
371
+
372
+    /**
373
+     * @param array $reminders
374
+     */
375
+    private function writeRemindersToDatabase(array $reminders): void {
376
+        foreach($reminders as $reminder) {
377
+            $this->backend->insertReminder(
378
+                (int) $reminder['calendar_id'],
379
+                (int) $reminder['object_id'],
380
+                $reminder['uid'],
381
+                $reminder['is_recurring'],
382
+                (int) $reminder['recurrence_id'],
383
+                $reminder['is_recurrence_exception'],
384
+                $reminder['event_hash'],
385
+                $reminder['alarm_hash'],
386
+                $reminder['type'],
387
+                $reminder['is_relative'],
388
+                (int) $reminder['notification_date'],
389
+                $reminder['is_repeat_based']
390
+            );
391
+        }
392
+    }
393
+
394
+    /**
395
+     * @param array $reminder
396
+     * @param VEvent $vevent
397
+     */
398
+    private function deleteOrProcessNext(array $reminder,
399
+                                            VObject\Component\VEvent $vevent):void {
400
+        if ($reminder['is_repeat_based'] ||
401
+            !$reminder['is_recurring'] ||
402
+            !$reminder['is_relative'] ||
403
+            $reminder['is_recurrence_exception']) {
404
+
405
+            $this->backend->removeReminder($reminder['id']);
406
+            return;
407
+        }
408
+
409
+        $vevents = $this->getAllVEventsFromVCalendar($vevent->parent);
410
+        $recurrenceExceptions = $this->getRecurrenceExceptionFromListOfVEvents($vevents);
411
+        $now = $this->timeFactory->getDateTime();
412
+
413
+        try {
414
+            $iterator = new EventIterator($vevents, $reminder['uid']);
415
+        } catch (NoInstancesException $e) {
416
+            // This event is recurring, but it doesn't have a single
417
+            // instance. We are skipping this event from the output
418
+            // entirely.
419
+            return;
420
+        }
421
+
422
+        while($iterator->valid()) {
423
+            $event = $iterator->getEventObject();
424
+
425
+            // Recurrence-exceptions are handled separately, so just ignore them here
426
+            if (\in_array($event, $recurrenceExceptions, true)) {
427
+                $iterator->next();
428
+                continue;
429
+            }
430
+
431
+            $recurrenceId = $this->getEffectiveRecurrenceIdOfVEvent($event);
432
+            if ($reminder['recurrence_id'] >= $recurrenceId) {
433
+                $iterator->next();
434
+                continue;
435
+            }
436
+
437
+            foreach($event->VALARM as $valarm) {
438
+                /** @var VAlarm $valarm */
439
+                $alarmHash = $this->getAlarmHash($valarm);
440
+                if ($alarmHash !== $reminder['alarm_hash']) {
441
+                    continue;
442
+                }
443
+
444
+                $triggerTime = $valarm->getEffectiveTriggerTime();
445
+
446
+                // If effective trigger time is in the past
447
+                // just skip and generate for next event
448
+                $diff = $now->diff($triggerTime);
449
+                if ($diff->invert === 1) {
450
+                    continue;
451
+                }
452
+
453
+                $this->backend->removeReminder($reminder['id']);
454
+                $alarms = $this->getRemindersForVAlarm($valarm, [
455
+                    'calendarid' => $reminder['calendar_id'],
456
+                    'id' => $reminder['object_id'],
457
+                ], $reminder['event_hash'], $alarmHash, true, false);
458
+                $this->writeRemindersToDatabase($alarms);
459
+
460
+                // Abort generating reminders after creating one successfully
461
+                return;
462
+            }
463
+
464
+            $iterator->next();
465
+        }
466
+
467
+        $this->backend->removeReminder($reminder['id']);
468
+    }
469
+
470
+    /**
471
+     * @param int $calendarId
472
+     * @return IUser[]
473
+     */
474
+    private function getAllUsersWithWriteAccessToCalendar(int $calendarId):array {
475
+        $shares = $this->caldavBackend->getShares($calendarId);
476
+
477
+        $users = [];
478
+        $userIds = [];
479
+        $groups = [];
480
+        foreach ($shares as $share) {
481
+            // Only consider writable shares
482
+            if ($share['readOnly']) {
483
+                continue;
484
+            }
485
+
486
+            $principal = explode('/', $share['{http://owncloud.org/ns}principal']);
487
+            if ($principal[1] === 'users') {
488
+                $user = $this->userManager->get($principal[2]);
489
+                if ($user) {
490
+                    $users[] = $user;
491
+                    $userIds[] = $principal[2];
492
+                }
493
+            } else if ($principal[1] === 'groups') {
494
+                $groups[] = $principal[2];
495
+            }
496
+        }
497
+
498
+        foreach ($groups as $gid) {
499
+            $group = $this->groupManager->get($gid);
500
+            if ($group instanceof IGroup) {
501
+                foreach ($group->getUsers() as $user) {
502
+                    if (!\in_array($user->getUID(), $userIds, true)) {
503
+                        $users[] = $user;
504
+                        $userIds[] = $user->getUID();
505
+                    }
506
+                }
507
+            }
508
+        }
509
+
510
+        return $users;
511
+    }
512
+
513
+    /**
514
+     * Gets a hash of the event.
515
+     * If the hash changes, we have to update all relative alarms.
516
+     *
517
+     * @param VEvent $vevent
518
+     * @return string
519
+     */
520
+    private function getEventHash(VEvent $vevent):string {
521
+        $properties = [
522
+            (string) $vevent->DTSTART->serialize(),
523
+        ];
524
+
525
+        if ($vevent->DTEND) {
526
+            $properties[] = (string) $vevent->DTEND->serialize();
527
+        }
528
+        if ($vevent->DURATION) {
529
+            $properties[] = (string) $vevent->DURATION->serialize();
530
+        }
531
+        if ($vevent->{'RECURRENCE-ID'}) {
532
+            $properties[] = (string) $vevent->{'RECURRENCE-ID'}->serialize();
533
+        }
534
+        if ($vevent->RRULE) {
535
+            $properties[] = (string) $vevent->RRULE->serialize();
536
+        }
537
+        if ($vevent->EXDATE) {
538
+            $properties[] = (string) $vevent->EXDATE->serialize();
539
+        }
540
+        if ($vevent->RDATE) {
541
+            $properties[] = (string) $vevent->RDATE->serialize();
542
+        }
543
+
544
+        return md5(implode('::', $properties));
545
+    }
546
+
547
+    /**
548
+     * Gets a hash of the alarm.
549
+     * If the hash changes, we have to update oc_dav_reminders.
550
+     *
551
+     * @param VAlarm $valarm
552
+     * @return string
553
+     */
554
+    private function getAlarmHash(VAlarm $valarm):string {
555
+        $properties = [
556
+            (string) $valarm->ACTION->serialize(),
557
+            (string) $valarm->TRIGGER->serialize(),
558
+        ];
559
+
560
+        if ($valarm->DURATION) {
561
+            $properties[] = (string) $valarm->DURATION->serialize();
562
+        }
563
+        if ($valarm->REPEAT) {
564
+            $properties[] = (string) $valarm->REPEAT->serialize();
565
+        }
566
+
567
+        return md5(implode('::', $properties));
568
+    }
569
+
570
+    /**
571
+     * @param VObject\Component\VCalendar $vcalendar
572
+     * @param int $recurrenceId
573
+     * @param bool $isRecurrenceException
574
+     * @return VEvent|null
575
+     */
576
+    private function getVEventByRecurrenceId(VObject\Component\VCalendar $vcalendar,
577
+                                                int $recurrenceId,
578
+                                                bool $isRecurrenceException):?VEvent {
579
+        $vevents = $this->getAllVEventsFromVCalendar($vcalendar);
580
+        if (count($vevents) === 0) {
581
+            return null;
582
+        }
583
+
584
+        $uid = (string) $vevents[0]->UID;
585
+        $recurrenceExceptions = $this->getRecurrenceExceptionFromListOfVEvents($vevents);
586
+        $masterItem = $this->getMasterItemFromListOfVEvents($vevents);
587
+
588
+        // Handle recurrence-exceptions first, because recurrence-expansion is expensive
589
+        if ($isRecurrenceException) {
590
+            foreach($recurrenceExceptions as $recurrenceException) {
591
+                if ($this->getEffectiveRecurrenceIdOfVEvent($recurrenceException) === $recurrenceId) {
592
+                    return $recurrenceException;
593
+                }
594
+            }
595
+
596
+            return null;
597
+        }
598
+
599
+        if ($masterItem) {
600
+            try {
601
+                $iterator = new EventIterator($vevents, $uid);
602
+            } catch (NoInstancesException $e) {
603
+                // This event is recurring, but it doesn't have a single
604
+                // instance. We are skipping this event from the output
605
+                // entirely.
606
+                return null;
607
+            }
608
+
609
+            while ($iterator->valid()) {
610
+                $event = $iterator->getEventObject();
611
+
612
+                // Recurrence-exceptions are handled separately, so just ignore them here
613
+                if (\in_array($event, $recurrenceExceptions, true)) {
614
+                    $iterator->next();
615
+                    continue;
616
+                }
617
+
618
+                if ($this->getEffectiveRecurrenceIdOfVEvent($event) === $recurrenceId) {
619
+                    return $event;
620
+                }
621
+
622
+                $iterator->next();
623
+            }
624
+        }
625
+
626
+        return null;
627
+    }
628
+
629
+    /**
630
+     * @param VEvent $vevent
631
+     * @return string
632
+     */
633
+    private function getStatusOfEvent(VEvent $vevent):string {
634
+        if ($vevent->STATUS) {
635
+            return (string) $vevent->STATUS;
636
+        }
637
+
638
+        // Doesn't say so in the standard,
639
+        // but we consider events without a status
640
+        // to be confirmed
641
+        return 'CONFIRMED';
642
+    }
643
+
644
+    /**
645
+     * @param VObject\Component\VEvent $vevent
646
+     * @return bool
647
+     */
648
+    private function wasEventCancelled(VObject\Component\VEvent $vevent):bool {
649
+        return $this->getStatusOfEvent($vevent) === 'CANCELLED';
650
+    }
651
+
652
+    /**
653
+     * @param string $calendarData
654
+     * @return VObject\Component\VCalendar|null
655
+     */
656
+    private function parseCalendarData(string $calendarData):?VObject\Component\VCalendar {
657
+        try {
658
+            return VObject\Reader::read($calendarData,
659
+                VObject\Reader::OPTION_FORGIVING);
660
+        } catch(ParseException $ex) {
661
+            return null;
662
+        }
663
+    }
664
+
665
+    /**
666
+     * @param string $principalUri
667
+     * @return IUser|null
668
+     */
669
+    private function getUserFromPrincipalURI(string $principalUri):?IUser {
670
+        if (!$principalUri) {
671
+            return null;
672
+        }
673
+
674
+        if (stripos($principalUri, 'principals/users/') !== 0) {
675
+            return null;
676
+        }
677
+
678
+        $userId = substr($principalUri, 17);
679
+        return $this->userManager->get($userId);
680
+    }
681
+
682
+    /**
683
+     * @param VObject\Component\VCalendar $vcalendar
684
+     * @return VObject\Component\VEvent[]
685
+     */
686
+    private function getAllVEventsFromVCalendar(VObject\Component\VCalendar $vcalendar):array {
687
+        $vevents = [];
688
+
689
+        foreach($vcalendar->children() as $child) {
690
+            if (!($child instanceof VObject\Component)) {
691
+                continue;
692
+            }
693
+
694
+            if ($child->name !== 'VEVENT') {
695
+                continue;
696
+            }
697
+
698
+            $vevents[] = $child;
699
+        }
700
+
701
+        return $vevents;
702
+    }
703
+
704
+    /**
705
+     * @param array $vevents
706
+     * @return VObject\Component\VEvent[]
707
+     */
708
+    private function getRecurrenceExceptionFromListOfVEvents(array $vevents):array {
709
+        return array_values(array_filter($vevents, function(VEvent $vevent) {
710
+            return $vevent->{'RECURRENCE-ID'} !== null;
711
+        }));
712
+    }
713
+
714
+    /**
715
+     * @param array $vevents
716
+     * @return VEvent|null
717
+     */
718
+    private function getMasterItemFromListOfVEvents(array $vevents):?VEvent {
719
+        $elements = array_values(array_filter($vevents, function(VEvent $vevent) {
720
+            return $vevent->{'RECURRENCE-ID'} === null;
721
+        }));
722
+
723
+        if (count($elements) === 0) {
724
+            return null;
725
+        }
726
+        if (count($elements) > 1) {
727
+            throw new \TypeError('Multiple master objects');
728
+        }
729
+
730
+        return $elements[0];
731
+    }
732
+
733
+    /**
734
+     * @param VAlarm $valarm
735
+     * @return bool
736
+     */
737
+    private function isAlarmRelative(VAlarm $valarm):bool {
738
+        $trigger = $valarm->TRIGGER;
739
+        return $trigger instanceof VObject\Property\ICalendar\Duration;
740
+    }
741
+
742
+    /**
743
+     * @param VEvent $vevent
744
+     * @return int
745
+     */
746
+    private function getEffectiveRecurrenceIdOfVEvent(VEvent $vevent):int {
747
+        if (isset($vevent->{'RECURRENCE-ID'})) {
748
+            return $vevent->{'RECURRENCE-ID'}->getDateTime()->getTimestamp();
749
+        }
750
+
751
+        return $vevent->DTSTART->getDateTime()->getTimestamp();
752
+    }
753
+
754
+    /**
755
+     * @param VEvent $vevent
756
+     * @return bool
757
+     */
758
+    private function isRecurring(VEvent $vevent):bool {
759
+        return isset($vevent->RRULE) || isset($vevent->RDATE);
760
+    }
761 761
 }
Please login to merge, or discard this patch.
apps/dav/lib/CalDAV/Reminder/NotificationProviderManager.php 1 patch
Indentation   +42 added lines, -42 removed lines patch added patch discarded remove patch
@@ -31,51 +31,51 @@
 block discarded – undo
31 31
  */
32 32
 class NotificationProviderManager {
33 33
 
34
-	/** @var INotificationProvider[] */
35
-	private $providers = [];
34
+    /** @var INotificationProvider[] */
35
+    private $providers = [];
36 36
 
37
-	/**
38
-	 * Checks whether a provider for a given ACTION exists
39
-	 *
40
-	 * @param string $type
41
-	 * @return bool
42
-	 */
43
-	public function hasProvider(string $type):bool {
44
-		return (\in_array($type, ReminderService::REMINDER_TYPES, true)
45
-			&& isset($this->providers[$type]));
46
-	}
37
+    /**
38
+     * Checks whether a provider for a given ACTION exists
39
+     *
40
+     * @param string $type
41
+     * @return bool
42
+     */
43
+    public function hasProvider(string $type):bool {
44
+        return (\in_array($type, ReminderService::REMINDER_TYPES, true)
45
+            && isset($this->providers[$type]));
46
+    }
47 47
 
48
-	/**
49
-	 * Get provider for a given ACTION
50
-	 *
51
-	 * @param string $type
52
-	 * @return INotificationProvider
53
-	 * @throws NotificationProvider\ProviderNotAvailableException
54
-	 * @throws NotificationTypeDoesNotExistException
55
-	 */
56
-	public function getProvider(string $type):INotificationProvider {
57
-		if (in_array($type, ReminderService::REMINDER_TYPES, true)) {
58
-			if (isset($this->providers[$type])) {
59
-				return $this->providers[$type];
60
-			}
61
-			throw new NotificationProvider\ProviderNotAvailableException($type);
62
-		}
63
-		throw new NotificationTypeDoesNotExistException($type);
64
-	}
48
+    /**
49
+     * Get provider for a given ACTION
50
+     *
51
+     * @param string $type
52
+     * @return INotificationProvider
53
+     * @throws NotificationProvider\ProviderNotAvailableException
54
+     * @throws NotificationTypeDoesNotExistException
55
+     */
56
+    public function getProvider(string $type):INotificationProvider {
57
+        if (in_array($type, ReminderService::REMINDER_TYPES, true)) {
58
+            if (isset($this->providers[$type])) {
59
+                return $this->providers[$type];
60
+            }
61
+            throw new NotificationProvider\ProviderNotAvailableException($type);
62
+        }
63
+        throw new NotificationTypeDoesNotExistException($type);
64
+    }
65 65
 
66
-	/**
67
-	 * Registers a new provider
68
-	 *
69
-	 * @param string $providerClassName
70
-	 * @throws \OCP\AppFramework\QueryException
71
-	 */
72
-	public function registerProvider(string $providerClassName):void {
73
-		$provider = \OC::$server->query($providerClassName);
66
+    /**
67
+     * Registers a new provider
68
+     *
69
+     * @param string $providerClassName
70
+     * @throws \OCP\AppFramework\QueryException
71
+     */
72
+    public function registerProvider(string $providerClassName):void {
73
+        $provider = \OC::$server->query($providerClassName);
74 74
 
75
-		if (!$provider instanceof INotificationProvider) {
76
-			throw new \InvalidArgumentException('Invalid notification provider registered');
77
-		}
75
+        if (!$provider instanceof INotificationProvider) {
76
+            throw new \InvalidArgumentException('Invalid notification provider registered');
77
+        }
78 78
 
79
-		$this->providers[$provider::NOTIFICATION_TYPE] = $provider;
80
-	}
79
+        $this->providers[$provider::NOTIFICATION_TYPE] = $provider;
80
+    }
81 81
 }
Please login to merge, or discard this patch.
apps/dav/lib/CalDAV/Reminder/NotificationTypeDoesNotExistException.php 1 patch
Indentation   +10 added lines, -10 removed lines patch added patch discarded remove patch
@@ -26,15 +26,15 @@
 block discarded – undo
26 26
 
27 27
 class NotificationTypeDoesNotExistException extends \Exception {
28 28
 
29
-	/**
30
-	 * NotificationTypeDoesNotExistException constructor.
31
-	 *
32
-	 * @since 16.0.0
33
-	 *
34
-	 * @param string $type ReminderType
35
-	 */
36
-	public function __construct(string $type) {
37
-		parent::__construct("Type $type is not an accepted type of notification");
38
-	}
29
+    /**
30
+     * NotificationTypeDoesNotExistException constructor.
31
+     *
32
+     * @since 16.0.0
33
+     *
34
+     * @param string $type ReminderType
35
+     */
36
+    public function __construct(string $type) {
37
+        parent::__construct("Type $type is not an accepted type of notification");
38
+    }
39 39
 
40 40
 }
Please login to merge, or discard this patch.
apps/dav/lib/CalDAV/Reminder/NotificationProvider/AbstractProvider.php 1 patch
Indentation   +142 added lines, -142 removed lines patch added patch discarded remove patch
@@ -43,146 +43,146 @@
 block discarded – undo
43 43
  */
44 44
 abstract class AbstractProvider implements INotificationProvider  {
45 45
 
46
-	/** @var string */
47
-	public const NOTIFICATION_TYPE = '';
48
-
49
-	/** @var ILogger */
50
-	protected $logger;
51
-
52
-	/** @var L10NFactory */
53
-	private $l10nFactory;
54
-
55
-	/** @var IL10N[] */
56
-	private $l10ns;
57
-
58
-	/** @var string */
59
-	private $fallbackLanguage;
60
-
61
-	/** @var IURLGenerator */
62
-	protected $urlGenerator;
63
-
64
-	/** @var IConfig */
65
-	protected $config;
66
-
67
-	/**
68
-	 * @param ILogger $logger
69
-	 * @param L10NFactory $l10nFactory
70
-	 * @param IConfig $config
71
-	 * @param IUrlGenerator $urlGenerator
72
-	 */
73
-	public function __construct(ILogger $logger,
74
-								L10NFactory $l10nFactory,
75
-								IURLGenerator $urlGenerator,
76
-								IConfig $config) {
77
-		$this->logger = $logger;
78
-		$this->l10nFactory = $l10nFactory;
79
-		$this->urlGenerator = $urlGenerator;
80
-		$this->config = $config;
81
-	}
82
-
83
-	/**
84
-	 * Send notification
85
-	 *
86
-	 * @param VEvent $vevent
87
-	 * @param string $calendarDisplayName
88
-	 * @param IUser[] $users
89
-	 * @return void
90
-	 */
91
-	abstract public function send(VEvent $vevent,
92
-						   string $calendarDisplayName,
93
-						   array $users=[]): void;
94
-
95
-	/**
96
-	 * @return string
97
-	 */
98
-	protected function getFallbackLanguage():string {
99
-		if ($this->fallbackLanguage) {
100
-			return $this->fallbackLanguage;
101
-		}
102
-
103
-		$fallbackLanguage = $this->l10nFactory->findLanguage();
104
-		$this->fallbackLanguage = $fallbackLanguage;
105
-
106
-		return $fallbackLanguage;
107
-	}
108
-
109
-	/**
110
-	 * @param string $lang
111
-	 * @return bool
112
-	 */
113
-	protected function hasL10NForLang(string $lang):bool {
114
-		return $this->l10nFactory->languageExists('dav', $lang);
115
-	}
116
-
117
-	/**
118
-	 * @param string $lang
119
-	 * @return IL10N
120
-	 */
121
-	protected function getL10NForLang(string $lang):IL10N {
122
-		if (isset($this->l10ns[$lang])) {
123
-			return $this->l10ns[$lang];
124
-		}
125
-
126
-		$l10n = $this->l10nFactory->get('dav', $lang);
127
-		$this->l10ns[$lang] = $l10n;
128
-
129
-		return $l10n;
130
-	}
131
-
132
-	/**
133
-	 * @param VEvent $vevent
134
-	 * @return string
135
-	 */
136
-	private function getStatusOfEvent(VEvent $vevent):string {
137
-		if ($vevent->STATUS) {
138
-			return (string) $vevent->STATUS;
139
-		}
140
-
141
-		// Doesn't say so in the standard,
142
-		// but we consider events without a status
143
-		// to be confirmed
144
-		return 'CONFIRMED';
145
-	}
146
-
147
-	/**
148
-	 * @param VEvent $vevent
149
-	 * @return bool
150
-	 */
151
-	protected function isEventTentative(VEvent $vevent):bool {
152
-		return $this->getStatusOfEvent($vevent) === 'TENTATIVE';
153
-	}
154
-
155
-	/**
156
-	 * @param VEvent $vevent
157
-	 * @return Property\ICalendar\DateTime
158
-	 */
159
-	protected function getDTEndFromEvent(VEvent $vevent):Property\ICalendar\DateTime {
160
-		if (isset($vevent->DTEND)) {
161
-			return $vevent->DTEND;
162
-		}
163
-
164
-		if (isset($vevent->DURATION)) {
165
-			$isFloating = $vevent->DTSTART->isFloating();
166
-			/** @var Property\ICalendar\DateTime $end */
167
-			$end = clone $vevent->DTSTART;
168
-			$endDateTime = $end->getDateTime();
169
-			$endDateTime = $endDateTime->add(DateTimeParser::parse($vevent->DURATION->getValue()));
170
-			$end->setDateTime($endDateTime, $isFloating);
171
-
172
-			return $end;
173
-		}
174
-
175
-		if (!$vevent->DTSTART->hasTime()) {
176
-			$isFloating = $vevent->DTSTART->isFloating();
177
-			/** @var Property\ICalendar\DateTime $end */
178
-			$end = clone $vevent->DTSTART;
179
-			$endDateTime = $end->getDateTime();
180
-			$endDateTime = $endDateTime->modify('+1 day');
181
-			$end->setDateTime($endDateTime, $isFloating);
182
-
183
-			return $end;
184
-		}
185
-
186
-		return clone $vevent->DTSTART;
187
-	}
46
+    /** @var string */
47
+    public const NOTIFICATION_TYPE = '';
48
+
49
+    /** @var ILogger */
50
+    protected $logger;
51
+
52
+    /** @var L10NFactory */
53
+    private $l10nFactory;
54
+
55
+    /** @var IL10N[] */
56
+    private $l10ns;
57
+
58
+    /** @var string */
59
+    private $fallbackLanguage;
60
+
61
+    /** @var IURLGenerator */
62
+    protected $urlGenerator;
63
+
64
+    /** @var IConfig */
65
+    protected $config;
66
+
67
+    /**
68
+     * @param ILogger $logger
69
+     * @param L10NFactory $l10nFactory
70
+     * @param IConfig $config
71
+     * @param IUrlGenerator $urlGenerator
72
+     */
73
+    public function __construct(ILogger $logger,
74
+                                L10NFactory $l10nFactory,
75
+                                IURLGenerator $urlGenerator,
76
+                                IConfig $config) {
77
+        $this->logger = $logger;
78
+        $this->l10nFactory = $l10nFactory;
79
+        $this->urlGenerator = $urlGenerator;
80
+        $this->config = $config;
81
+    }
82
+
83
+    /**
84
+     * Send notification
85
+     *
86
+     * @param VEvent $vevent
87
+     * @param string $calendarDisplayName
88
+     * @param IUser[] $users
89
+     * @return void
90
+     */
91
+    abstract public function send(VEvent $vevent,
92
+                            string $calendarDisplayName,
93
+                            array $users=[]): void;
94
+
95
+    /**
96
+     * @return string
97
+     */
98
+    protected function getFallbackLanguage():string {
99
+        if ($this->fallbackLanguage) {
100
+            return $this->fallbackLanguage;
101
+        }
102
+
103
+        $fallbackLanguage = $this->l10nFactory->findLanguage();
104
+        $this->fallbackLanguage = $fallbackLanguage;
105
+
106
+        return $fallbackLanguage;
107
+    }
108
+
109
+    /**
110
+     * @param string $lang
111
+     * @return bool
112
+     */
113
+    protected function hasL10NForLang(string $lang):bool {
114
+        return $this->l10nFactory->languageExists('dav', $lang);
115
+    }
116
+
117
+    /**
118
+     * @param string $lang
119
+     * @return IL10N
120
+     */
121
+    protected function getL10NForLang(string $lang):IL10N {
122
+        if (isset($this->l10ns[$lang])) {
123
+            return $this->l10ns[$lang];
124
+        }
125
+
126
+        $l10n = $this->l10nFactory->get('dav', $lang);
127
+        $this->l10ns[$lang] = $l10n;
128
+
129
+        return $l10n;
130
+    }
131
+
132
+    /**
133
+     * @param VEvent $vevent
134
+     * @return string
135
+     */
136
+    private function getStatusOfEvent(VEvent $vevent):string {
137
+        if ($vevent->STATUS) {
138
+            return (string) $vevent->STATUS;
139
+        }
140
+
141
+        // Doesn't say so in the standard,
142
+        // but we consider events without a status
143
+        // to be confirmed
144
+        return 'CONFIRMED';
145
+    }
146
+
147
+    /**
148
+     * @param VEvent $vevent
149
+     * @return bool
150
+     */
151
+    protected function isEventTentative(VEvent $vevent):bool {
152
+        return $this->getStatusOfEvent($vevent) === 'TENTATIVE';
153
+    }
154
+
155
+    /**
156
+     * @param VEvent $vevent
157
+     * @return Property\ICalendar\DateTime
158
+     */
159
+    protected function getDTEndFromEvent(VEvent $vevent):Property\ICalendar\DateTime {
160
+        if (isset($vevent->DTEND)) {
161
+            return $vevent->DTEND;
162
+        }
163
+
164
+        if (isset($vevent->DURATION)) {
165
+            $isFloating = $vevent->DTSTART->isFloating();
166
+            /** @var Property\ICalendar\DateTime $end */
167
+            $end = clone $vevent->DTSTART;
168
+            $endDateTime = $end->getDateTime();
169
+            $endDateTime = $endDateTime->add(DateTimeParser::parse($vevent->DURATION->getValue()));
170
+            $end->setDateTime($endDateTime, $isFloating);
171
+
172
+            return $end;
173
+        }
174
+
175
+        if (!$vevent->DTSTART->hasTime()) {
176
+            $isFloating = $vevent->DTSTART->isFloating();
177
+            /** @var Property\ICalendar\DateTime $end */
178
+            $end = clone $vevent->DTSTART;
179
+            $endDateTime = $end->getDateTime();
180
+            $endDateTime = $endDateTime->modify('+1 day');
181
+            $end->setDateTime($endDateTime, $isFloating);
182
+
183
+            return $end;
184
+        }
185
+
186
+        return clone $vevent->DTSTART;
187
+    }
188 188
 }
Please login to merge, or discard this patch.
apps/dav/lib/CalDAV/Reminder/NotificationProvider/EmailProvider.php 1 patch
Indentation   +454 added lines, -454 removed lines patch added patch discarded remove patch
@@ -46,458 +46,458 @@
 block discarded – undo
46 46
  */
47 47
 class EmailProvider extends AbstractProvider {
48 48
 
49
-	/** @var string */
50
-	public const NOTIFICATION_TYPE = 'EMAIL';
51
-
52
-	/** @var IMailer */
53
-	private $mailer;
54
-
55
-	/**
56
-	 * @param IConfig $config
57
-	 * @param IMailer $mailer
58
-	 * @param ILogger $logger
59
-	 * @param L10NFactory $l10nFactory
60
-	 * @param IUrlGenerator $urlGenerator
61
-	 */
62
-	public function __construct(IConfig $config,
63
-								IMailer $mailer,
64
-								ILogger $logger,
65
-								L10NFactory $l10nFactory,
66
-								IURLGenerator $urlGenerator) {
67
-		parent::__construct($logger, $l10nFactory, $urlGenerator, $config);
68
-		$this->mailer = $mailer;
69
-	}
70
-
71
-	/**
72
-	 * Send out notification via email
73
-	 *
74
-	 * @param VEvent $vevent
75
-	 * @param string $calendarDisplayName
76
-	 * @param array $users
77
-	 * @throws \Exception
78
-	 */
79
-	public function send(VEvent $vevent,
80
-						 string $calendarDisplayName,
81
-						 array $users=[]):void {
82
-		$fallbackLanguage = $this->getFallbackLanguage();
83
-
84
-		$emailAddressesOfSharees = $this->getEMailAddressesOfAllUsersWithWriteAccessToCalendar($users);
85
-		$emailAddressesOfAttendees = $this->getAllEMailAddressesFromEvent($vevent);
86
-
87
-		// Quote from php.net:
88
-		// If the input arrays have the same string keys, then the later value for that key will overwrite the previous one.
89
-		// => if there are duplicate email addresses, it will always take the system value
90
-		$emailAddresses = array_merge(
91
-			$emailAddressesOfAttendees,
92
-			$emailAddressesOfSharees
93
-		);
94
-
95
-		$sortedByLanguage = $this->sortEMailAddressesByLanguage($emailAddresses, $fallbackLanguage);
96
-		$organizer = $this->getOrganizerEMailAndNameFromEvent($vevent);
97
-
98
-		foreach($sortedByLanguage as $lang => $emailAddresses) {
99
-			if (!$this->hasL10NForLang($lang)) {
100
-				$lang = $fallbackLanguage;
101
-			}
102
-			$l10n = $this->getL10NForLang($lang);
103
-			$fromEMail = \OCP\Util::getDefaultEmailAddress('reminders-noreply');
104
-
105
-			$template = $this->mailer->createEMailTemplate('dav.calendarReminder');
106
-			$template->addHeader();
107
-			$this->addSubjectAndHeading($template, $l10n, $vevent);
108
-			$this->addBulletList($template, $l10n, $calendarDisplayName, $vevent);
109
-			$template->addFooter();
110
-
111
-			foreach ($emailAddresses as $emailAddress) {
112
-				$message = $this->mailer->createMessage();
113
-				$message->setFrom([$fromEMail]);
114
-				if ($organizer) {
115
-					$message->setReplyTo($organizer);
116
-				}
117
-				$message->setTo([$emailAddress]);
118
-				$message->useTemplate($template);
119
-
120
-				try {
121
-					$failed = $this->mailer->send($message);
122
-					if ($failed) {
123
-						$this->logger->error('Unable to deliver message to {failed}', ['app' => 'dav', 'failed' => implode(', ', $failed)]);
124
-					}
125
-				} catch (\Exception $ex) {
126
-					$this->logger->logException($ex, ['app' => 'dav']);
127
-				}
128
-			}
129
-		}
130
-	}
131
-
132
-	/**
133
-	 * @param IEMailTemplate $template
134
-	 * @param IL10N $l10n
135
-	 * @param VEvent $vevent
136
-	 */
137
-	private function addSubjectAndHeading(IEMailTemplate $template, IL10N $l10n, VEvent $vevent):void {
138
-		$template->setSubject('Notification: ' . $this->getTitleFromVEvent($vevent, $l10n));
139
-		$template->addHeading($this->getTitleFromVEvent($vevent, $l10n));
140
-	}
141
-
142
-	/**
143
-	 * @param IEMailTemplate $template
144
-	 * @param IL10N $l10n
145
-	 * @param string $calendarDisplayName
146
-	 * @param array $eventData
147
-	 */
148
-	private function addBulletList(IEMailTemplate $template,
149
-								   IL10N $l10n,
150
-								   string $calendarDisplayName,
151
-								   VEvent $vevent):void {
152
-		$template->addBodyListItem($calendarDisplayName, $l10n->t('Calendar:'),
153
-			$this->getAbsoluteImagePath('actions/info.svg'));
154
-
155
-		$template->addBodyListItem($this->generateDateString($l10n, $vevent), $l10n->t('Date:'),
156
-			$this->getAbsoluteImagePath('places/calendar.svg'));
157
-
158
-		if (isset($vevent->LOCATION)) {
159
-			$template->addBodyListItem((string) $vevent->LOCATION, $l10n->t('Where:'),
160
-				$this->getAbsoluteImagePath('actions/address.svg'));
161
-		}
162
-		if (isset($vevent->DESCRIPTION)) {
163
-			$template->addBodyListItem((string) $vevent->DESCRIPTION, $l10n->t('Description:'),
164
-				$this->getAbsoluteImagePath('actions/more.svg'));
165
-		}
166
-	}
167
-
168
-	/**
169
-	 * @param string $path
170
-	 * @return string
171
-	 */
172
-	private function getAbsoluteImagePath(string $path):string {
173
-		return $this->urlGenerator->getAbsoluteURL(
174
-			$this->urlGenerator->imagePath('core', $path)
175
-		);
176
-	}
177
-
178
-	/**
179
-	 * @param VEvent $vevent
180
-	 * @return array|null
181
-	 */
182
-	private function getOrganizerEMailAndNameFromEvent(VEvent $vevent):?array {
183
-		if (!$vevent->ORGANIZER) {
184
-			return null;
185
-		}
186
-
187
-		$organizer = $vevent->ORGANIZER;
188
-		if (strcasecmp($organizer->getValue(), 'mailto:') !== 0) {
189
-			return null;
190
-		}
191
-
192
-		$organizerEMail = substr($organizer->getValue(), 7);
193
-
194
-		$name = $organizer->offsetGet('CN');
195
-		if ($name instanceof Parameter) {
196
-			return [$organizerEMail => $name];
197
-		}
198
-
199
-		return [$organizerEMail];
200
-	}
201
-
202
-	/**
203
-	 * @param array $emails
204
-	 * @param string $defaultLanguage
205
-	 * @return array
206
-	 */
207
-	private function sortEMailAddressesByLanguage(array $emails,
208
-												  string $defaultLanguage):array {
209
-		$sortedByLanguage = [];
210
-
211
-		foreach($emails as $emailAddress => $parameters) {
212
-			if (isset($parameters['LANG'])) {
213
-				$lang = $parameters['LANG'];
214
-			} else {
215
-				$lang = $defaultLanguage;
216
-			}
217
-
218
-			if (!isset($sortedByLanguage[$lang])) {
219
-				$sortedByLanguage[$lang] = [];
220
-			}
221
-
222
-			$sortedByLanguage[$lang][] = $emailAddress;
223
-		}
224
-
225
-		return $sortedByLanguage;
226
-	}
227
-
228
-	/**
229
-	 * @param VEvent $vevent
230
-	 * @return array
231
-	 */
232
-	private function getAllEMailAddressesFromEvent(VEvent $vevent):array {
233
-		$emailAddresses = [];
234
-
235
-		if (isset($vevent->ATTENDEE)) {
236
-			foreach ($vevent->ATTENDEE as $attendee) {
237
-				if (!($attendee instanceof VObject\Property)) {
238
-					continue;
239
-				}
240
-
241
-				$cuType = $this->getCUTypeOfAttendee($attendee);
242
-				if (\in_array($cuType, ['RESOURCE', 'ROOM', 'UNKNOWN'])) {
243
-					// Don't send emails to things
244
-					continue;
245
-				}
246
-
247
-				$partstat = $this->getPartstatOfAttendee($attendee);
248
-				if ($partstat === 'DECLINED') {
249
-					// Don't send out emails to people who declined
250
-					continue;
251
-				}
252
-				if ($partstat === 'DELEGATED') {
253
-					$delegates = $attendee->offsetGet('DELEGATED-TO');
254
-					if (!($delegates instanceof VObject\Parameter)) {
255
-						continue;
256
-					}
257
-
258
-					$emailAddressesOfDelegates = $delegates->getParts();
259
-					foreach($emailAddressesOfDelegates as $addressesOfDelegate) {
260
-						if (strcasecmp($addressesOfDelegate, 'mailto:') === 0) {
261
-							$emailAddresses[substr($addressesOfDelegate, 7)] = [];
262
-						}
263
-					}
264
-
265
-					continue;
266
-				}
267
-
268
-				$emailAddressOfAttendee = $this->getEMailAddressOfAttendee($attendee);
269
-				if ($emailAddressOfAttendee !== null) {
270
-					$properties = [];
271
-
272
-					$langProp = $attendee->offsetGet('LANG');
273
-					if ($langProp instanceof VObject\Parameter) {
274
-						$properties['LANG'] = $langProp->getValue();
275
-					}
276
-
277
-					$emailAddresses[$emailAddressOfAttendee] = $properties;
278
-				}
279
-			}
280
-		}
281
-
282
-		if (isset($vevent->ORGANIZER) && $this->hasAttendeeMailURI($vevent->ORGANIZER)) {
283
-			$emailAddresses[$this->getEMailAddressOfAttendee($vevent->ORGANIZER)] = [];
284
-		}
285
-
286
-		return $emailAddresses;
287
-	}
288
-
289
-
290
-
291
-	/**
292
-	 * @param VObject\Property $attendee
293
-	 * @return string
294
-	 */
295
-	private function getCUTypeOfAttendee(VObject\Property $attendee):string {
296
-		$cuType = $attendee->offsetGet('CUTYPE');
297
-		if ($cuType instanceof VObject\Parameter) {
298
-			return strtoupper($cuType->getValue());
299
-		}
300
-
301
-		return 'INDIVIDUAL';
302
-	}
303
-
304
-	/**
305
-	 * @param VObject\Property $attendee
306
-	 * @return string
307
-	 */
308
-	private function getPartstatOfAttendee(VObject\Property $attendee):string {
309
-		$partstat = $attendee->offsetGet('PARTSTAT');
310
-		if ($partstat instanceof VObject\Parameter) {
311
-			return strtoupper($partstat->getValue());
312
-		}
313
-
314
-		return 'NEEDS-ACTION';
315
-	}
316
-
317
-	/**
318
-	 * @param VObject\Property $attendee
319
-	 * @return bool
320
-	 */
321
-	private function hasAttendeeMailURI(VObject\Property $attendee):bool {
322
-		return stripos($attendee->getValue(), 'mailto:') === 0;
323
-	}
324
-
325
-	/**
326
-	 * @param VObject\Property $attendee
327
-	 * @return string|null
328
-	 */
329
-	private function getEMailAddressOfAttendee(VObject\Property $attendee):?string {
330
-		if (!$this->hasAttendeeMailURI($attendee)) {
331
-			return null;
332
-		}
333
-
334
-		return substr($attendee->getValue(), 7);
335
-	}
336
-
337
-	/**
338
-	 * @param array $users
339
-	 * @return array
340
-	 */
341
-	private function getEMailAddressesOfAllUsersWithWriteAccessToCalendar(array $users):array {
342
-		$emailAddresses = [];
343
-
344
-		foreach($users as $user) {
345
-			$emailAddress = $user->getEMailAddress();
346
-			if ($emailAddress) {
347
-				$lang = $this->getLangForUser($user);
348
-				if ($lang) {
349
-					$emailAddresses[$emailAddress] = [
350
-						'LANG' => $lang,
351
-					];
352
-				} else {
353
-					$emailAddresses[$emailAddress] = [];
354
-				}
355
-			}
356
-		}
357
-
358
-		return $emailAddresses;
359
-	}
360
-
361
-	/**
362
-	 * @param IUser $user
363
-	 * @return string
364
-	 */
365
-	private function getLangForUser(IUser $user): ?string {
366
-		return $this->config->getUserValue($user->getUID(), 'core', 'lang', null);
367
-	}
368
-
369
-	/**
370
-	 * @param IL10N $l10n
371
-	 * @param VEvent $vevent
372
-	 * @return string
373
-	 * @throws \Exception
374
-	 */
375
-	private function generateDateString(IL10N $l10n, VEvent $vevent):string {
376
-		$isAllDay = $vevent->DTSTART instanceof Property\ICalendar\Date;
377
-
378
-		/** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtstart */
379
-		/** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtend */
380
-		/** @var \DateTimeImmutable $dtstartDt */
381
-		$dtstartDt = $vevent->DTSTART->getDateTime();
382
-		/** @var \DateTimeImmutable $dtendDt */
383
-		$dtendDt = $this->getDTEndFromEvent($vevent)->getDateTime();
384
-
385
-		$diff = $dtstartDt->diff($dtendDt);
386
-
387
-		/** @phan-suppress-next-line PhanUndeclaredClassMethod */
388
-		$dtstartDt = new \DateTime($dtstartDt->format(\DateTime::ATOM));
389
-		/** @phan-suppress-next-line PhanUndeclaredClassMethod */
390
-		$dtendDt = new \DateTime($dtendDt->format(\DateTime::ATOM));
391
-
392
-		if ($isAllDay) {
393
-			// One day event
394
-			if ($diff->days === 1) {
395
-				return $this->getDateString($l10n, $dtstartDt);
396
-			}
397
-
398
-			return implode(' - ', [
399
-				$this->getDateString($l10n, $dtstartDt),
400
-				$this->getDateString($l10n, $dtendDt),
401
-			]);
402
-		}
403
-
404
-		$startTimezone = $endTimezone = null;
405
-		if (!$vevent->DTSTART->isFloating()) {
406
-			/** @phan-suppress-next-line PhanUndeclaredClassMethod */
407
-			$startTimezone = $vevent->DTSTART->getDateTime()->getTimezone()->getName();
408
-			/** @phan-suppress-next-line PhanUndeclaredClassMethod */
409
-			$endTimezone = $this->getDTEndFromEvent($vevent)->getDateTime()->getTimezone()->getName();
410
-		}
411
-
412
-		$localeStart = implode(', ', [
413
-			$this->getWeekDayName($l10n, $dtstartDt),
414
-			$this->getDateTimeString($l10n, $dtstartDt)
415
-		]);
416
-
417
-		// always show full date with timezone if timezones are different
418
-		if ($startTimezone !== $endTimezone) {
419
-			$localeEnd = implode(', ', [
420
-				$this->getWeekDayName($l10n, $dtendDt),
421
-				$this->getDateTimeString($l10n, $dtendDt)
422
-			]);
423
-
424
-			return $localeStart
425
-				. ' (' . $startTimezone . ') '
426
-				. ' - '
427
-				. $localeEnd
428
-				. ' (' . $endTimezone . ')';
429
-		}
430
-
431
-		// Show only the time if the day is the same
432
-		$localeEnd = $this->isDayEqual($dtstartDt, $dtendDt)
433
-			? $this->getTimeString($l10n, $dtendDt)
434
-			: implode(', ', [
435
-				$this->getWeekDayName($l10n, $dtendDt),
436
-				$this->getDateTimeString($l10n, $dtendDt)
437
-			]);
438
-
439
-		return $localeStart
440
-			. ' - '
441
-			. $localeEnd
442
-			. ' (' . $startTimezone . ')';
443
-	}
444
-
445
-	/**
446
-	 * @param DateTime $dtStart
447
-	 * @param DateTime $dtEnd
448
-	 * @return bool
449
-	 */
450
-	private function isDayEqual(DateTime $dtStart,
451
-								DateTime $dtEnd):bool {
452
-		return $dtStart->format('Y-m-d') === $dtEnd->format('Y-m-d');
453
-	}
454
-
455
-	/**
456
-	 * @param IL10N $l10n
457
-	 * @param DateTime $dt
458
-	 * @return string
459
-	 */
460
-	private function getWeekDayName(IL10N $l10n, DateTime $dt):string {
461
-		return $l10n->l('weekdayName', $dt, ['width' => 'abbreviated']);
462
-	}
463
-
464
-	/**
465
-	 * @param IL10N $l10n
466
-	 * @param DateTime $dt
467
-	 * @return string
468
-	 */
469
-	private function getDateString(IL10N $l10n, DateTime $dt):string {
470
-		return $l10n->l('date', $dt, ['width' => 'medium']);
471
-	}
472
-
473
-	/**
474
-	 * @param IL10N $l10n
475
-	 * @param DateTime $dt
476
-	 * @return string
477
-	 */
478
-	private function getDateTimeString(IL10N $l10n, DateTime $dt):string {
479
-		return $l10n->l('datetime', $dt, ['width' => 'medium|short']);
480
-	}
481
-
482
-	/**
483
-	 * @param IL10N $l10n
484
-	 * @param DateTime $dt
485
-	 * @return string
486
-	 */
487
-	private function getTimeString(IL10N $l10n, DateTime $dt):string {
488
-		return $l10n->l('time', $dt, ['width' => 'short']);
489
-	}
490
-
491
-	/**
492
-	 * @param VEvent $vevent
493
-	 * @param IL10N $l10n
494
-	 * @return string
495
-	 */
496
-	private function getTitleFromVEvent(VEvent $vevent, IL10N $l10n):string {
497
-		if (isset($vevent->SUMMARY)) {
498
-			return (string)$vevent->SUMMARY;
499
-		}
500
-
501
-		return $l10n->t('Untitled event');
502
-	}
49
+    /** @var string */
50
+    public const NOTIFICATION_TYPE = 'EMAIL';
51
+
52
+    /** @var IMailer */
53
+    private $mailer;
54
+
55
+    /**
56
+     * @param IConfig $config
57
+     * @param IMailer $mailer
58
+     * @param ILogger $logger
59
+     * @param L10NFactory $l10nFactory
60
+     * @param IUrlGenerator $urlGenerator
61
+     */
62
+    public function __construct(IConfig $config,
63
+                                IMailer $mailer,
64
+                                ILogger $logger,
65
+                                L10NFactory $l10nFactory,
66
+                                IURLGenerator $urlGenerator) {
67
+        parent::__construct($logger, $l10nFactory, $urlGenerator, $config);
68
+        $this->mailer = $mailer;
69
+    }
70
+
71
+    /**
72
+     * Send out notification via email
73
+     *
74
+     * @param VEvent $vevent
75
+     * @param string $calendarDisplayName
76
+     * @param array $users
77
+     * @throws \Exception
78
+     */
79
+    public function send(VEvent $vevent,
80
+                            string $calendarDisplayName,
81
+                            array $users=[]):void {
82
+        $fallbackLanguage = $this->getFallbackLanguage();
83
+
84
+        $emailAddressesOfSharees = $this->getEMailAddressesOfAllUsersWithWriteAccessToCalendar($users);
85
+        $emailAddressesOfAttendees = $this->getAllEMailAddressesFromEvent($vevent);
86
+
87
+        // Quote from php.net:
88
+        // If the input arrays have the same string keys, then the later value for that key will overwrite the previous one.
89
+        // => if there are duplicate email addresses, it will always take the system value
90
+        $emailAddresses = array_merge(
91
+            $emailAddressesOfAttendees,
92
+            $emailAddressesOfSharees
93
+        );
94
+
95
+        $sortedByLanguage = $this->sortEMailAddressesByLanguage($emailAddresses, $fallbackLanguage);
96
+        $organizer = $this->getOrganizerEMailAndNameFromEvent($vevent);
97
+
98
+        foreach($sortedByLanguage as $lang => $emailAddresses) {
99
+            if (!$this->hasL10NForLang($lang)) {
100
+                $lang = $fallbackLanguage;
101
+            }
102
+            $l10n = $this->getL10NForLang($lang);
103
+            $fromEMail = \OCP\Util::getDefaultEmailAddress('reminders-noreply');
104
+
105
+            $template = $this->mailer->createEMailTemplate('dav.calendarReminder');
106
+            $template->addHeader();
107
+            $this->addSubjectAndHeading($template, $l10n, $vevent);
108
+            $this->addBulletList($template, $l10n, $calendarDisplayName, $vevent);
109
+            $template->addFooter();
110
+
111
+            foreach ($emailAddresses as $emailAddress) {
112
+                $message = $this->mailer->createMessage();
113
+                $message->setFrom([$fromEMail]);
114
+                if ($organizer) {
115
+                    $message->setReplyTo($organizer);
116
+                }
117
+                $message->setTo([$emailAddress]);
118
+                $message->useTemplate($template);
119
+
120
+                try {
121
+                    $failed = $this->mailer->send($message);
122
+                    if ($failed) {
123
+                        $this->logger->error('Unable to deliver message to {failed}', ['app' => 'dav', 'failed' => implode(', ', $failed)]);
124
+                    }
125
+                } catch (\Exception $ex) {
126
+                    $this->logger->logException($ex, ['app' => 'dav']);
127
+                }
128
+            }
129
+        }
130
+    }
131
+
132
+    /**
133
+     * @param IEMailTemplate $template
134
+     * @param IL10N $l10n
135
+     * @param VEvent $vevent
136
+     */
137
+    private function addSubjectAndHeading(IEMailTemplate $template, IL10N $l10n, VEvent $vevent):void {
138
+        $template->setSubject('Notification: ' . $this->getTitleFromVEvent($vevent, $l10n));
139
+        $template->addHeading($this->getTitleFromVEvent($vevent, $l10n));
140
+    }
141
+
142
+    /**
143
+     * @param IEMailTemplate $template
144
+     * @param IL10N $l10n
145
+     * @param string $calendarDisplayName
146
+     * @param array $eventData
147
+     */
148
+    private function addBulletList(IEMailTemplate $template,
149
+                                    IL10N $l10n,
150
+                                    string $calendarDisplayName,
151
+                                    VEvent $vevent):void {
152
+        $template->addBodyListItem($calendarDisplayName, $l10n->t('Calendar:'),
153
+            $this->getAbsoluteImagePath('actions/info.svg'));
154
+
155
+        $template->addBodyListItem($this->generateDateString($l10n, $vevent), $l10n->t('Date:'),
156
+            $this->getAbsoluteImagePath('places/calendar.svg'));
157
+
158
+        if (isset($vevent->LOCATION)) {
159
+            $template->addBodyListItem((string) $vevent->LOCATION, $l10n->t('Where:'),
160
+                $this->getAbsoluteImagePath('actions/address.svg'));
161
+        }
162
+        if (isset($vevent->DESCRIPTION)) {
163
+            $template->addBodyListItem((string) $vevent->DESCRIPTION, $l10n->t('Description:'),
164
+                $this->getAbsoluteImagePath('actions/more.svg'));
165
+        }
166
+    }
167
+
168
+    /**
169
+     * @param string $path
170
+     * @return string
171
+     */
172
+    private function getAbsoluteImagePath(string $path):string {
173
+        return $this->urlGenerator->getAbsoluteURL(
174
+            $this->urlGenerator->imagePath('core', $path)
175
+        );
176
+    }
177
+
178
+    /**
179
+     * @param VEvent $vevent
180
+     * @return array|null
181
+     */
182
+    private function getOrganizerEMailAndNameFromEvent(VEvent $vevent):?array {
183
+        if (!$vevent->ORGANIZER) {
184
+            return null;
185
+        }
186
+
187
+        $organizer = $vevent->ORGANIZER;
188
+        if (strcasecmp($organizer->getValue(), 'mailto:') !== 0) {
189
+            return null;
190
+        }
191
+
192
+        $organizerEMail = substr($organizer->getValue(), 7);
193
+
194
+        $name = $organizer->offsetGet('CN');
195
+        if ($name instanceof Parameter) {
196
+            return [$organizerEMail => $name];
197
+        }
198
+
199
+        return [$organizerEMail];
200
+    }
201
+
202
+    /**
203
+     * @param array $emails
204
+     * @param string $defaultLanguage
205
+     * @return array
206
+     */
207
+    private function sortEMailAddressesByLanguage(array $emails,
208
+                                                    string $defaultLanguage):array {
209
+        $sortedByLanguage = [];
210
+
211
+        foreach($emails as $emailAddress => $parameters) {
212
+            if (isset($parameters['LANG'])) {
213
+                $lang = $parameters['LANG'];
214
+            } else {
215
+                $lang = $defaultLanguage;
216
+            }
217
+
218
+            if (!isset($sortedByLanguage[$lang])) {
219
+                $sortedByLanguage[$lang] = [];
220
+            }
221
+
222
+            $sortedByLanguage[$lang][] = $emailAddress;
223
+        }
224
+
225
+        return $sortedByLanguage;
226
+    }
227
+
228
+    /**
229
+     * @param VEvent $vevent
230
+     * @return array
231
+     */
232
+    private function getAllEMailAddressesFromEvent(VEvent $vevent):array {
233
+        $emailAddresses = [];
234
+
235
+        if (isset($vevent->ATTENDEE)) {
236
+            foreach ($vevent->ATTENDEE as $attendee) {
237
+                if (!($attendee instanceof VObject\Property)) {
238
+                    continue;
239
+                }
240
+
241
+                $cuType = $this->getCUTypeOfAttendee($attendee);
242
+                if (\in_array($cuType, ['RESOURCE', 'ROOM', 'UNKNOWN'])) {
243
+                    // Don't send emails to things
244
+                    continue;
245
+                }
246
+
247
+                $partstat = $this->getPartstatOfAttendee($attendee);
248
+                if ($partstat === 'DECLINED') {
249
+                    // Don't send out emails to people who declined
250
+                    continue;
251
+                }
252
+                if ($partstat === 'DELEGATED') {
253
+                    $delegates = $attendee->offsetGet('DELEGATED-TO');
254
+                    if (!($delegates instanceof VObject\Parameter)) {
255
+                        continue;
256
+                    }
257
+
258
+                    $emailAddressesOfDelegates = $delegates->getParts();
259
+                    foreach($emailAddressesOfDelegates as $addressesOfDelegate) {
260
+                        if (strcasecmp($addressesOfDelegate, 'mailto:') === 0) {
261
+                            $emailAddresses[substr($addressesOfDelegate, 7)] = [];
262
+                        }
263
+                    }
264
+
265
+                    continue;
266
+                }
267
+
268
+                $emailAddressOfAttendee = $this->getEMailAddressOfAttendee($attendee);
269
+                if ($emailAddressOfAttendee !== null) {
270
+                    $properties = [];
271
+
272
+                    $langProp = $attendee->offsetGet('LANG');
273
+                    if ($langProp instanceof VObject\Parameter) {
274
+                        $properties['LANG'] = $langProp->getValue();
275
+                    }
276
+
277
+                    $emailAddresses[$emailAddressOfAttendee] = $properties;
278
+                }
279
+            }
280
+        }
281
+
282
+        if (isset($vevent->ORGANIZER) && $this->hasAttendeeMailURI($vevent->ORGANIZER)) {
283
+            $emailAddresses[$this->getEMailAddressOfAttendee($vevent->ORGANIZER)] = [];
284
+        }
285
+
286
+        return $emailAddresses;
287
+    }
288
+
289
+
290
+
291
+    /**
292
+     * @param VObject\Property $attendee
293
+     * @return string
294
+     */
295
+    private function getCUTypeOfAttendee(VObject\Property $attendee):string {
296
+        $cuType = $attendee->offsetGet('CUTYPE');
297
+        if ($cuType instanceof VObject\Parameter) {
298
+            return strtoupper($cuType->getValue());
299
+        }
300
+
301
+        return 'INDIVIDUAL';
302
+    }
303
+
304
+    /**
305
+     * @param VObject\Property $attendee
306
+     * @return string
307
+     */
308
+    private function getPartstatOfAttendee(VObject\Property $attendee):string {
309
+        $partstat = $attendee->offsetGet('PARTSTAT');
310
+        if ($partstat instanceof VObject\Parameter) {
311
+            return strtoupper($partstat->getValue());
312
+        }
313
+
314
+        return 'NEEDS-ACTION';
315
+    }
316
+
317
+    /**
318
+     * @param VObject\Property $attendee
319
+     * @return bool
320
+     */
321
+    private function hasAttendeeMailURI(VObject\Property $attendee):bool {
322
+        return stripos($attendee->getValue(), 'mailto:') === 0;
323
+    }
324
+
325
+    /**
326
+     * @param VObject\Property $attendee
327
+     * @return string|null
328
+     */
329
+    private function getEMailAddressOfAttendee(VObject\Property $attendee):?string {
330
+        if (!$this->hasAttendeeMailURI($attendee)) {
331
+            return null;
332
+        }
333
+
334
+        return substr($attendee->getValue(), 7);
335
+    }
336
+
337
+    /**
338
+     * @param array $users
339
+     * @return array
340
+     */
341
+    private function getEMailAddressesOfAllUsersWithWriteAccessToCalendar(array $users):array {
342
+        $emailAddresses = [];
343
+
344
+        foreach($users as $user) {
345
+            $emailAddress = $user->getEMailAddress();
346
+            if ($emailAddress) {
347
+                $lang = $this->getLangForUser($user);
348
+                if ($lang) {
349
+                    $emailAddresses[$emailAddress] = [
350
+                        'LANG' => $lang,
351
+                    ];
352
+                } else {
353
+                    $emailAddresses[$emailAddress] = [];
354
+                }
355
+            }
356
+        }
357
+
358
+        return $emailAddresses;
359
+    }
360
+
361
+    /**
362
+     * @param IUser $user
363
+     * @return string
364
+     */
365
+    private function getLangForUser(IUser $user): ?string {
366
+        return $this->config->getUserValue($user->getUID(), 'core', 'lang', null);
367
+    }
368
+
369
+    /**
370
+     * @param IL10N $l10n
371
+     * @param VEvent $vevent
372
+     * @return string
373
+     * @throws \Exception
374
+     */
375
+    private function generateDateString(IL10N $l10n, VEvent $vevent):string {
376
+        $isAllDay = $vevent->DTSTART instanceof Property\ICalendar\Date;
377
+
378
+        /** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtstart */
379
+        /** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtend */
380
+        /** @var \DateTimeImmutable $dtstartDt */
381
+        $dtstartDt = $vevent->DTSTART->getDateTime();
382
+        /** @var \DateTimeImmutable $dtendDt */
383
+        $dtendDt = $this->getDTEndFromEvent($vevent)->getDateTime();
384
+
385
+        $diff = $dtstartDt->diff($dtendDt);
386
+
387
+        /** @phan-suppress-next-line PhanUndeclaredClassMethod */
388
+        $dtstartDt = new \DateTime($dtstartDt->format(\DateTime::ATOM));
389
+        /** @phan-suppress-next-line PhanUndeclaredClassMethod */
390
+        $dtendDt = new \DateTime($dtendDt->format(\DateTime::ATOM));
391
+
392
+        if ($isAllDay) {
393
+            // One day event
394
+            if ($diff->days === 1) {
395
+                return $this->getDateString($l10n, $dtstartDt);
396
+            }
397
+
398
+            return implode(' - ', [
399
+                $this->getDateString($l10n, $dtstartDt),
400
+                $this->getDateString($l10n, $dtendDt),
401
+            ]);
402
+        }
403
+
404
+        $startTimezone = $endTimezone = null;
405
+        if (!$vevent->DTSTART->isFloating()) {
406
+            /** @phan-suppress-next-line PhanUndeclaredClassMethod */
407
+            $startTimezone = $vevent->DTSTART->getDateTime()->getTimezone()->getName();
408
+            /** @phan-suppress-next-line PhanUndeclaredClassMethod */
409
+            $endTimezone = $this->getDTEndFromEvent($vevent)->getDateTime()->getTimezone()->getName();
410
+        }
411
+
412
+        $localeStart = implode(', ', [
413
+            $this->getWeekDayName($l10n, $dtstartDt),
414
+            $this->getDateTimeString($l10n, $dtstartDt)
415
+        ]);
416
+
417
+        // always show full date with timezone if timezones are different
418
+        if ($startTimezone !== $endTimezone) {
419
+            $localeEnd = implode(', ', [
420
+                $this->getWeekDayName($l10n, $dtendDt),
421
+                $this->getDateTimeString($l10n, $dtendDt)
422
+            ]);
423
+
424
+            return $localeStart
425
+                . ' (' . $startTimezone . ') '
426
+                . ' - '
427
+                . $localeEnd
428
+                . ' (' . $endTimezone . ')';
429
+        }
430
+
431
+        // Show only the time if the day is the same
432
+        $localeEnd = $this->isDayEqual($dtstartDt, $dtendDt)
433
+            ? $this->getTimeString($l10n, $dtendDt)
434
+            : implode(', ', [
435
+                $this->getWeekDayName($l10n, $dtendDt),
436
+                $this->getDateTimeString($l10n, $dtendDt)
437
+            ]);
438
+
439
+        return $localeStart
440
+            . ' - '
441
+            . $localeEnd
442
+            . ' (' . $startTimezone . ')';
443
+    }
444
+
445
+    /**
446
+     * @param DateTime $dtStart
447
+     * @param DateTime $dtEnd
448
+     * @return bool
449
+     */
450
+    private function isDayEqual(DateTime $dtStart,
451
+                                DateTime $dtEnd):bool {
452
+        return $dtStart->format('Y-m-d') === $dtEnd->format('Y-m-d');
453
+    }
454
+
455
+    /**
456
+     * @param IL10N $l10n
457
+     * @param DateTime $dt
458
+     * @return string
459
+     */
460
+    private function getWeekDayName(IL10N $l10n, DateTime $dt):string {
461
+        return $l10n->l('weekdayName', $dt, ['width' => 'abbreviated']);
462
+    }
463
+
464
+    /**
465
+     * @param IL10N $l10n
466
+     * @param DateTime $dt
467
+     * @return string
468
+     */
469
+    private function getDateString(IL10N $l10n, DateTime $dt):string {
470
+        return $l10n->l('date', $dt, ['width' => 'medium']);
471
+    }
472
+
473
+    /**
474
+     * @param IL10N $l10n
475
+     * @param DateTime $dt
476
+     * @return string
477
+     */
478
+    private function getDateTimeString(IL10N $l10n, DateTime $dt):string {
479
+        return $l10n->l('datetime', $dt, ['width' => 'medium|short']);
480
+    }
481
+
482
+    /**
483
+     * @param IL10N $l10n
484
+     * @param DateTime $dt
485
+     * @return string
486
+     */
487
+    private function getTimeString(IL10N $l10n, DateTime $dt):string {
488
+        return $l10n->l('time', $dt, ['width' => 'short']);
489
+    }
490
+
491
+    /**
492
+     * @param VEvent $vevent
493
+     * @param IL10N $l10n
494
+     * @return string
495
+     */
496
+    private function getTitleFromVEvent(VEvent $vevent, IL10N $l10n):string {
497
+        if (isset($vevent->SUMMARY)) {
498
+            return (string)$vevent->SUMMARY;
499
+        }
500
+
501
+        return $l10n->t('Untitled event');
502
+    }
503 503
 }
Please login to merge, or discard this patch.
apps/dav/lib/CalDAV/Reminder/NotificationProvider/PushProvider.php 1 patch
Indentation   +84 added lines, -84 removed lines patch added patch discarded remove patch
@@ -44,96 +44,96 @@
 block discarded – undo
44 44
  */
45 45
 class PushProvider extends AbstractProvider {
46 46
 
47
-	/** @var string */
48
-	public const NOTIFICATION_TYPE = 'DISPLAY';
47
+    /** @var string */
48
+    public const NOTIFICATION_TYPE = 'DISPLAY';
49 49
 
50
-	/** @var IManager */
51
-	private $manager;
50
+    /** @var IManager */
51
+    private $manager;
52 52
 
53
-	/** @var ITimeFactory */
54
-	private $timeFactory;
53
+    /** @var ITimeFactory */
54
+    private $timeFactory;
55 55
 
56
-	/**
57
-	 * @param IConfig $config
58
-	 * @param IManager $manager
59
-	 * @param ILogger $logger
60
-	 * @param L10NFactory $l10nFactory
61
-	 * @param IUrlGenerator $urlGenerator
62
-	 * @param ITimeFactory $timeFactory
63
-	 */
64
-	public function __construct(IConfig $config,
65
-								IManager $manager,
66
-								ILogger $logger,
67
-								L10NFactory $l10nFactory,
68
-								IURLGenerator $urlGenerator,
69
-								ITimeFactory $timeFactory) {
70
-		parent::__construct($logger, $l10nFactory, $urlGenerator, $config);
71
-		$this->manager = $manager;
72
-		$this->timeFactory = $timeFactory;
73
-	}
56
+    /**
57
+     * @param IConfig $config
58
+     * @param IManager $manager
59
+     * @param ILogger $logger
60
+     * @param L10NFactory $l10nFactory
61
+     * @param IUrlGenerator $urlGenerator
62
+     * @param ITimeFactory $timeFactory
63
+     */
64
+    public function __construct(IConfig $config,
65
+                                IManager $manager,
66
+                                ILogger $logger,
67
+                                L10NFactory $l10nFactory,
68
+                                IURLGenerator $urlGenerator,
69
+                                ITimeFactory $timeFactory) {
70
+        parent::__construct($logger, $l10nFactory, $urlGenerator, $config);
71
+        $this->manager = $manager;
72
+        $this->timeFactory = $timeFactory;
73
+    }
74 74
 
75
-	/**
76
-	 * Send push notification to all users.
77
-	 *
78
-	 * @param VEvent $vevent
79
-	 * @param string $calendarDisplayName
80
-	 * @param IUser[] $users
81
-	 * @throws \Exception
82
-	 */
83
-	public function send(VEvent $vevent,
84
-						 string $calendarDisplayName=null,
85
-						 array $users=[]):void {
86
-		$eventDetails = $this->extractEventDetails($vevent);
87
-		$eventDetails['calendar_displayname'] = $calendarDisplayName;
75
+    /**
76
+     * Send push notification to all users.
77
+     *
78
+     * @param VEvent $vevent
79
+     * @param string $calendarDisplayName
80
+     * @param IUser[] $users
81
+     * @throws \Exception
82
+     */
83
+    public function send(VEvent $vevent,
84
+                            string $calendarDisplayName=null,
85
+                            array $users=[]):void {
86
+        $eventDetails = $this->extractEventDetails($vevent);
87
+        $eventDetails['calendar_displayname'] = $calendarDisplayName;
88 88
 
89
-		foreach($users as $user) {
90
-			/** @var INotification $notification */
91
-			$notification = $this->manager->createNotification();
92
-			$notification->setApp(Application::APP_ID)
93
-				->setUser($user->getUID())
94
-				->setDateTime($this->timeFactory->getDateTime())
95
-				->setObject(Application::APP_ID, (string) $vevent->UID)
96
-				->setSubject('calendar_reminder', [
97
-					'title' => $eventDetails['title'],
98
-					'start_atom' => $eventDetails['start_atom']
99
-				])
100
-				->setMessage('calendar_reminder', $eventDetails);
89
+        foreach($users as $user) {
90
+            /** @var INotification $notification */
91
+            $notification = $this->manager->createNotification();
92
+            $notification->setApp(Application::APP_ID)
93
+                ->setUser($user->getUID())
94
+                ->setDateTime($this->timeFactory->getDateTime())
95
+                ->setObject(Application::APP_ID, (string) $vevent->UID)
96
+                ->setSubject('calendar_reminder', [
97
+                    'title' => $eventDetails['title'],
98
+                    'start_atom' => $eventDetails['start_atom']
99
+                ])
100
+                ->setMessage('calendar_reminder', $eventDetails);
101 101
 
102
-			$this->manager->notify($notification);
103
-		}
104
-	}
102
+            $this->manager->notify($notification);
103
+        }
104
+    }
105 105
 
106
-	/**
107
-	 * @var VEvent $vevent
108
-	 * @return array
109
-	 * @throws \Exception
110
-	 */
111
-	protected function extractEventDetails(VEvent $vevent):array {
112
-		/** @var Property\ICalendar\DateTime $start */
113
-		$start = $vevent->DTSTART;
114
-		$end = $this->getDTEndFromEvent($vevent);
106
+    /**
107
+     * @var VEvent $vevent
108
+     * @return array
109
+     * @throws \Exception
110
+     */
111
+    protected function extractEventDetails(VEvent $vevent):array {
112
+        /** @var Property\ICalendar\DateTime $start */
113
+        $start = $vevent->DTSTART;
114
+        $end = $this->getDTEndFromEvent($vevent);
115 115
 
116
-		return [
117
-			'title' => isset($vevent->SUMMARY)
118
-				? ((string) $vevent->SUMMARY)
119
-				: null,
120
-			'description' => isset($vevent->DESCRIPTION)
121
-				? ((string) $vevent->DESCRIPTION)
122
-				: null,
123
-			'location' => isset($vevent->LOCATION)
124
-				? ((string) $vevent->LOCATION)
125
-				: null,
126
-			'all_day' => $start instanceof Property\ICalendar\Date,
127
-			/** @phan-suppress-next-line PhanUndeclaredClassMethod */
128
-			'start_atom' => $start->getDateTime()->format(\DateTime::ATOM),
129
-			'start_is_floating' => $start->isFloating(),
130
-			/** @phan-suppress-next-line PhanUndeclaredClassMethod */
131
-			'start_timezone' => $start->getDateTime()->getTimezone()->getName(),
132
-			/** @phan-suppress-next-line PhanUndeclaredClassMethod */
133
-			'end_atom' => $end->getDateTime()->format(\DateTime::ATOM),
134
-			'end_is_floating' => $end->isFloating(),
135
-			/** @phan-suppress-next-line PhanUndeclaredClassMethod */
136
-			'end_timezone' => $end->getDateTime()->getTimezone()->getName(),
137
-		];
138
-	}
116
+        return [
117
+            'title' => isset($vevent->SUMMARY)
118
+                ? ((string) $vevent->SUMMARY)
119
+                : null,
120
+            'description' => isset($vevent->DESCRIPTION)
121
+                ? ((string) $vevent->DESCRIPTION)
122
+                : null,
123
+            'location' => isset($vevent->LOCATION)
124
+                ? ((string) $vevent->LOCATION)
125
+                : null,
126
+            'all_day' => $start instanceof Property\ICalendar\Date,
127
+            /** @phan-suppress-next-line PhanUndeclaredClassMethod */
128
+            'start_atom' => $start->getDateTime()->format(\DateTime::ATOM),
129
+            'start_is_floating' => $start->isFloating(),
130
+            /** @phan-suppress-next-line PhanUndeclaredClassMethod */
131
+            'start_timezone' => $start->getDateTime()->getTimezone()->getName(),
132
+            /** @phan-suppress-next-line PhanUndeclaredClassMethod */
133
+            'end_atom' => $end->getDateTime()->format(\DateTime::ATOM),
134
+            'end_is_floating' => $end->isFloating(),
135
+            /** @phan-suppress-next-line PhanUndeclaredClassMethod */
136
+            'end_timezone' => $end->getDateTime()->getTimezone()->getName(),
137
+        ];
138
+    }
139 139
 }
Please login to merge, or discard this patch.
lib/CalDAV/Reminder/NotificationProvider/ProviderNotAvailableException.php 1 patch
Indentation   +10 added lines, -10 removed lines patch added patch discarded remove patch
@@ -25,15 +25,15 @@
 block discarded – undo
25 25
 
26 26
 class ProviderNotAvailableException extends \Exception {
27 27
 
28
-	/**
29
-	 * ProviderNotAvailableException constructor.
30
-	 *
31
-	 * @since 16.0.0
32
-	 *
33
-	 * @param string $type ReminderType
34
-	 */
35
-	public function __construct(string $type) {
36
-		parent::__construct("No notification provider for type $type available");
37
-	}
28
+    /**
29
+     * ProviderNotAvailableException constructor.
30
+     *
31
+     * @since 16.0.0
32
+     *
33
+     * @param string $type ReminderType
34
+     */
35
+    public function __construct(string $type) {
36
+        parent::__construct("No notification provider for type $type available");
37
+    }
38 38
 
39 39
 }
Please login to merge, or discard this patch.
apps/dav/lib/CalDAV/Reminder/NotificationProvider/AudioProvider.php 1 patch
Indentation   +2 added lines, -2 removed lines patch added patch discarded remove patch
@@ -32,6 +32,6 @@
 block discarded – undo
32 32
  */
33 33
 class AudioProvider extends PushProvider {
34 34
 
35
-	/** @var string */
36
-	public const NOTIFICATION_TYPE = 'AUDIO';
35
+    /** @var string */
36
+    public const NOTIFICATION_TYPE = 'AUDIO';
37 37
 }
Please login to merge, or discard this patch.