Plugin   F
last analyzed

Complexity

Total Complexity 77

Size/Duplication

Total Lines 573
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 258
dl 0
loc 573
rs 2.24
c 0
b 0
f 0
wmc 77

18 Methods

Rating   Name   Duplication   Size   Complexity  
A getDTEndFromVEvent() 0 26 4
A initialize() 0 5 1
A getAttendeeRSVP() 0 9 4
A dispatchSchedulingResponses() 0 7 3
C isAvailableAtTime() 0 128 10
A isCalendarDeleted() 0 3 2
C scheduleLocalDelivery() 0 109 15
A getCurrentAttendee() 0 11 3
A createCalendar() 0 3 1
A __construct() 0 3 1
A getCalendar() 0 2 1
A getCalendarUserTypeForPrincipal() 0 13 2
D propFindDefaultCalendarUrl() 0 80 19
A calendarObjectChange() 0 7 2
A propFind() 0 17 4
A stripOffMailTo() 0 6 2
A getAddressesForPrincipal() 0 8 2
A setPathOfCalendarObjectChange() 0 2 1

How to fix   Complexity   

Complex Class

Complex classes like Plugin often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Plugin, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, Roeland Jago Douma <[email protected]>
4
 * @copyright Copyright (c) 2016, Joas Schilling <[email protected]>
5
 *
6
 * @author Christoph Wurst <[email protected]>
7
 * @author Daniel Kesselberg <[email protected]>
8
 * @author Georg Ehrke <[email protected]>
9
 * @author Joas Schilling <[email protected]>
10
 * @author Roeland Jago Douma <[email protected]>
11
 * @author Thomas Citharel <[email protected]>
12
 * @author Richard Steinmetz <[email protected]>
13
 *
14
 * @license GNU AGPL version 3 or any later version
15
 *
16
 * This program is free software: you can redistribute it and/or modify
17
 * it under the terms of the GNU Affero General Public License as
18
 * published by the Free Software Foundation, either version 3 of the
19
 * License, or (at your option) any later version.
20
 *
21
 * This program is distributed in the hope that it will be useful,
22
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
23
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24
 * GNU Affero General Public License for more details.
25
 *
26
 * You should have received a copy of the GNU Affero General Public License
27
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
28
 *
29
 */
