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

ReminderService::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 18
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 9
nc 1
nop 9
dl 0
loc 18
rs 9.9666
c 0
b 0
f 0

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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