ReminderService::onCalendarObjectCreate()   F
last analyzed

Complexity

Conditions 27
Paths 415

Size

Total Lines 138
Code Lines 75

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 27
eloc 75
c 0
b 0
f 0
nc 415
nop 1
dl 0
loc 138
rs 0.8125

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
			try {
151
				$vevent = $this->getVEventByRecurrenceId($vcalendar, $reminder['recurrence_id'], $reminder['is_recurrence_exception']);
152
			} catch (MaxInstancesExceededException $e) {
153
				$this->logger->debug('Recurrence with too many instances detected, skipping VEVENT', ['exception' => $e]);
154
				$this->backend->removeReminder($reminder['id']);
155
				continue;
156
			}
157
158
			if (!$vevent) {
159
				$this->logger->debug('Reminder {id} does not belong to a valid event', [
160
					'id' => $reminder['id'],
161
				]);
162
				$this->backend->removeReminder($reminder['id']);
163
				continue;
164
			}
165
166
			if ($this->wasEventCancelled($vevent)) {
167
				$this->logger->debug('Reminder {id} belongs to a cancelled event', [
168
					'id' => $reminder['id'],
169
				]);
170
				$this->deleteOrProcessNext($reminder, $vevent);
171
				continue;
172
			}
173
174
			if (!$this->notificationProviderManager->hasProvider($reminder['type'])) {
175
				$this->logger->debug('Reminder {id} does not belong to a valid notification provider', [
176
					'id' => $reminder['id'],
177
				]);
178
				$this->deleteOrProcessNext($reminder, $vevent);
179
				continue;
180
			}
181
182
			if ($this->config->getAppValue('dav', 'sendEventRemindersToSharedUsers', 'yes') === 'no') {
183
				$users = $this->getAllUsersWithWriteAccessToCalendar($reminder['calendar_id']);
184
			} else {
185
				$users = [];
186
			}
187
188
			$user = $this->getUserFromPrincipalURI($reminder['principaluri']);
189
			if ($user) {
190
				$users[] = $user;
191
			}
192
193
			$userPrincipalEmailAddresses = [];
194
			$userPrincipal = $this->principalConnector->getPrincipalByPath($reminder['principaluri']);
195
			if ($userPrincipal) {
196
				$userPrincipalEmailAddresses = $this->principalConnector->getEmailAddressesOfPrincipal($userPrincipal);
197
			}
198
199
			$this->logger->debug('Reminder {id} will be sent to {numUsers} users', [
200
				'id' => $reminder['id'],
201
				'numUsers' => count($users),
202
			]);
203
			$notificationProvider = $this->notificationProviderManager->getProvider($reminder['type']);
204
			$notificationProvider->send($vevent, $reminder['displayname'], $userPrincipalEmailAddresses, $users);
205
206
			$this->deleteOrProcessNext($reminder, $vevent);
207
		}
208
	}
209
210
	/**
211
	 * @param array $objectData
212
	 * @throws VObject\InvalidDataException
213
	 */