30
namespace OCA\DAV\CalDAV\Schedule;
31
32
use DateTimeZone;
33
use OCA\DAV\CalDAV\CalDavBackend;
34
use OCA\DAV\CalDAV\Calendar;
35
use OCA\DAV\CalDAV\CalendarHome;
36
use OCP\IConfig;
37
use Psr\Log\LoggerInterface;
38
use Sabre\CalDAV\ICalendar;
39
use Sabre\DAV\INode;
40
use Sabre\DAV\IProperties;
41
use Sabre\DAV\PropFind;
42
use Sabre\DAV\Server;
43
use Sabre\DAV\Xml\Property\LocalHref;
44
use Sabre\DAVACL\IPrincipal;
45
use Sabre\HTTP\RequestInterface;
46
use Sabre\HTTP\ResponseInterface;
47
use Sabre\VObject\Component;
48
use Sabre\VObject\Component\VCalendar;
49
use Sabre\VObject\Component\VEvent;
50
use Sabre\VObject\DateTimeParser;
51
use Sabre\VObject\FreeBusyGenerator;
52
use Sabre\VObject\ITip;
53
use Sabre\VObject\Parameter;
54
use Sabre\VObject\Property;
55
use Sabre\VObject\Reader;
56
use function \Sabre\Uri\split;
0 ignored issues
show
introduced by
The function \Sabre\Uri\split was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
57
58
class Plugin extends \Sabre\CalDAV\Schedule\Plugin {
59
60
	/**
61
	 * @var IConfig
62
	 */
63
	private $config;
64
65
	/** @var ITip\Message[] */
66
	private $schedulingResponses = [];
67
68
	/** @var string|null */
69
	private $pathOfCalendarObjectChange = null;
70
71
	public const CALENDAR_USER_TYPE = '{' . self::NS_CALDAV . '}calendar-user-type';
72
	public const SCHEDULE_DEFAULT_CALENDAR_URL = '{' . Plugin::NS_CALDAV . '}schedule-default-calendar-URL';
73
	private LoggerInterface $logger;
74
75
	/**
76
	 * @param IConfig $config
77
	 */
78
	public function __construct(IConfig $config, LoggerInterface $logger) {
79
		$this->config = $config;
80
		$this->logger = $logger;
81
	}
82
83
	/**
84
	 * Initializes the plugin
85
	 *
86
	 * @param Server $server
87
	 * @return void
88
	 */
89
	public function initialize(Server $server) {
90
		parent::initialize($server);
91
		$server->on('propFind', [$this, 'propFindDefaultCalendarUrl'], 90);
92
		$server->on('afterWriteContent', [$this, 'dispatchSchedulingResponses']);
93
		$server->on('afterCreateFile', [$this, 'dispatchSchedulingResponses']);
94
	}
95
96
	/**
97
	 * Allow manual setting of the object change URL
98
	 * to support public write
99
	 *
100
	 * @param string $path
101
	 */
102
	public function setPathOfCalendarObjectChange(string $path): void {
103
		$this->pathOfCalendarObjectChange = $path;
104
	}
105
106
	/**
107
	 * This method handler is invoked during fetching of properties.
108
	 *
109
	 * We use this event to add calendar-auto-schedule-specific properties.
110
	 *
111
	 * @param PropFind $propFind
112
	 * @param INode $node
113
	 * @return void
114
	 */
115
	public function propFind(PropFind $propFind, INode $node) {
116
		if ($node instanceof IPrincipal) {
117
			// overwrite Sabre/Dav's implementation
118
			$propFind->handle(self::CALENDAR_USER_TYPE, function () use ($node) {
119
				if ($node instanceof IProperties) {
120
					$props = $node->getProperties([self::CALENDAR_USER_TYPE]);
121
122
					if (isset($props[self::CALENDAR_USER_TYPE])) {
123
						return $props[self::CALENDAR_USER_TYPE];
124
					}
125
				}
126
127
				return 'INDIVIDUAL';
128
			});
129
		}
130
131
		parent::propFind($propFind, $node);
132
	}
133
134
	/**
135
	 * Returns a list of addresses that are associated with a principal.
136
	 *
137
	 * @param string $principal
138
	 * @return array
139
	 */
140
	protected function getAddressesForPrincipal($principal) {
141
		$result = parent::getAddressesForPrincipal($principal);
142
143
		if ($result === null) {
0 ignored issues
show
introduced by
The condition $result === null is always false.
Loading history...
144
			$result = [];
145
		}
146
147
		return $result;
148
	}
149
150
	/**
151
	 * @param RequestInterface $request
152
	 * @param ResponseInterface $response
153
	 * @param VCalendar $vCal
154
	 * @param mixed $calendarPath
155
	 * @param mixed $modified
156
	 * @param mixed $isNew
157
	 */
158
	public function calendarObjectChange(RequestInterface $request, ResponseInterface $response, VCalendar $vCal, $calendarPath, &$modified, $isNew) {
159
		// Save the first path we get as a calendar-object-change request
160
		if (!$this->pathOfCalendarObjectChange) {
161
			$this->pathOfCalendarObjectChange = $request->getPath();
162
		}
163
164
		parent::calendarObjectChange($request, $response, $vCal, $calendarPath, $modified, $isNew);
165
	}
166
167
	/**
168
	 * @inheritDoc
169
	 */
170
	public function scheduleLocalDelivery(ITip\Message $iTipMessage):void {
171
		/** @var VEvent|null $vevent */
172
		$vevent = $iTipMessage->message->VEVENT ?? null;
173
174
		// Strip VALARMs from incoming VEVENT
175
		if ($vevent && isset($vevent->VALARM)) {
176
			$vevent->remove('VALARM');
177
		}
178
179
		parent::scheduleLocalDelivery($iTipMessage);
180
		// We only care when the message was successfully delivered locally
181
		// Log all possible codes returned from the parent method that mean something went wrong
182
		// 3.7, 3.8, 5.0, 5.2
183
		if ($iTipMessage->scheduleStatus !== '1.2;Message delivered locally') {
184
			$this->logger->debug('Message not delivered locally with status: ' . $iTipMessage->scheduleStatus);
185
			return;
186
		}
187
		// We only care about request. reply and cancel are properly handled
188
		// by parent::scheduleLocalDelivery already
189
		if (strcasecmp($iTipMessage->method, 'REQUEST') !== 0) {
190
			return;
191
		}
192
193
		// If parent::scheduleLocalDelivery set scheduleStatus to 1.2,
194
		// it means that it was successfully delivered locally.
195
		// Meaning that the ACL plugin is loaded and that a principal
196
		// exists for the given recipient id, no need to double check
197
		/** @var \Sabre\DAVACL\Plugin $aclPlugin */
198
		$aclPlugin = $this->server->getPlugin('acl');
199
		$principalUri = $aclPlugin->getPrincipalByUri($iTipMessage->recipient);
200
		$calendarUserType = $this->getCalendarUserTypeForPrincipal($principalUri);
201
		if (strcasecmp($calendarUserType, 'ROOM') !== 0 && strcasecmp($calendarUserType, 'RESOURCE') !== 0) {
202
			$this->logger->debug('Calendar user type is room or resource, not processing further');
203
			return;
204
		}
205
206
		$attendee = $this->getCurrentAttendee($iTipMessage);
207
		if (!$attendee) {
208
			$this->logger->debug('No attendee set for scheduling message');
209
			return;
210
		}
211
212
		// We only respond when a response was actually requested
213
		$rsvp = $this->getAttendeeRSVP($attendee);
214
		if (!$rsvp) {
215
			$this->logger->debug('No RSVP requested for attendee ' . $attendee->getValue());
216
			return;
217
		}
218
219
		if (!$vevent) {
220
			$this->logger->debug('No VEVENT set to process on scheduling message');
221
			return;
222
		}
223
224
		// We don't support autoresponses for recurrencing events for now
225
		if (isset($vevent->RRULE) || isset($vevent->RDATE)) {
226
			$this->logger->debug('VEVENT is a recurring event, autoresponding not supported');
227
			return;
228
		}
229
230
		$dtstart = $vevent->DTSTART;
231
		$dtend = $this->getDTEndFromVEvent($vevent);
232
		$uid = $vevent->UID->getValue();
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

232
		/** @scrutinizer ignore-call */ 
233
  $uid = $vevent->UID->getValue();

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...
233
		$sequence = isset($vevent->SEQUENCE) ? $vevent->SEQUENCE->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

233
		$sequence = isset($vevent->SEQUENCE) ? $vevent->SEQUENCE->/** @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...
234
		$recurrenceId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->serialize() : '';
235
236
		$message = <<<EOF
237
BEGIN:VCALENDAR
238
PRODID:-//Nextcloud/Nextcloud CalDAV Server//EN
239
METHOD:REPLY
240
VERSION:2.0
241
BEGIN:VEVENT
242
ATTENDEE;PARTSTAT=%s:%s
243
ORGANIZER:%s
244
UID:%s
245
SEQUENCE:%s
246
REQUEST-STATUS:2.0;Success
247
%sEND:VEVENT
248
END:VCALENDAR
249
EOF;
250
251
		if ($this->isAvailableAtTime($attendee->getValue(), $dtstart->getDateTime(), $dtend->getDateTime(), $uid)) {
0 ignored issues
show
Bug introduced by
The method getDateTime() 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

251
		if ($this->isAvailableAtTime($attendee->getValue(), $dtstart->/** @scrutinizer ignore-call */ getDateTime(), $dtend->getDateTime(), $uid)) {

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...
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

251
		if ($this->isAvailableAtTime($attendee->getValue(), $dtstart->/** @scrutinizer ignore-call */ getDateTime(), $dtend->getDateTime(), $uid)) {
Loading history...
252
			$partStat = 'ACCEPTED';
253
		} else {
254
			$partStat = 'DECLINED';
255
		}
256
257
		$vObject = Reader::read(vsprintf($message, [
258
			$partStat,
259
			$iTipMessage->recipient,
260
			$iTipMessage->sender,
261
			$uid,
262
			$sequence,
263
			$recurrenceId
264
		]));
265
266
		$responseITipMessage = new ITip\Message();
267
		$responseITipMessage->uid = $uid;
268
		$responseITipMessage->component = 'VEVENT';
269
		$responseITipMessage->method = 'REPLY';
270
		$responseITipMessage->sequence = $sequence;
0 ignored issues
show
Documentation Bug introduced by
It seems like $sequence can also be of type string. However, the property $sequence is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
271
		$responseITipMessage->sender = $iTipMessage->recipient;
272
		$responseITipMessage->recipient = $iTipMessage->sender;
273
		$responseITipMessage->message = $vObject;
274
275
		// We can't dispatch them now already, because the organizers calendar-object
276
		// was not yet created. Hence Sabre/DAV won't find a calendar-object, when we
277
		// send our reply.
278
		$this->schedulingResponses[] = $responseITipMessage;
279
	}
