Passed
Push — master ( ed2d6e...f8c519 )
by Christoph
13:58 queued 12s
created

ReminderService::onTouchCalendarObject()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 22
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 14
nc 5
nop 2
dl 0
loc 22
rs 9.4888
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * @copyright Copyright (c) 2019, Thomas Citharel
7
 * @copyright Copyright (c) 2019, Georg Ehrke
8
 *
9
 * @author Christoph Wurst <[email protected]>
10
 * @author Georg Ehrke <[email protected]>
11
 * @author Roeland Jago Douma <[email protected]>
12
 * @author Thomas Citharel <[email protected]>
13
 *
14
 * @license GNU AGPL version 3 or any later version
15
 *
16
 * This program is free software: you can redistribute it and/or modify
17
 * it under the terms of the GNU Affero General Public License as
18
 * published by the Free Software Foundation, either version 3 of the
19
 * License, or (at your option) any later version.
20
 *
21
 * This program is distributed in the hope that it will be useful,
22
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
23
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
24
 * GNU Affero General Public License for more details.
25
 *
26
 * You should have received a copy of the GNU Affero General Public License
27
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
28
 *
29
 */
30
31
namespace OCA\DAV\CalDAV\Reminder;
32
33
use DateTimeImmutable;
34
use OCA\DAV\CalDAV\CalDavBackend;
35
use OCP\AppFramework\Utility\ITimeFactory;
36
use OCP\IGroup;
37
use OCP\IGroupManager;
38
use OCP\IUser;
39
use OCP\IUserManager;
40
use Sabre\VObject;
41
use Sabre\VObject\Component\VAlarm;
42
use Sabre\VObject\Component\VEvent;
43
use Sabre\VObject\InvalidDataException;
44
use Sabre\VObject\ParseException;
45
use Sabre\VObject\Recur\EventIterator;
46
use Sabre\VObject\Recur\NoInstancesException;
47
use function strcasecmp;
48
49
class ReminderService {
50
51
	/** @var Backend */
52
	private $backend;
53
54
	/** @var NotificationProviderManager */
55
	private $notificationProviderManager;
56
57
	/** @var IUserManager */
58
	private $userManager;
59
60
	/** @var IGroupManager */
61
	private $groupManager;
62
63
	/** @var CalDavBackend */
64
	private $caldavBackend;
65
66
	/** @var ITimeFactory */
67
	private $timeFactory;
68
69
	public const REMINDER_TYPE_EMAIL = 'EMAIL';
70
	public const REMINDER_TYPE_DISPLAY = 'DISPLAY';
71
	public const REMINDER_TYPE_AUDIO = 'AUDIO';
72
73
	/**
74
	 * @var String[]
75
	 *
76
	 * Official RFC5545 reminder types
77
	 */
78
	public const REMINDER_TYPES = [
79
		self::REMINDER_TYPE_EMAIL,
80
		self::REMINDER_TYPE_DISPLAY,
81
		self::REMINDER_TYPE_AUDIO
82
	];
83
84
	/**
85
	 * ReminderService constructor.
86
	 *
87
	 * @param Backend $backend
88
	 * @param NotificationProviderManager $notificationProviderManager
89
	 * @param IUserManager $userManager
90
	 * @param IGroupManager $groupManager
91
	 * @param CalDavBackend $caldavBackend
92
	 * @param ITimeFactory $timeFactory
93
	 */
94
	public function __construct(Backend $backend,
95
								NotificationProviderManager $notificationProviderManager,
96
								IUserManager $userManager,
97
								IGroupManager $groupManager,
98
								CalDavBackend $caldavBackend,
99
								ITimeFactory $timeFactory) {
100
		$this->backend = $backend;
101
		$this->notificationProviderManager = $notificationProviderManager;
102
		$this->userManager = $userManager;
103
		$this->groupManager = $groupManager;
104
		$this->caldavBackend = $caldavBackend;
105
		$this->timeFactory = $timeFactory;
106
	}
107
108
	/**
109
	 * Process reminders to activate
110
	 *
111
	 * @throws NotificationProvider\ProviderNotAvailableException
112
	 * @throws NotificationTypeDoesNotExistException
113
	 */
114
	public function processReminders():void {
115
		$reminders = $this->backend->getRemindersToProcess();
116
117
		foreach ($reminders as $reminder) {
118
			$calendarData = is_resource($reminder['calendardata'])
119
				? stream_get_contents($reminder['calendardata'])
120
				: $reminder['calendardata'];
121
122
			$vcalendar = $this->parseCalendarData($calendarData);
123
			if (!$vcalendar) {
124
				$this->backend->removeReminder($reminder['id']);
125
				continue;
126
			}
127
128
			$vevent = $this->getVEventByRecurrenceId($vcalendar, $reminder['recurrence_id'], $reminder['is_recurrence_exception']);
129
			if (!$vevent) {
130
				$this->backend->removeReminder($reminder['id']);
131
				continue;
132
			}
133
134
			if ($this->wasEventCancelled($vevent)) {
135
				$this->deleteOrProcessNext($reminder, $vevent);
136
				continue;
137
			}
138
139
			if (!$this->notificationProviderManager->hasProvider($reminder['type'])) {
140
				$this->deleteOrProcessNext($reminder, $vevent);
141
				continue;
142
			}
143
144
			$users = $this->getAllUsersWithWriteAccessToCalendar($reminder['calendar_id']);
145
			$user = $this->getUserFromPrincipalURI($reminder['principaluri']);
146
			if ($user) {
147
				$users[] = $user;
148
			}
149
150
			$notificationProvider = $this->notificationProviderManager->getProvider($reminder['type']);
151
			$notificationProvider->send($vevent, $reminder['displayname'], $users);
152
153
			$this->deleteOrProcessNext($reminder, $vevent);
154
		}
155
	}
156
157
	/**
158
	 * @param array $objectData
159
	 * @throws VObject\InvalidDataException
160
	 */
161
	public function onCalendarObjectCreate(array $objectData):void {
162
		// We only support VEvents for now
163
		if (strcasecmp($objectData['component'], 'vevent') !== 0) {
164
			return;
165
		}
166
167
		$calendarData = is_resource($objectData['calendardata'])
168
			? stream_get_contents($objectData['calendardata'])
169
			: $objectData['calendardata'];
170
171
		/** @var VObject\Component\VCalendar $vcalendar */
172
		$vcalendar = $this->parseCalendarData($calendarData);
173
		if (!$vcalendar) {
0 ignored issues
show
introduced by
$vcalendar is of type Sabre\VObject\Component\VCalendar, thus it always evaluated to true.
Loading history...
174
			return;
175
		}
176
177
		$vevents = $this->getAllVEventsFromVCalendar($vcalendar);
178
		if (count($vevents) === 0) {
179
			return;
180
		}
181
182
		$uid = (string) $vevents[0]->UID;
183
		$recurrenceExceptions = $this->getRecurrenceExceptionFromListOfVEvents($vevents);
184
		$masterItem = $this->getMasterItemFromListOfVEvents($vevents);
185
		$now = $this->timeFactory->getDateTime();
186
		$isRecurring = $masterItem ? $this->isRecurring($masterItem) : false;
187
188
		foreach ($recurrenceExceptions as $recurrenceException) {
189
			$eventHash = $this->getEventHash($recurrenceException);
190
191
			if (!isset($recurrenceException->VALARM)) {
192
				continue;
193
			}
194
195
			foreach ($recurrenceException->VALARM as $valarm) {
196
				/** @var VAlarm $valarm */
197
				$alarmHash = $this->getAlarmHash($valarm);
198
				$triggerTime = $valarm->getEffectiveTriggerTime();
199
				$diff = $now->diff($triggerTime);
200
				if ($diff->invert === 1) {
201
					continue;
202
				}
203
204
				$alarms = $this->getRemindersForVAlarm($valarm, $objectData,
205
					$eventHash, $alarmHash, true, true);
206
				$this->writeRemindersToDatabase($alarms);
207
			}
208
		}
209
210
		if ($masterItem) {
211
			$processedAlarms = [];
212
			$masterAlarms = [];
213
			$masterHash = $this->getEventHash($masterItem);
214
215
			if (!isset($masterItem->VALARM)) {
216
				return;
217
			}
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
					try {
256
						$triggerTime = $valarm->getEffectiveTriggerTime();
257
					} catch (InvalidDataException $e) {
258
						continue;
259
					}
260
261
					// If effective trigger time is in the past
262
					// just skip and generate for next event
263
					$diff = $now->diff($triggerTime);
264
					if ($diff->invert === 1) {
265
						// If an absolute alarm is in the past,
266
						// just add it to processedAlarms, so
267
						// we don't extend till eternity
268
						if (!$this->isAlarmRelative($valarm)) {
269
							$processedAlarms[] = $alarmHash;
270
						}
271
272
						continue;
273
					}
274
275
					$alarms = $this->getRemindersForVAlarm($valarm, $objectData, $masterHash, $alarmHash, $isRecurring, false);
276
					$this->writeRemindersToDatabase($alarms);
277
					$processedAlarms[] = $alarmHash;
278
				}
279
280
				$iterator->next();
281
			}
282
		}