214
	public function onCalendarObjectCreate(array $objectData):void {
215
		// We only support VEvents for now
216
		if (strcasecmp($objectData['component'], 'vevent') !== 0) {
217
			return;
218
		}
219
220
		$calendarData = is_resource($objectData['calendardata'])
221
			? stream_get_contents($objectData['calendardata'])
222
			: $objectData['calendardata'];
223
224
		if (!$calendarData) {
225
			return;
226
		}
227
228
		$vcalendar = $this->parseCalendarData($calendarData);
229
		if (!$vcalendar) {
0 ignored issues
show
introduced by
$vcalendar is of type Sabre\VObject\Component\VCalendar, thus it always evaluated to true.
Loading history...
230
			return;
231
		}
232
		$calendarTimeZone = $this->getCalendarTimeZone((int) $objectData['calendarid']);
233
234
		$vevents = $this->getAllVEventsFromVCalendar($vcalendar);
235
		if (count($vevents) === 0) {
236
			return;
237
		}
238
239
		$uid = (string) $vevents[0]->UID;
240
		$recurrenceExceptions = $this->getRecurrenceExceptionFromListOfVEvents($vevents);
241
		$masterItem = $this->getMasterItemFromListOfVEvents($vevents);
242
		$now = $this->timeFactory->getDateTime();
243
		$isRecurring = $masterItem ? $this->isRecurring($masterItem) : false;
244
245
		foreach ($recurrenceExceptions as $recurrenceException) {
246
			$eventHash = $this->getEventHash($recurrenceException);
247
248
			if (!isset($recurrenceException->VALARM)) {
249
				continue;
250
			}
251
252
			foreach ($recurrenceException->VALARM as $valarm) {
253
				/** @var VAlarm $valarm */
254
				$alarmHash = $this->getAlarmHash($valarm);
255
				$triggerTime = $valarm->getEffectiveTriggerTime();
256
				$diff = $now->diff($triggerTime);
257
				if ($diff->invert === 1) {
258
					continue;
259
				}
260
261
				$alarms = $this->getRemindersForVAlarm($valarm, $objectData, $calendarTimeZone,
262
					$eventHash, $alarmHash, true, true);
263
				$this->writeRemindersToDatabase($alarms);
264
			}
265
		}
266
267
		if ($masterItem) {
268
			$processedAlarms = [];
269
			$masterAlarms = [];
270
			$masterHash = $this->getEventHash($masterItem);
271
272
			if (!isset($masterItem->VALARM)) {
273
				return;
274
			}
275
276
			foreach ($masterItem->VALARM as $valarm) {
277
				$masterAlarms[] = $this->getAlarmHash($valarm);
278
			}
279
280
			try {
281
				$iterator = new EventIterator($vevents, $uid);
282
			} catch (NoInstancesException $e) {
283
				// This event is recurring, but it doesn't have a single
284
				// instance. We are skipping this event from the output
285
				// entirely.
286
				return;
287
			} catch (MaxInstancesExceededException $e) {
288
				// The event has more than 3500 recurring-instances
289
				// so we can ignore it
290
				return;
291
			}
292
293
			while ($iterator->valid() && count($processedAlarms) < count($masterAlarms)) {
294
				$event = $iterator->getEventObject();
295
296
				// Recurrence-exceptions are handled separately, so just ignore them here
297
				if (\in_array($event, $recurrenceExceptions, true)) {
298
					$iterator->next();
299
					continue;
300
				}
301
302
				foreach ($event->VALARM as $valarm) {
303
					/** @var VAlarm $valarm */
304
					$alarmHash = $this->getAlarmHash($valarm);
305
					if (\in_array($alarmHash, $processedAlarms, true)) {
306
						continue;
307
					}
308
309
					if (!\in_array((string) $valarm->ACTION, self::REMINDER_TYPES, true)) {
310
						// Action allows x-name, we don't insert reminders
311
						// into the database if they are not standard
312
						$processedAlarms[] = $alarmHash;
313
						continue;
314
					}
315
316
					try {
317
						$triggerTime = $valarm->getEffectiveTriggerTime();
318
						/**
319
						 * @psalm-suppress DocblockTypeContradiction
320
						 *   https://github.com/vimeo/psalm/issues/9244
321
						 */
322
						if ($triggerTime->getTimezone() === false || $triggerTime->getTimezone()->getName() === 'UTC') {
323
							$triggerTime = new DateTimeImmutable(
324
								$triggerTime->format('Y-m-d H:i:s'),
325
								$calendarTimeZone
326
							);
327
						}
328
					} catch (InvalidDataException $e) {
329
						continue;
330
					}
331
332
					// If effective trigger time is in the past
333
					// just skip and generate for next event
334
					$diff = $now->diff($triggerTime);
335
					if ($diff->invert === 1) {
336
						// If an absolute alarm is in the past,
337
						// just add it to processedAlarms, so
338
						// we don't extend till eternity
339
						if (!$this->isAlarmRelative($valarm)) {
340
							$processedAlarms[] = $alarmHash;
341
						}
342
343
						continue;
344
					}
345
346
					$alarms = $this->getRemindersForVAlarm($valarm, $objectData, $calendarTimeZone, $masterHash, $alarmHash, $isRecurring, false);
347
					$this->writeRemindersToDatabase($alarms);
348
					$processedAlarms[] = $alarmHash;
349
				}
350
351
				$iterator->next();
352
			}
353
		}
354
	}
