Completed
Push — master ( de5142...21c128 )
by
unknown
30:04 queued 08:28
created
apps/dav/lib/CalDAV/Schedule/Plugin.php 1 patch
Indentation   +699 added lines, -699 removed lines patch added patch discarded remove patch
@@ -42,260 +42,260 @@  discard block
 block discarded – undo
42 42
 
43 43
 class Plugin extends \Sabre\CalDAV\Schedule\Plugin {
44 44
 
45
-	/** @var ITip\Message[] */
46
-	private $schedulingResponses = [];
47
-
48
-	/** @var string|null */
49
-	private $pathOfCalendarObjectChange = null;
50
-
51
-	public const CALENDAR_USER_TYPE = '{' . self::NS_CALDAV . '}calendar-user-type';
52
-	public const SCHEDULE_DEFAULT_CALENDAR_URL = '{' . Plugin::NS_CALDAV . '}schedule-default-calendar-URL';
53
-
54
-	/**
55
-	 * @param IConfig $config
56
-	 */
57
-	public function __construct(
58
-		private IConfig $config,
59
-		private LoggerInterface $logger,
60
-		private DefaultCalendarValidator $defaultCalendarValidator,
61
-	) {
62
-	}
63
-
64
-	/**
65
-	 * Initializes the plugin
66
-	 *
67
-	 * @param Server $server
68
-	 * @return void
69
-	 */
70
-	public function initialize(Server $server) {
71
-		parent::initialize($server);
72
-		$server->on('propFind', [$this, 'propFindDefaultCalendarUrl'], 90);
73
-		$server->on('afterWriteContent', [$this, 'dispatchSchedulingResponses']);
74
-		$server->on('afterCreateFile', [$this, 'dispatchSchedulingResponses']);
75
-
76
-		// We allow mutating the default calendar URL through the CustomPropertiesBackend
77
-		// (oc_properties table)
78
-		$server->protectedProperties = array_filter(
79
-			$server->protectedProperties,
80
-			static fn (string $property) => $property !== self::SCHEDULE_DEFAULT_CALENDAR_URL,
81
-		);
82
-	}
83
-
84
-	/**
85
-	 * Returns an instance of the iTip\Broker.
86
-	 */
87
-	protected function createITipBroker(): TipBroker {
88
-		return new TipBroker();
89
-	}
90
-
91
-	/**
92
-	 * Allow manual setting of the object change URL
93
-	 * to support public write
94
-	 *
95
-	 * @param string $path
96
-	 */
97
-	public function setPathOfCalendarObjectChange(string $path): void {
98
-		$this->pathOfCalendarObjectChange = $path;
99
-	}
100
-
101
-	/**
102
-	 * This method handler is invoked during fetching of properties.
103
-	 *
104
-	 * We use this event to add calendar-auto-schedule-specific properties.
105
-	 *
106
-	 * @param PropFind $propFind
107
-	 * @param INode $node
108
-	 * @return void
109
-	 */
110
-	public function propFind(PropFind $propFind, INode $node) {
111
-		if ($node instanceof IPrincipal) {
112
-			// overwrite Sabre/Dav's implementation
113
-			$propFind->handle(self::CALENDAR_USER_TYPE, function () use ($node) {
114
-				if ($node instanceof IProperties) {
115
-					$props = $node->getProperties([self::CALENDAR_USER_TYPE]);
116
-
117
-					if (isset($props[self::CALENDAR_USER_TYPE])) {
118
-						return $props[self::CALENDAR_USER_TYPE];
119
-					}
120
-				}
121
-
122
-				return 'INDIVIDUAL';
123
-			});
124
-		}
125
-
126
-		parent::propFind($propFind, $node);
127
-	}
128
-
129
-	/**
130
-	 * Returns a list of addresses that are associated with a principal.
131
-	 *
132
-	 * @param string $principal
133
-	 * @return array
134
-	 */
135
-	protected function getAddressesForPrincipal($principal) {
136
-		$result = parent::getAddressesForPrincipal($principal);
137
-
138
-		if ($result === null) {
139
-			$result = [];
140
-		}
141
-
142
-		// iterate through items and html decode values
143
-		foreach ($result as $key => $value) {
144
-			$result[$key] = urldecode($value);
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
-		try {
165
-
166
-			// Do not generate iTip and iMip messages if scheduling is disabled for this message
167
-			if ($request->getHeader('x-nc-scheduling') === 'false') {
168
-				return;
169
-			}
170
-
171
-			if (!$this->scheduleReply($this->server->httpRequest)) {
172
-				return;
173
-			}
174
-
175
-			/** @var Calendar $calendarNode */
176
-			$calendarNode = $this->server->tree->getNodeForPath($calendarPath);
177
-			// extract addresses for owner
178
-			$addresses = $this->getAddressesForPrincipal($calendarNode->getOwner());
179
-			// determine if request is from a sharee
180
-			if ($calendarNode->isShared()) {
181
-				// extract addresses for sharee and add to address collection
182
-				$addresses = array_merge(
183
-					$addresses,
184
-					$this->getAddressesForPrincipal($calendarNode->getPrincipalURI())
185
-				);
186
-			}
187
-			// determine if we are updating a calendar event
188
-			if (!$isNew) {
189
-				// retrieve current calendar event node
190
-				/** @var CalendarObject $currentNode */
191
-				$currentNode = $this->server->tree->getNodeForPath($request->getPath());
192
-				// convert calendar event string data to VCalendar object
193
-				/** @var \Sabre\VObject\Component\VCalendar $currentObject */
194
-				$currentObject = Reader::read($currentNode->get());
195
-			} else {
196
-				$currentObject = null;
197
-			}
198
-			// process request
199
-			$this->processICalendarChange($currentObject, $vCal, $addresses, [], $modified);
200
-
201
-			if ($currentObject) {
202
-				// Destroy circular references so PHP will GC the object.
203
-				$currentObject->destroy();
204
-			}
205
-
206
-		} catch (SameOrganizerForAllComponentsException $e) {
207
-			$this->handleSameOrganizerException($e, $vCal, $calendarPath);
208
-		}
209
-	}
210
-
211
-	/**
212
-	 * @inheritDoc
213
-	 */
214
-	public function beforeUnbind($path): void {
215
-		try {
216
-			parent::beforeUnbind($path);
217
-		} catch (SameOrganizerForAllComponentsException $e) {
218
-			$node = $this->server->tree->getNodeForPath($path);
219
-			if (!$node instanceof ICalendarObject || $node instanceof ISchedulingObject) {
220
-				throw $e;
221
-			}
222
-
223
-			/** @var VCalendar $vCal */
224
-			$vCal = Reader::read($node->get());
225
-			$this->handleSameOrganizerException($e, $vCal, $path);
226
-		}
227
-	}
228
-
229
-	/**
230
-	 * @inheritDoc
231
-	 */
232
-	public function scheduleLocalDelivery(ITip\Message $iTipMessage):void {
233
-		/** @var VEvent|null $vevent */
234
-		$vevent = $iTipMessage->message->VEVENT ?? null;
235
-
236
-		// Strip VALARMs from incoming VEVENT
237
-		if ($vevent && isset($vevent->VALARM)) {
238
-			$vevent->remove('VALARM');
239
-		}
240
-
241
-		parent::scheduleLocalDelivery($iTipMessage);
242
-		// We only care when the message was successfully delivered locally
243
-		// Log all possible codes returned from the parent method that mean something went wrong
244
-		// 3.7, 3.8, 5.0, 5.2
245
-		if ($iTipMessage->scheduleStatus !== '1.2;Message delivered locally') {
246
-			$this->logger->debug('Message not delivered locally with status: ' . $iTipMessage->scheduleStatus);
247
-			return;
248
-		}
249
-		// We only care about request. reply and cancel are properly handled
250
-		// by parent::scheduleLocalDelivery already
251
-		if (strcasecmp($iTipMessage->method, 'REQUEST') !== 0) {
252
-			return;
253
-		}
254
-
255
-		// If parent::scheduleLocalDelivery set scheduleStatus to 1.2,
256
-		// it means that it was successfully delivered locally.
257
-		// Meaning that the ACL plugin is loaded and that a principal
258
-		// exists for the given recipient id, no need to double check
259
-		/** @var \Sabre\DAVACL\Plugin $aclPlugin */
260
-		$aclPlugin = $this->server->getPlugin('acl');
261
-		$principalUri = $aclPlugin->getPrincipalByUri($iTipMessage->recipient);
262
-		$calendarUserType = $this->getCalendarUserTypeForPrincipal($principalUri);
263
-		if (strcasecmp($calendarUserType, 'ROOM') !== 0 && strcasecmp($calendarUserType, 'RESOURCE') !== 0) {
264
-			$this->logger->debug('Calendar user type is neither room nor resource, not processing further');
265
-			return;
266
-		}
267
-
268
-		$attendee = $this->getCurrentAttendee($iTipMessage);
269
-		if (!$attendee) {
270
-			$this->logger->debug('No attendee set for scheduling message');
271
-			return;
272
-		}
273
-
274
-		// We only respond when a response was actually requested
275
-		$rsvp = $this->getAttendeeRSVP($attendee);
276
-		if (!$rsvp) {
277
-			$this->logger->debug('No RSVP requested for attendee ' . $attendee->getValue());
278
-			return;
279
-		}
280
-
281
-		if (!$vevent) {
282
-			$this->logger->debug('No VEVENT set to process on scheduling message');
283
-			return;
284
-		}
285
-
286
-		// We don't support autoresponses for recurrencing events for now
287
-		if (isset($vevent->RRULE) || isset($vevent->RDATE)) {
288
-			$this->logger->debug('VEVENT is a recurring event, autoresponding not supported');
289
-			return;
290
-		}
291
-
292
-		$dtstart = $vevent->DTSTART;
293
-		$dtend = $this->getDTEndFromVEvent($vevent);
294
-		$uid = $vevent->UID->getValue();
295
-		$sequence = isset($vevent->SEQUENCE) ? $vevent->SEQUENCE->getValue() : 0;
296
-		$recurrenceId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->serialize() : '';
297
-
298
-		$message = <<<EOF
45
+    /** @var ITip\Message[] */
46
+    private $schedulingResponses = [];
47
+
48
+    /** @var string|null */
49
+    private $pathOfCalendarObjectChange = null;
50
+
51
+    public const CALENDAR_USER_TYPE = '{' . self::NS_CALDAV . '}calendar-user-type';
52
+    public const SCHEDULE_DEFAULT_CALENDAR_URL = '{' . Plugin::NS_CALDAV . '}schedule-default-calendar-URL';
53
+
54
+    /**
55
+     * @param IConfig $config
56
+     */
57
+    public function __construct(
58
+        private IConfig $config,
59
+        private LoggerInterface $logger,
60
+        private DefaultCalendarValidator $defaultCalendarValidator,
61
+    ) {
62
+    }
63
+
64
+    /**
65
+     * Initializes the plugin
66
+     *
67
+     * @param Server $server
68
+     * @return void
69
+     */
70
+    public function initialize(Server $server) {
71
+        parent::initialize($server);
72
+        $server->on('propFind', [$this, 'propFindDefaultCalendarUrl'], 90);
73
+        $server->on('afterWriteContent', [$this, 'dispatchSchedulingResponses']);
74
+        $server->on('afterCreateFile', [$this, 'dispatchSchedulingResponses']);
75
+
76
+        // We allow mutating the default calendar URL through the CustomPropertiesBackend
77
+        // (oc_properties table)
78
+        $server->protectedProperties = array_filter(
79
+            $server->protectedProperties,
80
+            static fn (string $property) => $property !== self::SCHEDULE_DEFAULT_CALENDAR_URL,
81
+        );
82
+    }
83
+
84
+    /**
85
+     * Returns an instance of the iTip\Broker.
86
+     */
87
+    protected function createITipBroker(): TipBroker {
88
+        return new TipBroker();
89
+    }
90
+
91
+    /**
92
+     * Allow manual setting of the object change URL
93
+     * to support public write
94
+     *
95
+     * @param string $path
96
+     */
97
+    public function setPathOfCalendarObjectChange(string $path): void {
98
+        $this->pathOfCalendarObjectChange = $path;
99
+    }
100
+
101
+    /**
102
+     * This method handler is invoked during fetching of properties.
103
+     *
104
+     * We use this event to add calendar-auto-schedule-specific properties.
105
+     *
106
+     * @param PropFind $propFind
107
+     * @param INode $node
108
+     * @return void
109
+     */
110
+    public function propFind(PropFind $propFind, INode $node) {
111
+        if ($node instanceof IPrincipal) {
112
+            // overwrite Sabre/Dav's implementation
113
+            $propFind->handle(self::CALENDAR_USER_TYPE, function () use ($node) {
114
+                if ($node instanceof IProperties) {
115
+                    $props = $node->getProperties([self::CALENDAR_USER_TYPE]);
116
+
117
+                    if (isset($props[self::CALENDAR_USER_TYPE])) {
118
+                        return $props[self::CALENDAR_USER_TYPE];
119
+                    }
120
+                }
121
+
122
+                return 'INDIVIDUAL';
123
+            });
124
+        }
125
+
126
+        parent::propFind($propFind, $node);
127
+    }
128
+
129
+    /**
130
+     * Returns a list of addresses that are associated with a principal.
131
+     *
132
+     * @param string $principal
133
+     * @return array
134
+     */
135
+    protected function getAddressesForPrincipal($principal) {
136
+        $result = parent::getAddressesForPrincipal($principal);
137
+
138
+        if ($result === null) {
139
+            $result = [];
140
+        }
141
+
142
+        // iterate through items and html decode values
143
+        foreach ($result as $key => $value) {
144
+            $result[$key] = urldecode($value);
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
+        try {
165
+
166
+            // Do not generate iTip and iMip messages if scheduling is disabled for this message
167
+            if ($request->getHeader('x-nc-scheduling') === 'false') {
168
+                return;
169
+            }
170
+
171
+            if (!$this->scheduleReply($this->server->httpRequest)) {
172
+                return;
173
+            }
174
+
175
+            /** @var Calendar $calendarNode */
176
+            $calendarNode = $this->server->tree->getNodeForPath($calendarPath);
177
+            // extract addresses for owner
178
+            $addresses = $this->getAddressesForPrincipal($calendarNode->getOwner());
179
+            // determine if request is from a sharee
180
+            if ($calendarNode->isShared()) {
181
+                // extract addresses for sharee and add to address collection
182
+                $addresses = array_merge(
183
+                    $addresses,
184
+                    $this->getAddressesForPrincipal($calendarNode->getPrincipalURI())
185
+                );
186
+            }
187
+            // determine if we are updating a calendar event
188
+            if (!$isNew) {
189
+                // retrieve current calendar event node
190
+                /** @var CalendarObject $currentNode */
191
+                $currentNode = $this->server->tree->getNodeForPath($request->getPath());
192
+                // convert calendar event string data to VCalendar object
193
+                /** @var \Sabre\VObject\Component\VCalendar $currentObject */
194
+                $currentObject = Reader::read($currentNode->get());
195
+            } else {
196
+                $currentObject = null;
197
+            }
198
+            // process request
199
+            $this->processICalendarChange($currentObject, $vCal, $addresses, [], $modified);
200
+
201
+            if ($currentObject) {
202
+                // Destroy circular references so PHP will GC the object.
203
+                $currentObject->destroy();
204
+            }
205
+
206
+        } catch (SameOrganizerForAllComponentsException $e) {
207
+            $this->handleSameOrganizerException($e, $vCal, $calendarPath);
208
+        }
209
+    }
210
+
211
+    /**
212
+     * @inheritDoc
213
+     */
214
+    public function beforeUnbind($path): void {
215
+        try {
216
+            parent::beforeUnbind($path);
217
+        } catch (SameOrganizerForAllComponentsException $e) {
218
+            $node = $this->server->tree->getNodeForPath($path);
219
+            if (!$node instanceof ICalendarObject || $node instanceof ISchedulingObject) {
220
+                throw $e;
221
+            }
222
+
223
+            /** @var VCalendar $vCal */
224
+            $vCal = Reader::read($node->get());
225
+            $this->handleSameOrganizerException($e, $vCal, $path);
226
+        }
227
+    }
228
+
229
+    /**
230
+     * @inheritDoc
231
+     */
232
+    public function scheduleLocalDelivery(ITip\Message $iTipMessage):void {
233
+        /** @var VEvent|null $vevent */
234
+        $vevent = $iTipMessage->message->VEVENT ?? null;
235
+
236
+        // Strip VALARMs from incoming VEVENT
237
+        if ($vevent && isset($vevent->VALARM)) {
238
+            $vevent->remove('VALARM');
239
+        }
240
+
241
+        parent::scheduleLocalDelivery($iTipMessage);
242
+        // We only care when the message was successfully delivered locally
243
+        // Log all possible codes returned from the parent method that mean something went wrong
244
+        // 3.7, 3.8, 5.0, 5.2
245
+        if ($iTipMessage->scheduleStatus !== '1.2;Message delivered locally') {
246
+            $this->logger->debug('Message not delivered locally with status: ' . $iTipMessage->scheduleStatus);
247
+            return;
248
+        }
249
+        // We only care about request. reply and cancel are properly handled
250
+        // by parent::scheduleLocalDelivery already
251
+        if (strcasecmp($iTipMessage->method, 'REQUEST') !== 0) {
252
+            return;
253
+        }
254
+
255
+        // If parent::scheduleLocalDelivery set scheduleStatus to 1.2,
256
+        // it means that it was successfully delivered locally.
257
+        // Meaning that the ACL plugin is loaded and that a principal
258
+        // exists for the given recipient id, no need to double check
259
+        /** @var \Sabre\DAVACL\Plugin $aclPlugin */
260
+        $aclPlugin = $this->server->getPlugin('acl');
261
+        $principalUri = $aclPlugin->getPrincipalByUri($iTipMessage->recipient);
262
+        $calendarUserType = $this->getCalendarUserTypeForPrincipal($principalUri);
263
+        if (strcasecmp($calendarUserType, 'ROOM') !== 0 && strcasecmp($calendarUserType, 'RESOURCE') !== 0) {
264
+            $this->logger->debug('Calendar user type is neither room nor resource, not processing further');
265
+            return;
266
+        }
267
+
268
+        $attendee = $this->getCurrentAttendee($iTipMessage);
269
+        if (!$attendee) {
270
+            $this->logger->debug('No attendee set for scheduling message');
271
+            return;
272
+        }
273
+
274
+        // We only respond when a response was actually requested
275
+        $rsvp = $this->getAttendeeRSVP($attendee);
276
+        if (!$rsvp) {
277
+            $this->logger->debug('No RSVP requested for attendee ' . $attendee->getValue());
278
+            return;
279
+        }
280
+
281
+        if (!$vevent) {
282
+            $this->logger->debug('No VEVENT set to process on scheduling message');
283
+            return;
284
+        }
285
+
286
+        // We don't support autoresponses for recurrencing events for now
287
+        if (isset($vevent->RRULE) || isset($vevent->RDATE)) {
288
+            $this->logger->debug('VEVENT is a recurring event, autoresponding not supported');
289
+            return;
290
+        }
291
+
292
+        $dtstart = $vevent->DTSTART;
293
+        $dtend = $this->getDTEndFromVEvent($vevent);
294
+        $uid = $vevent->UID->getValue();
295
+        $sequence = isset($vevent->SEQUENCE) ? $vevent->SEQUENCE->getValue() : 0;
296
+        $recurrenceId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->serialize() : '';
297
+
298
+        $message = <<<EOF
299 299
 BEGIN:VCALENDAR
300 300
 PRODID:-//Nextcloud/Nextcloud CalDAV Server//EN
301 301
 METHOD:REPLY
@@ -310,449 +310,449 @@  discard block
 block discarded – undo
310 310
 END:VCALENDAR
311 311
 EOF;
312 312
 
313
-		if ($this->isAvailableAtTime($attendee->getValue(), $dtstart->getDateTime(), $dtend->getDateTime(), $uid)) {
314
-			$partStat = 'ACCEPTED';
315
-		} else {
316
-			$partStat = 'DECLINED';
317
-		}
318
-
319
-		$vObject = Reader::read(vsprintf($message, [
320
-			$partStat,
321
-			$iTipMessage->recipient,
322
-			$iTipMessage->sender,
323
-			$uid,
324
-			$sequence,
325
-			$recurrenceId
326
-		]));
327
-
328
-		$responseITipMessage = new ITip\Message();
329
-		$responseITipMessage->uid = $uid;
330
-		$responseITipMessage->component = 'VEVENT';
331
-		$responseITipMessage->method = 'REPLY';
332
-		$responseITipMessage->sequence = $sequence;
333
-		$responseITipMessage->sender = $iTipMessage->recipient;
334
-		$responseITipMessage->recipient = $iTipMessage->sender;
335
-		$responseITipMessage->message = $vObject;
336
-
337
-		// We can't dispatch them now already, because the organizers calendar-object
338
-		// was not yet created. Hence Sabre/DAV won't find a calendar-object, when we
339
-		// send our reply.
340
-		$this->schedulingResponses[] = $responseITipMessage;
341
-	}
342
-
343
-	/**
344
-	 * @param string $uri
345
-	 */
346
-	public function dispatchSchedulingResponses(string $uri):void {
347
-		if ($uri !== $this->pathOfCalendarObjectChange) {
348
-			return;
349
-		}
350
-
351
-		foreach ($this->schedulingResponses as $schedulingResponse) {
352
-			$this->scheduleLocalDelivery($schedulingResponse);
353
-		}
354
-	}
355
-
356
-	/**
357
-	 * Always use the personal calendar as target for scheduled events
358
-	 *
359
-	 * @param PropFind $propFind
360
-	 * @param INode $node
361
-	 * @return void
362
-	 */
363
-	public function propFindDefaultCalendarUrl(PropFind $propFind, INode $node) {
364
-		if ($node instanceof IPrincipal) {
365
-			$propFind->handle(self::SCHEDULE_DEFAULT_CALENDAR_URL, function () use ($node) {
366
-				/** @var \OCA\DAV\CalDAV\Plugin $caldavPlugin */
367
-				$caldavPlugin = $this->server->getPlugin('caldav');
368
-				$principalUrl = $node->getPrincipalUrl();
369
-
370
-				$calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl);
371
-				if (!$calendarHomePath) {
372
-					return null;
373
-				}
374
-
375
-				$isResourceOrRoom = str_starts_with($principalUrl, 'principals/calendar-resources') ||
376
-					str_starts_with($principalUrl, 'principals/calendar-rooms');
377
-
378
-				if (str_starts_with($principalUrl, 'principals/users')) {
379
-					[, $userId] = split($principalUrl);
380
-					$uri = $this->config->getUserValue($userId, 'dav', 'defaultCalendar', CalDavBackend::PERSONAL_CALENDAR_URI);
381
-					$displayName = CalDavBackend::PERSONAL_CALENDAR_NAME;
382
-				} elseif ($isResourceOrRoom) {
383
-					$uri = CalDavBackend::RESOURCE_BOOKING_CALENDAR_URI;
384
-					$displayName = CalDavBackend::RESOURCE_BOOKING_CALENDAR_NAME;
385
-				} else {
386
-					// How did we end up here?
387
-					// TODO - throw exception or just ignore?
388
-					return null;
389
-				}
390
-
391
-				/** @var CalendarHome $calendarHome */
392
-				$calendarHome = $this->server->tree->getNodeForPath($calendarHomePath);
393
-				$currentCalendarDeleted = false;
394
-				if (!$calendarHome->childExists($uri) || $currentCalendarDeleted = $this->isCalendarDeleted($calendarHome, $uri)) {
395
-					// If the default calendar doesn't exist
396
-					if ($isResourceOrRoom) {
397
-						// Resources or rooms can't be in the trashbin, so we're fine
398
-						$this->createCalendar($calendarHome, $principalUrl, $uri, $displayName);
399
-					} else {
400
-						// And we're not handling scheduling on resource/room booking
401
-						$userCalendars = [];
402
-						/**
403
-						 * If the default calendar of the user isn't set and the
404
-						 * fallback doesn't match any of the user's calendar
405
-						 * try to find the first "personal" calendar we can write to
406
-						 * instead of creating a new one.
407
-						 * A appropriate personal calendar to receive invites:
408
-						 * - isn't a calendar subscription
409
-						 * - user can write to it (no virtual/3rd-party calendars)
410
-						 * - calendar isn't a share
411
-						 * - calendar supports VEVENTs
412
-						 */
413
-						foreach ($calendarHome->getChildren() as $node) {
414
-							if (!($node instanceof Calendar)) {
415
-								continue;
416
-							}
417
-
418
-							try {
419
-								$this->defaultCalendarValidator->validateScheduleDefaultCalendar($node);
420
-							} catch (DavException $e) {
421
-								continue;
422
-							}
423
-
424
-							$userCalendars[] = $node;
425
-						}
426
-
427
-						if (count($userCalendars) > 0) {
428
-							// Calendar backend returns calendar by calendarorder property
429
-							$uri = $userCalendars[0]->getName();
430
-						} else {
431
-							// Otherwise if we have really nothing, create a new calendar
432
-							if ($currentCalendarDeleted) {
433
-								// If the calendar exists but is in the trash bin, we try to rename its uri
434
-								// so that we can create the new one and still restore the previous one
435
-								// otherwise we just purge the calendar by removing it before recreating it
436
-								$calendar = $this->getCalendar($calendarHome, $uri);
437
-								if ($calendar instanceof Calendar) {
438
-									$backend = $calendarHome->getCalDAVBackend();
439
-									if ($backend instanceof CalDavBackend) {
440
-										// If the CalDAV backend supports moving calendars
441
-										$this->moveCalendar($backend, $principalUrl, $uri, $uri . '-back-' . time());
442
-									} else {
443
-										// Otherwise just purge the calendar
444
-										$calendar->disableTrashbin();
445
-										$calendar->delete();
446
-									}
447
-								}
448
-							}
449
-							$this->createCalendar($calendarHome, $principalUrl, $uri, $displayName);
450
-						}
451
-					}
452
-				}
453
-
454
-				$result = $this->server->getPropertiesForPath($calendarHomePath . '/' . $uri, [], 1);
455
-				if (empty($result)) {
456
-					return null;
457
-				}
458
-
459
-				return new LocalHref($result[0]['href']);
460
-			});
461
-		}
462
-	}
463
-
464
-	/**
465
-	 * Returns a list of addresses that are associated with a principal.
466
-	 *
467
-	 * @param string $principal
468
-	 * @return string|null
469
-	 */
470
-	protected function getCalendarUserTypeForPrincipal($principal):?string {
471
-		$calendarUserType = '{' . self::NS_CALDAV . '}calendar-user-type';
472
-		$properties = $this->server->getProperties(
473
-			$principal,
474
-			[$calendarUserType]
475
-		);
476
-
477
-		// If we can't find this information, we'll stop processing
478
-		if (!isset($properties[$calendarUserType])) {
479
-			return null;
480
-		}
481
-
482
-		return $properties[$calendarUserType];
483
-	}
484
-
485
-	/**
486
-	 * @param ITip\Message $iTipMessage
487
-	 * @return null|Property
488
-	 */
489
-	private function getCurrentAttendee(ITip\Message $iTipMessage):?Property {
490
-		/** @var VEvent $vevent */
491
-		$vevent = $iTipMessage->message->VEVENT;
492
-		$attendees = $vevent->select('ATTENDEE');
493
-		foreach ($attendees as $attendee) {
494
-			/** @var Property $attendee */
495
-			if (strcasecmp($attendee->getValue(), $iTipMessage->recipient) === 0) {
496
-				return $attendee;
497
-			}
498
-		}
499
-		return null;
500
-	}
501
-
502
-	/**
503
-	 * @param Property|null $attendee
504
-	 * @return bool
505
-	 */
506
-	private function getAttendeeRSVP(?Property $attendee = null):bool {
507
-		if ($attendee !== null) {
508
-			$rsvp = $attendee->offsetGet('RSVP');
509
-			if (($rsvp instanceof Parameter) && (strcasecmp($rsvp->getValue(), 'TRUE') === 0)) {
510
-				return true;
511
-			}
512
-		}
513
-		// RFC 5545 3.2.17: default RSVP is false
514
-		return false;
515
-	}
516
-
517
-	/**
518
-	 * @param VEvent $vevent
519
-	 * @return Property\ICalendar\DateTime
520
-	 */
521
-	private function getDTEndFromVEvent(VEvent $vevent):Property\ICalendar\DateTime {
522
-		if (isset($vevent->DTEND)) {
523
-			return $vevent->DTEND;
524
-		}
525
-
526
-		if (isset($vevent->DURATION)) {
527
-			$isFloating = $vevent->DTSTART->isFloating();
528
-			/** @var Property\ICalendar\DateTime $end */
529
-			$end = clone $vevent->DTSTART;
530
-			$endDateTime = $end->getDateTime();
531
-			$endDateTime = $endDateTime->add(DateTimeParser::parse($vevent->DURATION->getValue()));
532
-			$end->setDateTime($endDateTime, $isFloating);
533
-			return $end;
534
-		}
535
-
536
-		if (!$vevent->DTSTART->hasTime()) {
537
-			$isFloating = $vevent->DTSTART->isFloating();
538
-			/** @var Property\ICalendar\DateTime $end */
539
-			$end = clone $vevent->DTSTART;
540
-			$endDateTime = $end->getDateTime();
541
-			$endDateTime = $endDateTime->modify('+1 day');
542
-			$end->setDateTime($endDateTime, $isFloating);
543
-			return $end;
544
-		}
545
-
546
-		return clone $vevent->DTSTART;
547
-	}
548
-
549
-	/**
550
-	 * @param string $email
551
-	 * @param \DateTimeInterface $start
552
-	 * @param \DateTimeInterface $end
553
-	 * @param string $ignoreUID
554
-	 * @return bool
555
-	 */
556
-	private function isAvailableAtTime(string $email, \DateTimeInterface $start, \DateTimeInterface $end, string $ignoreUID):bool {
557
-		// This method is heavily inspired by Sabre\CalDAV\Schedule\Plugin::scheduleLocalDelivery
558
-		// and Sabre\CalDAV\Schedule\Plugin::getFreeBusyForEmail
559
-
560
-		$aclPlugin = $this->server->getPlugin('acl');
561
-		$this->server->removeListener('propFind', [$aclPlugin, 'propFind']);
562
-
563
-		$result = $aclPlugin->principalSearch(
564
-			['{http://sabredav.org/ns}email-address' => $this->stripOffMailTo($email)],
565
-			[
566
-				'{DAV:}principal-URL',
567
-				'{' . self::NS_CALDAV . '}calendar-home-set',
568
-				'{' . self::NS_CALDAV . '}schedule-inbox-URL',
569
-				'{http://sabredav.org/ns}email-address',
570
-
571
-			]
572
-		);
573
-		$this->server->on('propFind', [$aclPlugin, 'propFind'], 20);
574
-
575
-
576
-		// Grabbing the calendar list
577
-		$objects = [];
578
-		$calendarTimeZone = new DateTimeZone('UTC');
579
-
580
-		$homePath = $result[0][200]['{' . self::NS_CALDAV . '}calendar-home-set']->getHref();
581
-		/** @var Calendar $node */
582
-		foreach ($this->server->tree->getNodeForPath($homePath)->getChildren() as $node) {
583
-
584
-			if (!$node instanceof ICalendar) {
585
-				continue;
586
-			}
587
-
588
-			// Getting the list of object uris within the time-range
589
-			$urls = $node->calendarQuery([
590
-				'name' => 'VCALENDAR',
591
-				'comp-filters' => [
592
-					[
593
-						'name' => 'VEVENT',
594
-						'is-not-defined' => false,
595
-						'time-range' => [
596
-							'start' => $start,
597
-							'end' => $end,
598
-						],
599
-						'comp-filters' => [],
600
-						'prop-filters' => [],
601
-					],
602
-					[
603
-						'name' => 'VEVENT',
604
-						'is-not-defined' => false,
605
-						'time-range' => null,
606
-						'comp-filters' => [],
607
-						'prop-filters' => [
608
-							[
609
-								'name' => 'UID',
610
-								'is-not-defined' => false,
611
-								'time-range' => null,
612
-								'text-match' => [
613
-									'value' => $ignoreUID,
614
-									'negate-condition' => true,
615
-									'collation' => 'i;octet',
616
-								],
617
-								'param-filters' => [],
618
-							],
619
-						]
620
-					],
621
-				],
622
-				'prop-filters' => [],
623
-				'is-not-defined' => false,
624
-				'time-range' => null,
625
-			]);
626
-
627
-			foreach ($urls as $url) {
628
-				$objects[] = $node->getChild($url)->get();
629
-			}
630
-		}
631
-
632
-		$inboxProps = $this->server->getProperties(
633
-			$result[0][200]['{' . self::NS_CALDAV . '}schedule-inbox-URL']->getHref(),
634
-			['{' . self::NS_CALDAV . '}calendar-availability']
635
-		);
636
-
637
-		$vcalendar = new VCalendar();
638
-		$vcalendar->METHOD = 'REPLY';
639
-
640
-		$generator = new FreeBusyGenerator();
641
-		$generator->setObjects($objects);
642
-		$generator->setTimeRange($start, $end);
643
-		$generator->setBaseObject($vcalendar);
644
-		$generator->setTimeZone($calendarTimeZone);
645
-
646
-		if (isset($inboxProps['{' . self::NS_CALDAV . '}calendar-availability'])) {
647
-			$generator->setVAvailability(
648
-				Reader::read(
649
-					$inboxProps['{' . self::NS_CALDAV . '}calendar-availability']
650
-				)
651
-			);
652
-		}
653
-
654
-		$result = $generator->getResult();
655
-		if (!isset($result->VFREEBUSY)) {
656
-			return false;
657
-		}
658
-
659
-		/** @var Component $freeBusyComponent */
660
-		$freeBusyComponent = $result->VFREEBUSY;
661
-		$freeBusyProperties = $freeBusyComponent->select('FREEBUSY');
662
-		// If there is no Free-busy property at all, the time-range is empty and available
663
-		if (count($freeBusyProperties) === 0) {
664
-			return true;
665
-		}
666
-
667
-		// If more than one Free-Busy property was returned, it means that an event
668
-		// starts or ends inside this time-range, so it's not available and we return false
669
-		if (count($freeBusyProperties) > 1) {
670
-			return false;
671
-		}
672
-
673
-		/** @var Property $freeBusyProperty */
674
-		$freeBusyProperty = $freeBusyProperties[0];
675
-		if (!$freeBusyProperty->offsetExists('FBTYPE')) {
676
-			// If there is no FBTYPE, it means it's busy
677
-			return false;
678
-		}
679
-
680
-		$fbTypeParameter = $freeBusyProperty->offsetGet('FBTYPE');
681
-		if (!($fbTypeParameter instanceof Parameter)) {
682
-			return false;
683
-		}
684
-
685
-		return (strcasecmp($fbTypeParameter->getValue(), 'FREE') === 0);
686
-	}
687
-
688
-	/**
689
-	 * @param string $email
690
-	 * @return string
691
-	 */
692
-	private function stripOffMailTo(string $email): string {
693
-		if (stripos($email, 'mailto:') === 0) {
694
-			return substr($email, 7);
695
-		}
696
-
697
-		return $email;
698
-	}
699
-
700
-	private function getCalendar(CalendarHome $calendarHome, string $uri): INode {
701
-		return $calendarHome->getChild($uri);
702
-	}
703
-
704
-	private function isCalendarDeleted(CalendarHome $calendarHome, string $uri): bool {
705
-		$calendar = $this->getCalendar($calendarHome, $uri);
706
-		return $calendar instanceof Calendar && $calendar->isDeleted();
707
-	}
708
-
709
-	private function createCalendar(CalendarHome $calendarHome, string $principalUri, string $uri, string $displayName): void {
710
-		$calendarHome->getCalDAVBackend()->createCalendar($principalUri, $uri, [
711
-			'{DAV:}displayname' => $displayName,
712
-		]);
713
-	}
714
-
715
-	private function moveCalendar(CalDavBackend $calDavBackend, string $principalUri, string $oldUri, string $newUri): void {
716
-		$calDavBackend->moveCalendar($oldUri, $principalUri, $principalUri, $newUri);
717
-	}
718
-
719
-	/**
720
-	 * Try to handle the given exception gracefully or throw it if necessary.
721
-	 *
722
-	 * @throws SameOrganizerForAllComponentsException If the exception should not be ignored
723
-	 */
724
-	private function handleSameOrganizerException(
725
-		SameOrganizerForAllComponentsException $e,
726
-		VCalendar $vCal,
727
-		string $calendarPath,
728
-	): void {
729
-		// This is very hacky! However, we want to allow saving events with multiple
730
-		// organizers. Those events are not RFC compliant, but sometimes imported from major
731
-		// external calendar services (e.g. Google). If the current user is not an organizer of
732
-		// the event we ignore the exception as no scheduling messages will be sent anyway.
733
-
734
-		// It would be cleaner to patch Sabre to validate organizers *after* checking if
735
-		// scheduling messages are necessary. Currently, organizers are validated first and
736
-		// afterwards the broker checks if messages should be scheduled. So the code will throw
737
-		// even if the organizers are not relevant. This is to ensure compliance with RFCs but
738
-		// a bit too strict for real world usage.
739
-
740
-		if (!isset($vCal->VEVENT)) {
741
-			throw $e;
742
-		}
743
-
744
-		$calendarNode = $this->server->tree->getNodeForPath($calendarPath);
745
-		if (!($calendarNode instanceof IACL)) {
746
-			// Should always be an instance of IACL but just to be sure
747
-			throw $e;
748
-		}
749
-
750
-		$addresses = $this->getAddressesForPrincipal($calendarNode->getOwner());
751
-		foreach ($vCal->VEVENT as $vevent) {
752
-			if (in_array($vevent->ORGANIZER->getNormalizedValue(), $addresses, true)) {
753
-				// User is an organizer => throw the exception
754
-				throw $e;
755
-			}
756
-		}
757
-	}
313
+        if ($this->isAvailableAtTime($attendee->getValue(), $dtstart->getDateTime(), $dtend->getDateTime(), $uid)) {
314
+            $partStat = 'ACCEPTED';
315
+        } else {
316
+            $partStat = 'DECLINED';
317
+        }
318
+
319
+        $vObject = Reader::read(vsprintf($message, [
320
+            $partStat,
321
+            $iTipMessage->recipient,
322
+            $iTipMessage->sender,
323
+            $uid,
324
+            $sequence,
325
+            $recurrenceId
326
+        ]));
327
+
328
+        $responseITipMessage = new ITip\Message();
329
+        $responseITipMessage->uid = $uid;
330
+        $responseITipMessage->component = 'VEVENT';
331
+        $responseITipMessage->method = 'REPLY';
332
+        $responseITipMessage->sequence = $sequence;
333
+        $responseITipMessage->sender = $iTipMessage->recipient;
334
+        $responseITipMessage->recipient = $iTipMessage->sender;
335
+        $responseITipMessage->message = $vObject;
336
+
337
+        // We can't dispatch them now already, because the organizers calendar-object
338
+        // was not yet created. Hence Sabre/DAV won't find a calendar-object, when we
339
+        // send our reply.
340
+        $this->schedulingResponses[] = $responseITipMessage;
341
+    }
342
+
343
+    /**
344
+     * @param string $uri
345
+     */
346
+    public function dispatchSchedulingResponses(string $uri):void {
347
+        if ($uri !== $this->pathOfCalendarObjectChange) {
348
+            return;
349
+        }
350
+
351
+        foreach ($this->schedulingResponses as $schedulingResponse) {
352
+            $this->scheduleLocalDelivery($schedulingResponse);
353
+        }
354
+    }
355
+
356
+    /**
357
+     * Always use the personal calendar as target for scheduled events
358
+     *
359
+     * @param PropFind $propFind
360
+     * @param INode $node
361
+     * @return void
362
+     */
363
+    public function propFindDefaultCalendarUrl(PropFind $propFind, INode $node) {
364
+        if ($node instanceof IPrincipal) {
365
+            $propFind->handle(self::SCHEDULE_DEFAULT_CALENDAR_URL, function () use ($node) {
366
+                /** @var \OCA\DAV\CalDAV\Plugin $caldavPlugin */
367
+                $caldavPlugin = $this->server->getPlugin('caldav');
368
+                $principalUrl = $node->getPrincipalUrl();
369
+
370
+                $calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl);
371
+                if (!$calendarHomePath) {
372
+                    return null;
373
+                }
374
+
375
+                $isResourceOrRoom = str_starts_with($principalUrl, 'principals/calendar-resources') ||
376
+                    str_starts_with($principalUrl, 'principals/calendar-rooms');
377
+
378
+                if (str_starts_with($principalUrl, 'principals/users')) {
379
+                    [, $userId] = split($principalUrl);
380
+                    $uri = $this->config->getUserValue($userId, 'dav', 'defaultCalendar', CalDavBackend::PERSONAL_CALENDAR_URI);
381
+                    $displayName = CalDavBackend::PERSONAL_CALENDAR_NAME;
382
+                } elseif ($isResourceOrRoom) {
383
+                    $uri = CalDavBackend::RESOURCE_BOOKING_CALENDAR_URI;
384
+                    $displayName = CalDavBackend::RESOURCE_BOOKING_CALENDAR_NAME;
385
+                } else {
386
+                    // How did we end up here?
387
+                    // TODO - throw exception or just ignore?
388
+                    return null;
389
+                }
390
+
391
+                /** @var CalendarHome $calendarHome */
392
+                $calendarHome = $this->server->tree->getNodeForPath($calendarHomePath);
393
+                $currentCalendarDeleted = false;
394
+                if (!$calendarHome->childExists($uri) || $currentCalendarDeleted = $this->isCalendarDeleted($calendarHome, $uri)) {
395
+                    // If the default calendar doesn't exist
396
+                    if ($isResourceOrRoom) {
397
+                        // Resources or rooms can't be in the trashbin, so we're fine
398
+                        $this->createCalendar($calendarHome, $principalUrl, $uri, $displayName);
399
+                    } else {
400
+                        // And we're not handling scheduling on resource/room booking
401
+                        $userCalendars = [];
402
+                        /**
403
+                         * If the default calendar of the user isn't set and the
404
+                         * fallback doesn't match any of the user's calendar
405
+                         * try to find the first "personal" calendar we can write to
406
+                         * instead of creating a new one.
407
+                         * A appropriate personal calendar to receive invites:
408
+                         * - isn't a calendar subscription
409
+                         * - user can write to it (no virtual/3rd-party calendars)
410
+                         * - calendar isn't a share
411
+                         * - calendar supports VEVENTs
412
+                         */
413
+                        foreach ($calendarHome->getChildren() as $node) {
414
+                            if (!($node instanceof Calendar)) {
415
+                                continue;
416
+                            }
417
+
418
+                            try {
419
+                                $this->defaultCalendarValidator->validateScheduleDefaultCalendar($node);
420
+                            } catch (DavException $e) {
421
+                                continue;
422
+                            }
423
+
424
+                            $userCalendars[] = $node;
425
+                        }
426
+
427
+                        if (count($userCalendars) > 0) {
428
+                            // Calendar backend returns calendar by calendarorder property
429
+                            $uri = $userCalendars[0]->getName();
430
+                        } else {
431
+                            // Otherwise if we have really nothing, create a new calendar
432
+                            if ($currentCalendarDeleted) {
433
+                                // If the calendar exists but is in the trash bin, we try to rename its uri
434
+                                // so that we can create the new one and still restore the previous one
435
+                                // otherwise we just purge the calendar by removing it before recreating it
436
+                                $calendar = $this->getCalendar($calendarHome, $uri);
437
+                                if ($calendar instanceof Calendar) {
438
+                                    $backend = $calendarHome->getCalDAVBackend();
439
+                                    if ($backend instanceof CalDavBackend) {
440
+                                        // If the CalDAV backend supports moving calendars
441
+                                        $this->moveCalendar($backend, $principalUrl, $uri, $uri . '-back-' . time());
442
+                                    } else {
443
+                                        // Otherwise just purge the calendar
444
+                                        $calendar->disableTrashbin();
445
+                                        $calendar->delete();
446
+                                    }
447
+                                }
448
+                            }
449
+                            $this->createCalendar($calendarHome, $principalUrl, $uri, $displayName);
450
+                        }
451
+                    }
452
+                }
453
+
454
+                $result = $this->server->getPropertiesForPath($calendarHomePath . '/' . $uri, [], 1);
455
+                if (empty($result)) {
456
+                    return null;
457
+                }
458
+
459
+                return new LocalHref($result[0]['href']);
460
+            });
461
+        }
462
+    }
463
+
464
+    /**
465
+     * Returns a list of addresses that are associated with a principal.
466
+     *
467
+     * @param string $principal
468
+     * @return string|null
469
+     */
470
+    protected function getCalendarUserTypeForPrincipal($principal):?string {
471
+        $calendarUserType = '{' . self::NS_CALDAV . '}calendar-user-type';
472
+        $properties = $this->server->getProperties(
473
+            $principal,
474
+            [$calendarUserType]
475
+        );
476
+
477
+        // If we can't find this information, we'll stop processing
478
+        if (!isset($properties[$calendarUserType])) {
479
+            return null;
480
+        }
481
+
482
+        return $properties[$calendarUserType];
483
+    }
484
+
485
+    /**
486
+     * @param ITip\Message $iTipMessage
487
+     * @return null|Property
488
+     */
489
+    private function getCurrentAttendee(ITip\Message $iTipMessage):?Property {
490
+        /** @var VEvent $vevent */
491
+        $vevent = $iTipMessage->message->VEVENT;
492
+        $attendees = $vevent->select('ATTENDEE');
493
+        foreach ($attendees as $attendee) {
494
+            /** @var Property $attendee */
495
+            if (strcasecmp($attendee->getValue(), $iTipMessage->recipient) === 0) {
496
+                return $attendee;
497
+            }
498
+        }
499
+        return null;
500
+    }
501
+
502
+    /**
503
+     * @param Property|null $attendee
504
+     * @return bool
505
+     */
506
+    private function getAttendeeRSVP(?Property $attendee = null):bool {
507
+        if ($attendee !== null) {
508
+            $rsvp = $attendee->offsetGet('RSVP');
509
+            if (($rsvp instanceof Parameter) && (strcasecmp($rsvp->getValue(), 'TRUE') === 0)) {
510
+                return true;
511
+            }
512
+        }
513
+        // RFC 5545 3.2.17: default RSVP is false
514
+        return false;
515
+    }
516
+
517
+    /**
518
+     * @param VEvent $vevent
519
+     * @return Property\ICalendar\DateTime
520
+     */
521
+    private function getDTEndFromVEvent(VEvent $vevent):Property\ICalendar\DateTime {
522
+        if (isset($vevent->DTEND)) {
523
+            return $vevent->DTEND;
524
+        }
525
+
526
+        if (isset($vevent->DURATION)) {
527
+            $isFloating = $vevent->DTSTART->isFloating();
528
+            /** @var Property\ICalendar\DateTime $end */
529
+            $end = clone $vevent->DTSTART;
530
+            $endDateTime = $end->getDateTime();
531
+            $endDateTime = $endDateTime->add(DateTimeParser::parse($vevent->DURATION->getValue()));
532
+            $end->setDateTime($endDateTime, $isFloating);
533
+            return $end;
534
+        }
535
+
536
+        if (!$vevent->DTSTART->hasTime()) {
537
+            $isFloating = $vevent->DTSTART->isFloating();
538
+            /** @var Property\ICalendar\DateTime $end */
539
+            $end = clone $vevent->DTSTART;
540
+            $endDateTime = $end->getDateTime();
541
+            $endDateTime = $endDateTime->modify('+1 day');
542
+            $end->setDateTime($endDateTime, $isFloating);
543
+            return $end;
544
+        }
545
+
546
+        return clone $vevent->DTSTART;
547
+    }
548
+
549
+    /**
550
+     * @param string $email
551
+     * @param \DateTimeInterface $start
552
+     * @param \DateTimeInterface $end
553
+     * @param string $ignoreUID
554
+     * @return bool
555
+     */
556
+    private function isAvailableAtTime(string $email, \DateTimeInterface $start, \DateTimeInterface $end, string $ignoreUID):bool {
557
+        // This method is heavily inspired by Sabre\CalDAV\Schedule\Plugin::scheduleLocalDelivery
558
+        // and Sabre\CalDAV\Schedule\Plugin::getFreeBusyForEmail
559
+
560
+        $aclPlugin = $this->server->getPlugin('acl');
561
+        $this->server->removeListener('propFind', [$aclPlugin, 'propFind']);
562
+
563
+        $result = $aclPlugin->principalSearch(
564
+            ['{http://sabredav.org/ns}email-address' => $this->stripOffMailTo($email)],
565
+            [
566
+                '{DAV:}principal-URL',
567
+                '{' . self::NS_CALDAV . '}calendar-home-set',
568
+                '{' . self::NS_CALDAV . '}schedule-inbox-URL',
569
+                '{http://sabredav.org/ns}email-address',
570
+
571
+            ]
572
+        );
573
+        $this->server->on('propFind', [$aclPlugin, 'propFind'], 20);
574
+
575
+
576
+        // Grabbing the calendar list
577
+        $objects = [];
578
+        $calendarTimeZone = new DateTimeZone('UTC');
579
+
580
+        $homePath = $result[0][200]['{' . self::NS_CALDAV . '}calendar-home-set']->getHref();
581
+        /** @var Calendar $node */
582
+        foreach ($this->server->tree->getNodeForPath($homePath)->getChildren() as $node) {
583
+
584
+            if (!$node instanceof ICalendar) {
585
+                continue;
586
+            }
587
+
588
+            // Getting the list of object uris within the time-range
589
+            $urls = $node->calendarQuery([
590
+                'name' => 'VCALENDAR',
591
+                'comp-filters' => [
592
+                    [
593
+                        'name' => 'VEVENT',
594
+                        'is-not-defined' => false,
595
+                        'time-range' => [
596
+                            'start' => $start,
597
+                            'end' => $end,
598
+                        ],
599
+                        'comp-filters' => [],
600
+                        'prop-filters' => [],
601
+                    ],
602
+                    [
603
+                        'name' => 'VEVENT',
604
+                        'is-not-defined' => false,
605
+                        'time-range' => null,
606
+                        'comp-filters' => [],
607
+                        'prop-filters' => [
608
+                            [
609
+                                'name' => 'UID',
610
+                                'is-not-defined' => false,
611
+                                'time-range' => null,
612
+                                'text-match' => [
613
+                                    'value' => $ignoreUID,
614
+                                    'negate-condition' => true,
615
+                                    'collation' => 'i;octet',
616
+                                ],
617
+                                'param-filters' => [],
618
+                            ],
619
+                        ]
620
+                    ],
621
+                ],
622
+                'prop-filters' => [],
623
+                'is-not-defined' => false,
624
+                'time-range' => null,
625
+            ]);
626
+
627
+            foreach ($urls as $url) {
628
+                $objects[] = $node->getChild($url)->get();
629
+            }
630
+        }
631
+
632
+        $inboxProps = $this->server->getProperties(
633
+            $result[0][200]['{' . self::NS_CALDAV . '}schedule-inbox-URL']->getHref(),
634
+            ['{' . self::NS_CALDAV . '}calendar-availability']
635
+        );
636
+
637
+        $vcalendar = new VCalendar();
638
+        $vcalendar->METHOD = 'REPLY';
639
+
640
+        $generator = new FreeBusyGenerator();
641
+        $generator->setObjects($objects);
642
+        $generator->setTimeRange($start, $end);
643
+        $generator->setBaseObject($vcalendar);
644
+        $generator->setTimeZone($calendarTimeZone);
645
+
646
+        if (isset($inboxProps['{' . self::NS_CALDAV . '}calendar-availability'])) {
647
+            $generator->setVAvailability(
648
+                Reader::read(
649
+                    $inboxProps['{' . self::NS_CALDAV . '}calendar-availability']
650
+                )
651
+            );
652
+        }
653
+
654
+        $result = $generator->getResult();
655
+        if (!isset($result->VFREEBUSY)) {
656
+            return false;
657
+        }
658
+
659
+        /** @var Component $freeBusyComponent */
660
+        $freeBusyComponent = $result->VFREEBUSY;
661
+        $freeBusyProperties = $freeBusyComponent->select('FREEBUSY');
662
+        // If there is no Free-busy property at all, the time-range is empty and available
663
+        if (count($freeBusyProperties) === 0) {
664
+            return true;
665
+        }
666
+
667
+        // If more than one Free-Busy property was returned, it means that an event
668
+        // starts or ends inside this time-range, so it's not available and we return false
669
+        if (count($freeBusyProperties) > 1) {
670
+            return false;
671
+        }
672
+
673
+        /** @var Property $freeBusyProperty */
674
+        $freeBusyProperty = $freeBusyProperties[0];
675
+        if (!$freeBusyProperty->offsetExists('FBTYPE')) {
676
+            // If there is no FBTYPE, it means it's busy
677
+            return false;
678
+        }
679
+
680
+        $fbTypeParameter = $freeBusyProperty->offsetGet('FBTYPE');
681
+        if (!($fbTypeParameter instanceof Parameter)) {
682
+            return false;
683
+        }
684
+
685
+        return (strcasecmp($fbTypeParameter->getValue(), 'FREE') === 0);
686
+    }
687
+
688
+    /**
689
+     * @param string $email
690
+     * @return string
691
+     */
692
+    private function stripOffMailTo(string $email): string {
693
+        if (stripos($email, 'mailto:') === 0) {
694
+            return substr($email, 7);
695
+        }
696
+
697
+        return $email;
698
+    }
699
+
700
+    private function getCalendar(CalendarHome $calendarHome, string $uri): INode {
701
+        return $calendarHome->getChild($uri);
702
+    }
703
+
704
+    private function isCalendarDeleted(CalendarHome $calendarHome, string $uri): bool {
705
+        $calendar = $this->getCalendar($calendarHome, $uri);
706
+        return $calendar instanceof Calendar && $calendar->isDeleted();
707
+    }
708
+
709
+    private function createCalendar(CalendarHome $calendarHome, string $principalUri, string $uri, string $displayName): void {
710
+        $calendarHome->getCalDAVBackend()->createCalendar($principalUri, $uri, [
711
+            '{DAV:}displayname' => $displayName,
712
+        ]);
713
+    }
714
+
715
+    private function moveCalendar(CalDavBackend $calDavBackend, string $principalUri, string $oldUri, string $newUri): void {
716
+        $calDavBackend->moveCalendar($oldUri, $principalUri, $principalUri, $newUri);
717
+    }
718
+
719
+    /**
720
+     * Try to handle the given exception gracefully or throw it if necessary.
721
+     *
722
+     * @throws SameOrganizerForAllComponentsException If the exception should not be ignored
723
+     */
724
+    private function handleSameOrganizerException(
725
+        SameOrganizerForAllComponentsException $e,
726
+        VCalendar $vCal,
727
+        string $calendarPath,
728
+    ): void {
729
+        // This is very hacky! However, we want to allow saving events with multiple
730
+        // organizers. Those events are not RFC compliant, but sometimes imported from major
731
+        // external calendar services (e.g. Google). If the current user is not an organizer of
732
+        // the event we ignore the exception as no scheduling messages will be sent anyway.
733
+
734
+        // It would be cleaner to patch Sabre to validate organizers *after* checking if
735
+        // scheduling messages are necessary. Currently, organizers are validated first and
736
+        // afterwards the broker checks if messages should be scheduled. So the code will throw
737
+        // even if the organizers are not relevant. This is to ensure compliance with RFCs but
738
+        // a bit too strict for real world usage.
739
+
740
+        if (!isset($vCal->VEVENT)) {
741
+            throw $e;
742
+        }
743
+
744
+        $calendarNode = $this->server->tree->getNodeForPath($calendarPath);
745
+        if (!($calendarNode instanceof IACL)) {
746
+            // Should always be an instance of IACL but just to be sure
747
+            throw $e;
748
+        }
749
+
750
+        $addresses = $this->getAddressesForPrincipal($calendarNode->getOwner());
751
+        foreach ($vCal->VEVENT as $vevent) {
752
+            if (in_array($vevent->ORGANIZER->getNormalizedValue(), $addresses, true)) {
753
+                // User is an organizer => throw the exception
754
+                throw $e;
755
+            }
756
+        }
757
+    }
758 758
 }
Please login to merge, or discard this patch.