283
	}
284
285
	/**
286
	 * @param array $objectData
287
	 * @throws VObject\InvalidDataException
288
	 */
289
	public function onCalendarObjectEdit(array $objectData):void {
290
		// TODO - this can be vastly improved
291
		//  - get cached reminders
292
		//  - ...
293
294
		$this->onCalendarObjectDelete($objectData);
295
		$this->onCalendarObjectCreate($objectData);
296
	}
297
298
	/**
299
	 * @param array $objectData
300
	 * @throws VObject\InvalidDataException
301
	 */
302
	public function onCalendarObjectDelete(array $objectData):void {
303
		// We only support VEvents for now
304
		if (strcasecmp($objectData['component'], 'vevent') !== 0) {
305
			return;
306
		}
307
308
		$this->backend->cleanRemindersForEvent((int) $objectData['id']);
309
	}
310
311
	/**
312
	 * @param VAlarm $valarm
313
	 * @param array $objectData
314
	 * @param string|null $eventHash
315
	 * @param string|null $alarmHash
316
	 * @param bool $isRecurring
317
	 * @param bool $isRecurrenceException
318
	 * @return array
319
	 */
320
	private function getRemindersForVAlarm(VAlarm $valarm,
321
										   array $objectData,
322
										   string $eventHash = null,
323
										   string $alarmHash = null,
324
										   bool $isRecurring = false,
325
										   bool $isRecurrenceException = false):array {
326
		if ($eventHash === null) {
327
			$eventHash = $this->getEventHash($valarm->parent);
328
		}
329
		if ($alarmHash === null) {
330
			$alarmHash = $this->getAlarmHash($valarm);
331
		}
332
333
		$recurrenceId = $this->getEffectiveRecurrenceIdOfVEvent($valarm->parent);
334
		$isRelative = $this->isAlarmRelative($valarm);
335
		/** @var DateTimeImmutable $notificationDate */
336
		$notificationDate = $valarm->getEffectiveTriggerTime();
337
		$clonedNotificationDate = new \DateTime('now', $notificationDate->getTimezone());
338
		$clonedNotificationDate->setTimestamp($notificationDate->getTimestamp());
339
340
		$alarms = [];
341
342
		$alarms[] = [
343
			'calendar_id' => $objectData['calendarid'],
344
			'object_id' => $objectData['id'],
345
			'uid' => (string) $valarm->parent->UID,
346
			'is_recurring' => $isRecurring,
347
			'recurrence_id' => $recurrenceId,
348
			'is_recurrence_exception' => $isRecurrenceException,
349
			'event_hash' => $eventHash,
350
			'alarm_hash' => $alarmHash,
351
			'type' => (string) $valarm->ACTION,
352
			'is_relative' => $isRelative,
353
			'notification_date' => $notificationDate->getTimestamp(),
354
			'is_repeat_based' => false,
355
		];
356
357
		$repeat = isset($valarm->REPEAT) ? (int) $valarm->REPEAT->getValue() : 0;
0 ignored issues
show
Bug introduced by
The method getValue() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

357
		$repeat = isset($valarm->REPEAT) ? (int) $valarm->REPEAT->/** @scrutinizer ignore-call */ getValue() : 0;

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
358
		for ($i = 0; $i < $repeat; $i++) {
359
			if ($valarm->DURATION === null) {
360
				continue;
361
			}
362
363
			$clonedNotificationDate->add($valarm->DURATION->getDateInterval());
0 ignored issues
show
Bug introduced by
The method getDateInterval() does not exist on Sabre\VObject\Property. It seems like you code against a sub-type of Sabre\VObject\Property such as Sabre\VObject\Property\ICalendar\Duration. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

363
			$clonedNotificationDate->add($valarm->DURATION->/** @scrutinizer ignore-call */ getDateInterval());
Loading history...
364
			$alarms[] = [
365
				'calendar_id' => $objectData['calendarid'],
366
				'object_id' => $objectData['id'],
367
				'uid' => (string) $valarm->parent->UID,
368
				'is_recurring' => $isRecurring,
369
				'recurrence_id' => $recurrenceId,
370
				'is_recurrence_exception' => $isRecurrenceException,
371
				'event_hash' => $eventHash,
372
				'alarm_hash' => $alarmHash,
373
				'type' => (string) $valarm->ACTION,
374
				'is_relative' => $isRelative,
375
				'notification_date' => $clonedNotificationDate->getTimestamp(),
376
				'is_repeat_based' => true,
377
			];
378
		}
379
380
		return $alarms;
381
	}