280
281
	/**
282
	 * @param string $uri
283
	 */
284
	public function dispatchSchedulingResponses(string $uri):void {
285
		if ($uri !== $this->pathOfCalendarObjectChange) {
286
			return;
287
		}
288
289
		foreach ($this->schedulingResponses as $schedulingResponse) {
290
			$this->scheduleLocalDelivery($schedulingResponse);
291
		}
292
	}
293
294
	/**
295
	 * Always use the personal calendar as target for scheduled events
296
	 *
297
	 * @param PropFind $propFind
298
	 * @param INode $node
299
	 * @return void
300
	 */
301
	public function propFindDefaultCalendarUrl(PropFind $propFind, INode $node) {
302
		if ($node instanceof IPrincipal) {
303
			$propFind->handle(self::SCHEDULE_DEFAULT_CALENDAR_URL, function () use ($node) {
304
				/** @var \OCA\DAV\CalDAV\Plugin $caldavPlugin */
305
				$caldavPlugin = $this->server->getPlugin('caldav');
306
				$principalUrl = $node->getPrincipalUrl();
307
308
				$calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl);
309
				if (!$calendarHomePath) {
310
					return null;
311
				}
312
313
				$isResourceOrRoom = strpos($principalUrl, 'principals/calendar-resources') === 0 ||
314
					strpos($principalUrl, 'principals/calendar-rooms') === 0;
315
316
				if (strpos($principalUrl, 'principals/users') === 0) {
317
					[, $userId] = split($principalUrl);
0 ignored issues
show
Bug introduced by
The call to split() has too few arguments starting with string. ( Ignorable by Annotation )

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

317
					[, $userId] = /** @scrutinizer ignore-call */ split($principalUrl);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
318
					$uri = $this->config->getUserValue($userId, 'dav', 'defaultCalendar', CalDavBackend::PERSONAL_CALENDAR_URI);
319
					$displayName = CalDavBackend::PERSONAL_CALENDAR_NAME;
320
				} elseif ($isResourceOrRoom) {
321
					$uri = CalDavBackend::RESOURCE_BOOKING_CALENDAR_URI;
322
					$displayName = CalDavBackend::RESOURCE_BOOKING_CALENDAR_NAME;
323
				} else {
324
					// How did we end up here?
325
					// TODO - throw exception or just ignore?
326
					return null;
327
				}
328
329
				/** @var CalendarHome $calendarHome */
330
				$calendarHome = $this->server->tree->getNodeForPath($calendarHomePath);
331
				$currentCalendarDeleted = false;
332
				if (!$calendarHome->childExists($uri) || $currentCalendarDeleted = $this->isCalendarDeleted($calendarHome, $uri)) {
333
					// If the default calendar doesn't exist
334
					if ($isResourceOrRoom) {
335
						// Resources or rooms can't be in the trashbin, so we're fine
336
						$this->createCalendar($calendarHome, $principalUrl, $uri, $displayName);
337
					} else {
338
						// And we're not handling scheduling on resource/room booking
339
						$userCalendars = [];
340
						/**
341
						 * If the default calendar of the user isn't set and the
342
						 * fallback doesn't match any of the user's calendar
343
						 * try to find the first "personal" calendar we can write to
344
						 * instead of creating a new one.
345
						 * A appropriate personal calendar to receive invites:
346
						 * - isn't a calendar subscription
347
						 * - user can write to it (no virtual/3rd-party calendars)
348
						 * - calendar isn't a share
349
						 */
350
						foreach ($calendarHome->getChildren() as $node) {
351
							if ($node instanceof Calendar && !$node->isSubscription() && $node->canWrite() && !$node->isShared() && !$node->isDeleted()) {
352
								$userCalendars[] = $node;
353
							}
354
						}
355
356
						if (count($userCalendars) > 0) {
357
							// Calendar backend returns calendar by calendarorder property
358
							$uri = $userCalendars[0]->getName();
359
						} else {
360
							// Otherwise if we have really nothing, create a new calendar
361
							if ($currentCalendarDeleted) {
362
								// If the calendar exists but is deleted, we need to purge it first
363
								// This may cause some issues in a non synchronous database setup
364
								$calendar = $this->getCalendar($calendarHome, $uri);
365
								if ($calendar instanceof Calendar) {
366
									$calendar->disableTrashbin();
367
									$calendar->delete();
368
								}
369
							}
370
							$this->createCalendar($calendarHome, $principalUrl, $uri, $displayName);
371
						}
372
					}
373
				}
374
375
				$result = $this->server->getPropertiesForPath($calendarHomePath . '/' . $uri, [], 1);
376
				if (empty($result)) {
377
					return null;
378
				}
379
380
				return new LocalHref($result[0]['href']);
381
			});
382
		}
