Completed
Push — master ( 1cb6dc...94c80a )
by
unknown
27:27
created
lib/public/Calendar/IManager.php 1 patch
Indentation   +151 added lines, -151 removed lines patch added patch discarded remove patch
@@ -42,155 +42,155 @@
 block discarded – undo
42 42
  * @since 13.0.0
43 43
  */
44 44
 interface IManager {
45
-	/**
46
-	 * This function is used to search and find objects within the user's calendars.
47
-	 * In case $pattern is empty all events/journals/todos will be returned.
48
-	 *
49
-	 * @param string $pattern which should match within the $searchProperties
50
-	 * @param array $searchProperties defines the properties within the query pattern should match
51
-	 * @param array $options - optional parameters:
52
-	 *                       ['timerange' => ['start' => new DateTime(...), 'end' => new DateTime(...)]]
53
-	 * @param integer|null $limit - limit number of search results
54
-	 * @param integer|null $offset - offset for paging of search results
55
-	 * @return array an array of events/journals/todos which are arrays of arrays of key-value-pairs
56
-	 * @since 13.0.0
57
-	 * @deprecated 23.0.0 use \OCP\Calendar\IManager::searchForPrincipal
58
-	 */
59
-	public function search($pattern, array $searchProperties = [], array $options = [], $limit = null, $offset = null);
60
-
61
-	/**
62
-	 * Check if calendars are available
63
-	 *
64
-	 * @return bool true if enabled, false if not
65
-	 * @since 13.0.0
66
-	 * @deprecated 23.0.0
67
-	 */
68
-	public function isEnabled();
69
-
70
-	/**
71
-	 * Registers a calendar
72
-	 *
73
-	 * @param ICalendar $calendar
74
-	 * @return void
75
-	 * @since 13.0.0
76
-	 * @deprecated 23.0.0 use \OCP\AppFramework\Bootstrap\IRegistrationContext::registerCalendarProvider
77
-	 */
78
-	public function registerCalendar(ICalendar $calendar);
79
-
80
-	/**
81
-	 * Unregisters a calendar
82
-	 *
83
-	 * @param ICalendar $calendar
84
-	 * @return void
85
-	 * @since 13.0.0
86
-	 * @deprecated 23.0.0
87
-	 */
88
-	public function unregisterCalendar(ICalendar $calendar);
89
-
90
-	/**
91
-	 * In order to improve lazy loading a closure can be registered which will be called in case
92
-	 * calendars are actually requested
93
-	 *
94
-	 * @param \Closure $callable
95
-	 * @return void
96
-	 * @since 13.0.0
97
-	 * @deprecated 23.0.0 use \OCP\AppFramework\Bootstrap\IRegistrationContext::registerCalendarProvider
98
-	 */
99
-	public function register(\Closure $callable);
100
-
101
-	/**
102
-	 * @return ICalendar[]
103
-	 * @since 13.0.0
104
-	 * @deprecated 23.0.0 use \OCP\Calendar\IManager::getCalendarsForPrincipal
105
-	 */
106
-	public function getCalendars();
107
-
108
-	/**
109
-	 * removes all registered calendar instances
110
-	 *
111
-	 * @return void
112
-	 * @since 13.0.0
113
-	 * @deprecated 23.0.0
114
-	 */
115
-	public function clear();
116
-
117
-	/**
118
-	 * @param string $principalUri URI of the principal
119
-	 * @param string[] $calendarUris optionally specify which calendars to load, or all if this array is empty
120
-	 *
121
-	 * @return ICalendar[]
122
-	 * @since 23.0.0
123
-	 */
124
-	public function getCalendarsForPrincipal(string $principalUri, array $calendarUris = []): array;
125
-
126
-	/**
127
-	 * Query a principals calendar(s)
128
-	 *
129
-	 * @param ICalendarQuery $query
130
-	 * @return array[]
131
-	 * @since 23.0.0
132
-	 */
133
-	public function searchForPrincipal(ICalendarQuery $query): array;
134
-
135
-	/**
136
-	 * Build a new query for searchForPrincipal
137
-	 *
138
-	 * @return ICalendarQuery
139
-	 * @since 23.0.0
140
-	 */
141
-	public function newQuery(string $principalUri) : ICalendarQuery;
142
-
143
-	/**
144
-	 * Handles a iMip message
145
-	 *
146
-	 * @since 32.0.0
147
-	 *
148
-	 * @throws \OCP\DB\Exception
149
-	 */
150
-	public function handleIMip(string $userId, string $message): bool;
151
-
152
-	/**
153
-	 * Handle a iMip REQUEST message
154
-	 *
155
-	 * @since 31.0.0
156
-	 */
157
-	public function handleIMipRequest(string $principalUri, string $sender, string $recipient, string $calendarData): bool;
158
-
159
-	/**
160
-	 * Handle a iMip REPLY message
161
-	 *
162
-	 * @since 25.0.0
163
-	 */
164
-	public function handleIMipReply(string $principalUri, string $sender, string $recipient, string $calendarData): bool;
165
-
166
-	/**
167
-	 * Handle a iMip CANCEL message
168
-	 *
169
-	 * @since 25.0.0
170
-	 */
171
-	public function handleIMipCancel(string $principalUri, string $sender, ?string $replyTo, string $recipient, string $calendarData): bool;
172
-
173
-	/**
174
-	 * Create a new event builder instance. Please have a look at its documentation and the
175
-	 * \OCP\Calendar\ICreateFromString interface on how to use it.
176
-	 *
177
-	 * @since 31.0.0
178
-	 */
179
-	public function createEventBuilder(): ICalendarEventBuilder;
180
-
181
-	/**
182
-	 * Check the availability of the given organizer and attendees in the given time range.
183
-	 *
184
-	 * @since 31.0.0
185
-	 *
186
-	 * @param IUser $organizer The organizing user from whose perspective to do the availability check.
187
-	 * @param string[] $attendees Email addresses of attendees to check for (with or without a "mailto:" prefix). Only users on this instance can be checked. The rest will be silently ignored.
188
-	 * @return IAvailabilityResult[] Availabilities of the organizer and all attendees which are also users on this instance. As such, the array might not contain an entry for each given attendee.
189
-	 */
190
-	public function checkAvailability(
191
-		DateTimeInterface $start,
192
-		DateTimeInterface $end,
193
-		IUser $organizer,
194
-		array $attendees,
195
-	): array;
45
+    /**
46
+     * This function is used to search and find objects within the user's calendars.
47
+     * In case $pattern is empty all events/journals/todos will be returned.
48
+     *
49
+     * @param string $pattern which should match within the $searchProperties
50
+     * @param array $searchProperties defines the properties within the query pattern should match
51
+     * @param array $options - optional parameters:
52
+     *                       ['timerange' => ['start' => new DateTime(...), 'end' => new DateTime(...)]]
53
+     * @param integer|null $limit - limit number of search results
54
+     * @param integer|null $offset - offset for paging of search results
55
+     * @return array an array of events/journals/todos which are arrays of arrays of key-value-pairs
56
+     * @since 13.0.0
57
+     * @deprecated 23.0.0 use \OCP\Calendar\IManager::searchForPrincipal
58
+     */
59
+    public function search($pattern, array $searchProperties = [], array $options = [], $limit = null, $offset = null);
60
+
61
+    /**
62
+     * Check if calendars are available
63
+     *
64
+     * @return bool true if enabled, false if not
65
+     * @since 13.0.0
66
+     * @deprecated 23.0.0
67
+     */
68
+    public function isEnabled();
69
+
70
+    /**
71
+     * Registers a calendar
72
+     *
73
+     * @param ICalendar $calendar
74
+     * @return void
75
+     * @since 13.0.0
76
+     * @deprecated 23.0.0 use \OCP\AppFramework\Bootstrap\IRegistrationContext::registerCalendarProvider
77
+     */
78
+    public function registerCalendar(ICalendar $calendar);
79
+
80
+    /**
81
+     * Unregisters a calendar
82
+     *
83
+     * @param ICalendar $calendar
84
+     * @return void
85
+     * @since 13.0.0
86
+     * @deprecated 23.0.0
87
+     */
88
+    public function unregisterCalendar(ICalendar $calendar);
89
+
90
+    /**
91
+     * In order to improve lazy loading a closure can be registered which will be called in case
92
+     * calendars are actually requested
93
+     *
94
+     * @param \Closure $callable
95
+     * @return void
96
+     * @since 13.0.0
97
+     * @deprecated 23.0.0 use \OCP\AppFramework\Bootstrap\IRegistrationContext::registerCalendarProvider
98
+     */
99
+    public function register(\Closure $callable);
100
+
101
+    /**
102
+     * @return ICalendar[]
103
+     * @since 13.0.0
104
+     * @deprecated 23.0.0 use \OCP\Calendar\IManager::getCalendarsForPrincipal
105
+     */
106
+    public function getCalendars();
107
+
108
+    /**
109
+     * removes all registered calendar instances
110
+     *
111
+     * @return void
112
+     * @since 13.0.0
113
+     * @deprecated 23.0.0
114
+     */
115
+    public function clear();
116
+
117
+    /**
118
+     * @param string $principalUri URI of the principal
119
+     * @param string[] $calendarUris optionally specify which calendars to load, or all if this array is empty
120
+     *
121
+     * @return ICalendar[]
122
+     * @since 23.0.0
123
+     */
124
+    public function getCalendarsForPrincipal(string $principalUri, array $calendarUris = []): array;
125
+
126
+    /**
127
+     * Query a principals calendar(s)
128
+     *
129
+     * @param ICalendarQuery $query
130
+     * @return array[]
131
+     * @since 23.0.0
132
+     */
133
+    public function searchForPrincipal(ICalendarQuery $query): array;
134
+
135
+    /**
136
+     * Build a new query for searchForPrincipal
137
+     *
138
+     * @return ICalendarQuery
139
+     * @since 23.0.0
140
+     */
141
+    public function newQuery(string $principalUri) : ICalendarQuery;
142
+
143
+    /**
144
+     * Handles a iMip message
145
+     *
146
+     * @since 32.0.0
147
+     *
148
+     * @throws \OCP\DB\Exception
149
+     */
150
+    public function handleIMip(string $userId, string $message): bool;
151
+
152
+    /**
153
+     * Handle a iMip REQUEST message
154
+     *
155
+     * @since 31.0.0
156
+     */
157
+    public function handleIMipRequest(string $principalUri, string $sender, string $recipient, string $calendarData): bool;
158
+
159
+    /**
160
+     * Handle a iMip REPLY message
161
+     *
162
+     * @since 25.0.0
163
+     */
164
+    public function handleIMipReply(string $principalUri, string $sender, string $recipient, string $calendarData): bool;
165
+
166
+    /**
167
+     * Handle a iMip CANCEL message
168
+     *
169
+     * @since 25.0.0
170
+     */
171
+    public function handleIMipCancel(string $principalUri, string $sender, ?string $replyTo, string $recipient, string $calendarData): bool;
172
+
173
+    /**
174
+     * Create a new event builder instance. Please have a look at its documentation and the
175
+     * \OCP\Calendar\ICreateFromString interface on how to use it.
176
+     *
177
+     * @since 31.0.0
178
+     */
179
+    public function createEventBuilder(): ICalendarEventBuilder;
180
+
181
+    /**
182
+     * Check the availability of the given organizer and attendees in the given time range.
183
+     *
184
+     * @since 31.0.0
185
+     *
186
+     * @param IUser $organizer The organizing user from whose perspective to do the availability check.
187
+     * @param string[] $attendees Email addresses of attendees to check for (with or without a "mailto:" prefix). Only users on this instance can be checked. The rest will be silently ignored.
188
+     * @return IAvailabilityResult[] Availabilities of the organizer and all attendees which are also users on this instance. As such, the array might not contain an entry for each given attendee.
189
+     */
190
+    public function checkAvailability(
191
+        DateTimeInterface $start,
192
+        DateTimeInterface $end,
193
+        IUser $organizer,
194
+        array $attendees,
195
+    ): array;
196 196
 }
Please login to merge, or discard this patch.
lib/private/Calendar/Manager.php 2 patches
Indentation   +397 added lines, -397 removed lines patch added patch discarded remove patch
@@ -40,401 +40,401 @@
 block discarded – undo
40 40
 use function array_merge;
41 41
 