382
383
	/**
384
	 * @param array $reminders
385
	 */
386
	private function writeRemindersToDatabase(array $reminders): void {
387
		foreach ($reminders as $reminder) {
388
			$this->backend->insertReminder(
389
				(int) $reminder['calendar_id'],
390
				(int) $reminder['object_id'],
391
				$reminder['uid'],
392
				$reminder['is_recurring'],
393
				(int) $reminder['recurrence_id'],
394
				$reminder['is_recurrence_exception'],
395
				$reminder['event_hash'],
396
				$reminder['alarm_hash'],
397
				$reminder['type'],
398
				$reminder['is_relative'],
399
				(int) $reminder['notification_date'],
400
				$reminder['is_repeat_based']
401
			);
402
		}
403
	}
404
405
	/**
406
	 * @param array $reminder
407
	 * @param VEvent $vevent
408
	 */
409
	private function deleteOrProcessNext(array $reminder,
410
										 VObject\Component\VEvent $vevent):void {
411
		if ($reminder['is_repeat_based'] ||
412
			!$reminder['is_recurring'] ||
413
			!$reminder['is_relative'] ||
414
			$reminder['is_recurrence_exception']) {
415
			$this->backend->removeReminder($reminder['id']);
416
			return;
417
		}
418
419
		$vevents = $this->getAllVEventsFromVCalendar($vevent->parent);
420
		$recurrenceExceptions = $this->getRecurrenceExceptionFromListOfVEvents($vevents);
421
		$now = $this->timeFactory->getDateTime();
422
423
		try {
424
			$iterator = new EventIterator($vevents, $reminder['uid']);
425
		} catch (NoInstancesException $e) {
426
			// This event is recurring, but it doesn't have a single
427
			// instance. We are skipping this event from the output
428
			// entirely.
429
			return;
430
		}
431
432
		while ($iterator->valid()) {
433
			$event = $iterator->getEventObject();
434
435
			// Recurrence-exceptions are handled separately, so just ignore them here
436
			if (\in_array($event, $recurrenceExceptions, true)) {
437
				$iterator->next();
438
				continue;
439
			}
440
441
			$recurrenceId = $this->getEffectiveRecurrenceIdOfVEvent($event);
442
			if ($reminder['recurrence_id'] >= $recurrenceId) {
443
				$iterator->next();
444
				continue;
445
			}
446
447
			foreach ($event->VALARM as $valarm) {
448
				/** @var VAlarm $valarm */
449
				$alarmHash = $this->getAlarmHash($valarm);
450
				if ($alarmHash !== $reminder['alarm_hash']) {
451
					continue;
452
				}
453
454
				$triggerTime = $valarm->getEffectiveTriggerTime();
455
456
				// If effective trigger time is in the past
457
				// just skip and generate for next event
458
				$diff = $now->diff($triggerTime);
459
				if ($diff->invert === 1) {
460
					continue;
461
				}
462
463
				$this->backend->removeReminder($reminder['id']);
464
				$alarms = $this->getRemindersForVAlarm($valarm, [
465
					'calendarid' => $reminder['calendar_id'],
466
					'id' => $reminder['object_id'],
467
				], $reminder['event_hash'], $alarmHash, true, false);
468
				$this->writeRemindersToDatabase($alarms);
469
470
				// Abort generating reminders after creating one successfully
471
				return;
472
			}
473
474
			$iterator->next();
475
		}
476
477
		$this->backend->removeReminder($reminder['id']);
478
	}
