Passed
Push — master ( 674f4e...7aa786 )
by Christoph
12:53 queued 12s
created

ReminderService::getCalendarTimeZone()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 8
c 0
b 0
f 0
nc 2
nop 1
dl 0
loc 15
rs 10
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 Joas Schilling <[email protected]>
12
 * @author Roeland Jago Douma <[email protected]>
13
 * @author Thomas Citharel <[email protected]>
14
 * @author Richard Steinmetz <[email protected]>
15
 *
16
 * @license GNU AGPL version 3 or any later version
17
 *
18
 * This program is free software: you can redistribute it and/or modify
19
 * it under the terms of the GNU Affero General Public License as
20
 * published by the Free Software Foundation, either version 3 of the
21
 * License, or (at your option) any later version.
22
 *
23
 * This program is distributed in the hope that it will be useful,
24
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
25
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
26
 * GNU Affero General Public License for more details.
27
 *
28
 * You should have received a copy of the GNU Affero General Public License
29
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
30
 *
31
 */
32
namespace OCA\DAV\CalDAV\Reminder;
33
34
use DateTimeImmutable;
35
use DateTimeZone;
36
use OCA\DAV\CalDAV\CalDavBackend;
37
use OCA\DAV\Connector\Sabre\Principal;
38
use OCP\AppFramework\Utility\ITimeFactory;
39
use OCP\IConfig;
40
use OCP\IGroup;
41
use OCP\IGroupManager;
42
use OCP\IUser;
43
use OCP\IUserManager;
44
use Psr\Log\LoggerInterface;
45
use Sabre\VObject;
46
use Sabre\VObject\Component\VAlarm;
47
use Sabre\VObject\Component\VEvent;
48
use Sabre\VObject\InvalidDataException;
49
use Sabre\VObject\ParseException;
50
use Sabre\VObject\Recur\EventIterator;
51
use Sabre\VObject\Recur\MaxInstancesExceededException;
52
use Sabre\VObject\Recur\NoInstancesException;
53
use function count;
54
use function strcasecmp;
55
56
class ReminderService {
57
58
	/** @var Backend */
59
	private $backend;
60
61
	/** @var NotificationProviderManager */
62
	private $notificationProviderManager;
63
64
	/** @var IUserManager */
65
	private $userManager;
66
67
	/** @var IGroupManager */
68
	private $groupManager;
69
70
	/** @var CalDavBackend */
71
	private $caldavBackend;
72
73
	/** @var ITimeFactory */
74
	private $timeFactory;
75
76
	/** @var IConfig */
77
	private $config;
78
79
	/** @var LoggerInterface */
80
	private $logger;
81
82
	/** @var Principal */
83
	private $principalConnector;
84
85
	public const REMINDER_TYPE_EMAIL = 'EMAIL';
86
	public const REMINDER_TYPE_DISPLAY = 'DISPLAY';
87
	public const REMINDER_TYPE_AUDIO = 'AUDIO';
88
89
	/**
90
	 * @var String[]
91
	 *
92
	 * Official RFC5545 reminder types
93
	 */
94
	public const REMINDER_TYPES = [
95
		self::REMINDER_TYPE_EMAIL,
96
		self::REMINDER_TYPE_DISPLAY,
97
		self::REMINDER_TYPE_AUDIO
98
	];
99
100
	public function __construct(Backend $backend,
101
								NotificationProviderManager $notificationProviderManager,
102
								IUserManager $userManager,
103
								IGroupManager $groupManager,
104
								CalDavBackend $caldavBackend,
105
								ITimeFactory $timeFactory,
106
								IConfig $config,
107
								LoggerInterface $logger,
108
								Principal $principalConnector) {
109
		$this->backend = $backend;
110
		$this->notificationProviderManager = $notificationProviderManager;
111
		$this->userManager = $userManager;
112
		$this->groupManager = $groupManager;
113
		$this->caldavBackend = $caldavBackend;
114
		$this->timeFactory = $timeFactory;
115
		$this->config = $config;
116
		$this->logger = $logger;
117
		$this->principalConnector = $principalConnector;
118
	}
119
120
	/**
121
	 * Process reminders to activate
122
	 *
123
	 * @throws NotificationProvider\ProviderNotAvailableException
124
	 * @throws NotificationTypeDoesNotExistException
125
	 */
126
	public function processReminders() :void {
127
		$reminders = $this->backend->getRemindersToProcess();
128
		$this->logger->debug('{numReminders} reminders to process', [
129
			'numReminders' => count($reminders),
130
		]);
131
132
		foreach ($reminders as $reminder) {
133
			$calendarData = is_resource($reminder['calendardata'])
134
				? stream_get_contents($reminder['calendardata'])
135
				: $reminder['calendardata'];
136
137
			if (!$calendarData) {
138
				continue;
139
			}
140
141
			$vcalendar = $this->parseCalendarData($calendarData);
142
			if (!$vcalendar) {
143
				$this->logger->debug('Reminder {id} does not belong to a valid calendar', [
144
					'id' => $reminder['id'],
145
				]);
146
				$this->backend->removeReminder($reminder['id']);
147
				continue;
148
			}
149
150
			$vevent = $this->getVEventByRecurrenceId($vcalendar, $reminder['recurrence_id'], $reminder['is_recurrence_exception']);
151
			if (!$vevent) {
152
				$this->logger->debug('Reminder {id} does not belong to a valid event', [
153
					'id' => $reminder['id'],
154
				]);
155
				$this->backend->removeReminder($reminder['id']);
156
				continue;
157
			}
158
159
			if ($this->wasEventCancelled($vevent)) {
160
				$this->logger->debug('Reminder {id} belongs to a cancelled event', [
161
					'id' => $reminder['id'],
162
				]);
163
				$this->deleteOrProcessNext($reminder, $vevent);
164
				continue;
165
			}
166
167
			if (!$this->notificationProviderManager->hasProvider($reminder['type'])) {
168
				$this->logger->debug('Reminder {id} does not belong to a valid notification provider', [
169
					'id' => $reminder['id'],
170
				]);
171
				$this->deleteOrProcessNext($reminder, $vevent);
172
				continue;
173
			}
174
175
			if ($this->config->getAppValue('dav', 'sendEventRemindersToSharedGroupMembers', 'yes') === 'no') {
176
				$users = $this->getAllUsersWithWriteAccessToCalendar($reminder['calendar_id']);
177
			} else {
178
				$users = [];
179
			}
180
181
			$user = $this->getUserFromPrincipalURI($reminder['principaluri']);
182
			if ($user) {
183
				$users[] = $user;
184
			}
185
186
			$userPrincipalEmailAddresses = [];
187
			$userPrincipal = $this->principalConnector->getPrincipalByPath($reminder['principaluri']);
188
			if ($userPrincipal) {
189
				$userPrincipalEmailAddresses = $this->principalConnector->getEmailAddressesOfPrincipal($userPrincipal);
190
			}
191
192
			$this->logger->debug('Reminder {id} will be sent to {numUsers} users', [
193
				'id' => $reminder['id'],
194
				'numUsers' => count($users),
195
			]);
196
			$notificationProvider = $this->notificationProviderManager->getProvider($reminder['type']);
197
			$notificationProvider->send($vevent, $reminder['displayname'], $userPrincipalEmailAddresses, $users);
198
199
			$this->deleteOrProcessNext($reminder, $vevent);
200
		}
201
	}
202
203
	/**
204
	 * @param array $objectData
205
	 * @throws VObject\InvalidDataException
206
	 */
207
	public function onCalendarObjectCreate(array $objectData):void {
208
		// We only support VEvents for now
209
		if (strcasecmp($objectData['component'], 'vevent') !== 0) {
210
			return;
211
		}
212
213
		$calendarData = is_resource($objectData['calendardata'])
214
			? stream_get_contents($objectData['calendardata'])
215
			: $objectData['calendardata'];
216
217
		if (!$calendarData) {
218
			return;
219
		}
220
221
		$vcalendar = $this->parseCalendarData($calendarData);
222
		if (!$vcalendar) {
0 ignored issues
show
introduced by
$vcalendar is of type Sabre\VObject\Component\VCalendar, thus it always evaluated to true.
Loading history...
223
			return;
224
		}
225
		$calendarTimeZone = $this->getCalendarTimeZone((int) $objectData['calendarid']);
226
227
		$vevents = $this->getAllVEventsFromVCalendar($vcalendar);
228
		if (count($vevents) === 0) {
229
			return;
230
		}
231
232
		$uid = (string) $vevents[0]->UID;
233
		$recurrenceExceptions = $this->getRecurrenceExceptionFromListOfVEvents($vevents);
234
		$masterItem = $this->getMasterItemFromListOfVEvents($vevents);
235
		$now = $this->timeFactory->getDateTime();
236
		$isRecurring = $masterItem ? $this->isRecurring($masterItem) : false;
237
238
		foreach ($recurrenceExceptions as $recurrenceException) {
239
			$eventHash = $this->getEventHash($recurrenceException);
240
241
			if (!isset($recurrenceException->VALARM)) {
242
				continue;
243
			}
244
245
			foreach ($recurrenceException->VALARM as $valarm) {
246
				/** @var VAlarm $valarm */
247
				$alarmHash = $this->getAlarmHash($valarm);
248
				$triggerTime = $valarm->getEffectiveTriggerTime();
249
				$diff = $now->diff($triggerTime);
250
				if ($diff->invert === 1) {
251
					continue;
252
				}
253
254
				$alarms = $this->getRemindersForVAlarm($valarm, $objectData, $calendarTimeZone,
255
					$eventHash, $alarmHash, true, true);
256
				$this->writeRemindersToDatabase($alarms);
257
			}
258
		}
259
260
		if ($masterItem) {
261
			$processedAlarms = [];
262
			$masterAlarms = [];
263
			$masterHash = $this->getEventHash($masterItem);
264
265
			if (!isset($masterItem->VALARM)) {
266
				return;
267
			}
268
269
			foreach ($masterItem->VALARM as $valarm) {
270
				$masterAlarms[] = $this->getAlarmHash($valarm);
271
			}
272
273
			try {
274
				$iterator = new EventIterator($vevents, $uid);
275
			} catch (NoInstancesException $e) {
276
				// This event is recurring, but it doesn't have a single
277
				// instance. We are skipping this event from the output
278
				// entirely.
279
				return;
280
			} catch (MaxInstancesExceededException $e) {
281
				// The event has more than 3500 recurring-instances
282
				// so we can ignore it
283
				return;
284
			}
285
286
			while ($iterator->valid() && count($processedAlarms) < count($masterAlarms)) {
287
				$event = $iterator->getEventObject();
288
289
				// Recurrence-exceptions are handled separately, so just ignore them here
290
				if (\in_array($event, $recurrenceExceptions, true)) {
291
					$iterator->next();
292
					continue;
293
				}
294
295
				foreach ($event->VALARM as $valarm) {
296
					/** @var VAlarm $valarm */
297
					$alarmHash = $this->getAlarmHash($valarm);
298
					if (\in_array($alarmHash, $processedAlarms, true)) {
299
						continue;
300
					}
301
302
					if (!\in_array((string) $valarm->ACTION, self::REMINDER_TYPES, true)) {
303
						// Action allows x-name, we don't insert reminders
304
						// into the database if they are not standard
305
						$processedAlarms[] = $alarmHash;
306
						continue;
307
					}
308
309
					try {
310
						$triggerTime = $valarm->getEffectiveTriggerTime();
311
						/**
312
						 * @psalm-suppress DocblockTypeContradiction
313
						 *   https://github.com/vimeo/psalm/issues/9244
314
						 */
315
						if ($triggerTime->getTimezone() === false || $triggerTime->getTimezone()->getName() === 'UTC') {
316
							$triggerTime = new DateTimeImmutable(
317
								$triggerTime->format('Y-m-d H:i:s'),
318
								$calendarTimeZone
319
							);
320
						}
321
					} catch (InvalidDataException $e) {
322
						continue;
323
					}
324
325
					// If effective trigger time is in the past
326
					// just skip and generate for next event
327
					$diff = $now->diff($triggerTime);
328
					if ($diff->invert === 1) {
329
						// If an absolute alarm is in the past,
330
						// just add it to processedAlarms, so
331
						// we don't extend till eternity
332
						if (!$this->isAlarmRelative($valarm)) {
333
							$processedAlarms[] = $alarmHash;
334
						}
335
336
						continue;
337
					}
338
339
					$alarms = $this->getRemindersForVAlarm($valarm, $objectData, $calendarTimeZone, $masterHash, $alarmHash, $isRecurring, false);
340
					$this->writeRemindersToDatabase($alarms);
341
					$processedAlarms[] = $alarmHash;
342
				}
343
344
				$iterator->next();
345
			}
346
		}
347
	}
348
349
	/**
350
	 * @param array $objectData
351
	 * @throws VObject\InvalidDataException
352
	 */
353
	public function onCalendarObjectEdit(array $objectData):void {
354
		// TODO - this can be vastly improved
355
		//  - get cached reminders
356
		//  - ...
357
358
		$this->onCalendarObjectDelete($objectData);
359
		$this->onCalendarObjectCreate($objectData);
360
	}
361
362
	/**
363
	 * @param array $objectData
364
	 * @throws VObject\InvalidDataException
365
	 */
366
	public function onCalendarObjectDelete(array $objectData):void {
367
		// We only support VEvents for now
368
		if (strcasecmp($objectData['component'], 'vevent') !== 0) {
369
			return;
370
		}
371
372
		$this->backend->cleanRemindersForEvent((int) $objectData['id']);
373
	}
374
375
	/**
376
	 * @param VAlarm $valarm
377
	 * @param array $objectData
378
	 * @param DateTimeZone $calendarTimeZone
379
	 * @param string|null $eventHash
380
	 * @param string|null $alarmHash
381
	 * @param bool $isRecurring
382
	 * @param bool $isRecurrenceException
383
	 * @return array
384
	 */
385
	private function getRemindersForVAlarm(VAlarm $valarm,
386
										   array $objectData,
387
										   DateTimeZone $calendarTimeZone,
388
										   string $eventHash = null,
389
										   string $alarmHash = null,
390
										   bool $isRecurring = false,
391
										   bool $isRecurrenceException = false):array {
392
		if ($eventHash === null) {
393
			$eventHash = $this->getEventHash($valarm->parent);
394
		}
395
		if ($alarmHash === null) {
396
			$alarmHash = $this->getAlarmHash($valarm);
397
		}
398
399
		$recurrenceId = $this->getEffectiveRecurrenceIdOfVEvent($valarm->parent);
400
		$isRelative = $this->isAlarmRelative($valarm);
401
		/** @var DateTimeImmutable $notificationDate */
402
		$notificationDate = $valarm->getEffectiveTriggerTime();
403
		/**
404
		 * @psalm-suppress DocblockTypeContradiction
405
		 *   https://github.com/vimeo/psalm/issues/9244
406
		 */
407
		if ($notificationDate->getTimezone() === false || $notificationDate->getTimezone()->getName() === 'UTC') {
408
			$notificationDate = new DateTimeImmutable(
409
				$notificationDate->format('Y-m-d H:i:s'),
410
				$calendarTimeZone
411
			);
412
		}
413
		$clonedNotificationDate = new \DateTime('now', $notificationDate->getTimezone());
414
		$clonedNotificationDate->setTimestamp($notificationDate->getTimestamp());
415
416
		$alarms = [];
417
418
		$alarms[] = [
419
			'calendar_id' => $objectData['calendarid'],
420
			'object_id' => $objectData['id'],
421
			'uid' => (string) $valarm->parent->UID,
422
			'is_recurring' => $isRecurring,
423
			'recurrence_id' => $recurrenceId,
424
			'is_recurrence_exception' => $isRecurrenceException,
425
			'event_hash' => $eventHash,
426
			'alarm_hash' => $alarmHash,
427
			'type' => (string) $valarm->ACTION,
428
			'is_relative' => $isRelative,
429
			'notification_date' => $notificationDate->getTimestamp(),
430
			'is_repeat_based' => false,
431
		];
432
433
		$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

433
		$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...
434
		for ($i = 0; $i < $repeat; $i++) {
435
			if ($valarm->DURATION === null) {
436
				continue;
437
			}
438
439
			$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

439
			$clonedNotificationDate->add($valarm->DURATION->/** @scrutinizer ignore-call */ getDateInterval());
Loading history...
440
			$alarms[] = [
441
				'calendar_id' => $objectData['calendarid'],
442
				'object_id' => $objectData['id'],
443
				'uid' => (string) $valarm->parent->UID,
444
				'is_recurring' => $isRecurring,
445
				'recurrence_id' => $recurrenceId,
446
				'is_recurrence_exception' => $isRecurrenceException,
447
				'event_hash' => $eventHash,
448
				'alarm_hash' => $alarmHash,
449
				'type' => (string) $valarm->ACTION,
450
				'is_relative' => $isRelative,
451
				'notification_date' => $clonedNotificationDate->getTimestamp(),
452
				'is_repeat_based' => true,
453
			];
454
		}
455
456
		return $alarms;
457
	}
458
459
	/**
460
	 * @param array $reminders
461
	 */
462
	private function writeRemindersToDatabase(array $reminders): void {
463
		foreach ($reminders as $reminder) {
464
			$this->backend->insertReminder(
465
				(int) $reminder['calendar_id'],
466
				(int) $reminder['object_id'],
467
				$reminder['uid'],
468
				$reminder['is_recurring'],
469
				(int) $reminder['recurrence_id'],
470
				$reminder['is_recurrence_exception'],
471
				$reminder['event_hash'],
472
				$reminder['alarm_hash'],
473
				$reminder['type'],
474
				$reminder['is_relative'],
475
				(int) $reminder['notification_date'],
476
				$reminder['is_repeat_based']
477
			);
478
		}
479
	}
480
481
	/**
482
	 * @param array $reminder
483
	 * @param VEvent $vevent
484
	 */
485
	private function deleteOrProcessNext(array $reminder,
486
										 VObject\Component\VEvent $vevent):void {
487
		if ($reminder['is_repeat_based'] ||
488
			!$reminder['is_recurring'] ||
489
			!$reminder['is_relative'] ||
490
			$reminder['is_recurrence_exception']) {
491
			$this->backend->removeReminder($reminder['id']);
492
			return;
493
		}
494
495
		$vevents = $this->getAllVEventsFromVCalendar($vevent->parent);
496
		$recurrenceExceptions = $this->getRecurrenceExceptionFromListOfVEvents($vevents);
497
		$now = $this->timeFactory->getDateTime();
498
		$calendarTimeZone = $this->getCalendarTimeZone((int) $reminder['calendar_id']);
499
500
		try {
501
			$iterator = new EventIterator($vevents, $reminder['uid']);
502
		} catch (NoInstancesException $e) {
503
			// This event is recurring, but it doesn't have a single
504
			// instance. We are skipping this event from the output
505
			// entirely.
506
			return;
507
		}
508
509
		try {
510
			while ($iterator->valid()) {
511
				$event = $iterator->getEventObject();
512
513
				// Recurrence-exceptions are handled separately, so just ignore them here
514
				if (\in_array($event, $recurrenceExceptions, true)) {
515
					$iterator->next();
516
					continue;
517
				}
518
519
				$recurrenceId = $this->getEffectiveRecurrenceIdOfVEvent($event);
520
				if ($reminder['recurrence_id'] >= $recurrenceId) {
521
					$iterator->next();
522
					continue;
523
				}
524
525
				foreach ($event->VALARM as $valarm) {
526
					/** @var VAlarm $valarm */
527
					$alarmHash = $this->getAlarmHash($valarm);
528
					if ($alarmHash !== $reminder['alarm_hash']) {
529
						continue;
530
					}
531
532
					$triggerTime = $valarm->getEffectiveTriggerTime();
533
534
					// If effective trigger time is in the past
535
					// just skip and generate for next event
536
					$diff = $now->diff($triggerTime);
537
					if ($diff->invert === 1) {
538
						continue;
539
					}
540
541
					$this->backend->removeReminder($reminder['id']);
542
					$alarms = $this->getRemindersForVAlarm($valarm, [
543
						'calendarid' => $reminder['calendar_id'],
544
						'id' => $reminder['object_id'],
545
					], $calendarTimeZone, $reminder['event_hash'], $alarmHash, true, false);
546
					$this->writeRemindersToDatabase($alarms);
547
548
					// Abort generating reminders after creating one successfully
549
					return;
550
				}
551
552
				$iterator->next();
553
			}
554
		} catch (MaxInstancesExceededException $e) {
555
			// Using debug logger as this isn't really an error
556
			$this->logger->debug('Recurrence with too many instances detected, skipping VEVENT', ['exception' => $e]);
557
		}
558
559
		$this->backend->removeReminder($reminder['id']);
560
	}
561
562
	/**
563
	 * @param int $calendarId
564
	 * @return IUser[]
565
	 */
566
	private function getAllUsersWithWriteAccessToCalendar(int $calendarId):array {
567
		$shares = $this->caldavBackend->getShares($calendarId);
568
569
		$users = [];
570
		$userIds = [];
571
		$groups = [];
572
		foreach ($shares as $share) {
573
			// Only consider writable shares
574
			if ($share['readOnly']) {
575
				continue;
576
			}
577
578
			$principal = explode('/', $share['{http://owncloud.org/ns}principal']);
579
			if ($principal[1] === 'users') {
580
				$user = $this->userManager->get($principal[2]);
581
				if ($user) {
582
					$users[] = $user;
583
					$userIds[] = $principal[2];
584
				}
585
			} elseif ($principal[1] === 'groups') {
586
				$groups[] = $principal[2];
587
			}
588
		}
589
590
		foreach ($groups as $gid) {
591
			$group = $this->groupManager->get($gid);
592
			if ($group instanceof IGroup) {
593
				foreach ($group->getUsers() as $user) {
594
					if (!\in_array($user->getUID(), $userIds, true)) {
595
						$users[] = $user;
596
						$userIds[] = $user->getUID();
597
					}
598
				}
599
			}
600
		}
601
602
		return $users;
603
	}
604
605
	/**
606
	 * Gets a hash of the event.
607
	 * If the hash changes, we have to update all relative alarms.
608
	 *
609
	 * @param VEvent $vevent
610
	 * @return string
611
	 */
612
	private function getEventHash(VEvent $vevent):string {
613
		$properties = [
614
			(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

614
			(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...
615
		];
616
617
		if ($vevent->DTEND) {
618
			$properties[] = (string) $vevent->DTEND->serialize();
619
		}
620
		if ($vevent->DURATION) {
621
			$properties[] = (string) $vevent->DURATION->serialize();
622
		}
623
		if ($vevent->{'RECURRENCE-ID'}) {
624
			$properties[] = (string) $vevent->{'RECURRENCE-ID'}->serialize();
625
		}
626
		if ($vevent->RRULE) {
627
			$properties[] = (string) $vevent->RRULE->serialize();
628
		}
629
		if ($vevent->EXDATE) {
630
			$properties[] = (string) $vevent->EXDATE->serialize();
631
		}
632
		if ($vevent->RDATE) {
633
			$properties[] = (string) $vevent->RDATE->serialize();
634
		}
635
636
		return md5(implode('::', $properties));
637
	}
638
639
	/**
640
	 * Gets a hash of the alarm.
641
	 * If the hash changes, we have to update oc_dav_reminders.
642
	 *
643
	 * @param VAlarm $valarm
644
	 * @return string
645
	 */
646
	private function getAlarmHash(VAlarm $valarm):string {
647
		$properties = [
648
			(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

648
			(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...
649
			(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

649
			(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...
650
		];
651
652
		if ($valarm->DURATION) {
653
			$properties[] = (string) $valarm->DURATION->serialize();
654
		}
655
		if ($valarm->REPEAT) {
656
			$properties[] = (string) $valarm->REPEAT->serialize();
657
		}
658
659
		return md5(implode('::', $properties));
660
	}
661
662
	/**
663
	 * @param VObject\Component\VCalendar $vcalendar
664
	 * @param int $recurrenceId
665
	 * @param bool $isRecurrenceException
666
	 * @return VEvent|null
667
	 */
668
	private function getVEventByRecurrenceId(VObject\Component\VCalendar $vcalendar,
669
											 int $recurrenceId,
670
											 bool $isRecurrenceException):?VEvent {
671
		$vevents = $this->getAllVEventsFromVCalendar($vcalendar);
672
		if (count($vevents) === 0) {
673
			return null;
674
		}
675
676
		$uid = (string) $vevents[0]->UID;
677
		$recurrenceExceptions = $this->getRecurrenceExceptionFromListOfVEvents($vevents);
678
		$masterItem = $this->getMasterItemFromListOfVEvents($vevents);
679
680
		// Handle recurrence-exceptions first, because recurrence-expansion is expensive
681
		if ($isRecurrenceException) {
682
			foreach ($recurrenceExceptions as $recurrenceException) {
683
				if ($this->getEffectiveRecurrenceIdOfVEvent($recurrenceException) === $recurrenceId) {
684
					return $recurrenceException;
685
				}
686
			}
687
688
			return null;
689
		}
690
691
		if ($masterItem) {
692
			try {
693
				$iterator = new EventIterator($vevents, $uid);
694
			} catch (NoInstancesException $e) {
695
				// This event is recurring, but it doesn't have a single
696
				// instance. We are skipping this event from the output
697
				// entirely.
698
				return null;
699
			}
700
701
			while ($iterator->valid()) {
702
				$event = $iterator->getEventObject();
703
704
				// Recurrence-exceptions are handled separately, so just ignore them here
705
				if (\in_array($event, $recurrenceExceptions, true)) {
706
					$iterator->next();
707
					continue;
708
				}
709
710
				if ($this->getEffectiveRecurrenceIdOfVEvent($event) === $recurrenceId) {
711
					return $event;
712
				}
713
714
				$iterator->next();
715
			}
716
		}
717
718
		return null;
719
	}
720
721
	/**
722
	 * @param VEvent $vevent
723
	 * @return string
724
	 */
725
	private function getStatusOfEvent(VEvent $vevent):string {
726
		if ($vevent->STATUS) {
727
			return (string) $vevent->STATUS;
728
		}
729
730
		// Doesn't say so in the standard,
731
		// but we consider events without a status
732
		// to be confirmed
733
		return 'CONFIRMED';
734
	}
735
736
	/**
737
	 * @param VObject\Component\VEvent $vevent
738
	 * @return bool
739
	 */
740
	private function wasEventCancelled(VObject\Component\VEvent $vevent):bool {
741
		return $this->getStatusOfEvent($vevent) === 'CANCELLED';
742
	}
743
744
	/**
745
	 * @param string $calendarData
746
	 * @return VObject\Component\VCalendar|null
747
	 */
748
	private function parseCalendarData(string $calendarData):?VObject\Component\VCalendar {
749
		try {
750
			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...
751
				VObject\Reader::OPTION_FORGIVING);
752
		} catch (ParseException $ex) {
753
			return null;
754
		}
755
	}
756
757
	/**
758
	 * @param string $principalUri
759
	 * @return IUser|null
760
	 */
761
	private function getUserFromPrincipalURI(string $principalUri):?IUser {
762
		if (!$principalUri) {
763
			return null;
764
		}
765
766
		if (stripos($principalUri, 'principals/users/') !== 0) {
767
			return null;
768
		}
769
770
		$userId = substr($principalUri, 17);
771
		return $this->userManager->get($userId);
772
	}
773
774
	/**
775
	 * @param VObject\Component\VCalendar $vcalendar
776
	 * @return VObject\Component\VEvent[]
777
	 */
778
	private function getAllVEventsFromVCalendar(VObject\Component\VCalendar $vcalendar):array {
779
		$vevents = [];
780
781
		foreach ($vcalendar->children() as $child) {
782
			if (!($child instanceof VObject\Component)) {
783
				continue;
784
			}
785
786
			if ($child->name !== 'VEVENT') {
787
				continue;
788
			}
789
790
			$vevents[] = $child;
791
		}
792
793
		return $vevents;
794
	}
795
796
	/**
797
	 * @param array $vevents
798
	 * @return VObject\Component\VEvent[]
799
	 */
800
	private function getRecurrenceExceptionFromListOfVEvents(array $vevents):array {
801
		return array_values(array_filter($vevents, function (VEvent $vevent) {
802
			return $vevent->{'RECURRENCE-ID'} !== null;
803
		}));
804
	}
805
806
	/**
807
	 * @param array $vevents
808
	 * @return VEvent|null
809
	 */
810
	private function getMasterItemFromListOfVEvents(array $vevents):?VEvent {
811
		$elements = array_values(array_filter($vevents, function (VEvent $vevent) {
812
			return $vevent->{'RECURRENCE-ID'} === null;
813
		}));
814
815
		if (count($elements) === 0) {
816
			return null;
817
		}
818
		if (count($elements) > 1) {
819
			throw new \TypeError('Multiple master objects');
820
		}
821
822
		return $elements[0];
823
	}
824
825
	/**
826
	 * @param VAlarm $valarm
827
	 * @return bool
828
	 */
829
	private function isAlarmRelative(VAlarm $valarm):bool {
830
		$trigger = $valarm->TRIGGER;
831
		return $trigger instanceof VObject\Property\ICalendar\Duration;
832
	}
833
834
	/**
835
	 * @param VEvent $vevent
836
	 * @return int
837
	 */
838
	private function getEffectiveRecurrenceIdOfVEvent(VEvent $vevent):int {
839
		if (isset($vevent->{'RECURRENCE-ID'})) {
840
			return $vevent->{'RECURRENCE-ID'}->getDateTime()->getTimestamp();
841
		}
842
843
		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

843
		return $vevent->DTSTART->/** @scrutinizer ignore-call */ getDateTime()->getTimestamp();
Loading history...
844
	}
845
846
	/**
847
	 * @param VEvent $vevent
848
	 * @return bool
849
	 */
850
	private function isRecurring(VEvent $vevent):bool {
851
		return isset($vevent->RRULE) || isset($vevent->RDATE);
852
	}
853
854
	/**
855
	 * @param int $calendarid
856
	 *
857
	 * @return DateTimeZone
858
	 */
859
	private function getCalendarTimeZone(int $calendarid): DateTimeZone {
860
		$calendarInfo = $this->caldavBackend->getCalendarById($calendarid);
861
		$tzProp = '{urn:ietf:params:xml:ns:caldav}calendar-timezone';
862
		if (!isset($calendarInfo[$tzProp])) {
863
			// Defaulting to UTC
864
			return new DateTimeZone('UTC');
865
		}
866
		// This property contains a VCALENDAR with a single VTIMEZONE
867
		/** @var string $timezoneProp */
868
		$timezoneProp = $calendarInfo[$tzProp];
869
		/** @var VObject\Component\VCalendar $vtimezoneObj */
870
		$vtimezoneObj = VObject\Reader::read($timezoneProp);
871
		/** @var VObject\Component\VTimeZone $vtimezone */
872
		$vtimezone = $vtimezoneObj->VTIMEZONE;
873
		return $vtimezone->getTimeZone();
874
	}
875
}
876