383
	}
384
385
	/**
386
	 * Returns a list of addresses that are associated with a principal.
387
	 *
388
	 * @param string $principal
389
	 * @return string|null
390
	 */
391
	protected function getCalendarUserTypeForPrincipal($principal):?string {
392
		$calendarUserType = '{' . self::NS_CALDAV . '}calendar-user-type';
393
		$properties = $this->server->getProperties(
394
			$principal,
395
			[$calendarUserType]
396
		);
397
398
		// If we can't find this information, we'll stop processing
399
		if (!isset($properties[$calendarUserType])) {
400
			return null;
401
		}
402
403
		return $properties[$calendarUserType];
404
	}
405
406
	/**
407
	 * @param ITip\Message $iTipMessage
408
	 * @return null|Property
409
	 */
410
	private function getCurrentAttendee(ITip\Message $iTipMessage):?Property {
411
		/** @var VEvent $vevent */
412
		$vevent = $iTipMessage->message->VEVENT;
413
		$attendees = $vevent->select('ATTENDEE');
414
		foreach ($attendees as $attendee) {
415
			/** @var Property $attendee */
416
			if (strcasecmp($attendee->getValue(), $iTipMessage->recipient) === 0) {
417
				return $attendee;
418
			}
419
		}
420
		return null;
421
	}