355
356
	/**
357
	 * @param array $objectData
358
	 * @throws VObject\InvalidDataException
359
	 */
360
	public function onCalendarObjectEdit(array $objectData):void {
361
		// TODO - this can be vastly improved
362
		//  - get cached reminders
363
		//  - ...
364
365
		$this->onCalendarObjectDelete($objectData);
366
		$this->onCalendarObjectCreate($objectData);
367
	}
368
369
	/**
370
	 * @param array $objectData
371
	 * @throws VObject\InvalidDataException
372
	 */
373
	public function onCalendarObjectDelete(array $objectData):void {
374
		// We only support VEvents for now
375
		if (strcasecmp($objectData['component'], 'vevent') !== 0) {
376
			return;
377
		}
378
379
		$this->backend->cleanRemindersForEvent((int) $objectData['id']);
380
	}
381
382
	/**
383
	 * @param VAlarm $valarm
384
	 * @param array $objectData
385
	 * @param DateTimeZone $calendarTimeZone
386
	 * @param string|null $eventHash
387
	 * @param string|null $alarmHash
388
	 * @param bool $isRecurring
389
	 * @param bool $isRecurrenceException
390
	 * @return array
391
	 */
392
	private function getRemindersForVAlarm(VAlarm $valarm,
393
										   array $objectData,
394
										   DateTimeZone $calendarTimeZone,
395
										   string $eventHash = null,
396
										   string $alarmHash = null,
397
										   bool $isRecurring = false,
398
										   bool $isRecurrenceException = false):array {
399
		if ($eventHash === null) {
400
			$eventHash = $this->getEventHash($valarm->parent);
401
		}
402
		if ($alarmHash === null) {
403
			$alarmHash = $this->getAlarmHash($valarm);
404
		}
405
406
		$recurrenceId = $this->getEffectiveRecurrenceIdOfVEvent($valarm->parent);
407
		$isRelative = $this->isAlarmRelative($valarm);
408
		/** @var DateTimeImmutable $notificationDate */
409
		$notificationDate = $valarm->getEffectiveTriggerTime();
410
		/**
411
		 * @psalm-suppress DocblockTypeContradiction
412
		 *   https://github.com/vimeo/psalm/issues/9244
413
		 */
414
		if ($notificationDate->getTimezone() === false || $notificationDate->getTimezone()->getName() === 'UTC') {
415
			$notificationDate = new DateTimeImmutable(
416
				$notificationDate->format('Y-m-d H:i:s'),
417
				$calendarTimeZone
418
			);
419
		}
420
		$clonedNotificationDate = new \DateTime('now', $notificationDate->getTimezone());
421
		$clonedNotificationDate->setTimestamp($notificationDate->getTimestamp());
422
423
		$alarms = [];
424
425
		$alarms[] = [
426
			'calendar_id' => $objectData['calendarid'],
427
			'object_id' => $objectData['id'],
428
			'uid' => (string) $valarm->parent->UID,
429
			'is_recurring' => $isRecurring,
430
			'recurrence_id' => $recurrenceId,
431
			'is_recurrence_exception' => $isRecurrenceException,
432
			'event_hash' => $eventHash,
433
			'alarm_hash' => $alarmHash,
434
			'type' => (string) $valarm->ACTION,
435
			'is_relative' => $isRelative,
436
			'notification_date' => $notificationDate->getTimestamp(),
437
			'is_repeat_based' => false,
438
		];
439
440
		$repeat = isset($valarm->REPEAT) ? (int) $valarm->REPEAT->getValue() : 0;
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

440
		$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...
441
		for ($i = 0; $i < $repeat; $i++) {
442
			if ($valarm->DURATION === null) {
443
				continue;
444
			}
445
446
			$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

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

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

655
			(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...
656
			(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

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

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