Passed
Push — master ( 2f64d1...e08fa7 )
by Blizzz
16:00 queued 13s
created
apps/dav/lib/CalDAV/Schedule/Plugin.php 1 patch
Indentation   +559 added lines, -559 removed lines patch added patch discarded remove patch
@@ -57,183 +57,183 @@  discard block
 block discarded – undo
57 57
 
58 58
 class Plugin extends \Sabre\CalDAV\Schedule\Plugin {
59 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) {
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();
233
-		$sequence = isset($vevent->SEQUENCE) ? $vevent->SEQUENCE->getValue() : 0;
234
-		$recurrenceId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->serialize() : '';
235
-
236
-		$message = <<<EOF
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) {
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();
233
+        $sequence = isset($vevent->SEQUENCE) ? $vevent->SEQUENCE->getValue() : 0;
234
+        $recurrenceId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->serialize() : '';
235
+
236
+        $message = <<<EOF
237 237
 BEGIN:VCALENDAR
238 238
 PRODID:-//Nextcloud/Nextcloud CalDAV Server//EN
239 239
 METHOD:REPLY
@@ -248,386 +248,386 @@  discard block
 block discarded – undo
248 248
 END:VCALENDAR
249 249
 EOF;
250 250
 
251
-		if ($this->isAvailableAtTime($attendee->getValue(), $dtstart->getDateTime(), $dtend->getDateTime(), $uid)) {
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;
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);
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;
445
-		}
446
-
447
-		if (isset($vevent->DURATION)) {
448
-			$isFloating = $vevent->DTSTART->isFloating();
449
-			/** @var Property\ICalendar\DateTime $end */
450
-			$end = clone $vevent->DTSTART;
451
-			$endDateTime = $end->getDateTime();
452
-			$endDateTime = $endDateTime->add(DateTimeParser::parse($vevent->DURATION->getValue()));
453
-			$end->setDateTime($endDateTime, $isFloating);
454
-			return $end;
455
-		}
456
-
457
-		if (!$vevent->DTSTART->hasTime()) {
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;
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(
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) {
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();
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
-	}
251
+        if ($this->isAvailableAtTime($attendee->getValue(), $dtstart->getDateTime(), $dtend->getDateTime(), $uid)) {
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;
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);
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;
445
+        }
446
+
447
+        if (isset($vevent->DURATION)) {
448
+            $isFloating = $vevent->DTSTART->isFloating();
449
+            /** @var Property\ICalendar\DateTime $end */
450
+            $end = clone $vevent->DTSTART;
451
+            $endDateTime = $end->getDateTime();
452
+            $endDateTime = $endDateTime->add(DateTimeParser::parse($vevent->DURATION->getValue()));
453
+            $end->setDateTime($endDateTime, $isFloating);
454
+            return $end;
455
+        }
456
+
457
+        if (!$vevent->DTSTART->hasTime()) {
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;
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(
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) {
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();
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 633
 }
Please login to merge, or discard this patch.