422
423
	/**
424
	 * @param Property|null $attendee
425
	 * @return bool
426
	 */
427
	private function getAttendeeRSVP(Property $attendee = null):bool {
428
		if ($attendee !== null) {
429
			$rsvp = $attendee->offsetGet('RSVP');
430
			if (($rsvp instanceof Parameter) && (strcasecmp($rsvp->getValue(), 'TRUE') === 0)) {
431
				return true;
432
			}
433
		}
434
		// RFC 5545 3.2.17: default RSVP is false
435
		return false;
436
	}
437
438
	/**
439
	 * @param VEvent $vevent
440
	 * @return Property\ICalendar\DateTime
441
	 */
442
	private function getDTEndFromVEvent(VEvent $vevent):Property\ICalendar\DateTime {
443
		if (isset($vevent->DTEND)) {
444
			return $vevent->DTEND;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $vevent->DTEND returns the type null which is incompatible with the type-hinted return Sabre\VObject\Property\ICalendar\DateTime.
Loading history...
445
		}
446
447
		if (isset($vevent->DURATION)) {
448
			$isFloating = $vevent->DTSTART->isFloating();
0 ignored issues
show
Bug introduced by
The method isFloating() 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

448
			/** @scrutinizer ignore-call */ 
449
   $isFloating = $vevent->DTSTART->isFloating();

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...
Bug introduced by
The method isFloating() 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. ( Ignorable by Annotation )

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

448
			/** @scrutinizer ignore-call */ 
449
   $isFloating = $vevent->DTSTART->isFloating();
Loading history...
449
			/** @var Property\ICalendar\DateTime $end */
450
			$end = clone $vevent->DTSTART;
451
			$endDateTime = $end->getDateTime();
452
			$endDateTime = $endDateTime->add(DateTimeParser::parse($vevent->DURATION->getValue()));
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

452
			$endDateTime = $endDateTime->add(DateTimeParser::parse($vevent->DURATION->/** @scrutinizer ignore-call */ getValue()));

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...
453
			$end->setDateTime($endDateTime, $isFloating);
454
			return $end;
455
		}
456
457
		if (!$vevent->DTSTART->hasTime()) {
0 ignored issues
show
Bug introduced by
The method hasTime() 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. ( Ignorable by Annotation )

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

457
		if (!$vevent->DTSTART->/** @scrutinizer ignore-call */ hasTime()) {
Loading history...
458
			$isFloating = $vevent->DTSTART->isFloating();
459
			/** @var Property\ICalendar\DateTime $end */
460
			$end = clone $vevent->DTSTART;
461
			$endDateTime = $end->getDateTime();
462
			$endDateTime = $endDateTime->modify('+1 day');
463
			$end->setDateTime($endDateTime, $isFloating);
464
			return $end;
465
		}
466
467
		return clone $vevent->DTSTART;
0 ignored issues
show
Bug Best Practice introduced by
The expression return clone $vevent->DTSTART returns the type Sabre\VObject\Property which includes types incompatible with the type-hinted return Sabre\VObject\Property\ICalendar\DateTime.
Loading history...
468
	}
469
470
	/**
471
	 * @param string $email
472
	 * @param \DateTimeInterface $start
473
	 * @param \DateTimeInterface $end
474
	 * @param string $ignoreUID
475
	 * @return bool
476
	 */
477
	private function isAvailableAtTime(string $email, \DateTimeInterface $start, \DateTimeInterface $end, string $ignoreUID):bool {
478
		// This method is heavily inspired by Sabre\CalDAV\Schedule\Plugin::scheduleLocalDelivery
479
		// and Sabre\CalDAV\Schedule\Plugin::getFreeBusyForEmail
480
481
		$aclPlugin = $this->server->getPlugin('acl');
482
		$this->server->removeListener('propFind', [$aclPlugin, 'propFind']);
483
484
		$result = $aclPlugin->principalSearch(
0 ignored issues
show
Bug introduced by
The method principalSearch() does not exist on Sabre\DAV\ServerPlugin. It seems like you code against a sub-type of Sabre\DAV\ServerPlugin such as Sabre\DAVACL\Plugin. ( Ignorable by Annotation )

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

484
		/** @scrutinizer ignore-call */ 
485
  $result = $aclPlugin->principalSearch(
Loading history...
485
			['{http://sabredav.org/ns}email-address' => $this->stripOffMailTo($email)],
486
			[
487
				'{DAV:}principal-URL',
488
				'{' . self::NS_CALDAV . '}calendar-home-set',
489
				'{' . self::NS_CALDAV . '}schedule-inbox-URL',
490
				'{http://sabredav.org/ns}email-address',
491
492
			]
493
		);
494
		$this->server->on('propFind', [$aclPlugin, 'propFind'], 20);
495
496
497
		// Grabbing the calendar list
498
		$objects = [];
499
		$calendarTimeZone = new DateTimeZone('UTC');
500
501
		$homePath = $result[0][200]['{' . self::NS_CALDAV . '}calendar-home-set']->getHref();
502
		foreach ($this->server->tree->getNodeForPath($homePath)->getChildren() as $node) {
0 ignored issues
show
Bug introduced by
The method getChildren() does not exist on Sabre\DAV\INode. It seems like you code against a sub-type of Sabre\DAV\INode such as Sabre\DAV\ICollection or Sabre\DAV\Collection or Sabre\CalDAV\Principal\User or OCA\DAV\CardDAV\Integration\ExternalAddressBook or OCA\DAV\CalDAV\Trashbin\TrashbinHome or Sabre\CalDAV\Calendar or OCA\DAV\Comments\EntityCollection or Sabre\CardDAV\AddressBook or OCA\DAV\CalDAV\Integration\ExternalCalendar or Sabre\CalDAV\Subscriptions\ISubscription or Sabre\CalDAV\Principal\User or Sabre\CalDAV\SharedCalendar or OCA\DAV\Connector\Sabre\Directory or Sabre\CardDAV\AddressBookHome or Sabre\CalDAV\CalendarHome or Sabre\DAVACL\FS\HomeCollection or OCA\DAV\CalDAV\Trashbin\TrashbinHome or Sabre\CalDAV\ICalendar or Sabre\CalDAV\Schedule\IInbox or Sabre\DAVACL\PrincipalCollection or Sabre\CalDAV\Subscriptions\Subscription or OCA\ContactsInteraction\AddressBook or Sabre\CalDAV\Schedule\IOutbox or Sabre\CalDAV\Notifications\Collection or Sabre\DAVACL\FS\Collection or Sabre\CardDAV\AddressBook or Sabre\CalDAV\Principal\User or Sabre\CalDAV\Principal\User or OCA\DAV\CalDAV\Calendar or OCA\DAV\CardDAV\AddressBook or Sabre\DAV\FSExt\Directory or Sabre\DAV\FS\Directory. ( Ignorable by Annotation )

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

502
		foreach ($this->server->tree->getNodeForPath($homePath)->/** @scrutinizer ignore-call */ getChildren() as $node) {
Loading history...
503
			if (!$node instanceof ICalendar) {
504
				continue;
505
			}
506
507
			// Getting the list of object uris within the time-range
508
			$urls = $node->calendarQuery([
509
				'name' => 'VCALENDAR',
510
				'comp-filters' => [
511
					[
512
						'name' => 'VEVENT',
513
						'is-not-defined' => false,
514
						'time-range' => [
515
							'start' => $start,
516
							'end' => $end,
517
						],
518
						'comp-filters' => [],
519
						'prop-filters' => [],
520
					],
521
					[
522
						'name' => 'VEVENT',
523
						'is-not-defined' => false,
524
						'time-range' => null,
525
						'comp-filters' => [],
526
						'prop-filters' => [
527
							[
528
								'name' => 'UID',
529
								'is-not-defined' => false,
530
								'time-range' => null,
531
								'text-match' => [
532
									'value' => $ignoreUID,
533
									'negate-condition' => true,
534
									'collation' => 'i;octet',
535
								],
536
								'param-filters' => [],
537
							],
538
						]
539
					],
540
				],
541
				'prop-filters' => [],
542
				'is-not-defined' => false,
543
				'time-range' => null,
544
			]);
545
546
			foreach ($urls as $url) {
547
				$objects[] = $node->getChild($url)->get();
0 ignored issues
show
Bug introduced by
The method get() does not exist on Sabre\DAV\INode. It seems like you code against a sub-type of Sabre\DAV\INode such as Sabre\DAV\IFile or Sabre\DAV\File or OCA\DAV\Connector\Sabre\File or OCA\ContactsInteraction\Card or Sabre\DAVACL\FS\File or Sabre\CardDAV\Card or Sabre\CalDAV\CalendarObject or OCA\DAV\CalDAV\AppCalendar\CalendarObject or Sabre\CalDAV\Notifications\Node or OCA\DAV\CalDAV\Trashbin\DeletedCalendarObject or Sabre\DAV\FSExt\File or Sabre\DAV\FS\File. ( Ignorable by Annotation )

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

547
				$objects[] = $node->getChild($url)->/** @scrutinizer ignore-call */ get();
Loading history...
548
			}
549
		}
550
551
		$inboxProps = $this->server->getProperties(
552
			$result[0][200]['{' . self::NS_CALDAV . '}schedule-inbox-URL']->getHref(),
553
			['{' . self::NS_CALDAV . '}calendar-availability']
554
		);
555
556
		$vcalendar = new VCalendar();
557
		$vcalendar->METHOD = 'REPLY';
558
559
		$generator = new FreeBusyGenerator();
560
		$generator->setObjects($objects);
561
		$generator->setTimeRange($start, $end);
562
		$generator->setBaseObject($vcalendar);
563
		$generator->setTimeZone($calendarTimeZone);
564
565
		if (isset($inboxProps['{' . self::NS_CALDAV . '}calendar-availability'])) {
566
			$generator->setVAvailability(
567
				Reader::read(
568
					$inboxProps['{' . self::NS_CALDAV . '}calendar-availability']
569
				)
570
			);
571
		}
572
573
		$result = $generator->getResult();
574
		if (!isset($result->VFREEBUSY)) {
575
			return false;
576
		}
577
578
		/** @var Component $freeBusyComponent */
579
		$freeBusyComponent = $result->VFREEBUSY;
580
		$freeBusyProperties = $freeBusyComponent->select('FREEBUSY');
581
		// If there is no Free-busy property at all, the time-range is empty and available
582
		if (count($freeBusyProperties) === 0) {
583
			return true;
584
		}
585
586
		// If more than one Free-Busy property was returned, it means that an event
587
		// starts or ends inside this time-range, so it's not available and we return false
588
		if (count($freeBusyProperties) > 1) {
589
			return false;
590
		}
591
592
		/** @var Property $freeBusyProperty */
593
		$freeBusyProperty = $freeBusyProperties[0];
594
		if (!$freeBusyProperty->offsetExists('FBTYPE')) {
595
			// If there is no FBTYPE, it means it's busy
596
			return false;
597
		}
598
599
		$fbTypeParameter = $freeBusyProperty->offsetGet('FBTYPE');
600
		if (!($fbTypeParameter instanceof Parameter)) {
601
			return false;
602
		}
603
604
		return (strcasecmp($fbTypeParameter->getValue(), 'FREE') === 0);
605
	}
606
607
	/**
608
	 * @param string $email
609
	 * @return string
610
	 */
611
	private function stripOffMailTo(string $email): string {
612
		if (stripos($email, 'mailto:') === 0) {
613
			return substr($email, 7);
614
		}
615
616
		return $email;
617
	}
618
619
	private function getCalendar(CalendarHome $calendarHome, string $uri): INode {
620
		return $calendarHome->getChild($uri);
621
	}
622
623
	private function isCalendarDeleted(CalendarHome $calendarHome, string $uri): bool {
624
		$calendar = $this->getCalendar($calendarHome, $uri);
625
		return $calendar instanceof Calendar && $calendar->isDeleted();
626
	}
627
628
	private function createCalendar(CalendarHome $calendarHome, string $principalUri, string $uri, string $displayName): void {
629
		$calendarHome->getCalDAVBackend()->createCalendar($principalUri, $uri, [
630
			'{DAV:}displayname' => $displayName,
631
		]);
632
	}
633
}
634