479
480
	/**
481
	 * @param int $calendarId
482
	 * @return IUser[]
483
	 */
484
	private function getAllUsersWithWriteAccessToCalendar(int $calendarId):array {
485
		$shares = $this->caldavBackend->getShares($calendarId);
486
487
		$users = [];
488
		$userIds = [];
489
		$groups = [];
490
		foreach ($shares as $share) {
491
			// Only consider writable shares
492
			if ($share['readOnly']) {
493
				continue;
494
			}
495
496
			$principal = explode('/', $share['{http://owncloud.org/ns}principal']);
497
			if ($principal[1] === 'users') {
498
				$user = $this->userManager->get($principal[2]);
499
				if ($user) {
500
					$users[] = $user;
501
					$userIds[] = $principal[2];
502
				}
503
			} elseif ($principal[1] === 'groups') {
504
				$groups[] = $principal[2];
505
			}
506
		}
507
508
		foreach ($groups as $gid) {
509
			$group = $this->groupManager->get($gid);
510
			if ($group instanceof IGroup) {
511
				foreach ($group->getUsers() as $user) {
512
					if (!\in_array($user->getUID(), $userIds, true)) {
513
						$users[] = $user;
514
						$userIds[] = $user->getUID();
515
					}
516
				}
517
			}
518
		}
519
520
		return $users;
521
	}