42 42
 class Manager implements IManager {
43
-	/**
44
-	 * @var ICalendar[] holds all registered calendars
45
-	 */
46
-	private array $calendars = [];
47
-
48
-	/**
49
-	 * @var \Closure[] to call to load/register calendar providers
50
-	 */
51
-	private array $calendarLoaders = [];
52
-
53
-	public function __construct(
54
-		private Coordinator $coordinator,
55
-		private ContainerInterface $container,
56
-		private LoggerInterface $logger,
57
-		private ITimeFactory $timeFactory,
58
-		private ISecureRandom $random,
59
-		private IUserManager $userManager,
60
-		private ServerFactory $serverFactory,
61
-	) {
62
-	}
63
-
64
-	/**
65
-	 * This function is used to search and find objects within the user's calendars.
66
-	 * In case $pattern is empty all events/journals/todos will be returned.
67
-	 *
68
-	 * @param string $pattern which should match within the $searchProperties
69
-	 * @param array $searchProperties defines the properties within the query pattern should match
70
-	 * @param array $options - optional parameters:
71
-	 *                       ['timerange' => ['start' => new DateTime(...), 'end' => new DateTime(...)]]
72
-	 * @param integer|null $limit - limit number of search results
73
-	 * @param integer|null $offset - offset for paging of search results
74
-	 * @return array an array of events/journals/todos which are arrays of arrays of key-value-pairs
75
-	 * @since 13.0.0
76
-	 */
77
-	public function search(
78
-		$pattern,
79
-		array $searchProperties = [],
80
-		array $options = [],
81
-		$limit = null,
82
-		$offset = null,
83
-	): array {
84
-		$this->loadCalendars();
85
-		$result = [];
86
-		foreach ($this->calendars as $calendar) {
87
-			$r = $calendar->search($pattern, $searchProperties, $options, $limit, $offset);
88
-			foreach ($r as $o) {
89
-				$o['calendar-key'] = $calendar->getKey();
90
-				$result[] = $o;
91
-			}
92
-		}
93
-
94
-		return $result;
95
-	}
96
-
97
-	/**
98
-	 * Check if calendars are available
99
-	 *
100
-	 * @return bool true if enabled, false if not
101
-	 * @since 13.0.0
102
-	 */
103
-	public function isEnabled(): bool {
104
-		return !empty($this->calendars) || !empty($this->calendarLoaders);
105
-	}
106
-
107
-	/**
108
-	 * Registers a calendar
109
-	 *
110
-	 * @since 13.0.0
111
-	 */
112
-	public function registerCalendar(ICalendar $calendar): void {
113
-		$this->calendars[$calendar->getKey()] = $calendar;
114
-	}
115
-
116
-	/**
117
-	 * Unregisters a calendar
118
-	 *
119
-	 * @since 13.0.0
120
-	 */
121
-	public function unregisterCalendar(ICalendar $calendar): void {
122
-		unset($this->calendars[$calendar->getKey()]);
123
-	}
124
-
125
-	/**
126
-	 * In order to improve lazy loading a closure can be registered which will be called in case
127
-	 * calendars are actually requested
128
-	 *
129
-	 * @since 13.0.0
130
-	 */
131
-	public function register(\Closure $callable): void {
132
-		$this->calendarLoaders[] = $callable;
133
-	}
134
-
135
-	/**
136
-	 * @return ICalendar[]
137
-	 *
138
-	 * @since 13.0.0
139
-	 */
140
-	public function getCalendars(): array {
141
-		$this->loadCalendars();
142
-
143
-		return array_values($this->calendars);
144
-	}
145
-
146
-	/**
147
-	 * removes all registered calendar instances
148
-	 *
149
-	 * @since 13.0.0
150
-	 */
151
-	public function clear(): void {
152
-		$this->calendars = [];
153
-		$this->calendarLoaders = [];
154
-	}
155
-
156
-	/**
157
-	 * loads all calendars
158
-	 */
159
-	private function loadCalendars(): void {
160
-		foreach ($this->calendarLoaders as $callable) {
161
-			$callable($this);
162
-		}
163
-		$this->calendarLoaders = [];
164
-	}
165
-
166
-	/**
167
-	 * @return ICreateFromString[]
168
-	 */
169
-	public function getCalendarsForPrincipal(string $principalUri, array $calendarUris = []): array {
170
-		$context = $this->coordinator->getRegistrationContext();
171
-		if ($context === null) {
172
-			return [];
173
-		}
174
-
175
-		return array_merge(
176
-			...array_map(function ($registration) use ($principalUri, $calendarUris) {
177
-				try {
178
-					/** @var ICalendarProvider $provider */
179
-					$provider = $this->container->get($registration->getService());
180
-				} catch (Throwable $e) {
181
-					$this->logger->error('Could not load calendar provider ' . $registration->getService() . ': ' . $e->getMessage(), [
182
-						'exception' => $e,
183
-					]);
184
-					return [];
185
-				}
186
-
187
-				return $provider->getCalendars($principalUri, $calendarUris);
188
-			}, $context->getCalendarProviders())
189
-		);
190
-	}
191
-
192
-	public function searchForPrincipal(ICalendarQuery $query): array {
193
-		/** @var CalendarQuery $query */
194
-		$calendars = $this->getCalendarsForPrincipal(
195
-			$query->getPrincipalUri(),
196
-			$query->getCalendarUris(),
197
-		);
198
-
199
-		$results = [];
200
-		foreach ($calendars as $calendar) {
201
-			$r = $calendar->search(
202
-				$query->getSearchPattern() ?? '',
203
-				$query->getSearchProperties(),
204
-				$query->getOptions(),
205
-				$query->getLimit(),
206
-				$query->getOffset()
207
-			);
208
-
209
-			foreach ($r as $o) {
210
-				$o['calendar-key'] = $calendar->getKey();
211
-				$o['calendar-uri'] = $calendar->getUri();
212
-				$results[] = $o;
213
-			}
214
-		}
215
-		return $results;
216
-	}
217
-
218
-	public function newQuery(string $principalUri): ICalendarQuery {
219
-		return new CalendarQuery($principalUri);
220
-	}
221
-
222
-	/**
223
-	 * @since 32.0.0
224
-	 *
225
-	 * @throws \OCP\DB\Exception
226
-	 */
227
-	public function handleIMip(
228
-		string $userId,
229
-		string $message,
230
-	): bool {
231
-
232
-		$userUri = 'principals/users/' . $userId;
233
-
234
-		$userCalendars = $this->getCalendarsForPrincipal($userUri);
235
-		if (empty($userCalendars)) {
236
-			$this->logger->warning('iMip message could not be processed because user has no calendars');
237
-			return false;
238
-		}
239
-
240
-		try {
241
-			/** @var VCalendar $vObject|null */
242
-			$vObject = Reader::read($message);
243
-		} catch (ParseException $e) {
244
-			$this->logger->error('iMip message could not be processed because an error occurred while parsing the iMip message', ['exception' => $e]);
245
-			return false;
246
-		}
247
-
248
-		if (!isset($vObject->VEVENT)) {
249
-			$this->logger->warning('iMip message does not contain any event(s)');
250
-			return false;
251
-		}
252
-		/** @var VEvent $vEvent */
253
-		$vEvent = $vObject->VEVENT;
254
-
255
-		if (!isset($vEvent->UID)) {
256
-			$this->logger->warning('iMip message event dose not contains a UID');
257
-			return false;
258
-		}
259
-
260
-		if (!isset($vEvent->ORGANIZER)) {
261
-			$this->logger->warning('iMip message event dose not contains an organizer');
262
-			return false;
263
-		}
264
-
265
-		if (!isset($vEvent->ATTENDEE)) {
266
-			$this->logger->warning('iMip message event dose not contains any attendees');
267
-			return false;
268
-		}
269
-
270
-		foreach ($userCalendars as $calendar) {
271
-			if (!$calendar instanceof ICalendarIsWritable) {
272
-				continue;
273
-			}
274
-			if ($calendar->isDeleted() || !$calendar->isWritable()) {
275
-				continue;
276
-			}
277
-			if (!empty($calendar->search('', [], ['uid' => $vEvent->UID->getValue()]))) {
278
-				try {
279
-					if ($calendar instanceof IHandleImipMessage) {
280
-						$calendar->handleIMipMessage($userId, $vObject->serialize());
281
-					}
282
-					return true;
283
-				} catch (CalendarException $e) {
284
-					$this->logger->error('iMip message could not be processed because an error occurred', ['exception' => $e]);
285
-					return false;
286
-				}
287
-			}
288
-		}
289
-
290
-		$this->logger->warning('iMip message could not be processed because no corresponding event was found in any calendar');
291
-
292
-		return false;
293
-	}
294
-
295
-	/**
296
-	 * @since 31.0.0
297
-	 *
298
-	 * @throws \OCP\DB\Exception
299
-	 */
300
-	public function handleIMipRequest(
301
-		string $principalUri,
302
-		string $sender,
303
-		string $recipient,
304
-		string $calendarData,
305
-	): bool {
306
-		if (empty($principalUri) || !str_starts_with($principalUri, 'principals/users/')) {
307
-			$this->logger->error('Invalid principal URI provided for iMip request');
308
-			return false;
309
-		}
310
-		$userId = substr($principalUri, 17);
311
-		return $this->handleIMip($userId, $calendarData);
312
-	}
313
-
314
-	/**
315
-	 * @since 25.0.0
316
-	 *
317
-	 * @throws \OCP\DB\Exception
318
-	 */
319
-	public function handleIMipReply(
320
-		string $principalUri,
321
-		string $sender,
322
-		string $recipient,
323
-		string $calendarData,
324
-	): bool {
325
-		if (empty($principalUri) || !str_starts_with($principalUri, 'principals/users/')) {
326
-			$this->logger->error('Invalid principal URI provided for iMip reply');
327
-			return false;
328
-		}
329
-		$userId = substr($principalUri, 17);
330
-		return $this->handleIMip($userId, $calendarData);
331
-	}
332
-
333
-	/**
334
-	 * @since 25.0.0
335
-	 *
336
-	 * @throws \OCP\DB\Exception
337
-	 */
338
-	public function handleIMipCancel(
339
-		string $principalUri,
340
-		string $sender,
341
-		?string $replyTo,
342
-		string $recipient,
343
-		string $calendarData,
344
-	): bool {
345
-		if (empty($principalUri) || !str_starts_with($principalUri, 'principals/users/')) {
346
-			$this->logger->error('Invalid principal URI provided for iMip cancel');
347
-			return false;
348
-		}
349
-		$userId = substr($principalUri, 17);
350
-		return $this->handleIMip($userId, $calendarData);
351
-	}
352
-
353
-	public function createEventBuilder(): ICalendarEventBuilder {
354
-		$uid = $this->random->generate(32, ISecureRandom::CHAR_ALPHANUMERIC);
355
-		return new CalendarEventBuilder($uid, $this->timeFactory);
356
-	}
357
-
358
-	public function checkAvailability(
359
-		DateTimeInterface $start,
360
-		DateTimeInterface $end,
361
-		IUser $organizer,
362
-		array $attendees,
363
-	): array {
364
-		$organizerMailto = 'mailto:' . $organizer->getEMailAddress();
365
-		$request = new VCalendar();
366
-		$request->METHOD = 'REQUEST';
367
-		$request->add('VFREEBUSY', [
368
-			'DTSTART' => $start,
369
-			'DTEND' => $end,
370
-			'ORGANIZER' => $organizerMailto,
371
-			'ATTENDEE' => $organizerMailto,
372
-		]);
373
-
374
-		$mailtoLen = strlen('mailto:');
375
-		foreach ($attendees as $attendee) {
376
-			if (str_starts_with($attendee, 'mailto:')) {
377
-				$attendee = substr($attendee, $mailtoLen);
378
-			}
379
-
380
-			$attendeeUsers = $this->userManager->getByEmail($attendee);
381
-			if ($attendeeUsers === []) {
382
-				continue;
383
-			}
384
-
385
-			$request->VFREEBUSY->add('ATTENDEE', "mailto:$attendee");
386
-		}
387
-
388
-		$organizerUid = $organizer->getUID();
389
-		$server = $this->serverFactory->createAttendeeAvailabilityServer();
390
-		/** @var CustomPrincipalPlugin $plugin */
391
-		$plugin = $server->getPlugin('auth');
392
-		$plugin->setCurrentPrincipal("principals/users/$organizerUid");
393
-
394
-		$request = new Request(
395
-			'POST',
396
-			"/calendars/$organizerUid/outbox/",
397
-			[
398
-				'Content-Type' => 'text/calendar',
399
-				'Depth' => 0,
400
-			],
401
-			$request->serialize(),
402
-		);
403
-		$response = new Response();
404
-		$server->invokeMethod($request, $response, false);
405
-
406
-		$xmlService = new \Sabre\Xml\Service();
407
-		$xmlService->elementMap = [
408
-			'{urn:ietf:params:xml:ns:caldav}response' => 'Sabre\Xml\Deserializer\keyValue',
409
-			'{urn:ietf:params:xml:ns:caldav}recipient' => 'Sabre\Xml\Deserializer\keyValue',
410
-		];
411
-		$parsedResponse = $xmlService->parse($response->getBodyAsString());
412
-
413
-		$result = [];
414
-		foreach ($parsedResponse as $freeBusyResponse) {
415
-			$freeBusyResponse = $freeBusyResponse['value'];
416
-			if ($freeBusyResponse['{urn:ietf:params:xml:ns:caldav}request-status'] !== '2.0;Success') {
417
-				continue;
418
-			}
419
-
420
-			$freeBusyResponseData = \Sabre\VObject\Reader::read(
421
-				$freeBusyResponse['{urn:ietf:params:xml:ns:caldav}calendar-data']
422
-			);
423
-
424
-			$attendee = substr(
425
-				$freeBusyResponse['{urn:ietf:params:xml:ns:caldav}recipient']['{DAV:}href'],
426
-				$mailtoLen,
427
-			);
428
-
429
-			$vFreeBusy = $freeBusyResponseData->VFREEBUSY;
430
-			if (!($vFreeBusy instanceof VFreeBusy)) {
431
-				continue;
432
-			}
433
-
434
-			// TODO: actually check values of FREEBUSY properties to find a free slot
435
-			$result[] = new AvailabilityResult($attendee, $vFreeBusy->isFree($start, $end));
436
-		}
437
-
438
-		return $result;
439
-	}
43
+    /**
44
+     * @var ICalendar[] holds all registered calendars
45
+     */
46
+    private array $calendars = [];
47
+
48
+    /**
49
+     * @var \Closure[] to call to load/register calendar providers
50
+     */
51
+    private array $calendarLoaders = [];
52
+
53
+    public function __construct(
54
+        private Coordinator $coordinator,
55
+        private ContainerInterface $container,
56
+        private LoggerInterface $logger,
57
+        private ITimeFactory $timeFactory,
58
+        private ISecureRandom $random,
59
+        private IUserManager $userManager,
60
+        private ServerFactory $serverFactory,
61
+    ) {
62
+    }
63
+
64
+    /**
65
+     * This function is used to search and find objects within the user's calendars.
66
+     * In case $pattern is empty all events/journals/todos will be returned.
67
+     *
68
+     * @param string $pattern which should match within the $searchProperties
69
+     * @param array $searchProperties defines the properties within the query pattern should match
70
+     * @param array $options - optional parameters:
71
+     *                       ['timerange' => ['start' => new DateTime(...), 'end' => new DateTime(...)]]
72
+     * @param integer|null $limit - limit number of search results
73
+     * @param integer|null $offset - offset for paging of search results
74
+     * @return array an array of events/journals/todos which are arrays of arrays of key-value-pairs
75
+     * @since 13.0.0
76
+     */
77
+    public function search(
78
+        $pattern,
79
+        array $searchProperties = [],
80
+        array $options = [],
81
+        $limit = null,
82
+        $offset = null,
83
+    ): array {
84
+        $this->loadCalendars();
85
+        $result = [];
86
+        foreach ($this->calendars as $calendar) {
87
+            $r = $calendar->search($pattern, $searchProperties, $options, $limit, $offset);
88
+            foreach ($r as $o) {
89
+                $o['calendar-key'] = $calendar->getKey();
90
+                $result[] = $o;
91
+            }
92
+        }
93
+
94
+        return $result;
95
+    }
96
+
97
+    /**
98
+     * Check if calendars are available
99
+     *
100
+     * @return bool true if enabled, false if not
101
+     * @since 13.0.0
102
+     */
103
+    public function isEnabled(): bool {
104
+        return !empty($this->calendars) || !empty($this->calendarLoaders);
105
+    }
106
+
107
+    /**
108
+     * Registers a calendar
109
+     *
110
+     * @since 13.0.0
111
+     */
112
+    public function registerCalendar(ICalendar $calendar): void {
113
+        $this->calendars[$calendar->getKey()] = $calendar;
114
+    }
115
+
116
+    /**
117
+     * Unregisters a calendar
118
+     *
119
+     * @since 13.0.0
120
+     */
121
+    public function unregisterCalendar(ICalendar $calendar): void {
122
+        unset($this->calendars[$calendar->getKey()]);
123
+    }
124
+
125
+    /**
126
+     * In order to improve lazy loading a closure can be registered which will be called in case
127
+     * calendars are actually requested
128
+     *
129
+     * @since 13.0.0
130
+     */
131
+    public function register(\Closure $callable): void {
132
+        $this->calendarLoaders[] = $callable;
133
+    }
134
+
135
+    /**
136
+     * @return ICalendar[]
137
+     *
138
+     * @since 13.0.0
139
+     */
140
+    public function getCalendars(): array {
141
+        $this->loadCalendars();
142
+
143
+        return array_values($this->calendars);
144
+    }
145
+
146
+    /**
147
+     * removes all registered calendar instances
148
+     *
149
+     * @since 13.0.0
150
+     */
151
+    public function clear(): void {
152
+        $this->calendars = [];
153
+        $this->calendarLoaders = [];
154
+    }
155
+
156
+    /**
157
+     * loads all calendars
158
+     */
159
+    private function loadCalendars(): void {
160
+        foreach ($this->calendarLoaders as $callable) {
161
+            $callable($this);
162
+        }
163
+        $this->calendarLoaders = [];
164
+    }
165
+
166
+    /**
167
+     * @return ICreateFromString[]
168
+     */
169
+    public function getCalendarsForPrincipal(string $principalUri, array $calendarUris = []): array {
170
+        $context = $this->coordinator->getRegistrationContext();
171
+        if ($context === null) {
172
+            return [];
173
+        }
174
+
175
+        return array_merge(
176
+            ...array_map(function ($registration) use ($principalUri, $calendarUris) {
177
+                try {
178
+                    /** @var ICalendarProvider $provider */
179
+                    $provider = $this->container->get($registration->getService());
180
+                } catch (Throwable $e) {
181
+                    $this->logger->error('Could not load calendar provider ' . $registration->getService() . ': ' . $e->getMessage(), [
182
+                        'exception' => $e,
183
+                    ]);
184
+                    return [];
185
+                }
186
+
187
+                return $provider->getCalendars($principalUri, $calendarUris);
188
+            }, $context->getCalendarProviders())
189
+        );
190
+    }
191
+
192
+    public function searchForPrincipal(ICalendarQuery $query): array {
193
+        /** @var CalendarQuery $query */
194
+        $calendars = $this->getCalendarsForPrincipal(
195
+            $query->getPrincipalUri(),
196
+            $query->getCalendarUris(),
197
+        );
198
+
199
+        $results = [];
200
+        foreach ($calendars as $calendar) {
201
+            $r = $calendar->search(
202
+                $query->getSearchPattern() ?? '',
203
+                $query->getSearchProperties(),
204
+                $query->getOptions(),
205
+                $query->getLimit(),
206
+                $query->getOffset()
207
+            );
208
+
209
+            foreach ($r as $o) {
210
+                $o['calendar-key'] = $calendar->getKey();
211
+                $o['calendar-uri'] = $calendar->getUri();
212
+                $results[] = $o;
213
+            }
214
+        }
215
+        return $results;
216
+    }
217
+
218
+    public function newQuery(string $principalUri): ICalendarQuery {
219
+        return new CalendarQuery($principalUri);
220
+    }
221
+
222
+    /**
223
+     * @since 32.0.0
224
+     *
225
+     * @throws \OCP\DB\Exception
226
+     */
227
+    public function handleIMip(
228
+        string $userId,
229
+        string $message,
230
+    ): bool {
231
+
232
+        $userUri = 'principals/users/' . $userId;
233
+
234
+        $userCalendars = $this->getCalendarsForPrincipal($userUri);
235
+        if (empty($userCalendars)) {
236
+            $this->logger->warning('iMip message could not be processed because user has no calendars');
237
+            return false;
238
+        }
239
+
240
+        try {
241
+            /** @var VCalendar $vObject|null */
242
+            $vObject = Reader::read($message);
243
+        } catch (ParseException $e) {
244
+            $this->logger->error('iMip message could not be processed because an error occurred while parsing the iMip message', ['exception' => $e]);
245
+            return false;
246
+        }
247
+
248
+        if (!isset($vObject->VEVENT)) {
249
+            $this->logger->warning('iMip message does not contain any event(s)');
250
+            return false;
251
+        }
252
+        /** @var VEvent $vEvent */
253
+        $vEvent = $vObject->VEVENT;
254
+
255
+        if (!isset($vEvent->UID)) {
256
+            $this->logger->warning('iMip message event dose not contains a UID');
257
+            return false;
258
+        }
259
+
260
+        if (!isset($vEvent->ORGANIZER)) {
261
+            $this->logger->warning('iMip message event dose not contains an organizer');
262
+            return false;
263
+        }
264
+
265
+        if (!isset($vEvent->ATTENDEE)) {
266
+            $this->logger->warning('iMip message event dose not contains any attendees');
267
+            return false;
268
+        }
269
+
270
+        foreach ($userCalendars as $calendar) {
271
+            if (!$calendar instanceof ICalendarIsWritable) {
272
+                continue;
273
+            }
274
+            if ($calendar->isDeleted() || !$calendar->isWritable()) {
275
+                continue;
276
+            }
277
+            if (!empty($calendar->search('', [], ['uid' => $vEvent->UID->getValue()]))) {
278
+                try {
279
+                    if ($calendar instanceof IHandleImipMessage) {
280
+                        $calendar->handleIMipMessage($userId, $vObject->serialize());
281
+                    }
282
+                    return true;
283
+                } catch (CalendarException $e) {
284
+                    $this->logger->error('iMip message could not be processed because an error occurred', ['exception' => $e]);
285
+                    return false;
286
+                }
287
+            }
288
+        }
289
+
290
+        $this->logger->warning('iMip message could not be processed because no corresponding event was found in any calendar');
291
+
292
+        return false;
293
+    }
294
+
295
+    /**
296
+     * @since 31.0.0
297
+     *
298
+     * @throws \OCP\DB\Exception
299
+     */
300
+    public function handleIMipRequest(
301
+        string $principalUri,
302
+        string $sender,
303
+        string $recipient,
304
+        string $calendarData,
305
+    ): bool {
306
+        if (empty($principalUri) || !str_starts_with($principalUri, 'principals/users/')) {
307
+            $this->logger->error('Invalid principal URI provided for iMip request');
308
+            return false;
309
+        }
310
+        $userId = substr($principalUri, 17);
311
+        return $this->handleIMip($userId, $calendarData);
312
+    }
313
+
314
+    /**
315
+     * @since 25.0.0
316
+     *
317
+     * @throws \OCP\DB\Exception
318
+     */
319
+    public function handleIMipReply(
320
+        string $principalUri,
321
+        string $sender,
322
+        string $recipient,
323
+        string $calendarData,
324
+    ): bool {
325
+        if (empty($principalUri) || !str_starts_with($principalUri, 'principals/users/')) {
326
+            $this->logger->error('Invalid principal URI provided for iMip reply');
327
+            return false;
328
+        }
329
+        $userId = substr($principalUri, 17);
330
+        return $this->handleIMip($userId, $calendarData);
331
+    }
332
+
333
+    /**
334
+     * @since 25.0.0
335
+     *
336
+     * @throws \OCP\DB\Exception
337
+     */
338
+    public function handleIMipCancel(
339
+        string $principalUri,
340
+        string $sender,
341
+        ?string $replyTo,
342
+        string $recipient,
343
+        string $calendarData,
344
+    ): bool {
345
+        if (empty($principalUri) || !str_starts_with($principalUri, 'principals/users/')) {
346
+            $this->logger->error('Invalid principal URI provided for iMip cancel');
347
+            return false;
348
+        }
349
+        $userId = substr($principalUri, 17);
350
+        return $this->handleIMip($userId, $calendarData);
351
+    }
352
+
353
+    public function createEventBuilder(): ICalendarEventBuilder {
354
+        $uid = $this->random->generate(32, ISecureRandom::CHAR_ALPHANUMERIC);
355
+        return new CalendarEventBuilder($uid, $this->timeFactory);
356
+    }
357
+
358
+    public function checkAvailability(
359
+        DateTimeInterface $start,
360
+        DateTimeInterface $end,
361
+        IUser $organizer,
362
+        array $attendees,
363
+    ): array {
364
+        $organizerMailto = 'mailto:' . $organizer->getEMailAddress();
365
+        $request = new VCalendar();
366
+        $request->METHOD = 'REQUEST';
367
+        $request->add('VFREEBUSY', [
368
+            'DTSTART' => $start,
369
+            'DTEND' => $end,
370
+            'ORGANIZER' => $organizerMailto,
371
+            'ATTENDEE' => $organizerMailto,
372
+        ]);
373
+
374
+        $mailtoLen = strlen('mailto:');
375
+        foreach ($attendees as $attendee) {
376
+            if (str_starts_with($attendee, 'mailto:')) {
377
+                $attendee = substr($attendee, $mailtoLen);
378
+            }
379
+
380
+            $attendeeUsers = $this->userManager->getByEmail($attendee);
381
+            if ($attendeeUsers === []) {
382
+                continue;
383
+            }
384
+
385
+            $request->VFREEBUSY->add('ATTENDEE', "mailto:$attendee");
386
+        }
387
+
388
+        $organizerUid = $organizer->getUID();
389
+        $server = $this->serverFactory->createAttendeeAvailabilityServer();
390
+        /** @var CustomPrincipalPlugin $plugin */
391
+        $plugin = $server->getPlugin('auth');
392
+        $plugin->setCurrentPrincipal("principals/users/$organizerUid");
393
+
394
+        $request = new Request(
395
+            'POST',
396
+            "/calendars/$organizerUid/outbox/",
397
+            [
398
+                'Content-Type' => 'text/calendar',
399
+                'Depth' => 0,
400
+            ],
401
+            $request->serialize(),
402
+        );
403
+        $response = new Response();
404
+        $server->invokeMethod($request, $response, false);
405
+
406
+        $xmlService = new \Sabre\Xml\Service();
407
+        $xmlService->elementMap = [
408
+            '{urn:ietf:params:xml:ns:caldav}response' => 'Sabre\Xml\Deserializer\keyValue',
409
+            '{urn:ietf:params:xml:ns:caldav}recipient' => 'Sabre\Xml\Deserializer\keyValue',
410
+        ];
411
+        $parsedResponse = $xmlService->parse($response->getBodyAsString());
412
+
413
+        $result = [];
414
+        foreach ($parsedResponse as $freeBusyResponse) {
415
+            $freeBusyResponse = $freeBusyResponse['value'];
416
+            if ($freeBusyResponse['{urn:ietf:params:xml:ns:caldav}request-status'] !== '2.0;Success') {
417
+                continue;
418
+            }
419
+
420
+            $freeBusyResponseData = \Sabre\VObject\Reader::read(
421
+                $freeBusyResponse['{urn:ietf:params:xml:ns:caldav}calendar-data']
422
+            );
423
+
424
+            $attendee = substr(
425
+                $freeBusyResponse['{urn:ietf:params:xml:ns:caldav}recipient']['{DAV:}href'],
426
+                $mailtoLen,
427
+            );
428
+
429
+            $vFreeBusy = $freeBusyResponseData->VFREEBUSY;
430
+            if (!($vFreeBusy instanceof VFreeBusy)) {
431
+                continue;
432
+            }
433
+
434
+            // TODO: actually check values of FREEBUSY properties to find a free slot
435
+            $result[] = new AvailabilityResult($attendee, $vFreeBusy->isFree($start, $end));
436
+        }
437
+
438
+        return $result;
439
+    }
440 440
 }