522
523
	/**
524
	 * Gets a hash of the event.
525
	 * If the hash changes, we have to update all relative alarms.
526
	 *
527
	 * @param VEvent $vevent
528
	 * @return string
529
	 */
530
	private function getEventHash(VEvent $vevent):string {
531
		$properties = [
532
			(string) $vevent->DTSTART->serialize(),
0 ignored issues
show
Bug introduced by
The method serialize() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

532
			(string) $vevent->DTSTART->/** @scrutinizer ignore-call */ serialize(),

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
533
		];
534
535
		if ($vevent->DTEND) {
536
			$properties[] = (string) $vevent->DTEND->serialize();
537
		}
538
		if ($vevent->DURATION) {
539
			$properties[] = (string) $vevent->DURATION->serialize();
540
		}
541
		if ($vevent->{'RECURRENCE-ID'}) {
542
			$properties[] = (string) $vevent->{'RECURRENCE-ID'}->serialize();
543
		}
544
		if ($vevent->RRULE) {
545
			$properties[] = (string) $vevent->RRULE->serialize();
546
		}
547
		if ($vevent->EXDATE) {
548
			$properties[] = (string) $vevent->EXDATE->serialize();
549
		}
550
		if ($vevent->RDATE) {
551
			$properties[] = (string) $vevent->RDATE->serialize();
552
		}
553
554
		return md5(implode('::', $properties));
555
	}
556
557
	/**
558
	 * Gets a hash of the alarm.
559
	 * If the hash changes, we have to update oc_dav_reminders.
560
	 *
561
	 * @param VAlarm $valarm
562
	 * @return string
563
	 */
564
	private function getAlarmHash(VAlarm $valarm):string {
565
		$properties = [
566
			(string) $valarm->ACTION->serialize(),
0 ignored issues
show
Bug introduced by
The method serialize() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

566
			(string) $valarm->ACTION->/** @scrutinizer ignore-call */ serialize(),

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
567
			(string) $valarm->TRIGGER->serialize(),
0 ignored issues
show
Bug introduced by
The method serialize() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

567
			(string) $valarm->TRIGGER->/** @scrutinizer ignore-call */ serialize(),

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
568
		];
569
570
		if ($valarm->DURATION) {
571
			$properties[] = (string) $valarm->DURATION->serialize();
572
		}
573
		if ($valarm->REPEAT) {
574
			$properties[] = (string) $valarm->REPEAT->serialize();
575
		}
576
577
		return md5(implode('::', $properties));
578
	}
579
580
	/**
581
	 * @param VObject\Component\VCalendar $vcalendar
582
	 * @param int $recurrenceId
583
	 * @param bool $isRecurrenceException
584
	 * @return VEvent|null
585
	 */
586
	private function getVEventByRecurrenceId(VObject\Component\VCalendar $vcalendar,
587
											 int $recurrenceId,
588
											 bool $isRecurrenceException):?VEvent {
589
		$vevents = $this->getAllVEventsFromVCalendar($vcalendar);
590
		if (count($vevents) === 0) {
591
			return null;
592
		}
593
594
		$uid = (string) $vevents[0]->UID;
595
		$recurrenceExceptions = $this->getRecurrenceExceptionFromListOfVEvents($vevents);
596
		$masterItem = $this->getMasterItemFromListOfVEvents($vevents);
597
598
		// Handle recurrence-exceptions first, because recurrence-expansion is expensive
599
		if ($isRecurrenceException) {
600
			foreach ($recurrenceExceptions as $recurrenceException) {
601
				if ($this->getEffectiveRecurrenceIdOfVEvent($recurrenceException) === $recurrenceId) {
602
					return $recurrenceException;
603
				}
604
			}
605
606
			return null;
607
		}
608
609
		if ($masterItem) {
610
			try {
611
				$iterator = new EventIterator($vevents, $uid);
612
			} catch (NoInstancesException $e) {
613
				// This event is recurring, but it doesn't have a single
614
				// instance. We are skipping this event from the output
615
				// entirely.
616
				return null;
617
			}
618
619
			while ($iterator->valid()) {
620
				$event = $iterator->getEventObject();
621
622
				// Recurrence-exceptions are handled separately, so just ignore them here
623
				if (\in_array($event, $recurrenceExceptions, true)) {
624
					$iterator->next();
625
					continue;
626
				}
627
628
				if ($this->getEffectiveRecurrenceIdOfVEvent($event) === $recurrenceId) {
629
					return $event;
630
				}
631
632
				$iterator->next();
633
			}
634
		}
635
636
		return null;
637
	}
638
639
	/**
640
	 * @param VEvent $vevent
641
	 * @return string
642
	 */
643
	private function getStatusOfEvent(VEvent $vevent):string {
644
		if ($vevent->STATUS) {
645
			return (string) $vevent->STATUS;
646
		}
647
648
		// Doesn't say so in the standard,
649
		// but we consider events without a status
650
		// to be confirmed
651
		return 'CONFIRMED';
652
	}
653
654
	/**
655
	 * @param VObject\Component\VEvent $vevent
656
	 * @return bool
657
	 */
658
	private function wasEventCancelled(VObject\Component\VEvent $vevent):bool {
659
		return $this->getStatusOfEvent($vevent) === 'CANCELLED';
660
	}
661
662
	/**
663
	 * @param string $calendarData
664
	 * @return VObject\Component\VCalendar|null
665
	 */