Please login to merge, or discard this patch.
Spacing   +4 added lines, -4 removed lines patch added patch discarded remove patch
@@ -173,12 +173,12 @@  discard block
 block discarded – undo
173 173
 		}
174 174
 
175 175
 		return array_merge(
176
-			...array_map(function ($registration) use ($principalUri, $calendarUris) {
176
+			...array_map(function($registration) use ($principalUri, $calendarUris) {
177 177
 				try {
178 178
 					/** @var ICalendarProvider $provider */
179 179
 					$provider = $this->container->get($registration->getService());
180 180
 				} catch (Throwable $e) {
181
-					$this->logger->error('Could not load calendar provider ' . $registration->getService() . ': ' . $e->getMessage(), [
181
+					$this->logger->error('Could not load calendar provider '.$registration->getService().': '.$e->getMessage(), [
182 182
 						'exception' => $e,
183 183
 					]);
184 184
 					return [];
@@ -229,7 +229,7 @@  discard block
 block discarded – undo
229 229
 		string $message,
230 230
 	): bool {
231 231
 
232
-		$userUri = 'principals/users/' . $userId;
232
+		$userUri = 'principals/users/'.$userId;
233 233
 
234 234
 		$userCalendars = $this->getCalendarsForPrincipal($userUri);
235 235
 		if (empty($userCalendars)) {
@@ -361,7 +361,7 @@  discard block
 block discarded – undo
361 361
 		IUser $organizer,
362 362
 		array $attendees,
363 363
 	): array {
364
-		$organizerMailto = 'mailto:' . $organizer->getEMailAddress();
364
+		$organizerMailto = 'mailto:'.$organizer->getEMailAddress();
365 365
 		$request = new VCalendar();
366 366
 		$request->METHOD = 'REQUEST';
367 367
 		$request->add('VFREEBUSY', [
Please login to merge, or discard this patch.
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
-	public 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
+    public 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.
apps/dav/lib/CalDAV/CalendarImpl.php 2 patches
Indentation   +303 added lines, -303 removed lines patch added patch discarded remove patch
@@ -33,308 +33,308 @@
 block discarded – undo
33 33
 use function Sabre\Uri\split as uriSplit;
34 34
 
35 35
 class CalendarImpl implements ICreateFromString, IHandleImipMessage, ICalendarIsWritable, ICalendarIsShared, ICalendarExport, ICalendarIsEnabled {
36
-	public function __construct(
37
-		private Calendar $calendar,
38
-		/** @var array<string, mixed> */
39
-		private array $calendarInfo,
40
-		private CalDavBackend $backend,
41
-	) {
42
-	}
43
-
44
-	private const DAV_PROPERTY_USER_ADDRESS = '{http://sabredav.org/ns}email-address';
45
-	private const DAV_PROPERTY_USER_ADDRESSES = '{urn:ietf:params:xml:ns:caldav}calendar-user-address-set';
46
-
47
-	/**
48
-	 * @return string defining the technical unique key
49
-	 * @since 13.0.0
50
-	 */
51
-	public function getKey(): string {
52
-		return (string)$this->calendarInfo['id'];
53
-	}
54
-
55
-	/**
56
-	 * {@inheritDoc}
57
-	 */
58
-	public function getUri(): string {
59
-		return $this->calendarInfo['uri'];
60
-	}
61
-
62
-	/**
63
-	 * @return string the principal URI of the calendar owner
64
-	 * @since 32.0.0
65
-	 */
66
-	public function getPrincipalUri(): string {
67
-		return $this->calendarInfo['principaluri'];
68
-	}
69
-
70
-	/**
71
-	 * In comparison to getKey() this function returns a human readable (maybe translated) name
72
-	 * @since 13.0.0
73
-	 */
74
-	public function getDisplayName(): ?string {
75
-		return $this->calendarInfo['{DAV:}displayname'];
76
-	}
77
-
78
-	/**
79
-	 * Calendar color
80
-	 * @since 13.0.0
81
-	 */
82
-	public function getDisplayColor(): ?string {
83
-		return $this->calendarInfo['{http://apple.com/ns/ical/}calendar-color'];
84
-	}
85
-
86
-	public function getSchedulingTransparency(): ?ScheduleCalendarTransp {
87
-		return $this->calendarInfo['{' . \OCA\DAV\CalDAV\Schedule\Plugin::NS_CALDAV . '}schedule-calendar-transp'];
88
-	}
89
-
90
-	public function getSchedulingTimezone(): ?VTimeZone {
91
-		$tzProp = '{' . \OCA\DAV\CalDAV\Schedule\Plugin::NS_CALDAV . '}calendar-timezone';
92
-		if (!isset($this->calendarInfo[$tzProp])) {
93
-			return null;
94
-		}
95
-		// This property contains a VCALENDAR with a single VTIMEZONE
96
-		/** @var string $timezoneProp */
97
-		$timezoneProp = $this->calendarInfo[$tzProp];
98
-		/** @var VCalendar $vobj */
99
-		$vobj = Reader::read($timezoneProp);
100
-		$components = $vobj->getComponents();
101
-		if (empty($components)) {
102
-			return null;
103
-		}
104
-		/** @var VTimeZone $vtimezone */
105
-		$vtimezone = $components[0];
106
-		return $vtimezone;
107
-	}
108
-
109
-	public function search(string $pattern, array $searchProperties = [], array $options = [], $limit = null, $offset = null): array {
110
-		return $this->backend->search($this->calendarInfo, $pattern,
111
-			$searchProperties, $options, $limit, $offset);
112
-	}
113
-
114
-	/**
115
-	 * @return int build up using \OCP\Constants
116
-	 * @since 13.0.0
117
-	 */
118
-	public function getPermissions(): int {
119
-		$permissions = $this->calendar->getACL();
120
-		$result = 0;
121
-		foreach ($permissions as $permission) {
122
-			if ($this->calendarInfo['principaluri'] !== $permission['principal']) {
123
-				continue;
124
-			}
125
-
126
-			switch ($permission['privilege']) {
127
-				case '{DAV:}read':
128
-					$result |= Constants::PERMISSION_READ;
129
-					break;
130
-				case '{DAV:}write':
131
-					$result |= Constants::PERMISSION_CREATE;
132
-					$result |= Constants::PERMISSION_UPDATE;
133
-					break;
134
-				case '{DAV:}all':
135
-					$result |= Constants::PERMISSION_ALL;
136
-					break;
137
-			}
138
-		}
139
-
140
-		return $result;
141
-	}
142
-
143
-	/**
144
-	 * @since 32.0.0
145
-	 */
146
-	public function isEnabled(): bool {
147
-		return $this->calendarInfo['{http://owncloud.org/ns}calendar-enabled'] ?? true;
148
-	}
149
-
150
-	/**
151
-	 * @since 31.0.0
152
-	 */
153
-	public function isWritable(): bool {
154
-		return $this->calendar->canWrite();
155
-	}
156
-
157
-	/**
158
-	 * @since 26.0.0
159
-	 */
160
-	public function isDeleted(): bool {
161
-		return $this->calendar->isDeleted();
162
-	}
163
-
164
-	/**
165
-	 * @since 31.0.0
166
-	 */
167
-	public function isShared(): bool {
168
-		return $this->calendar->isShared();
169
-	}
170
-
171
-	/**
172
-	 * @throws CalendarException
173
-	 */
174
-	private function createFromStringInServer(
175
-		string $name,
176
-		string $calendarData,
177
-		Server $server,
178
-	): void {
179
-		/** @var CustomPrincipalPlugin $plugin */
180
-		$plugin = $server->getPlugin('auth');
181
-		// we're working around the previous implementation
182
-		// that only allowed the public system principal to be used
183
-		// so set the custom principal here
184
-		$plugin->setCurrentPrincipal($this->calendar->getPrincipalURI());
185
-
186
-		if (empty($this->calendarInfo['uri'])) {
187
-			throw new CalendarException('Could not write to calendar as URI parameter is missing');
188
-		}
189
-
190
-		// Build full calendar path
191
-		[, $user] = uriSplit($this->calendar->getPrincipalURI());
192
-		$fullCalendarFilename = sprintf('calendars/%s/%s/%s', $user, $this->calendarInfo['uri'], $name);
193
-
194
-		// Force calendar change URI
195
-		/** @var Schedule\Plugin $schedulingPlugin */
196
-		$schedulingPlugin = $server->getPlugin('caldav-schedule');
197
-		$schedulingPlugin->setPathOfCalendarObjectChange($fullCalendarFilename);
198
-
199
-		$stream = fopen('php://memory', 'rb+');
200
-		fwrite($stream, $calendarData);
201
-		rewind($stream);
202
-		try {
203
-			$server->createFile($fullCalendarFilename, $stream);
204
-		} catch (Conflict $e) {
205
-			throw new CalendarException('Could not create new calendar event: ' . $e->getMessage(), 0, $e);
206
-		} finally {
207
-			fclose($stream);
208
-		}
209
-	}
210
-
211
-	public function createFromString(string $name, string $calendarData): void {
212
-		$server = new EmbeddedCalDavServer(false);
213
-		$this->createFromStringInServer($name, $calendarData, $server->getServer());
214
-	}
215
-
216
-	public function createFromStringMinimal(string $name, string $calendarData): void {
217
-		$server = new InvitationResponseServer(false);
218
-		$this->createFromStringInServer($name, $calendarData, $server->getServer());
219
-	}
220
-
221
-	/**
222
-	 * @throws CalendarException
223
-	 */
224
-	public function handleIMipMessage(string $name, string $calendarData): void {
225
-
226
-		try {
227
-			/** @var VCalendar $vObject|null */
228
-			$vObject = Reader::read($calendarData);
229
-		} catch (ParseException $e) {
230
-			throw new CalendarException('iMip message could not be processed because an error occurred while parsing the iMip message', 0, $e);
231
-		}
232
-		// validate the iMip message
233
-		if (!isset($vObject->METHOD)) {
234
-			throw new CalendarException('iMip message contains no valid method');
235
-		}
236
-		if (!isset($vObject->VEVENT)) {
237
-			throw new CalendarException('iMip message contains no event');
238
-		}
239
-		if (!isset($vObject->VEVENT->UID)) {
240
-			throw new CalendarException('iMip message event dose not contain a UID');
241
-		}
242
-		if (!isset($vObject->VEVENT->ORGANIZER)) {
243
-			throw new CalendarException('iMip message event dose not contain an organizer');
244
-		}
245
-		if (!isset($vObject->VEVENT->ATTENDEE)) {
246
-			throw new CalendarException('iMip message event dose not contain an attendee');
247
-		}
248
-		if (empty($this->calendarInfo['uri'])) {
249
-			throw new CalendarException('Could not write to calendar as URI parameter is missing');
250
-		}
251
-		// construct dav server
252
-		$server = $this->getInvitationResponseServer();
253
-		/** @var CustomPrincipalPlugin $authPlugin */
254
-		$authPlugin = $server->getServer()->getPlugin('auth');
255
-		// we're working around the previous implementation
256
-		// that only allowed the public system principal to be used
257
-		// so set the custom principal here
258
-		$authPlugin->setCurrentPrincipal($this->calendar->getPrincipalURI());
259
-		// retrieve all users addresses
260
-		$userProperties = $server->getServer()->getProperties($this->calendar->getPrincipalURI(), [ self::DAV_PROPERTY_USER_ADDRESS, self::DAV_PROPERTY_USER_ADDRESSES ]);
261
-		$userAddress = 'mailto:' . ($userProperties[self::DAV_PROPERTY_USER_ADDRESS] ?? null);
262
-		$userAddresses = $userProperties[self::DAV_PROPERTY_USER_ADDRESSES]->getHrefs() ?? [];
263
-		$userAddresses = array_map('strtolower', array_map('urldecode', $userAddresses));
264
-		// validate the method, recipient and sender
265
-		$imipMethod = strtoupper($vObject->METHOD->getValue());
266
-		if (in_array($imipMethod, ['REPLY', 'REFRESH'], true)) {
267
-			// extract sender (REPLY and REFRESH method should only have one attendee)
268
-			$sender = strtolower($vObject->VEVENT->ATTENDEE->getValue());
269
-			// extract and verify the recipient
270
-			$recipient = strtolower($vObject->VEVENT->ORGANIZER->getValue());
271
-			if (!in_array($recipient, $userAddresses, true)) {
272
-				throw new CalendarException('iMip message dose not contain an organizer that matches the user');
273
-			}
274
-			// if the recipient address is not the same as the user address this means an alias was used
275
-			// the iTip broker uses the users primary email address during processing
276
-			if ($userAddress !== $recipient) {
277
-				$recipient = $userAddress;
278
-			}
279
-		} elseif (in_array($imipMethod, ['PUBLISH', 'REQUEST', 'ADD', 'CANCEL'], true)) {
280
-			// extract sender
281
-			$sender = strtolower($vObject->VEVENT->ORGANIZER->getValue());
282
-			// extract and verify the recipient
283
-			foreach ($vObject->VEVENT->ATTENDEE as $attendee) {
284
-				$recipient = strtolower($attendee->getValue());
285
-				if (in_array($recipient, $userAddresses, true)) {
286
-					break;
287
-				}
288
-				$recipient = null;
289
-			}
290
-			if ($recipient === null) {
291
-				throw new CalendarException('iMip message dose not contain an attendee that matches the user');
292
-			}
293
-			// if the recipient address is not the same as the user address this means an alias was used
294
-			// the iTip broker uses the users primary email address during processing
295
-			if ($userAddress !== $recipient) {
296
-				$recipient = $userAddress;
297
-			}
298
-		} else {
299
-			throw new CalendarException('iMip message contains a method that is not supported: ' . $imipMethod);
300
-		}
301
-		// generate the iTip message
302
-		$iTip = new Message();
303
-		$iTip->method = $imipMethod;
304
-		$iTip->sender = $sender;
305
-		$iTip->recipient = $recipient;
306
-		$iTip->component = 'VEVENT';
307
-		$iTip->uid = $vObject->VEVENT->UID->getValue();
308
-		$iTip->sequence = isset($vObject->VEVENT->SEQUENCE) ? (int)$vObject->VEVENT->SEQUENCE->getValue() : 1;
309
-		$iTip->message = $vObject;
310
-
311
-		$server->server->emit('schedule', [$iTip]);
312
-	}
313
-
314
-	public function getInvitationResponseServer(): InvitationResponseServer {
315
-		return new InvitationResponseServer(false);
316
-	}
317
-
318
-	/**
319
-	 * Export objects
320
-	 *
321
-	 * @since 32.0.0
322
-	 *
323
-	 * @return Generator<mixed, \Sabre\VObject\Component\VCalendar, mixed, mixed>
324
-	 */
325
-	public function export(?CalendarExportOptions $options = null): Generator {
326
-		foreach (
327
-			$this->backend->exportCalendar(
328
-				$this->calendarInfo['id'],
329
-				$this->backend::CALENDAR_TYPE_CALENDAR,
330
-				$options
331
-			) as $event
332
-		) {
333
-			$vObject = Reader::read($event['calendardata']);
334
-			if ($vObject instanceof VCalendar) {
335
-				yield $vObject;
336
-			}
337
-		}
338
-	}
36
+    public function __construct(
37
+        private Calendar $calendar,
38
+        /** @var array<string, mixed> */
39
+        private array $calendarInfo,
40
+        private CalDavBackend $backend,
41
+    ) {
42
+    }
43
+
44
+    private const DAV_PROPERTY_USER_ADDRESS = '{http://sabredav.org/ns}email-address';
45
+    private const DAV_PROPERTY_USER_ADDRESSES = '{urn:ietf:params:xml:ns:caldav}calendar-user-address-set';
46
+
47
+    /**
48
+     * @return string defining the technical unique key
49
+     * @since 13.0.0
50
+     */
51
+    public function getKey(): string {
52
+        return (string)$this->calendarInfo['id'];
53
+    }
54
+
55
+    /**
56
+     * {@inheritDoc}
57
+     */
58
+    public function getUri(): string {
59
+        return $this->calendarInfo['uri'];
60
+    }
61
+
62
+    /**
63
+     * @return string the principal URI of the calendar owner
64
+     * @since 32.0.0
65
+     */
66
+    public function getPrincipalUri(): string {
67
+        return $this->calendarInfo['principaluri'];
68
+    }
69
+
70
+    /**
71
+     * In comparison to getKey() this function returns a human readable (maybe translated) name
72
+     * @since 13.0.0
73
+     */
74
+    public function getDisplayName(): ?string {
75
+        return $this->calendarInfo['{DAV:}displayname'];
76
+    }
77
+
78
+    /**
79
+     * Calendar color
80
+     * @since 13.0.0
81
+     */
82
+    public function getDisplayColor(): ?string {
83
+        return $this->calendarInfo['{http://apple.com/ns/ical/}calendar-color'];
84
+    }
85
+
86
+    public function getSchedulingTransparency(): ?ScheduleCalendarTransp {
87
+        return $this->calendarInfo['{' . \OCA\DAV\CalDAV\Schedule\Plugin::NS_CALDAV . '}schedule-calendar-transp'];
88
+    }
89
+
90
+    public function getSchedulingTimezone(): ?VTimeZone {
91
+        $tzProp = '{' . \OCA\DAV\CalDAV\Schedule\Plugin::NS_CALDAV . '}calendar-timezone';
92
+        if (!isset($this->calendarInfo[$tzProp])) {
93
+            return null;
94
+        }
95
+        // This property contains a VCALENDAR with a single VTIMEZONE
96
+        /** @var string $timezoneProp */
97
+        $timezoneProp = $this->calendarInfo[$tzProp];
98
+        /** @var VCalendar $vobj */
99
+        $vobj = Reader::read($timezoneProp);
100
+        $components = $vobj->getComponents();
101
+        if (empty($components)) {
102
+            return null;
103
+        }
104
+        /** @var VTimeZone $vtimezone */
105
+        $vtimezone = $components[0];
106
+        return $vtimezone;
107
+    }
108
+
109
+    public function search(string $pattern, array $searchProperties = [], array $options = [], $limit = null, $offset = null): array {
110
+        return $this->backend->search($this->calendarInfo, $pattern,
111
+            $searchProperties, $options, $limit, $offset);
112
+    }
113
+
114
+    /**
115
+     * @return int build up using \OCP\Constants
116
+     * @since 13.0.0
117
+     */
118
+    public function getPermissions(): int {
119
+        $permissions = $this->calendar->getACL();
120
+        $result = 0;
121
+        foreach ($permissions as $permission) {
122
+            if ($this->calendarInfo['principaluri'] !== $permission['principal']) {
123
+                continue;
124
+            }
125
+
126
+            switch ($permission['privilege']) {
127
+                case '{DAV:}read':
128
+                    $result |= Constants::PERMISSION_READ;
129
+                    break;
130
+                case '{DAV:}write':
131
+                    $result |= Constants::PERMISSION_CREATE;
132
+                    $result |= Constants::PERMISSION_UPDATE;
133
+                    break;
134
+                case '{DAV:}all':
135
+                    $result |= Constants::PERMISSION_ALL;
136
+                    break;
137
+            }
138
+        }
139
+
140
+        return $result;
141
+    }
142
+
143
+    /**
144
+     * @since 32.0.0
145
+     */
146
+    public function isEnabled(): bool {
147
+        return $this->calendarInfo['{http://owncloud.org/ns}calendar-enabled'] ?? true;
148
+    }
149
+
150
+    /**
151
+     * @since 31.0.0
152
+     */
153
+    public function isWritable(): bool {
154
+        return $this->calendar->canWrite();
155
+    }
156
+
157
+    /**
158
+     * @since 26.0.0
159
+     */
160
+    public function isDeleted(): bool {
161
+        return $this->calendar->isDeleted();
162
+    }
163
+
164
+    /**
165
+     * @since 31.0.0
166
+     */
167
+    public function isShared(): bool {
168
+        return $this->calendar->isShared();
169
+    }
170
+
171
+    /**
172
+     * @throws CalendarException
173
+     */
174
+    private function createFromStringInServer(
175
+        string $name,
176
+        string $calendarData,
177
+        Server $server,
178
+    ): void {
179
+        /** @var CustomPrincipalPlugin $plugin */
180
+        $plugin = $server->getPlugin('auth');
181
+        // we're working around the previous implementation
182
+        // that only allowed the public system principal to be used
183
+        // so set the custom principal here
184
+        $plugin->setCurrentPrincipal($this->calendar->getPrincipalURI());
185
+
186
+        if (empty($this->calendarInfo['uri'])) {
187
+            throw new CalendarException('Could not write to calendar as URI parameter is missing');
188
+        }
189
+
190
+        // Build full calendar path
191
+        [, $user] = uriSplit($this->calendar->getPrincipalURI());
192
+        $fullCalendarFilename = sprintf('calendars/%s/%s/%s', $user, $this->calendarInfo['uri'], $name);
193
+
194
+        // Force calendar change URI
195
+        /** @var Schedule\Plugin $schedulingPlugin */
196
+        $schedulingPlugin = $server->getPlugin('caldav-schedule');
197
+        $schedulingPlugin->setPathOfCalendarObjectChange($fullCalendarFilename);
198
+
199
+        $stream = fopen('php://memory', 'rb+');
200
+        fwrite($stream, $calendarData);
201
+        rewind($stream);
202
+        try {
203
+            $server->createFile($fullCalendarFilename, $stream);
204
+        } catch (Conflict $e) {
205
+            throw new CalendarException('Could not create new calendar event: ' . $e->getMessage(), 0, $e);
206
+        } finally {
207
+            fclose($stream);
208
+        }
209
+    }
210
+
211
+    public function createFromString(string $name, string $calendarData): void {
212
+        $server = new EmbeddedCalDavServer(false);
213
+        $this->createFromStringInServer($name, $calendarData, $server->getServer());
214
+    }
215
+
216
+    public function createFromStringMinimal(string $name, string $calendarData): void {
217
+        $server = new InvitationResponseServer(false);
218
+        $this->createFromStringInServer($name, $calendarData, $server->getServer());
219
+    }
220
+
221
+    /**
222
+     * @throws CalendarException
223
+     */
224
+    public function handleIMipMessage(string $name, string $calendarData): void {
225
+
226
+        try {
227
+            /** @var VCalendar $vObject|null */
228
+            $vObject = Reader::read($calendarData);
229
+        } catch (ParseException $e) {
230
+            throw new CalendarException('iMip message could not be processed because an error occurred while parsing the iMip message', 0, $e);
231
+        }
232
+        // validate the iMip message
233
+        if (!isset($vObject->METHOD)) {
234
+            throw new CalendarException('iMip message contains no valid method');
235
+        }
236
+        if (!isset($vObject->VEVENT)) {
237
+            throw new CalendarException('iMip message contains no event');
238
+        }
239
+        if (!isset($vObject->VEVENT->UID)) {
240
+            throw new CalendarException('iMip message event dose not contain a UID');
241
+        }
242
+        if (!isset($vObject->VEVENT->ORGANIZER)) {
243
+            throw new CalendarException('iMip message event dose not contain an organizer');
244
+        }
245
+        if (!isset($vObject->VEVENT->ATTENDEE)) {
246
+            throw new CalendarException('iMip message event dose not contain an attendee');
247
+        }
248
+        if (empty($this->calendarInfo['uri'])) {
249
+            throw new CalendarException('Could not write to calendar as URI parameter is missing');
250
+        }
251
+        // construct dav server
252
+        $server = $this->getInvitationResponseServer();
253
+        /** @var CustomPrincipalPlugin $authPlugin */
254
+        $authPlugin = $server->getServer()->getPlugin('auth');
255
+        // we're working around the previous implementation
256
+        // that only allowed the public system principal to be used
257
+        // so set the custom principal here
258
+        $authPlugin->setCurrentPrincipal($this->calendar->getPrincipalURI());
259
+        // retrieve all users addresses
260
+        $userProperties = $server->getServer()->getProperties($this->calendar->getPrincipalURI(), [ self::DAV_PROPERTY_USER_ADDRESS, self::DAV_PROPERTY_USER_ADDRESSES ]);
261
+        $userAddress = 'mailto:' . ($userProperties[self::DAV_PROPERTY_USER_ADDRESS] ?? null);
262
+        $userAddresses = $userProperties[self::DAV_PROPERTY_USER_ADDRESSES]->getHrefs() ?? [];
263
+        $userAddresses = array_map('strtolower', array_map('urldecode', $userAddresses));
264
+        // validate the method, recipient and sender
265
+        $imipMethod = strtoupper($vObject->METHOD->getValue());
266
+        if (in_array($imipMethod, ['REPLY', 'REFRESH'], true)) {
267
+            // extract sender (REPLY and REFRESH method should only have one attendee)
268
+            $sender = strtolower($vObject->VEVENT->ATTENDEE->getValue());
269
+            // extract and verify the recipient
270
+            $recipient = strtolower($vObject->VEVENT->ORGANIZER->getValue());
271
+            if (!in_array($recipient, $userAddresses, true)) {
272
+                throw new CalendarException('iMip message dose not contain an organizer that matches the user');
273
+            }
274
+            // if the recipient address is not the same as the user address this means an alias was used
275
+            // the iTip broker uses the users primary email address during processing
276
+            if ($userAddress !== $recipient) {
277
+                $recipient = $userAddress;
278
+            }
279
+        } elseif (in_array($imipMethod, ['PUBLISH', 'REQUEST', 'ADD', 'CANCEL'], true)) {
280
+            // extract sender
281
+            $sender = strtolower($vObject->VEVENT->ORGANIZER->getValue());
282
+            // extract and verify the recipient
283
+            foreach ($vObject->VEVENT->ATTENDEE as $attendee) {
284
+                $recipient = strtolower($attendee->getValue());
285
+                if (in_array($recipient, $userAddresses, true)) {
286
+                    break;
287
+                }
288
+                $recipient = null;
289
+            }
290
+            if ($recipient === null) {
291
+                throw new CalendarException('iMip message dose not contain an attendee that matches the user');
292
+            }
293
+            // if the recipient address is not the same as the user address this means an alias was used
294
+            // the iTip broker uses the users primary email address during processing
295
+            if ($userAddress !== $recipient) {
296
+                $recipient = $userAddress;
297
+            }
298
+        } else {
299
+            throw new CalendarException('iMip message contains a method that is not supported: ' . $imipMethod);
300
+        }
301
+        // generate the iTip message
302
+        $iTip = new Message();
303
+        $iTip->method = $imipMethod;
304
+        $iTip->sender = $sender;
305
+        $iTip->recipient = $recipient;
306
+        $iTip->component = 'VEVENT';
307
+        $iTip->uid = $vObject->VEVENT->UID->getValue();
308
+        $iTip->sequence = isset($vObject->VEVENT->SEQUENCE) ? (int)$vObject->VEVENT->SEQUENCE->getValue() : 1;
309
+        $iTip->message = $vObject;
310
+
311
+        $server->server->emit('schedule', [$iTip]);
312
+    }
313
+
314
+    public function getInvitationResponseServer(): InvitationResponseServer {
315
+        return new InvitationResponseServer(false);
316
+    }
317
+
318
+    /**
319
+     * Export objects
320
+     *
321
+     * @since 32.0.0
322
+     *
323
+     * @return Generator<mixed, \Sabre\VObject\Component\VCalendar, mixed, mixed>
324
+     */
325
+    public function export(?CalendarExportOptions $options = null): Generator {
326
+        foreach (
327
+            $this->backend->exportCalendar(
328
+                $this->calendarInfo['id'],
329
+                $this->backend::CALENDAR_TYPE_CALENDAR,
330
+                $options
331
+            ) as $event
332
+        ) {
333
+            $vObject = Reader::read($event['calendardata']);
334
+            if ($vObject instanceof VCalendar) {
335
+                yield $vObject;
336
+            }
337
+        }
338
+    }
339 339
 
340 340
 }
Please login to merge, or discard this patch.
Spacing   +8 added lines, -8 removed lines patch added patch discarded remove patch
@@ -49,7 +49,7 @@  discard block
 block discarded – undo
49 49
 	 * @since 13.0.0
50 50
 	 */
51 51
 	public function getKey(): string {
52
-		return (string)$this->calendarInfo['id'];
52
+		return (string) $this->calendarInfo['id'];
53 53
 	}
54 54
 
55 55
 	/**
@@ -84,11 +84,11 @@  discard block
 block discarded – undo
84 84
 	}
85 85
 
86 86
 	public function getSchedulingTransparency(): ?ScheduleCalendarTransp {
87
-		return $this->calendarInfo['{' . \OCA\DAV\CalDAV\Schedule\Plugin::NS_CALDAV . '}schedule-calendar-transp'];
87
+		return $this->calendarInfo['{'.\OCA\DAV\CalDAV\Schedule\Plugin::NS_CALDAV.'}schedule-calendar-transp'];
88 88
 	}
89 89
 
90 90
 	public function getSchedulingTimezone(): ?VTimeZone {
91
-		$tzProp = '{' . \OCA\DAV\CalDAV\Schedule\Plugin::NS_CALDAV . '}calendar-timezone';
91
+		$tzProp = '{'.\OCA\DAV\CalDAV\Schedule\Plugin::NS_CALDAV.'}calendar-timezone';
92 92
 		if (!isset($this->calendarInfo[$tzProp])) {
93 93
 			return null;
94 94
 		}
@@ -202,7 +202,7 @@  discard block
 block discarded – undo
202 202
 		try {
203 203
 			$server->createFile($fullCalendarFilename, $stream);
204 204
 		} catch (Conflict $e) {
205
-			throw new CalendarException('Could not create new calendar event: ' . $e->getMessage(), 0, $e);
205
+			throw new CalendarException('Could not create new calendar event: '.$e->getMessage(), 0, $e);
206 206
 		} finally {
207 207
 			fclose($stream);
208 208
 		}
@@ -257,8 +257,8 @@  discard block
 block discarded – undo
257 257
 		// so set the custom principal here
258 258
 		$authPlugin->setCurrentPrincipal($this->calendar->getPrincipalURI());
259 259
 		// retrieve all users addresses
260
-		$userProperties = $server->getServer()->getProperties($this->calendar->getPrincipalURI(), [ self::DAV_PROPERTY_USER_ADDRESS, self::DAV_PROPERTY_USER_ADDRESSES ]);
261
-		$userAddress = 'mailto:' . ($userProperties[self::DAV_PROPERTY_USER_ADDRESS] ?? null);
260
+		$userProperties = $server->getServer()->getProperties($this->calendar->getPrincipalURI(), [self::DAV_PROPERTY_USER_ADDRESS, self::DAV_PROPERTY_USER_ADDRESSES]);
261
+		$userAddress = 'mailto:'.($userProperties[self::DAV_PROPERTY_USER_ADDRESS] ?? null);
262 262
 		$userAddresses = $userProperties[self::DAV_PROPERTY_USER_ADDRESSES]->getHrefs() ?? [];
263 263
 		$userAddresses = array_map('strtolower', array_map('urldecode', $userAddresses));
264 264
 		// validate the method, recipient and sender
@@ -296,7 +296,7 @@  discard block
 block discarded – undo
296 296
 				$recipient = $userAddress;
297 297
 			}
298 298
 		} else {
299
-			throw new CalendarException('iMip message contains a method that is not supported: ' . $imipMethod);
299
+			throw new CalendarException('iMip message contains a method that is not supported: '.$imipMethod);
300 300
 		}
301 301
 		// generate the iTip message
302 302
 		$iTip = new Message();
@@ -305,7 +305,7 @@  discard block
 block discarded – undo
305 305
 		$iTip->recipient = $recipient;
306 306
 		$iTip->component = 'VEVENT';
307 307
 		$iTip->uid = $vObject->VEVENT->UID->getValue();
308
-		$iTip->sequence = isset($vObject->VEVENT->SEQUENCE) ? (int)$vObject->VEVENT->SEQUENCE->getValue() : 1;
308
+		$iTip->sequence = isset($vObject->VEVENT->SEQUENCE) ? (int) $vObject->VEVENT->SEQUENCE->getValue() : 1;
309 309
 		$iTip->message = $vObject;
310 310
 
311 311
 		$server->server->emit('schedule', [$iTip]);
Please login to merge, or discard this patch.
apps/dav/tests/unit/CalDAV/CalendarImplTest.php 2 patches
Indentation   +246 added lines, -246 removed lines patch added patch discarded remove patch
@@ -20,251 +20,251 @@
 block discarded – undo
20 20
 use Sabre\VObject\ITip\Message;
21 21
 
22 22
 class CalendarImplTest extends \Test\TestCase {
23
-	private CalDavBackend|MockObject $backend;
24
-	private Calendar|MockObject $calendar;
25
-	private CalendarImpl|MockObject $calendarImpl;
26
-	private array $calendarInfo;
27
-	private VCalendar $vCalendar1a;
28
-	private array $mockExportCollection;
29
-
30
-	protected function setUp(): void {
31
-		parent::setUp();
32
-
33
-		$this->backend = $this->createMock(CalDavBackend::class);
34
-		$this->calendar = $this->createMock(Calendar::class);
35
-		$this->calendarInfo = [
36
-			'id' => 'fancy_id_123',
37
-			'{DAV:}displayname' => 'user readable name 123',
38
-			'{http://apple.com/ns/ical/}calendar-color' => '#AABBCC',
39
-			'uri' => '/this/is/a/uri',
40
-			'principaluri' => 'principal/users/foobar'
41
-		];
42
-		$this->calendarImpl = new CalendarImpl($this->calendar, $this->calendarInfo, $this->backend);
43
-
44
-		// construct calendar with a 1 hour event and same start/end time zones
45
-		$this->vCalendar1a = new VCalendar();
46
-		/** @var VEvent $vEvent */
47
-		$vEvent = $this->vCalendar1a->add('VEVENT', []);
48
-		$vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
49
-		$vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']);
50
-		$vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Toronto']);
51
-		$vEvent->add('SEQUENCE', 1);
52
-		$vEvent->add('SUMMARY', 'Test Event');
53
-		$vEvent->add('ORGANIZER', 'mailto:[email protected]', ['CN' => 'Organizer']);
54
-		$vEvent->add('ATTENDEE', 'mailto:[email protected]', [
55
-			'CN' => 'Attendee One',
56
-			'CUTYPE' => 'INDIVIDUAL',
57
-			'PARTSTAT' => 'NEEDS-ACTION',
58
-			'ROLE' => 'REQ-PARTICIPANT',
59
-			'RSVP' => 'TRUE'
60
-		]);
61
-	}
62
-
63
-
64
-	public function testGetKey(): void {
65
-		$this->assertEquals($this->calendarImpl->getKey(), 'fancy_id_123');
66
-	}
67
-
68
-	public function testGetDisplayname(): void {
69
-		$this->assertEquals($this->calendarImpl->getDisplayName(), 'user readable name 123');
70
-	}
71
-
72
-	public function testGetDisplayColor(): void {
73
-		$this->assertEquals($this->calendarImpl->getDisplayColor(), '#AABBCC');
74
-	}
75
-
76
-	public function testSearch(): void {
77
-		$this->backend->expects($this->once())
78
-			->method('search')
79
-			->with($this->calendarInfo, 'abc', ['def'], ['ghi'], 42, 1337)
80
-			->willReturn(['SEARCHRESULTS']);
81
-
82
-		$result = $this->calendarImpl->search('abc', ['def'], ['ghi'], 42, 1337);
83
-		$this->assertEquals($result, ['SEARCHRESULTS']);
84
-	}
85
-
86
-	public function testGetPermissionRead(): void {
87
-		$this->calendar->expects($this->once())
88
-			->method('getACL')
89
-			->with()
90
-			->willReturn([
91
-				['privilege' => '{DAV:}read', 'principal' => 'principal/users/foobar'],
92
-				['privilege' => '{DAV:}read', 'principal' => 'principal/users/other'],
93
-				['privilege' => '{DAV:}write', 'principal' => 'principal/users/other'],
94
-				['privilege' => '{DAV:}all', 'principal' => 'principal/users/other'],
95
-			]);
96
-
97
-		$this->assertEquals(1, $this->calendarImpl->getPermissions());
98
-	}
99
-
100
-	public function testGetPermissionWrite(): void {
101
-		$this->calendar->expects($this->once())
102
-			->method('getACL')
103
-			->with()
104
-			->willReturn([
105
-				['privilege' => '{DAV:}write', 'principal' => 'principal/users/foobar'],
106
-				['privilege' => '{DAV:}read', 'principal' => 'principal/users/other'],
107
-				['privilege' => '{DAV:}all', 'principal' => 'principal/users/other'],
108
-			]);
109
-
110
-		$this->assertEquals(6, $this->calendarImpl->getPermissions());
111
-	}
112
-
113
-	public function testGetPermissionReadWrite(): void {
114
-		$this->calendar->expects($this->once())
115
-			->method('getACL')
116
-			->with()
117
-			->willReturn([
118
-				['privilege' => '{DAV:}write', 'principal' => 'principal/users/foobar'],
119
-				['privilege' => '{DAV:}read', 'principal' => 'principal/users/foobar'],
120
-				['privilege' => '{DAV:}all', 'principal' => 'principal/users/other'],
121
-			]);
122
-
123
-		$this->assertEquals(7, $this->calendarImpl->getPermissions());
124
-	}
125
-
126
-	public function testGetPermissionAll(): void {
127
-		$this->calendar->expects($this->once())
128
-			->method('getACL')
129
-			->with()
130
-			->willReturn([
131
-				['privilege' => '{DAV:}all', 'principal' => 'principal/users/foobar'],
132
-			]);
133
-
134
-		$this->assertEquals(31, $this->calendarImpl->getPermissions());
135
-	}
136
-
137
-	public function testHandleImipNoMethod(): void {
138
-		// Arrange
139
-		$vObject = $this->vCalendar1a;
140
-
141
-		$this->expectException(CalendarException::class);
142
-		$this->expectExceptionMessage('iMip message contains no valid method');
143
-
144
-		// Act
145
-		$this->calendarImpl->handleIMipMessage('fakeUser', $vObject->serialize());
146
-	}
147
-
148
-	public function testHandleImipNoEvent(): void {
149
-		// Arrange
150
-		$vObject = $this->vCalendar1a;
151
-		$vObject->add('METHOD', 'REQUEST');
152
-		$vObject->remove('VEVENT');
153
-
154
-		$this->expectException(CalendarException::class);
155
-		$this->expectExceptionMessage('iMip message contains no event');
156
-
157
-		// Act
158
-		$this->calendarImpl->handleIMipMessage('fakeUser', $vObject->serialize());
159
-	}
160
-
161
-	public function testHandleImipNoUid(): void {
162
-		// Arrange
163
-		$vObject = $this->vCalendar1a;
164
-		$vObject->add('METHOD', 'REQUEST');
165
-		$vObject->VEVENT->remove('UID');
166
-
167
-		$this->expectException(CalendarException::class);
168
-		$this->expectExceptionMessage('iMip message event dose not contain a UID');
169
-
170
-		// Act
171
-		$this->calendarImpl->handleIMipMessage('fakeUser', $vObject->serialize());
172
-	}
173
-
174
-	public function testHandleImipNoOrganizer(): void {
175
-		// Arrange
176
-		$vObject = $this->vCalendar1a;
177
-		$vObject->add('METHOD', 'REQUEST');
178
-		$vObject->VEVENT->remove('ORGANIZER');
179
-
180
-		$this->expectException(CalendarException::class);
181
-		$this->expectExceptionMessage('iMip message event dose not contain an organizer');
182
-
183
-		// Act
184
-		$this->calendarImpl->handleIMipMessage('fakeUser', $vObject->serialize());
185
-	}
186
-
187
-	public function testHandleImipNoAttendee(): void {
188
-		// Arrange
189
-		$vObject = $this->vCalendar1a;
190
-		$vObject->add('METHOD', 'REQUEST');
191
-		$vObject->VEVENT->remove('ATTENDEE');
192
-
193
-		$this->expectException(CalendarException::class);
194
-		$this->expectExceptionMessage('iMip message event dose not contain an attendee');
195
-
196
-		// Act
197
-		$this->calendarImpl->handleIMipMessage('fakeUser', $vObject->serialize());
198
-	}
199
-
200
-	public function testHandleImipRequest(): void {
201
-		$userAddressSet = new class([ 'mailto:[email protected]', '/remote.php/dav/principals/users/attendee1/', ]) {
202
-			public function __construct(
203
-				private array $hrefs,
204
-			) {
205
-			}
206
-			public function getHrefs(): array {
207
-				return $this->hrefs;
208
-			}
209
-		};
210
-
211
-		$vObject = $this->vCalendar1a;
212
-		$vObject->add('METHOD', 'REQUEST');
213
-
214
-		$iTip = new Message();
215
-		$iTip->method = 'REQUEST';
216
-		$iTip->sender = $vObject->VEVENT->ORGANIZER->getValue();
217
-		$iTip->recipient = $vObject->VEVENT->ATTENDEE->getValue();
218
-		$iTip->component = 'VEVENT';
219
-		$iTip->uid = $vObject->VEVENT->UID->getValue();
220
-		$iTip->sequence = (int)$vObject->VEVENT->SEQUENCE->getValue() ?? 0;
221
-		$iTip->message = $vObject;
222
-
223
-		/** @var CustomPrincipalPlugin|MockObject $authPlugin */
224
-		$authPlugin = $this->createMock(CustomPrincipalPlugin::class);
225
-		$authPlugin->expects(self::once())
226
-			->method('setCurrentPrincipal')
227
-			->with($this->calendar->getPrincipalURI());
228
-		/** @var \Sabre\DAVACL\Plugin|MockObject $aclPlugin */
229
-		$aclPlugin = $this->createMock(\Sabre\DAVACL\Plugin::class);
230
-
231
-		$server = $this->createMock(Server::class);
232
-		$server->expects($this->any())
233
-			->method('getPlugin')
234
-			->willReturnMap([
235
-				['auth', $authPlugin],
236
-				['acl', $aclPlugin],
237
-			]);
238
-
239
-		$server->expects(self::once())
240
-			->method('getProperties')
241
-			->with(
242
-				$this->calendar->getPrincipalURI(),
243
-				[
244
-					'{http://sabredav.org/ns}email-address',
245
-					'{urn:ietf:params:xml:ns:caldav}calendar-user-address-set'
246
-				]
247
-			)
248
-			->willReturn([
249
-				'{http://sabredav.org/ns}email-address' => '[email protected]',
250
-				'{urn:ietf:params:xml:ns:caldav}calendar-user-address-set' => $userAddressSet,
251
-			]);
252
-		$server->expects(self::once())
253
-			->method('emit');
254
-		$invitationResponseServer = $this->createMock(InvitationResponseServer::class, ['getServer']);
255
-		$invitationResponseServer->server = $server;
256
-		$invitationResponseServer->expects($this->any())
257
-			->method('getServer')
258
-			->willReturn($server);
259
-		$calendarImpl = $this->getMockBuilder(CalendarImpl::class)
260
-			->setConstructorArgs([$this->calendar, $this->calendarInfo, $this->backend])
261
-			->onlyMethods(['getInvitationResponseServer'])
262
-			->getMock();
263
-		$calendarImpl->expects($this->once())
264
-			->method('getInvitationResponseServer')
265
-			->willReturn($invitationResponseServer);
266
-
267
-		$calendarImpl->handleIMipMessage('fakeUser', $vObject->serialize());
268
-	}
23
+    private CalDavBackend|MockObject $backend;
24
+    private Calendar|MockObject $calendar;
25
+    private CalendarImpl|MockObject $calendarImpl;
26
+    private array $calendarInfo;
27
+    private VCalendar $vCalendar1a;
28
+    private array $mockExportCollection;
29
+
30
+    protected function setUp(): void {
31
+        parent::setUp();
32
+
33
+        $this->backend = $this->createMock(CalDavBackend::class);
34
+        $this->calendar = $this->createMock(Calendar::class);
35
+        $this->calendarInfo = [
36
+            'id' => 'fancy_id_123',
37
+            '{DAV:}displayname' => 'user readable name 123',
38
+            '{http://apple.com/ns/ical/}calendar-color' => '#AABBCC',
39
+            'uri' => '/this/is/a/uri',
40
+            'principaluri' => 'principal/users/foobar'
41
+        ];
42
+        $this->calendarImpl = new CalendarImpl($this->calendar, $this->calendarInfo, $this->backend);
43
+
44
+        // construct calendar with a 1 hour event and same start/end time zones
45
+        $this->vCalendar1a = new VCalendar();
46
+        /** @var VEvent $vEvent */
47
+        $vEvent = $this->vCalendar1a->add('VEVENT', []);
48
+        $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
49
+        $vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']);
50
+        $vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Toronto']);
51
+        $vEvent->add('SEQUENCE', 1);
52
+        $vEvent->add('SUMMARY', 'Test Event');
53
+        $vEvent->add('ORGANIZER', 'mailto:[email protected]', ['CN' => 'Organizer']);
54
+        $vEvent->add('ATTENDEE', 'mailto:[email protected]', [
55
+            'CN' => 'Attendee One',
56
+            'CUTYPE' => 'INDIVIDUAL',
57
+            'PARTSTAT' => 'NEEDS-ACTION',
58
+            'ROLE' => 'REQ-PARTICIPANT',
59
+            'RSVP' => 'TRUE'
60
+        ]);
61
+    }
62
+
63
+
64
+    public function testGetKey(): void {
65
+        $this->assertEquals($this->calendarImpl->getKey(), 'fancy_id_123');
66
+    }
67
+
68
+    public function testGetDisplayname(): void {
69
+        $this->assertEquals($this->calendarImpl->getDisplayName(), 'user readable name 123');
70
+    }
71
+
72
+    public function testGetDisplayColor(): void {
73
+        $this->assertEquals($this->calendarImpl->getDisplayColor(), '#AABBCC');
74
+    }
75
+
76
+    public function testSearch(): void {
77
+        $this->backend->expects($this->once())
78
+            ->method('search')
79
+            ->with($this->calendarInfo, 'abc', ['def'], ['ghi'], 42, 1337)
80
+            ->willReturn(['SEARCHRESULTS']);
81
+
82
+        $result = $this->calendarImpl->search('abc', ['def'], ['ghi'], 42, 1337);
83
+        $this->assertEquals($result, ['SEARCHRESULTS']);
84
+    }
85
+
86
+    public function testGetPermissionRead(): void {
87
+        $this->calendar->expects($this->once())
88
+            ->method('getACL')
89
+            ->with()
90
+            ->willReturn([
91
+                ['privilege' => '{DAV:}read', 'principal' => 'principal/users/foobar'],
92
+                ['privilege' => '{DAV:}read', 'principal' => 'principal/users/other'],
93
+                ['privilege' => '{DAV:}write', 'principal' => 'principal/users/other'],
94
+                ['privilege' => '{DAV:}all', 'principal' => 'principal/users/other'],
95
+            ]);
96
+
97
+        $this->assertEquals(1, $this->calendarImpl->getPermissions());
98
+    }
99
+
100
+    public function testGetPermissionWrite(): void {
101
+        $this->calendar->expects($this->once())
102
+            ->method('getACL')
103
+            ->with()
104
+            ->willReturn([
105
+                ['privilege' => '{DAV:}write', 'principal' => 'principal/users/foobar'],
106
+                ['privilege' => '{DAV:}read', 'principal' => 'principal/users/other'],
107
+                ['privilege' => '{DAV:}all', 'principal' => 'principal/users/other'],
108
+            ]);
109
+
110
+        $this->assertEquals(6, $this->calendarImpl->getPermissions());
111
+    }
112
+
113
+    public function testGetPermissionReadWrite(): void {
114
+        $this->calendar->expects($this->once())
115
+            ->method('getACL')
116
+            ->with()
117
+            ->willReturn([
118
+                ['privilege' => '{DAV:}write', 'principal' => 'principal/users/foobar'],
119
+                ['privilege' => '{DAV:}read', 'principal' => 'principal/users/foobar'],
120
+                ['privilege' => '{DAV:}all', 'principal' => 'principal/users/other'],
121
+            ]);
122
+
123
+        $this->assertEquals(7, $this->calendarImpl->getPermissions());
124
+    }
125
+
126
+    public function testGetPermissionAll(): void {
127
+        $this->calendar->expects($this->once())
128
+            ->method('getACL')
129
+            ->with()
130
+            ->willReturn([
131
+                ['privilege' => '{DAV:}all', 'principal' => 'principal/users/foobar'],
132
+            ]);
133
+
134
+        $this->assertEquals(31, $this->calendarImpl->getPermissions());
135
+    }
136
+
137
+    public function testHandleImipNoMethod(): void {
138
+        // Arrange
139
+        $vObject = $this->vCalendar1a;
140
+
141
+        $this->expectException(CalendarException::class);
142
+        $this->expectExceptionMessage('iMip message contains no valid method');
143
+
144
+        // Act
145
+        $this->calendarImpl->handleIMipMessage('fakeUser', $vObject->serialize());
146
+    }
147
+
148
+    public function testHandleImipNoEvent(): void {
149
+        // Arrange
150
+        $vObject = $this->vCalendar1a;
151
+        $vObject->add('METHOD', 'REQUEST');
152
+        $vObject->remove('VEVENT');
153
+
154
+        $this->expectException(CalendarException::class);
155
+        $this->expectExceptionMessage('iMip message contains no event');
156
+
157
+        // Act
158
+        $this->calendarImpl->handleIMipMessage('fakeUser', $vObject->serialize());
159
+    }
160
+
161
+    public function testHandleImipNoUid(): void {
162
+        // Arrange
163
+        $vObject = $this->vCalendar1a;
164
+        $vObject->add('METHOD', 'REQUEST');
165
+        $vObject->VEVENT->remove('UID');
166
+
167
+        $this->expectException(CalendarException::class);
168
+        $this->expectExceptionMessage('iMip message event dose not contain a UID');
169
+
170
+        // Act
171
+        $this->calendarImpl->handleIMipMessage('fakeUser', $vObject->serialize());
172
+    }
173
+
174
+    public function testHandleImipNoOrganizer(): void {
175
+        // Arrange
176
+        $vObject = $this->vCalendar1a;
177
+        $vObject->add('METHOD', 'REQUEST');
178
+        $vObject->VEVENT->remove('ORGANIZER');
179
+
180
+        $this->expectException(CalendarException::class);
181
+        $this->expectExceptionMessage('iMip message event dose not contain an organizer');
182
+
183
+        // Act
184
+        $this->calendarImpl->handleIMipMessage('fakeUser', $vObject->serialize());
185
+    }
186
+
187
+    public function testHandleImipNoAttendee(): void {
188
+        // Arrange
189
+        $vObject = $this->vCalendar1a;
190
+        $vObject->add('METHOD', 'REQUEST');
191
+        $vObject->VEVENT->remove('ATTENDEE');
192
+
193
+        $this->expectException(CalendarException::class);
194
+        $this->expectExceptionMessage('iMip message event dose not contain an attendee');
195
+
196
+        // Act
197
+        $this->calendarImpl->handleIMipMessage('fakeUser', $vObject->serialize());
198
+    }
199
+
200
+    public function testHandleImipRequest(): void {
201
+        $userAddressSet = new class([ 'mailto:[email protected]', '/remote.php/dav/principals/users/attendee1/', ]) {
202
+            public function __construct(
203
+                private array $hrefs,
204
+            ) {
205
+            }
206
+            public function getHrefs(): array {
207
+                return $this->hrefs;
208
+            }
209
+        };
210
+
211
+        $vObject = $this->vCalendar1a;
212
+        $vObject->add('METHOD', 'REQUEST');
213
+
214
+        $iTip = new Message();
215
+        $iTip->method = 'REQUEST';
216
+        $iTip->sender = $vObject->VEVENT->ORGANIZER->getValue();
217
+        $iTip->recipient = $vObject->VEVENT->ATTENDEE->getValue();
218
+        $iTip->component = 'VEVENT';
219
+        $iTip->uid = $vObject->VEVENT->UID->getValue();
220
+        $iTip->sequence = (int)$vObject->VEVENT->SEQUENCE->getValue() ?? 0;
221
+        $iTip->message = $vObject;
222
+
223
+        /** @var CustomPrincipalPlugin|MockObject $authPlugin */
224
+        $authPlugin = $this->createMock(CustomPrincipalPlugin::class);
225
+        $authPlugin->expects(self::once())
226
+            ->method('setCurrentPrincipal')
227
+            ->with($this->calendar->getPrincipalURI());
228
+        /** @var \Sabre\DAVACL\Plugin|MockObject $aclPlugin */
229
+        $aclPlugin = $this->createMock(\Sabre\DAVACL\Plugin::class);
230
+
231
+        $server = $this->createMock(Server::class);
232
+        $server->expects($this->any())
233
+            ->method('getPlugin')
234
+            ->willReturnMap([
235
+                ['auth', $authPlugin],
236
+                ['acl', $aclPlugin],
237
+            ]);
238
+
239
+        $server->expects(self::once())
240
+            ->method('getProperties')
241
+            ->with(
242
+                $this->calendar->getPrincipalURI(),
243
+                [
244
+                    '{http://sabredav.org/ns}email-address',
245
+                    '{urn:ietf:params:xml:ns:caldav}calendar-user-address-set'
246
+                ]
247
+            )
248
+            ->willReturn([
249
+                '{http://sabredav.org/ns}email-address' => '[email protected]',
250
+                '{urn:ietf:params:xml:ns:caldav}calendar-user-address-set' => $userAddressSet,
251
+            ]);
252
+        $server->expects(self::once())
253
+            ->method('emit');
254
+        $invitationResponseServer = $this->createMock(InvitationResponseServer::class, ['getServer']);
255
+        $invitationResponseServer->server = $server;
256
+        $invitationResponseServer->expects($this->any())
257
+            ->method('getServer')
258
+            ->willReturn($server);
259
+        $calendarImpl = $this->getMockBuilder(CalendarImpl::class)
260
+            ->setConstructorArgs([$this->calendar, $this->calendarInfo, $this->backend])
261
+            ->onlyMethods(['getInvitationResponseServer'])
262
+            ->getMock();
263
+        $calendarImpl->expects($this->once())
264
+            ->method('getInvitationResponseServer')
265
+            ->willReturn($invitationResponseServer);
266
+
267
+        $calendarImpl->handleIMipMessage('fakeUser', $vObject->serialize());
268
+    }
269 269
 
270 270
 }
Please login to merge, or discard this patch.
Spacing   +5 added lines, -5 removed lines patch added patch discarded remove patch
@@ -20,9 +20,9 @@  discard block
 block discarded – undo
20 20
 use Sabre\VObject\ITip\Message;
21 21
 
22 22
 class CalendarImplTest extends \Test\TestCase {
23
-	private CalDavBackend|MockObject $backend;
24
-	private Calendar|MockObject $calendar;
25
-	private CalendarImpl|MockObject $calendarImpl;
23
+	private CalDavBackend | MockObject $backend;
24
+	private Calendar | MockObject $calendar;
25
+	private CalendarImpl | MockObject $calendarImpl;
26 26
 	private array $calendarInfo;
27 27
 	private VCalendar $vCalendar1a;
28 28
 	private array $mockExportCollection;
@@ -198,7 +198,7 @@  discard block
 block discarded – undo
198 198
 	}
199 199
 
200 200
 	public function testHandleImipRequest(): void {
201
-		$userAddressSet = new class([ 'mailto:[email protected]', '/remote.php/dav/principals/users/attendee1/', ]) {
201
+		$userAddressSet = new class(['mailto:[email protected]', '/remote.php/dav/principals/users/attendee1/', ]) {
202 202
 			public function __construct(
203 203
 				private array $hrefs,
204 204
 			) {
@@ -217,7 +217,7 @@  discard block
 block discarded – undo
217 217
 		$iTip->recipient = $vObject->VEVENT->ATTENDEE->getValue();
218 218
 		$iTip->component = 'VEVENT';
219 219
 		$iTip->uid = $vObject->VEVENT->UID->getValue();
220
-		$iTip->sequence = (int)$vObject->VEVENT->SEQUENCE->getValue() ?? 0;
220
+		$iTip->sequence = (int) $vObject->VEVENT->SEQUENCE->getValue() ?? 0;
221 221
 		$iTip->message = $vObject;
222 222
 
223 223
 		/** @var CustomPrincipalPlugin|MockObject $authPlugin */
Please login to merge, or discard this patch.
tests/lib/Calendar/ManagerTest.php 1 patch
Indentation   +721 added lines, -721 removed lines patch added patch discarded remove patch
@@ -41,592 +41,592 @@  discard block
 block discarded – undo
41 41
 }
42 42
 
43 43
 class ManagerTest extends TestCase {
44
-	/** @var Coordinator&MockObject */
45
-	private $coordinator;
46
-
47
-	/** @var ContainerInterface&MockObject */
48
-	private $container;
49
-
50
-	/** @var LoggerInterface&MockObject */
51
-	private $logger;
52
-
53
-	/** @var Manager */
54
-	private $manager;
55
-
56
-	/** @var ITimeFactory&MockObject */
57
-	private $time;
58
-
59
-	/** @var ISecureRandom&MockObject */
60
-	private ISecureRandom $secureRandom;
61
-
62
-	private IUserManager&MockObject $userManager;
63
-	private ServerFactory&MockObject $serverFactory;
64
-
65
-	private VCalendar $vCalendar1a;
66
-	private VCalendar $vCalendar2a;
67
-	private VCalendar $vCalendar3a;
68
-
69
-	protected function setUp(): void {
70
-		parent::setUp();
71
-
72
-		$this->coordinator = $this->createMock(Coordinator::class);
73
-		$this->container = $this->createMock(ContainerInterface::class);
74
-		$this->logger = $this->createMock(LoggerInterface::class);
75
-		$this->time = $this->createMock(ITimeFactory::class);
76
-		$this->secureRandom = $this->createMock(ISecureRandom::class);
77
-		$this->userManager = $this->createMock(IUserManager::class);
78
-		$this->serverFactory = $this->createMock(ServerFactory::class);
79
-
80
-		$this->manager = new Manager(
81
-			$this->coordinator,
82
-			$this->container,
83
-			$this->logger,
84
-			$this->time,
85
-			$this->secureRandom,
86
-			$this->userManager,
87
-			$this->serverFactory,
88
-		);
89
-
90
-		// construct calendar with a 1 hour event and same start/end time zones
91
-		$this->vCalendar1a = new VCalendar();
92
-		/** @var VEvent $vEvent */
93
-		$vEvent = $this->vCalendar1a->add('VEVENT', []);
94
-		$vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
95
-		$vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']);
96
-		$vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Toronto']);
97
-		$vEvent->add('SUMMARY', 'Test Event');
98
-		$vEvent->add('SEQUENCE', 3);
99
-		$vEvent->add('STATUS', 'CONFIRMED');
100
-		$vEvent->add('TRANSP', 'OPAQUE');
101
-		$vEvent->add('ORGANIZER', 'mailto:[email protected]', ['CN' => 'Organizer']);
102
-		$vEvent->add('ATTENDEE', 'mailto:[email protected]', [
103
-			'CN' => 'Attendee One',
104
-			'CUTYPE' => 'INDIVIDUAL',
105
-			'PARTSTAT' => 'NEEDS-ACTION',
106
-			'ROLE' => 'REQ-PARTICIPANT',
107
-			'RSVP' => 'TRUE'
108
-		]);
109
-
110
-		// construct calendar with a event for reply
111
-		$this->vCalendar2a = new VCalendar();
112
-		/** @var VEvent $vEvent */
113
-		$vEvent = $this->vCalendar2a->add('VEVENT', []);
114
-		$vEvent->UID->setValue('dcc733bf-b2b2-41f2-a8cf-550ae4b67aff');
115
-		$vEvent->add('DTSTART', '20210820');
116
-		$vEvent->add('DTEND', '20220821');
117
-		$vEvent->add('SUMMARY', 'berry basket');
118
-		$vEvent->add('SEQUENCE', 3);
119
-		$vEvent->add('STATUS', 'CONFIRMED');
120
-		$vEvent->add('TRANSP', 'OPAQUE');
121
-		$vEvent->add('ORGANIZER', 'mailto:[email protected]', ['CN' => 'admin']);
122
-		$vEvent->add('ATTENDEE', 'mailto:[email protected]', [
123
-			'CN' => '[email protected]',
124
-			'CUTYPE' => 'INDIVIDUAL',
125
-			'ROLE' => 'REQ-PARTICIPANT',
126
-			'PARTSTAT' => 'ACCEPTED',
127
-		]);
128
-
129
-		// construct calendar with a event for reply
130
-		$this->vCalendar3a = new VCalendar();
131
-		/** @var VEvent $vEvent */
132
-		$vEvent = $this->vCalendar3a->add('VEVENT', []);
133
-		$vEvent->UID->setValue('dcc733bf-b2b2-41f2-a8cf-550ae4b67aff');
134
-		$vEvent->add('DTSTART', '20210820');
135
-		$vEvent->add('DTEND', '20220821');
136
-		$vEvent->add('SUMMARY', 'berry basket');
137
-		$vEvent->add('SEQUENCE', 3);
138
-		$vEvent->add('STATUS', 'CANCELLED');
139
-		$vEvent->add('TRANSP', 'OPAQUE');
140
-		$vEvent->add('ORGANIZER', 'mailto:[email protected]', ['CN' => 'admin']);
141
-		$vEvent->add('ATTENDEE', 'mailto:[email protected]', [
142
-			'CN' => '[email protected]',
143
-			'CUTYPE' => 'INDIVIDUAL',
144
-			'ROLE' => 'REQ-PARTICIPANT',
145
-			'PARTSTAT' => 'ACCEPTED',
146
-		]);
147
-
148
-	}
149
-
150
-	#[\PHPUnit\Framework\Attributes\DataProvider('searchProvider')]
151
-	public function testSearch($search1, $search2, $expected): void {
152
-		/** @var ICalendar | MockObject $calendar1 */
153
-		$calendar1 = $this->createMock(ICalendar::class);
154
-		$calendar1->method('getKey')->willReturn('simple:1');
155
-		$calendar1->expects($this->once())
156
-			->method('search')
157
-			->with('', [], [], null, null)
158
-			->willReturn($search1);
159
-
160
-		/** @var ICalendar | MockObject $calendar2 */
161
-		$calendar2 = $this->createMock(ICalendar::class);
162
-		$calendar2->method('getKey')->willReturn('simple:2');
163
-		$calendar2->expects($this->once())
164
-			->method('search')
165
-			->with('', [], [], null, null)
166
-			->willReturn($search2);
167
-
168
-		$this->manager->registerCalendar($calendar1);
169
-		$this->manager->registerCalendar($calendar2);
170
-
171
-		$result = $this->manager->search('');
172
-		$this->assertEquals($expected, $result);
173
-	}
174
-
175
-	#[\PHPUnit\Framework\Attributes\DataProvider('searchProvider')]
176
-	public function testSearchOptions($search1, $search2, $expected): void {
177
-		/** @var ICalendar | MockObject $calendar1 */
178
-		$calendar1 = $this->createMock(ICalendar::class);
179
-		$calendar1->method('getKey')->willReturn('simple:1');
180
-		$calendar1->expects($this->once())
181
-			->method('search')
182
-			->with('searchTerm', ['SUMMARY', 'DESCRIPTION'],
183
-				['timerange' => ['start' => null, 'end' => null]], 5, 20)
184
-			->willReturn($search1);
185
-
186
-		/** @var ICalendar | MockObject $calendar2 */
187
-		$calendar2 = $this->createMock(ICalendar::class);
188
-		$calendar2->method('getKey')->willReturn('simple:2');
189
-		$calendar2->expects($this->once())
190
-			->method('search')
191
-			->with('searchTerm', ['SUMMARY', 'DESCRIPTION'],
192
-				['timerange' => ['start' => null, 'end' => null]], 5, 20)
193
-			->willReturn($search2);
194
-
195
-		$this->manager->registerCalendar($calendar1);
196
-		$this->manager->registerCalendar($calendar2);
197
-
198
-		$result = $this->manager->search('searchTerm', ['SUMMARY', 'DESCRIPTION'],
199
-			['timerange' => ['start' => null, 'end' => null]], 5, 20);
200
-		$this->assertEquals($expected, $result);
201
-	}
202
-
203
-	public static function searchProvider(): array {
204
-		$search1 = [
205
-			[
206
-				'id' => 1,
207
-				'data' => 'foobar',
208
-			],
209
-			[
210
-				'id' => 2,
211
-				'data' => 'barfoo',
212
-			]
213
-		];
214
-		$search2 = [
215
-			[
216
-				'id' => 3,
217
-				'data' => 'blablub',
218
-			],
219
-			[
220
-				'id' => 4,
221
-				'data' => 'blubbla',
222
-			]
223
-		];
224
-
225
-		$expected = [
226
-			[
227
-				'id' => 1,
228
-				'data' => 'foobar',
229
-				'calendar-key' => 'simple:1',
230
-			],
231
-			[
232
-				'id' => 2,
233
-				'data' => 'barfoo',
234
-				'calendar-key' => 'simple:1',
235
-			],
236
-			[
237
-				'id' => 3,
238
-				'data' => 'blablub',
239
-				'calendar-key' => 'simple:2',
240
-			],
241
-			[
242
-				'id' => 4,
243
-				'data' => 'blubbla',
244
-				'calendar-key' => 'simple:2',
245
-			]
246
-		];
247
-
248
-		return [
249
-			[
250
-				$search1,
251
-				$search2,
252
-				$expected
253
-			]
254
-		];
255
-	}
256
-
257
-	public function testRegisterUnregister(): void {
258
-		/** @var ICalendar | MockObject $calendar1 */
259
-		$calendar1 = $this->createMock(ICalendar::class);
260
-		$calendar1->method('getKey')->willReturn('key1');
261
-
262
-		/** @var ICalendar | MockObject $calendar2 */
263
-		$calendar2 = $this->createMock(ICalendar::class);
264
-		$calendar2->method('getKey')->willReturn('key2');
265
-
266
-		$this->manager->registerCalendar($calendar1);
267
-		$this->manager->registerCalendar($calendar2);
268
-
269
-		$result = $this->manager->getCalendars();
270
-		$this->assertCount(2, $result);
271
-		$this->assertContains($calendar1, $result);
272
-		$this->assertContains($calendar2, $result);
273
-
274
-		$this->manager->unregisterCalendar($calendar1);
275
-
276
-		$result = $this->manager->getCalendars();
277
-		$this->assertCount(1, $result);
278
-		$this->assertContains($calendar2, $result);
279
-	}
280
-
281
-	public function testGetCalendars(): void {
282
-		/** @var ICalendar | MockObject $calendar1 */
283
-		$calendar1 = $this->createMock(ICalendar::class);
284
-		$calendar1->method('getKey')->willReturn('key1');
285
-
286
-		/** @var ICalendar | MockObject $calendar2 */
287
-		$calendar2 = $this->createMock(ICalendar::class);
288
-		$calendar2->method('getKey')->willReturn('key2');
289
-
290
-		$this->manager->registerCalendar($calendar1);
291
-		$this->manager->registerCalendar($calendar2);
292
-
293
-		$result = $this->manager->getCalendars();
294
-		$this->assertCount(2, $result);
295
-		$this->assertContains($calendar1, $result);
296
-		$this->assertContains($calendar2, $result);
297
-
298
-		$this->manager->clear();
299
-
300
-		$result = $this->manager->getCalendars();
301
-
302
-		$this->assertCount(0, $result);
303
-	}
304
-
305
-	public function testEnabledIfNot(): void {
306
-		$isEnabled = $this->manager->isEnabled();
307
-		$this->assertFalse($isEnabled);
308
-	}
309
-
310
-	public function testIfEnabledIfSo(): void {
311
-		/** @var ICalendar | MockObject $calendar */
312
-		$calendar = $this->createMock(ICalendar::class);
313
-		$this->manager->registerCalendar($calendar);
314
-
315
-		$isEnabled = $this->manager->isEnabled();
316
-		$this->assertTrue($isEnabled);
317
-	}
318
-
319
-	public function testHandleImipWithNoCalendars(): void {
320
-		// construct calendar manager returns
321
-		/** @var Manager&MockObject $manager */
322
-		$manager = $this->getMockBuilder(Manager::class)
323
-			->setConstructorArgs([
324
-				$this->coordinator,
325
-				$this->container,
326
-				$this->logger,
327
-				$this->time,
328
-				$this->secureRandom,
329
-				$this->userManager,
330
-				$this->serverFactory,
331
-			])
332
-			->onlyMethods(['getCalendarsForPrincipal'])
333
-			->getMock();
334
-		$manager->expects(self::once())
335
-			->method('getCalendarsForPrincipal')
336
-			->willReturn([]);
337
-		// construct logger returns
338
-		$this->logger->expects(self::once())->method('warning')
339
-			->with('iMip message could not be processed because user has no calendars');
340
-		// construct parameters
341
-		$userId = 'attendee1';
342
-		$calendar = $this->vCalendar1a;
343
-		$calendar->add('METHOD', 'REQUEST');
344
-		// test method
345
-		$result = $manager->handleIMip($userId, $calendar->serialize());
346
-		// Assert
347
-		$this->assertFalse($result);
348
-	}
349
-
350
-	public function testHandleImipWithNoEvent(): void {
351
-		// construct mock user calendar
352
-		$userCalendar = $this->createMock(ITestCalendar::class);
353
-		// construct mock calendar manager and returns
354
-		/** @var Manager&MockObject $manager */
355
-		$manager = $this->getMockBuilder(Manager::class)
356
-			->setConstructorArgs([
357
-				$this->coordinator,
358
-				$this->container,
359
-				$this->logger,
360
-				$this->time,
361
-				$this->secureRandom,
362
-				$this->userManager,
363
-				$this->serverFactory,
364
-			])
365
-			->onlyMethods(['getCalendarsForPrincipal'])
366
-			->getMock();
367
-		$manager->expects(self::once())
368
-			->method('getCalendarsForPrincipal')
369
-			->willReturn([$userCalendar]);
370
-		// construct logger returns
371
-		$this->logger->expects(self::once())->method('warning')
372
-			->with('iMip message does not contain any event(s)');
373
-		// construct parameters
374
-		$userId = 'attendee1';
375
-		$calendar = $this->vCalendar1a;
376
-		$calendar->add('METHOD', 'REQUEST');
377
-		$calendar->remove('VEVENT');
378
-		// Act
379
-		$result = $manager->handleIMip($userId, $calendar->serialize());
380
-		// Assert
381
-		$this->assertFalse($result);
382
-	}
383
-
384
-	public function testHandleImipWithNoUid(): void {
385
-		// construct mock user calendar
386
-		$userCalendar = $this->createMock(ITestCalendar::class);
387
-		// construct mock calendar manager and returns
388
-		/** @var Manager&MockObject $manager */
389
-		$manager = $this->getMockBuilder(Manager::class)
390
-			->setConstructorArgs([
391
-				$this->coordinator,
392
-				$this->container,
393
-				$this->logger,
394
-				$this->time,
395
-				$this->secureRandom,
396
-				$this->userManager,
397
-				$this->serverFactory,
398
-			])
399
-			->onlyMethods(['getCalendarsForPrincipal'])
400
-			->getMock();
401
-		$manager->expects(self::once())
402
-			->method('getCalendarsForPrincipal')
403
-			->willReturn([$userCalendar]);
404
-		// construct logger returns
405
-		$this->logger->expects(self::once())->method('warning')
406
-			->with('iMip message event dose not contains a UID');
407
-		// construct parameters
408
-		$userId = 'attendee1';
409
-		$calendar = $this->vCalendar1a;
410
-		$calendar->add('METHOD', 'REQUEST');
411
-		$calendar->VEVENT->remove('UID');
412
-		// test method
413
-		$result = $manager->handleIMip($userId, $calendar->serialize());
414
-		// Assert
415
-		$this->assertFalse($result);
416
-	}
417
-
418
-	public function testHandleImipWithNoMatch(): void {
419
-		// construct mock user calendar
420
-		$userCalendar = $this->createMock(ITestCalendar::class);
421
-		$userCalendar->expects(self::once())
422
-			->method('isDeleted')
423
-			->willReturn(false);
424
-		$userCalendar->expects(self::once())
425
-			->method('isWritable')
426
-			->willReturn(true);
427
-		$userCalendar->expects(self::once())
428
-			->method('search')
429
-			->willReturn([]);
430
-		// construct mock calendar manager and returns
431
-		/** @var Manager&MockObject $manager */
432
-		$manager = $this->getMockBuilder(Manager::class)
433
-			->setConstructorArgs([
434
-				$this->coordinator,
435
-				$this->container,
436
-				$this->logger,
437
-				$this->time,
438
-				$this->secureRandom,
439
-				$this->userManager,
440
-				$this->serverFactory,
441
-			])
442
-			->onlyMethods(['getCalendarsForPrincipal'])
443
-			->getMock();
444
-		$manager->expects(self::once())
445
-			->method('getCalendarsForPrincipal')
446
-			->willReturn([$userCalendar]);
447
-		// construct logger returns
448
-		$this->logger->expects(self::once())->method('warning')
449
-			->with('iMip message could not be processed because no corresponding event was found in any calendar');
450
-		// construct parameters
451
-		$userId = 'attendee1';
452
-		$calendar = $this->vCalendar1a;
453
-		$calendar->add('METHOD', 'REQUEST');
454
-		// test method
455
-		$result = $manager->handleIMip($userId, $calendar->serialize());
456
-		// Assert
457
-		$this->assertFalse($result);
458
-	}
459
-
460
-	public function testHandleImip(): void {
461
-		// construct mock user calendar
462
-		$userCalendar = $this->createMock(ITestCalendar::class);
463
-		$userCalendar->expects(self::once())
464
-			->method('isDeleted')
465
-			->willReturn(false);
466
-		$userCalendar->expects(self::once())
467
-			->method('isWritable')
468
-			->willReturn(true);
469
-		$userCalendar->expects(self::once())
470
-			->method('search')
471
-			->willReturn([['uri' => 'principals/user/attendee1/personal']]);
472
-		// construct mock calendar manager and returns
473
-		/** @var Manager&MockObject $manager */
474
-		$manager = $this->getMockBuilder(Manager::class)
475
-			->setConstructorArgs([
476
-				$this->coordinator,
477
-				$this->container,
478
-				$this->logger,
479
-				$this->time,
480
-				$this->secureRandom,
481
-				$this->userManager,
482
-				$this->serverFactory,
483
-			])
484
-			->onlyMethods(['getCalendarsForPrincipal'])
485
-			->getMock();
486
-		$manager->expects(self::once())
487
-			->method('getCalendarsForPrincipal')
488
-			->willReturn([$userCalendar]);
489
-		// construct parameters
490
-		$userId = 'attendee1';
491
-		$calendar = $this->vCalendar1a;
492
-		$calendar->add('METHOD', 'REQUEST');
493
-		// construct user calendar returns
494
-		$userCalendar->expects(self::once())
495
-			->method('handleIMipMessage');
496
-		// test method
497
-		$result = $manager->handleIMip($userId, $calendar->serialize());
498
-	}
499
-
500
-	public function testhandleIMipRequestWithInvalidPrincipal() {
501
-		$invalidPrincipal = 'invalid-principal-uri';
502
-		$sender = '[email protected]';
503
-		$recipient = '[email protected]';
504
-		$calendarData = $this->vCalendar1a->serialize();
505
-
506
-		$this->logger->expects(self::once())
507
-			->method('error')
508
-			->with('Invalid principal URI provided for iMip request');
509
-
510
-		$result = $this->manager->handleIMipRequest($invalidPrincipal, $sender, $recipient, $calendarData);
511
-		$this->assertFalse($result);
512
-	}
513
-
514
-	public function testhandleIMipRequest() {
515
-		$principalUri = 'principals/users/attendee1';
516
-		$sender = '[email protected]';
517
-		$recipient = '[email protected]';
518
-		$calendarData = $this->vCalendar1a->serialize();
519
-
520
-		/** @var Manager&MockObject $manager */
521
-		$manager = $this->getMockBuilder(Manager::class)
522
-			->setConstructorArgs([
523
-				$this->coordinator,
524
-				$this->container,
525
-				$this->logger,
526
-				$this->time,
527
-				$this->secureRandom,
528
-				$this->userManager,
529
-				$this->serverFactory,
530
-			])
531
-			->onlyMethods(['handleIMip'])
532
-			->getMock();
533
-		$manager->expects(self::once())
534
-			->method('handleIMip')
535
-			->with('attendee1', $calendarData)
536
-			->willReturn(true);
537
-
538
-		$result = $manager->handleIMipRequest($principalUri, $sender, $recipient, $calendarData);
539
-		$this->assertTrue($result);
540
-	}
541
-
542
-	public function testhandleIMipReplyWithInvalidPrincipal() {
543
-		$invalidPrincipal = 'invalid-principal-uri';
544
-		$sender = '[email protected]';
545
-		$recipient = '[email protected]';
546
-		$calendarData = $this->vCalendar2a->serialize();
547
-
548
-		$this->logger->expects(self::once())
549
-			->method('error')
550
-			->with('Invalid principal URI provided for iMip reply');
551
-
552
-		$result = $this->manager->handleIMipReply($invalidPrincipal, $sender, $recipient, $calendarData);
553
-		$this->assertFalse($result);
554
-	}
555
-
556
-	public function testhandleIMipReply() {
557
-		$principalUri = 'principals/users/attendee2';
558
-		$sender = '[email protected]';
559
-		$recipient = '[email protected]';
560
-		$calendarData = $this->vCalendar2a->serialize();
561
-
562
-		/** @var Manager&MockObject $manager */
563
-		$manager = $this->getMockBuilder(Manager::class)
564
-			->setConstructorArgs([
565
-				$this->coordinator,
566
-				$this->container,
567
-				$this->logger,
568
-				$this->time,
569
-				$this->secureRandom,
570
-				$this->userManager,
571
-				$this->serverFactory,
572
-			])
573
-			->onlyMethods(['handleIMip'])
574
-			->getMock();
575
-		$manager->expects(self::once())
576
-			->method('handleIMip')
577
-			->with('attendee2', $calendarData)
578
-			->willReturn(true);
579
-
580
-		$result = $manager->handleIMipReply($principalUri, $sender, $recipient, $calendarData);
581
-		$this->assertTrue($result);
582
-	}
583
-
584
-	public function testhandleIMipCancelWithInvalidPrincipal() {
585
-		$invalidPrincipal = 'invalid-principal-uri';
586
-		$sender = '[email protected]';
587
-		$replyTo = null;
588
-		$recipient = '[email protected]';
589
-		$calendarData = $this->vCalendar3a->serialize();
590
-
591
-		$this->logger->expects(self::once())
592
-			->method('error')
593
-			->with('Invalid principal URI provided for iMip cancel');
594
-
595
-		$result = $this->manager->handleIMipCancel($invalidPrincipal, $sender, $replyTo, $recipient, $calendarData);
596
-		$this->assertFalse($result);
597
-	}
598
-
599
-	public function testhandleIMipCancel() {
600
-		$principalUri = 'principals/users/attendee3';
601
-		$sender = '[email protected]';
602
-		$replyTo = null;
603
-		$recipient = '[email protected]';
604
-		$calendarData = $this->vCalendar3a->serialize();
605
-
606
-		/** @var Manager&MockObject $manager */
607
-		$manager = $this->getMockBuilder(Manager::class)
608
-			->setConstructorArgs([
609
-				$this->coordinator,
610
-				$this->container,
611
-				$this->logger,
612
-				$this->time,
613
-				$this->secureRandom,
614
-				$this->userManager,
615
-				$this->serverFactory,
616
-			])
617
-			->onlyMethods(['handleIMip'])
618
-			->getMock();
619
-		$manager->expects(self::once())
620
-			->method('handleIMip')
621
-			->with('attendee3', $calendarData)
622
-			->willReturn(true);
623
-
624
-		$result = $manager->handleIMipCancel($principalUri, $sender, $replyTo, $recipient, $calendarData);
625
-		$this->assertTrue($result);
626
-	}
627
-
628
-	private function getFreeBusyResponse(): string {
629
-		return <<<EOF
44
+    /** @var Coordinator&MockObject */
45
+    private $coordinator;
46
+
47
+    /** @var ContainerInterface&MockObject */
48
+    private $container;
49
+
50
+    /** @var LoggerInterface&MockObject */
51
+    private $logger;
52
+
53
+    /** @var Manager */
54
+    private $manager;
55
+
56
+    /** @var ITimeFactory&MockObject */
57
+    private $time;
58
+
59
+    /** @var ISecureRandom&MockObject */
60
+    private ISecureRandom $secureRandom;
61
+
62
+    private IUserManager&MockObject $userManager;
63
+    private ServerFactory&MockObject $serverFactory;
64
+
65
+    private VCalendar $vCalendar1a;
66
+    private VCalendar $vCalendar2a;
67
+    private VCalendar $vCalendar3a;
68
+
69
+    protected function setUp(): void {
70
+        parent::setUp();
71
+
72
+        $this->coordinator = $this->createMock(Coordinator::class);
73
+        $this->container = $this->createMock(ContainerInterface::class);
74
+        $this->logger = $this->createMock(LoggerInterface::class);
75
+        $this->time = $this->createMock(ITimeFactory::class);
76
+        $this->secureRandom = $this->createMock(ISecureRandom::class);
77
+        $this->userManager = $this->createMock(IUserManager::class);
78
+        $this->serverFactory = $this->createMock(ServerFactory::class);
79
+
80
+        $this->manager = new Manager(
81
+            $this->coordinator,
82
+            $this->container,
83
+            $this->logger,
84
+            $this->time,
85
+            $this->secureRandom,
86
+            $this->userManager,
87
+            $this->serverFactory,
88
+        );
89
+
90
+        // construct calendar with a 1 hour event and same start/end time zones
91
+        $this->vCalendar1a = new VCalendar();
92
+        /** @var VEvent $vEvent */
93
+        $vEvent = $this->vCalendar1a->add('VEVENT', []);
94
+        $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
95
+        $vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']);
96
+        $vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Toronto']);
97
+        $vEvent->add('SUMMARY', 'Test Event');
98
+        $vEvent->add('SEQUENCE', 3);
99
+        $vEvent->add('STATUS', 'CONFIRMED');
100
+        $vEvent->add('TRANSP', 'OPAQUE');
101
+        $vEvent->add('ORGANIZER', 'mailto:[email protected]', ['CN' => 'Organizer']);
102
+        $vEvent->add('ATTENDEE', 'mailto:[email protected]', [
103
+            'CN' => 'Attendee One',
104
+            'CUTYPE' => 'INDIVIDUAL',
105
+            'PARTSTAT' => 'NEEDS-ACTION',
106
+            'ROLE' => 'REQ-PARTICIPANT',
107
+            'RSVP' => 'TRUE'
108
+        ]);
109
+
110
+        // construct calendar with a event for reply
111
+        $this->vCalendar2a = new VCalendar();
112
+        /** @var VEvent $vEvent */
113
+        $vEvent = $this->vCalendar2a->add('VEVENT', []);
114
+        $vEvent->UID->setValue('dcc733bf-b2b2-41f2-a8cf-550ae4b67aff');
115
+        $vEvent->add('DTSTART', '20210820');
116
+        $vEvent->add('DTEND', '20220821');
117
+        $vEvent->add('SUMMARY', 'berry basket');
118
+        $vEvent->add('SEQUENCE', 3);
119
+        $vEvent->add('STATUS', 'CONFIRMED');
120
+        $vEvent->add('TRANSP', 'OPAQUE');
121
+        $vEvent->add('ORGANIZER', 'mailto:[email protected]', ['CN' => 'admin']);
122
+        $vEvent->add('ATTENDEE', 'mailto:[email protected]', [
123
+            'CN' => '[email protected]',
124
+            'CUTYPE' => 'INDIVIDUAL',
125
+            'ROLE' => 'REQ-PARTICIPANT',
126
+            'PARTSTAT' => 'ACCEPTED',
127
+        ]);
128
+
129
+        // construct calendar with a event for reply
130
+        $this->vCalendar3a = new VCalendar();
131
+        /** @var VEvent $vEvent */
132
+        $vEvent = $this->vCalendar3a->add('VEVENT', []);
133
+        $vEvent->UID->setValue('dcc733bf-b2b2-41f2-a8cf-550ae4b67aff');
134
+        $vEvent->add('DTSTART', '20210820');
135
+        $vEvent->add('DTEND', '20220821');
136
+        $vEvent->add('SUMMARY', 'berry basket');
137
+        $vEvent->add('SEQUENCE', 3);
138
+        $vEvent->add('STATUS', 'CANCELLED');
139
+        $vEvent->add('TRANSP', 'OPAQUE');
140
+        $vEvent->add('ORGANIZER', 'mailto:[email protected]', ['CN' => 'admin']);
141
+        $vEvent->add('ATTENDEE', 'mailto:[email protected]', [
142
+            'CN' => '[email protected]',
143
+            'CUTYPE' => 'INDIVIDUAL',
144
+            'ROLE' => 'REQ-PARTICIPANT',
145
+            'PARTSTAT' => 'ACCEPTED',
146
+        ]);
147
+
148
+    }
149
+
150
+    #[\PHPUnit\Framework\Attributes\DataProvider('searchProvider')]
151
+    public function testSearch($search1, $search2, $expected): void {
152
+        /** @var ICalendar | MockObject $calendar1 */
153
+        $calendar1 = $this->createMock(ICalendar::class);
154
+        $calendar1->method('getKey')->willReturn('simple:1');
155
+        $calendar1->expects($this->once())
156
+            ->method('search')
157
+            ->with('', [], [], null, null)
158
+            ->willReturn($search1);
159
+
160
+        /** @var ICalendar | MockObject $calendar2 */
161
+        $calendar2 = $this->createMock(ICalendar::class);
162
+        $calendar2->method('getKey')->willReturn('simple:2');
163
+        $calendar2->expects($this->once())
164
+            ->method('search')
165
+            ->with('', [], [], null, null)
166
+            ->willReturn($search2);
167
+
168
+        $this->manager->registerCalendar($calendar1);
169
+        $this->manager->registerCalendar($calendar2);
170
+
171
+        $result = $this->manager->search('');
172
+        $this->assertEquals($expected, $result);
173
+    }
174
+
175
+    #[\PHPUnit\Framework\Attributes\DataProvider('searchProvider')]
176
+    public function testSearchOptions($search1, $search2, $expected): void {
177
+        /** @var ICalendar | MockObject $calendar1 */
178
+        $calendar1 = $this->createMock(ICalendar::class);
179
+        $calendar1->method('getKey')->willReturn('simple:1');
180
+        $calendar1->expects($this->once())
181
+            ->method('search')
182
+            ->with('searchTerm', ['SUMMARY', 'DESCRIPTION'],
183
+                ['timerange' => ['start' => null, 'end' => null]], 5, 20)
184
+            ->willReturn($search1);
185
+
186
+        /** @var ICalendar | MockObject $calendar2 */
187
+        $calendar2 = $this->createMock(ICalendar::class);
188
+        $calendar2->method('getKey')->willReturn('simple:2');
189
+        $calendar2->expects($this->once())
190
+            ->method('search')
191
+            ->with('searchTerm', ['SUMMARY', 'DESCRIPTION'],
192
+                ['timerange' => ['start' => null, 'end' => null]], 5, 20)
193
+            ->willReturn($search2);
194
+
195
+        $this->manager->registerCalendar($calendar1);
196
+        $this->manager->registerCalendar($calendar2);
197
+
198
+        $result = $this->manager->search('searchTerm', ['SUMMARY', 'DESCRIPTION'],
199
+            ['timerange' => ['start' => null, 'end' => null]], 5, 20);
200
+        $this->assertEquals($expected, $result);
201
+    }
202
+
203
+    public static function searchProvider(): array {
204
+        $search1 = [
205
+            [
206
+                'id' => 1,
207
+                'data' => 'foobar',
208
+            ],
209
+            [
210
+                'id' => 2,
211
+                'data' => 'barfoo',
212
+            ]
213
+        ];
214
+        $search2 = [
215
+            [
216
+                'id' => 3,
217
+                'data' => 'blablub',
218
+            ],
219
+            [
220
+                'id' => 4,
221
+                'data' => 'blubbla',
222
+            ]
223
+        ];
224
+
225
+        $expected = [
226
+            [
227
+                'id' => 1,
228
+                'data' => 'foobar',
229
+                'calendar-key' => 'simple:1',
230
+            ],
231
+            [
232
+                'id' => 2,
233
+                'data' => 'barfoo',
234
+                'calendar-key' => 'simple:1',
235
+            ],
236
+            [
237
+                'id' => 3,
238
+                'data' => 'blablub',
239
+                'calendar-key' => 'simple:2',
240
+            ],
241
+            [
242
+                'id' => 4,
243
+                'data' => 'blubbla',
244
+                'calendar-key' => 'simple:2',
245
+            ]
246
+        ];
247
+
248
+        return [
249
+            [
250
+                $search1,
251
+                $search2,
252
+                $expected
253
+            ]
254
+        ];
255
+    }
256
+
257
+    public function testRegisterUnregister(): void {
258
+        /** @var ICalendar | MockObject $calendar1 */
259
+        $calendar1 = $this->createMock(ICalendar::class);
260
+        $calendar1->method('getKey')->willReturn('key1');
261
+
262
+        /** @var ICalendar | MockObject $calendar2 */
263
+        $calendar2 = $this->createMock(ICalendar::class);
264
+        $calendar2->method('getKey')->willReturn('key2');
265
+
266
+        $this->manager->registerCalendar($calendar1);
267
+        $this->manager->registerCalendar($calendar2);
268
+
269
+        $result = $this->manager->getCalendars();
270
+        $this->assertCount(2, $result);
271
+        $this->assertContains($calendar1, $result);
272
+        $this->assertContains($calendar2, $result);
273
+
274
+        $this->manager->unregisterCalendar($calendar1);
275
+
276
+        $result = $this->manager->getCalendars();
277
+        $this->assertCount(1, $result);
278
+        $this->assertContains($calendar2, $result);
279
+    }
280
+
281
+    public function testGetCalendars(): void {
282
+        /** @var ICalendar | MockObject $calendar1 */
283
+        $calendar1 = $this->createMock(ICalendar::class);
284
+        $calendar1->method('getKey')->willReturn('key1');
285
+
286
+        /** @var ICalendar | MockObject $calendar2 */
287
+        $calendar2 = $this->createMock(ICalendar::class);
288
+        $calendar2->method('getKey')->willReturn('key2');
289
+
290
+        $this->manager->registerCalendar($calendar1);
291
+        $this->manager->registerCalendar($calendar2);
292
+
293
+        $result = $this->manager->getCalendars();
294
+        $this->assertCount(2, $result);
295
+        $this->assertContains($calendar1, $result);
296
+        $this->assertContains($calendar2, $result);
297
+
298
+        $this->manager->clear();
299
+
300
+        $result = $this->manager->getCalendars();
301
+
302
+        $this->assertCount(0, $result);
303
+    }
304
+
305
+    public function testEnabledIfNot(): void {
306
+        $isEnabled = $this->manager->isEnabled();
307
+        $this->assertFalse($isEnabled);
308
+    }
309
+
310
+    public function testIfEnabledIfSo(): void {
311
+        /** @var ICalendar | MockObject $calendar */
312
+        $calendar = $this->createMock(ICalendar::class);
313
+        $this->manager->registerCalendar($calendar);
314
+
315
+        $isEnabled = $this->manager->isEnabled();
316
+        $this->assertTrue($isEnabled);
317
+    }
318
+
319
+    public function testHandleImipWithNoCalendars(): void {
320
+        // construct calendar manager returns
321
+        /** @var Manager&MockObject $manager */
322
+        $manager = $this->getMockBuilder(Manager::class)
323
+            ->setConstructorArgs([
324
+                $this->coordinator,
325
+                $this->container,
326
+                $this->logger,
327
+                $this->time,
328
+                $this->secureRandom,
329
+                $this->userManager,
330
+                $this->serverFactory,
331
+            ])
332
+            ->onlyMethods(['getCalendarsForPrincipal'])
333
+            ->getMock();
334
+        $manager->expects(self::once())
335
+            ->method('getCalendarsForPrincipal')
336
+            ->willReturn([]);
337
+        // construct logger returns
338
+        $this->logger->expects(self::once())->method('warning')
339
+            ->with('iMip message could not be processed because user has no calendars');
340
+        // construct parameters
341
+        $userId = 'attendee1';
342
+        $calendar = $this->vCalendar1a;
343
+        $calendar->add('METHOD', 'REQUEST');
344
+        // test method
345
+        $result = $manager->handleIMip($userId, $calendar->serialize());
346
+        // Assert
347
+        $this->assertFalse($result);
348
+    }
349
+
350
+    public function testHandleImipWithNoEvent(): void {
351
+        // construct mock user calendar
352
+        $userCalendar = $this->createMock(ITestCalendar::class);
353
+        // construct mock calendar manager and returns
354
+        /** @var Manager&MockObject $manager */
355
+        $manager = $this->getMockBuilder(Manager::class)
356
+            ->setConstructorArgs([
357
+                $this->coordinator,
358
+                $this->container,
359
+                $this->logger,
360
+                $this->time,
361
+                $this->secureRandom,
362
+                $this->userManager,
363
+                $this->serverFactory,
364
+            ])
365
+            ->onlyMethods(['getCalendarsForPrincipal'])
366
+            ->getMock();
367
+        $manager->expects(self::once())
368
+            ->method('getCalendarsForPrincipal')
369
+            ->willReturn([$userCalendar]);
370
+        // construct logger returns
371
+        $this->logger->expects(self::once())->method('warning')
372
+            ->with('iMip message does not contain any event(s)');
373
+        // construct parameters
374
+        $userId = 'attendee1';
375
+        $calendar = $this->vCalendar1a;
376
+        $calendar->add('METHOD', 'REQUEST');
377
+        $calendar->remove('VEVENT');
378
+        // Act
379
+        $result = $manager->handleIMip($userId, $calendar->serialize());
380
+        // Assert
381
+        $this->assertFalse($result);
382
+    }
383
+
384
+    public function testHandleImipWithNoUid(): void {
385
+        // construct mock user calendar
386
+        $userCalendar = $this->createMock(ITestCalendar::class);
387
+        // construct mock calendar manager and returns
388
+        /** @var Manager&MockObject $manager */
389
+        $manager = $this->getMockBuilder(Manager::class)
390
+            ->setConstructorArgs([
391
+                $this->coordinator,
392
+                $this->container,
393
+                $this->logger,
394
+                $this->time,
395
+                $this->secureRandom,
396
+                $this->userManager,
397
+                $this->serverFactory,
398
+            ])
399
+            ->onlyMethods(['getCalendarsForPrincipal'])
400
+            ->getMock();
401
+        $manager->expects(self::once())
402
+            ->method('getCalendarsForPrincipal')
403
+            ->willReturn([$userCalendar]);
404
+        // construct logger returns
405
+        $this->logger->expects(self::once())->method('warning')
406
+            ->with('iMip message event dose not contains a UID');
407
+        // construct parameters
408
+        $userId = 'attendee1';
409
+        $calendar = $this->vCalendar1a;
410
+        $calendar->add('METHOD', 'REQUEST');
411
+        $calendar->VEVENT->remove('UID');
412
+        // test method
413
+        $result = $manager->handleIMip($userId, $calendar->serialize());
414
+        // Assert
415
+        $this->assertFalse($result);
416
+    }
417
+
418
+    public function testHandleImipWithNoMatch(): void {
419
+        // construct mock user calendar
420
+        $userCalendar = $this->createMock(ITestCalendar::class);
421
+        $userCalendar->expects(self::once())
422
+            ->method('isDeleted')
423
+            ->willReturn(false);
424
+        $userCalendar->expects(self::once())
425
+            ->method('isWritable')
426
+            ->willReturn(true);
427
+        $userCalendar->expects(self::once())
428
+            ->method('search')
429
+            ->willReturn([]);
430
+        // construct mock calendar manager and returns
431
+        /** @var Manager&MockObject $manager */
432
+        $manager = $this->getMockBuilder(Manager::class)
433
+            ->setConstructorArgs([
434
+                $this->coordinator,
435
+                $this->container,
436
+                $this->logger,
437
+                $this->time,
438
+                $this->secureRandom,
439
+                $this->userManager,
440
+                $this->serverFactory,
441
+            ])
442
+            ->onlyMethods(['getCalendarsForPrincipal'])
443
+            ->getMock();
444
+        $manager->expects(self::once())
445
+            ->method('getCalendarsForPrincipal')
446
+            ->willReturn([$userCalendar]);
447
+        // construct logger returns
448
+        $this->logger->expects(self::once())->method('warning')
449
+            ->with('iMip message could not be processed because no corresponding event was found in any calendar');
450
+        // construct parameters
451
+        $userId = 'attendee1';
452
+        $calendar = $this->vCalendar1a;
453
+        $calendar->add('METHOD', 'REQUEST');
454
+        // test method
455
+        $result = $manager->handleIMip($userId, $calendar->serialize());
456
+        // Assert
457
+        $this->assertFalse($result);
458
+    }
459
+
460
+    public function testHandleImip(): void {
461
+        // construct mock user calendar
462
+        $userCalendar = $this->createMock(ITestCalendar::class);
463
+        $userCalendar->expects(self::once())
464
+            ->method('isDeleted')
465
+            ->willReturn(false);
466
+        $userCalendar->expects(self::once())
467
+            ->method('isWritable')
468
+            ->willReturn(true);
469
+        $userCalendar->expects(self::once())
470
+            ->method('search')
471
+            ->willReturn([['uri' => 'principals/user/attendee1/personal']]);
472
+        // construct mock calendar manager and returns
473
+        /** @var Manager&MockObject $manager */
474
+        $manager = $this->getMockBuilder(Manager::class)
475
+            ->setConstructorArgs([
476
+                $this->coordinator,
477
+                $this->container,
478
+                $this->logger,
479
+                $this->time,
480
+                $this->secureRandom,
481
+                $this->userManager,
482
+                $this->serverFactory,
483
+            ])
484
+            ->onlyMethods(['getCalendarsForPrincipal'])
485
+            ->getMock();
486
+        $manager->expects(self::once())
487
+            ->method('getCalendarsForPrincipal')
488
+            ->willReturn([$userCalendar]);
489
+        // construct parameters
490
+        $userId = 'attendee1';
491
+        $calendar = $this->vCalendar1a;
492
+        $calendar->add('METHOD', 'REQUEST');
493
+        // construct user calendar returns
494
+        $userCalendar->expects(self::once())
495
+            ->method('handleIMipMessage');
496
+        // test method
497
+        $result = $manager->handleIMip($userId, $calendar->serialize());
498
+    }
499
+
500
+    public function testhandleIMipRequestWithInvalidPrincipal() {
501
+        $invalidPrincipal = 'invalid-principal-uri';
502
+        $sender = '[email protected]';
503
+        $recipient = '[email protected]';
504
+        $calendarData = $this->vCalendar1a->serialize();
505
+
506
+        $this->logger->expects(self::once())
507
+            ->method('error')
508
+            ->with('Invalid principal URI provided for iMip request');
509
+
510
+        $result = $this->manager->handleIMipRequest($invalidPrincipal, $sender, $recipient, $calendarData);
511
+        $this->assertFalse($result);
512
+    }
513
+
514
+    public function testhandleIMipRequest() {
515
+        $principalUri = 'principals/users/attendee1';
516
+        $sender = '[email protected]';
517
+        $recipient = '[email protected]';
518
+        $calendarData = $this->vCalendar1a->serialize();
519
+
520
+        /** @var Manager&MockObject $manager */
521
+        $manager = $this->getMockBuilder(Manager::class)
522
+            ->setConstructorArgs([
523
+                $this->coordinator,
524
+                $this->container,
525
+                $this->logger,
526
+                $this->time,
527
+                $this->secureRandom,
528
+                $this->userManager,
529
+                $this->serverFactory,
530
+            ])
531
+            ->onlyMethods(['handleIMip'])
532
+            ->getMock();
533
+        $manager->expects(self::once())
534
+            ->method('handleIMip')
535
+            ->with('attendee1', $calendarData)
536
+            ->willReturn(true);
537
+
538
+        $result = $manager->handleIMipRequest($principalUri, $sender, $recipient, $calendarData);
539
+        $this->assertTrue($result);
540
+    }
541
+
542
+    public function testhandleIMipReplyWithInvalidPrincipal() {
543
+        $invalidPrincipal = 'invalid-principal-uri';
544
+        $sender = '[email protected]';
545
+        $recipient = '[email protected]';
546
+        $calendarData = $this->vCalendar2a->serialize();
547
+
548
+        $this->logger->expects(self::once())
549
+            ->method('error')
550
+            ->with('Invalid principal URI provided for iMip reply');
551
+
552
+        $result = $this->manager->handleIMipReply($invalidPrincipal, $sender, $recipient, $calendarData);
553
+        $this->assertFalse($result);
554
+    }
555
+
556
+    public function testhandleIMipReply() {
557
+        $principalUri = 'principals/users/attendee2';
558
+        $sender = '[email protected]';
559
+        $recipient = '[email protected]';
560
+        $calendarData = $this->vCalendar2a->serialize();
561
+
562
+        /** @var Manager&MockObject $manager */
563
+        $manager = $this->getMockBuilder(Manager::class)
564
+            ->setConstructorArgs([
565
+                $this->coordinator,
566
+                $this->container,
567
+                $this->logger,
568
+                $this->time,
569
+                $this->secureRandom,
570
+                $this->userManager,
571
+                $this->serverFactory,
572
+            ])
573
+            ->onlyMethods(['handleIMip'])
574
+            ->getMock();
575
+        $manager->expects(self::once())
576
+            ->method('handleIMip')
577
+            ->with('attendee2', $calendarData)
578
+            ->willReturn(true);
579
+
580
+        $result = $manager->handleIMipReply($principalUri, $sender, $recipient, $calendarData);
581
+        $this->assertTrue($result);
582
+    }
583
+
584
+    public function testhandleIMipCancelWithInvalidPrincipal() {
585
+        $invalidPrincipal = 'invalid-principal-uri';
586
+        $sender = '[email protected]';
587
+        $replyTo = null;
588
+        $recipient = '[email protected]';
589
+        $calendarData = $this->vCalendar3a->serialize();
590
+
591
+        $this->logger->expects(self::once())
592
+            ->method('error')
593
+            ->with('Invalid principal URI provided for iMip cancel');
594
+
595
+        $result = $this->manager->handleIMipCancel($invalidPrincipal, $sender, $replyTo, $recipient, $calendarData);
596
+        $this->assertFalse($result);
597
+    }
598
+
599
+    public function testhandleIMipCancel() {
600
+        $principalUri = 'principals/users/attendee3';
601
+        $sender = '[email protected]';
602
+        $replyTo = null;
603
+        $recipient = '[email protected]';
604
+        $calendarData = $this->vCalendar3a->serialize();
605
+
606
+        /** @var Manager&MockObject $manager */
607
+        $manager = $this->getMockBuilder(Manager::class)
608
+            ->setConstructorArgs([
609
+                $this->coordinator,
610
+                $this->container,
611
+                $this->logger,
612
+                $this->time,
613
+                $this->secureRandom,
614
+                $this->userManager,
615
+                $this->serverFactory,
616
+            ])
617
+            ->onlyMethods(['handleIMip'])
618
+            ->getMock();
619
+        $manager->expects(self::once())
620
+            ->method('handleIMip')
621
+            ->with('attendee3', $calendarData)
622
+            ->willReturn(true);
623
+
624
+        $result = $manager->handleIMipCancel($principalUri, $sender, $replyTo, $recipient, $calendarData);
625
+        $this->assertTrue($result);
626
+    }
627
+
628
+    private function getFreeBusyResponse(): string {
629
+        return <<<EOF
630 630
 <?xml version="1.0" encoding="utf-8"?>
631 631
 <cal:schedule-response xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:cal="urn:ietf:params:xml:ns:caldav" xmlns:cs="http://calendarserver.org/ns/" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns">
632 632
   <cal:response>
@@ -704,139 +704,139 @@  discard block
 block discarded – undo
704 704
   </cal:response>
705 705
 </cal:schedule-response>
706 706
 EOF;
707
-	}
708
-
709
-	public function testCheckAvailability(): void {
710
-		$organizer = $this->createMock(IUser::class);
711
-		$organizer->expects(self::once())
712
-			->method('getUID')
713
-			->willReturn('admin');
714
-		$organizer->expects(self::once())
715
-			->method('getEMailAddress')
716
-			->willReturn('[email protected]');
717
-
718
-		$user1 = $this->createMock(IUser::class);
719
-		$user2 = $this->createMock(IUser::class);
720
-
721
-		$this->userManager->expects(self::exactly(3))
722
-			->method('getByEmail')
723
-			->willReturnMap([
724
-				['[email protected]', [$user1]],
725
-				['[email protected]', [$user2]],
726
-				['[email protected]', []],
727
-			]);
728
-
729
-		$authPlugin = $this->createMock(CustomPrincipalPlugin::class);
730
-		$authPlugin->expects(self::once())
731
-			->method('setCurrentPrincipal')
732
-			->with('principals/users/admin');
733
-
734
-		$server = $this->createMock(Server::class);
735
-		$server->expects(self::once())
736
-			->method('getPlugin')
737
-			->with('auth')
738
-			->willReturn($authPlugin);
739
-		$server->expects(self::once())
740
-			->method('invokeMethod')
741
-			->willReturnCallback(function (
742
-				RequestInterface $request,
743
-				ResponseInterface $response,
744
-				bool $sendResponse,
745
-			): void {
746
-				$requestBody = file_get_contents(__DIR__ . '/../../data/ics/free-busy-request.ics');
747
-				$this->assertEquals('POST', $request->getMethod());
748
-				$this->assertEquals('calendars/admin/outbox', $request->getPath());
749
-				$this->assertEquals('text/calendar', $request->getHeader('Content-Type'));
750
-				$this->assertEquals('0', $request->getHeader('Depth'));
751
-				$this->assertEquals($requestBody, $request->getBodyAsString());
752
-				$this->assertFalse($sendResponse);
753
-				$response->setStatus(200);
754
-				$response->setBody($this->getFreeBusyResponse());
755
-			});
756
-
757
-		$this->serverFactory->expects(self::once())
758
-			->method('createAttendeeAvailabilityServer')
759
-			->willReturn($server);
760
-
761
-		$start = new DateTimeImmutable('2025-01-16T06:00:00Z');
762
-		$end = new DateTimeImmutable('2025-01-17T06:00:00Z');
763
-		$actual = $this->manager->checkAvailability($start, $end, $organizer, [
764
-			'[email protected]',
765
-			'[email protected]',
766
-			'[email protected]',
767
-		]);
768
-		$expected = [
769
-			new AvailabilityResult('[email protected]', false),
770
-			new AvailabilityResult('[email protected]', true),
771
-			new AvailabilityResult('[email protected]', false),
772
-		];
773
-		$this->assertEquals($expected, $actual);
774
-	}
775
-
776
-	public function testCheckAvailabilityWithMailtoPrefix(): void {
777
-		$organizer = $this->createMock(IUser::class);
778
-		$organizer->expects(self::once())
779
-			->method('getUID')
780
-			->willReturn('admin');
781
-		$organizer->expects(self::once())
782
-			->method('getEMailAddress')
783
-			->willReturn('[email protected]');
784
-
785
-		$user1 = $this->createMock(IUser::class);
786
-		$user2 = $this->createMock(IUser::class);
787
-
788
-		$this->userManager->expects(self::exactly(3))
789
-			->method('getByEmail')
790
-			->willReturnMap([
791
-				['[email protected]', [$user1]],
792
-				['[email protected]', [$user2]],
793
-				['[email protected]', []],
794
-			]);
795
-
796
-		$authPlugin = $this->createMock(CustomPrincipalPlugin::class);
797
-		$authPlugin->expects(self::once())
798
-			->method('setCurrentPrincipal')
799
-			->with('principals/users/admin');
800
-
801
-		$server = $this->createMock(Server::class);
802
-		$server->expects(self::once())
803
-			->method('getPlugin')
804
-			->with('auth')
805
-			->willReturn($authPlugin);
806
-		$server->expects(self::once())
807
-			->method('invokeMethod')
808
-			->willReturnCallback(function (
809
-				RequestInterface $request,
810
-				ResponseInterface $response,
811
-				bool $sendResponse,
812
-			): void {
813
-				$requestBody = file_get_contents(__DIR__ . '/../../data/ics/free-busy-request.ics');
814
-				$this->assertEquals('POST', $request->getMethod());
815
-				$this->assertEquals('calendars/admin/outbox', $request->getPath());
816
-				$this->assertEquals('text/calendar', $request->getHeader('Content-Type'));
817
-				$this->assertEquals('0', $request->getHeader('Depth'));
818
-				$this->assertEquals($requestBody, $request->getBodyAsString());
819
-				$this->assertFalse($sendResponse);
820
-				$response->setStatus(200);
821
-				$response->setBody($this->getFreeBusyResponse());
822
-			});
823
-
824
-		$this->serverFactory->expects(self::once())
825
-			->method('createAttendeeAvailabilityServer')
826
-			->willReturn($server);
827
-
828
-		$start = new DateTimeImmutable('2025-01-16T06:00:00Z');
829
-		$end = new DateTimeImmutable('2025-01-17T06:00:00Z');
830
-		$actual = $this->manager->checkAvailability($start, $end, $organizer, [
831
-			'mailto:[email protected]',
832
-			'mailto:[email protected]',
833
-			'mailto:[email protected]',
834
-		]);
835
-		$expected = [
836
-			new AvailabilityResult('[email protected]', false),
837
-			new AvailabilityResult('[email protected]', true),
838
-			new AvailabilityResult('[email protected]', false),
839
-		];
840
-		$this->assertEquals($expected, $actual);
841
-	}
707
+    }
708
+
709
+    public function testCheckAvailability(): void {
710
+        $organizer = $this->createMock(IUser::class);
711
+        $organizer->expects(self::once())
712
+            ->method('getUID')
713
+            ->willReturn('admin');
714
+        $organizer->expects(self::once())
715
+            ->method('getEMailAddress')
716
+            ->willReturn('[email protected]');
717
+
718
+        $user1 = $this->createMock(IUser::class);
719
+        $user2 = $this->createMock(IUser::class);
720
+
721
+        $this->userManager->expects(self::exactly(3))
722
+            ->method('getByEmail')
723
+            ->willReturnMap([
724
+                ['[email protected]', [$user1]],
725
+                ['[email protected]', [$user2]],
726
+                ['[email protected]', []],
727
+            ]);
728
+
729
+        $authPlugin = $this->createMock(CustomPrincipalPlugin::class);
730
+        $authPlugin->expects(self::once())
731
+            ->method('setCurrentPrincipal')
732
+            ->with('principals/users/admin');
733
+
734
+        $server = $this->createMock(Server::class);
735
+        $server->expects(self::once())
736
+            ->method('getPlugin')
737
+            ->with('auth')
738
+            ->willReturn($authPlugin);
739
+        $server->expects(self::once())
740
+            ->method('invokeMethod')
741
+            ->willReturnCallback(function (
742
+                RequestInterface $request,
743
+                ResponseInterface $response,
744
+                bool $sendResponse,
745
+            ): void {
746
+                $requestBody = file_get_contents(__DIR__ . '/../../data/ics/free-busy-request.ics');
747
+                $this->assertEquals('POST', $request->getMethod());
748
+                $this->assertEquals('calendars/admin/outbox', $request->getPath());
749
+                $this->assertEquals('text/calendar', $request->getHeader('Content-Type'));
750
+                $this->assertEquals('0', $request->getHeader('Depth'));
751
+                $this->assertEquals($requestBody, $request->getBodyAsString());
752
+                $this->assertFalse($sendResponse);
753
+                $response->setStatus(200);
754
+                $response->setBody($this->getFreeBusyResponse());
755
+            });
756
+
757
+        $this->serverFactory->expects(self::once())
758
+            ->method('createAttendeeAvailabilityServer')
759
+            ->willReturn($server);
760
+
761
+        $start = new DateTimeImmutable('2025-01-16T06:00:00Z');
762
+        $end = new DateTimeImmutable('2025-01-17T06:00:00Z');
763
+        $actual = $this->manager->checkAvailability($start, $end, $organizer, [
764
+            '[email protected]',
765
+            '[email protected]',
766
+            '[email protected]',
767
+        ]);
768
+        $expected = [
769
+            new AvailabilityResult('[email protected]', false),
770
+            new AvailabilityResult('[email protected]', true),
771
+            new AvailabilityResult('[email protected]', false),
772
+        ];
773
+        $this->assertEquals($expected, $actual);
774
+    }
775
+
776
+    public function testCheckAvailabilityWithMailtoPrefix(): void {
777
+        $organizer = $this->createMock(IUser::class);
778
+        $organizer->expects(self::once())
779
+            ->method('getUID')
780
+            ->willReturn('admin');
781
+        $organizer->expects(self::once())
782
+            ->method('getEMailAddress')
783
+            ->willReturn('[email protected]');
784
+
785
+        $user1 = $this->createMock(IUser::class);
786
+        $user2 = $this->createMock(IUser::class);
787
+
788
+        $this->userManager->expects(self::exactly(3))
789
+            ->method('getByEmail')
790
+            ->willReturnMap([
791
+                ['[email protected]', [$user1]],
792
+                ['[email protected]', [$user2]],
793
+                ['[email protected]', []],
794
+            ]);
795
+
796
+        $authPlugin = $this->createMock(CustomPrincipalPlugin::class);
797
+        $authPlugin->expects(self::once())
798
+            ->method('setCurrentPrincipal')
799
+            ->with('principals/users/admin');
800
+
801
+        $server = $this->createMock(Server::class);
802
+        $server->expects(self::once())
803
+            ->method('getPlugin')
804
+            ->with('auth')
805
+            ->willReturn($authPlugin);
806
+        $server->expects(self::once())
807
+            ->method('invokeMethod')
808
+            ->willReturnCallback(function (
809
+                RequestInterface $request,
810
+                ResponseInterface $response,
811
+                bool $sendResponse,
812
+            ): void {
813
+                $requestBody = file_get_contents(__DIR__ . '/../../data/ics/free-busy-request.ics');
814
+                $this->assertEquals('POST', $request->getMethod());
815
+                $this->assertEquals('calendars/admin/outbox', $request->getPath());
816
+                $this->assertEquals('text/calendar', $request->getHeader('Content-Type'));
817
+                $this->assertEquals('0', $request->getHeader('Depth'));
818
+                $this->assertEquals($requestBody, $request->getBodyAsString());
819
+                $this->assertFalse($sendResponse);
820
+                $response->setStatus(200);
821
+                $response->setBody($this->getFreeBusyResponse());
822
+            });
823
+
824
+        $this->serverFactory->expects(self::once())
825
+            ->method('createAttendeeAvailabilityServer')
826
+            ->willReturn($server);
827
+
828
+        $start = new DateTimeImmutable('2025-01-16T06:00:00Z');
829
+        $end = new DateTimeImmutable('2025-01-17T06:00:00Z');
830
+        $actual = $this->manager->checkAvailability($start, $end, $organizer, [
831
+            'mailto:[email protected]',
832
+            'mailto:[email protected]',
833
+            'mailto:[email protected]',
834
+        ]);
835
+        $expected = [
836
+            new AvailabilityResult('[email protected]', false),
837
+            new AvailabilityResult('[email protected]', true),
838
+            new AvailabilityResult('[email protected]', false),
839
+        ];
840
+        $this->assertEquals($expected, $actual);
841
+    }
842 842
 }
Please login to merge, or discard this patch.