666
	private function parseCalendarData(string $calendarData):?VObject\Component\VCalendar {
667
		try {
668
			return VObject\Reader::read($calendarData,
0 ignored issues
show
Bug Best Practice introduced by
The expression return Sabre\VObject\Rea...ader::OPTION_FORGIVING) returns the type Sabre\VObject\Document which includes types incompatible with the type-hinted return Sabre\VObject\Component\VCalendar|null.
Loading history...
669
				VObject\Reader::OPTION_FORGIVING);
670
		} catch (ParseException $ex) {
671
			return null;
672
		}
673
	}
674
675
	/**
676
	 * @param string $principalUri
677
	 * @return IUser|null
678
	 */
679
	private function getUserFromPrincipalURI(string $principalUri):?IUser {
680
		if (!$principalUri) {
681
			return null;
682
		}
683
684
		if (stripos($principalUri, 'principals/users/') !== 0) {
685
			return null;
686
		}
687
688
		$userId = substr($principalUri, 17);
689
		return $this->userManager->get($userId);
690
	}
691
692
	/**
693
	 * @param VObject\Component\VCalendar $vcalendar
694
	 * @return VObject\Component\VEvent[]
695
	 */
696
	private function getAllVEventsFromVCalendar(VObject\Component\VCalendar $vcalendar):array {
697
		$vevents = [];
698
699
		foreach ($vcalendar->children() as $child) {
700
			if (!($child instanceof VObject\Component)) {
701
				continue;
702
			}
703
704
			if ($child->name !== 'VEVENT') {
705
				continue;
706
			}
707
708
			$vevents[] = $child;
709
		}
710
711
		return $vevents;
712
	}
713
714
	/**
715
	 * @param array $vevents
716
	 * @return VObject\Component\VEvent[]
717
	 */
718
	private function getRecurrenceExceptionFromListOfVEvents(array $vevents):array {
719
		return array_values(array_filter($vevents, function (VEvent $vevent) {
720
			return $vevent->{'RECURRENCE-ID'} !== null;
721
		}));
722
	}
723
724
	/**
725
	 * @param array $vevents
726
	 * @return VEvent|null
727
	 */
728
	private function getMasterItemFromListOfVEvents(array $vevents):?VEvent {
729
		$elements = array_values(array_filter($vevents, function (VEvent $vevent) {
730
			return $vevent->{'RECURRENCE-ID'} === null;
731
		}));
732
733
		if (count($elements) === 0) {
734
			return null;
735
		}
736
		if (count($elements) > 1) {
737
			throw new \TypeError('Multiple master objects');
738
		}
739
740
		return $elements[0];
741
	}
742
743
	/**
744
	 * @param VAlarm $valarm
745
	 * @return bool
746
	 */
747
	private function isAlarmRelative(VAlarm $valarm):bool {
748
		$trigger = $valarm->TRIGGER;
749
		return $trigger instanceof VObject\Property\ICalendar\Duration;
750
	}
751
752
	/**
753
	 * @param VEvent $vevent
754
	 * @return int
755
	 */
756
	private function getEffectiveRecurrenceIdOfVEvent(VEvent $vevent):int {
757
		if (isset($vevent->{'RECURRENCE-ID'})) {
758
			return $vevent->{'RECURRENCE-ID'}->getDateTime()->getTimestamp();
759
		}
760
761
		return $vevent->DTSTART->getDateTime()->getTimestamp();
0 ignored issues
show
Bug introduced by
The method getDateTime() does not exist on Sabre\VObject\Property. It seems like you code against a sub-type of Sabre\VObject\Property such as Sabre\VObject\Property\ICalendar\DateTime or Sabre\VObject\Property\VCard\DateAndOrTime. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

761
		return $vevent->DTSTART->/** @scrutinizer ignore-call */ getDateTime()->getTimestamp();
Loading history...
762
	}
763
764
	/**
765
	 * @param VEvent $vevent
766
	 * @return bool
767
	 */
768
	private function isRecurring(VEvent $vevent):bool {
769
		return isset($vevent->RRULE) || isset($vevent->RDATE);
770
	}
771
}
772