Completed
Push — master ( 391e09...535253 )
by Christoph
36:01 queued 15:56
created
apps/dav/lib/CalDAV/CalDavBackend.php 2 patches
Indentation   +3541 added lines, -3541 removed lines patch added patch discarded remove patch
@@ -90,3545 +90,3545 @@
 block discarded – undo
90 90
  * @package OCA\DAV\CalDAV
91 91
  */
92 92
 class CalDavBackend extends AbstractBackend implements SyncSupport, SubscriptionSupport, SchedulingSupport {
93
-	use TTransactional;
94
-
95
-	public const CALENDAR_TYPE_CALENDAR = 0;
96
-	public const CALENDAR_TYPE_SUBSCRIPTION = 1;
97
-
98
-	public const PERSONAL_CALENDAR_URI = 'personal';
99
-	public const PERSONAL_CALENDAR_NAME = 'Personal';
100
-
101
-	public const RESOURCE_BOOKING_CALENDAR_URI = 'calendar';
102
-	public const RESOURCE_BOOKING_CALENDAR_NAME = 'Calendar';
103
-
104
-	/**
105
-	 * We need to specify a max date, because we need to stop *somewhere*
106
-	 *
107
-	 * On 32 bit system the maximum for a signed integer is 2147483647, so
108
-	 * MAX_DATE cannot be higher than date('Y-m-d', 2147483647) which results
109
-	 * in 2038-01-19 to avoid problems when the date is converted
110
-	 * to a unix timestamp.
111
-	 */
112
-	public const MAX_DATE = '2038-01-01';
113
-
114
-	public const ACCESS_PUBLIC = 4;
115
-	public const CLASSIFICATION_PUBLIC = 0;
116
-	public const CLASSIFICATION_PRIVATE = 1;
117
-	public const CLASSIFICATION_CONFIDENTIAL = 2;
118
-
119
-	/**
120
-	 * List of CalDAV properties, and how they map to database field names and their type
121
-	 * Add your own properties by simply adding on to this array.
122
-	 *
123
-	 * @var array
124
-	 * @psalm-var array<string, string[]>
125
-	 */
126
-	public array $propertyMap = [
127
-		'{DAV:}displayname' => ['displayname', 'string'],
128
-		'{urn:ietf:params:xml:ns:caldav}calendar-description' => ['description', 'string'],
129
-		'{urn:ietf:params:xml:ns:caldav}calendar-timezone' => ['timezone', 'string'],
130
-		'{http://apple.com/ns/ical/}calendar-order' => ['calendarorder', 'int'],
131
-		'{http://apple.com/ns/ical/}calendar-color' => ['calendarcolor', 'string'],
132
-		'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => ['deleted_at', 'int'],
133
-	];
134
-
135
-	/**
136
-	 * List of subscription properties, and how they map to database field names.
137
-	 *
138
-	 * @var array
139
-	 */
140
-	public array $subscriptionPropertyMap = [
141
-		'{DAV:}displayname' => ['displayname', 'string'],
142
-		'{http://apple.com/ns/ical/}refreshrate' => ['refreshrate', 'string'],
143
-		'{http://apple.com/ns/ical/}calendar-order' => ['calendarorder', 'int'],
144
-		'{http://apple.com/ns/ical/}calendar-color' => ['calendarcolor', 'string'],
145
-		'{http://calendarserver.org/ns/}subscribed-strip-todos' => ['striptodos', 'bool'],
146
-		'{http://calendarserver.org/ns/}subscribed-strip-alarms' => ['stripalarms', 'string'],
147
-		'{http://calendarserver.org/ns/}subscribed-strip-attachments' => ['stripattachments', 'string'],
148
-	];
149
-
150
-	/**
151
-	 * properties to index
152
-	 *
153
-	 * This list has to be kept in sync with ICalendarQuery::SEARCH_PROPERTY_*
154
-	 *
155
-	 * @see \OCP\Calendar\ICalendarQuery
156
-	 */
157
-	private const INDEXED_PROPERTIES = [
158
-		'CATEGORIES',
159
-		'COMMENT',
160
-		'DESCRIPTION',
161
-		'LOCATION',
162
-		'RESOURCES',
163
-		'STATUS',
164
-		'SUMMARY',
165
-		'ATTENDEE',
166
-		'CONTACT',
167
-		'ORGANIZER'
168
-	];
169
-
170
-	/** @var array parameters to index */
171
-	public static array $indexParameters = [
172
-		'ATTENDEE' => ['CN'],
173
-		'ORGANIZER' => ['CN'],
174
-	];
175
-
176
-	/**
177
-	 * @var string[] Map of uid => display name
178
-	 */
179
-	protected array $userDisplayNames;
180
-
181
-	private string $dbObjectsTable = 'calendarobjects';
182
-	private string $dbObjectPropertiesTable = 'calendarobjects_props';
183
-	private string $dbObjectInvitationsTable = 'calendar_invitations';
184
-	private array $cachedObjects = [];
185
-
186
-	public function __construct(
187
-		private IDBConnection $db,
188
-		private Principal $principalBackend,
189
-		private IUserManager $userManager,
190
-		private ISecureRandom $random,
191
-		private LoggerInterface $logger,
192
-		private IEventDispatcher $dispatcher,
193
-		private IConfig $config,
194
-		private Sharing\Backend $calendarSharingBackend,
195
-		private bool $legacyEndpoint = false,
196
-	) {
197
-	}
198
-
199
-	/**
200
-	 * Return the number of calendars for a principal
201
-	 *
202
-	 * By default this excludes the automatically generated birthday calendar
203
-	 *
204
-	 * @param $principalUri
205
-	 * @param bool $excludeBirthday
206
-	 * @return int
207
-	 */
208
-	public function getCalendarsForUserCount($principalUri, $excludeBirthday = true) {
209
-		$principalUri = $this->convertPrincipal($principalUri, true);
210
-		$query = $this->db->getQueryBuilder();
211
-		$query->select($query->func()->count('*'))
212
-			->from('calendars');
213
-
214
-		if ($principalUri === '') {
215
-			$query->where($query->expr()->emptyString('principaluri'));
216
-		} else {
217
-			$query->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
218
-		}
219
-
220
-		if ($excludeBirthday) {
221
-			$query->andWhere($query->expr()->neq('uri', $query->createNamedParameter(BirthdayService::BIRTHDAY_CALENDAR_URI)));
222
-		}
223
-
224
-		$result = $query->executeQuery();
225
-		$column = (int)$result->fetchOne();
226
-		$result->closeCursor();
227
-		return $column;
228
-	}
229
-
230
-	/**
231
-	 * Return the number of subscriptions for a principal
232
-	 */
233
-	public function getSubscriptionsForUserCount(string $principalUri): int {
234
-		$principalUri = $this->convertPrincipal($principalUri, true);
235
-		$query = $this->db->getQueryBuilder();
236
-		$query->select($query->func()->count('*'))
237
-			->from('calendarsubscriptions');
238
-
239
-		if ($principalUri === '') {
240
-			$query->where($query->expr()->emptyString('principaluri'));
241
-		} else {
242
-			$query->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
243
-		}
244
-
245
-		$result = $query->executeQuery();
246
-		$column = (int)$result->fetchOne();
247
-		$result->closeCursor();
248
-		return $column;
249
-	}
250
-
251
-	/**
252
-	 * @return array{id: int, deleted_at: int}[]
253
-	 */
254
-	public function getDeletedCalendars(int $deletedBefore): array {
255
-		$qb = $this->db->getQueryBuilder();
256
-		$qb->select(['id', 'deleted_at'])
257
-			->from('calendars')
258
-			->where($qb->expr()->isNotNull('deleted_at'))
259
-			->andWhere($qb->expr()->lt('deleted_at', $qb->createNamedParameter($deletedBefore)));
260
-		$result = $qb->executeQuery();
261
-		$calendars = [];
262
-		while (($row = $result->fetch()) !== false) {
263
-			$calendars[] = [
264
-				'id' => (int)$row['id'],
265
-				'deleted_at' => (int)$row['deleted_at'],
266
-			];
267
-		}
268
-		$result->closeCursor();
269
-		return $calendars;
270
-	}
271
-
272
-	/**
273
-	 * Returns a list of calendars for a principal.
274
-	 *
275
-	 * Every project is an array with the following keys:
276
-	 *  * id, a unique id that will be used by other functions to modify the
277
-	 *    calendar. This can be the same as the uri or a database key.
278
-	 *  * uri, which the basename of the uri with which the calendar is
279
-	 *    accessed.
280
-	 *  * principaluri. The owner of the calendar. Almost always the same as
281
-	 *    principalUri passed to this method.
282
-	 *
283
-	 * Furthermore it can contain webdav properties in clark notation. A very
284
-	 * common one is '{DAV:}displayname'.
285
-	 *
286
-	 * Many clients also require:
287
-	 * {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
288
-	 * For this property, you can just return an instance of
289
-	 * Sabre\CalDAV\Property\SupportedCalendarComponentSet.
290
-	 *
291
-	 * If you return {http://sabredav.org/ns}read-only and set the value to 1,
292
-	 * ACL will automatically be put in read-only mode.
293
-	 *
294
-	 * @param string $principalUri
295
-	 * @return array
296
-	 */
297
-	public function getCalendarsForUser($principalUri) {
298
-		return $this->atomic(function () use ($principalUri) {
299
-			$principalUriOriginal = $principalUri;
300
-			$principalUri = $this->convertPrincipal($principalUri, true);
301
-			$fields = array_column($this->propertyMap, 0);
302
-			$fields[] = 'id';
303
-			$fields[] = 'uri';
304
-			$fields[] = 'synctoken';
305
-			$fields[] = 'components';
306
-			$fields[] = 'principaluri';
307
-			$fields[] = 'transparent';
308
-
309
-			// Making fields a comma-delimited list
310
-			$query = $this->db->getQueryBuilder();
311
-			$query->select($fields)
312
-				->from('calendars')
313
-				->orderBy('calendarorder', 'ASC');
314
-
315
-			if ($principalUri === '') {
316
-				$query->where($query->expr()->emptyString('principaluri'));
317
-			} else {
318
-				$query->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
319
-			}
320
-
321
-			$result = $query->executeQuery();
322
-
323
-			$calendars = [];
324
-			while ($row = $result->fetch()) {
325
-				$row['principaluri'] = (string)$row['principaluri'];
326
-				$components = [];
327
-				if ($row['components']) {
328
-					$components = explode(',', $row['components']);
329
-				}
330
-
331
-				$calendar = [
332
-					'id' => $row['id'],
333
-					'uri' => $row['uri'],
334
-					'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
335
-					'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ?: '0'),
336
-					'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
337
-					'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
338
-					'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
339
-					'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($principalUri, !$this->legacyEndpoint),
340
-				];
341
-
342
-				$calendar = $this->rowToCalendar($row, $calendar);
343
-				$calendar = $this->addOwnerPrincipalToCalendar($calendar);
344
-				$calendar = $this->addResourceTypeToCalendar($row, $calendar);
345
-
346
-				if (!isset($calendars[$calendar['id']])) {
347
-					$calendars[$calendar['id']] = $calendar;
348
-				}
349
-			}
350
-			$result->closeCursor();
351
-
352
-			// query for shared calendars
353
-			$principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true);
354
-			$principals = array_merge($principals, $this->principalBackend->getCircleMembership($principalUriOriginal));
355
-			$principals[] = $principalUri;
356
-
357
-			$fields = array_column($this->propertyMap, 0);
358
-			$fields = array_map(function (string $field) {
359
-				return 'a.' . $field;
360
-			}, $fields);
361
-			$fields[] = 'a.id';
362
-			$fields[] = 'a.uri';
363
-			$fields[] = 'a.synctoken';
364
-			$fields[] = 'a.components';
365
-			$fields[] = 'a.principaluri';
366
-			$fields[] = 'a.transparent';
367
-			$fields[] = 's.access';
368
-
369
-			$select = $this->db->getQueryBuilder();
370
-			$subSelect = $this->db->getQueryBuilder();
371
-
372
-			$subSelect->select('resourceid')
373
-				->from('dav_shares', 'd')
374
-				->where($subSelect->expr()->eq('d.access', $select->createNamedParameter(Backend::ACCESS_UNSHARED, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT))
375
-				->andWhere($subSelect->expr()->in('d.principaluri', $select->createNamedParameter($principals, IQueryBuilder::PARAM_STR_ARRAY), IQueryBuilder::PARAM_STR_ARRAY));
376
-
377
-			$select->select($fields)
378
-				->from('dav_shares', 's')
379
-				->join('s', 'calendars', 'a', $select->expr()->eq('s.resourceid', 'a.id', IQueryBuilder::PARAM_INT))
380
-				->where($select->expr()->in('s.principaluri', $select->createNamedParameter($principals, IQueryBuilder::PARAM_STR_ARRAY), IQueryBuilder::PARAM_STR_ARRAY))
381
-				->andWhere($select->expr()->eq('s.type', $select->createNamedParameter('calendar', IQueryBuilder::PARAM_STR), IQueryBuilder::PARAM_STR))
382
-				->andWhere($select->expr()->notIn('a.id', $select->createFunction($subSelect->getSQL()), IQueryBuilder::PARAM_INT_ARRAY));
383
-
384
-			$results = $select->executeQuery();
385
-
386
-			$readOnlyPropertyName = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only';
387
-			while ($row = $results->fetch()) {
388
-				$row['principaluri'] = (string)$row['principaluri'];
389
-				if ($row['principaluri'] === $principalUri) {
390
-					continue;
391
-				}
392
-
393
-				$readOnly = (int)$row['access'] === Backend::ACCESS_READ;
394
-				if (isset($calendars[$row['id']])) {
395
-					if ($readOnly) {
396
-						// New share can not have more permissions than the old one.
397
-						continue;
398
-					}
399
-					if (isset($calendars[$row['id']][$readOnlyPropertyName]) &&
400
-						$calendars[$row['id']][$readOnlyPropertyName] === 0) {
401
-						// Old share is already read-write, no more permissions can be gained
402
-						continue;
403
-					}
404
-				}
405
-
406
-				[, $name] = Uri\split($row['principaluri']);
407
-				$uri = $row['uri'] . '_shared_by_' . $name;
408
-				$row['displayname'] = $row['displayname'] . ' (' . ($this->userManager->getDisplayName($name) ?? ($name ?? '')) . ')';
409
-				$components = [];
410
-				if ($row['components']) {
411
-					$components = explode(',', $row['components']);
412
-				}
413
-				$calendar = [
414
-					'id' => $row['id'],
415
-					'uri' => $uri,
416
-					'principaluri' => $this->convertPrincipal($principalUri, !$this->legacyEndpoint),
417
-					'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ?: '0'),
418
-					'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
419
-					'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
420
-					'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp('transparent'),
421
-					'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
422
-					$readOnlyPropertyName => $readOnly,
423
-				];
424
-
425
-				$calendar = $this->rowToCalendar($row, $calendar);
426
-				$calendar = $this->addOwnerPrincipalToCalendar($calendar);
427
-				$calendar = $this->addResourceTypeToCalendar($row, $calendar);
428
-
429
-				$calendars[$calendar['id']] = $calendar;
430
-			}
431
-			$result->closeCursor();
432
-
433
-			return array_values($calendars);
434
-		}, $this->db);
435
-	}
436
-
437
-	/**
438
-	 * @param $principalUri
439
-	 * @return array
440
-	 */
441
-	public function getUsersOwnCalendars($principalUri) {
442
-		$principalUri = $this->convertPrincipal($principalUri, true);
443
-		$fields = array_column($this->propertyMap, 0);
444
-		$fields[] = 'id';
445
-		$fields[] = 'uri';
446
-		$fields[] = 'synctoken';
447
-		$fields[] = 'components';
448
-		$fields[] = 'principaluri';
449
-		$fields[] = 'transparent';
450
-		// Making fields a comma-delimited list
451
-		$query = $this->db->getQueryBuilder();
452
-		$query->select($fields)->from('calendars')
453
-			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
454
-			->orderBy('calendarorder', 'ASC');
455
-		$stmt = $query->executeQuery();
456
-		$calendars = [];
457
-		while ($row = $stmt->fetch()) {
458
-			$row['principaluri'] = (string)$row['principaluri'];
459
-			$components = [];
460
-			if ($row['components']) {
461
-				$components = explode(',', $row['components']);
462
-			}
463
-			$calendar = [
464
-				'id' => $row['id'],
465
-				'uri' => $row['uri'],
466
-				'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
467
-				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ?: '0'),
468
-				'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
469
-				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
470
-				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
471
-			];
472
-
473
-			$calendar = $this->rowToCalendar($row, $calendar);
474
-			$calendar = $this->addOwnerPrincipalToCalendar($calendar);
475
-			$calendar = $this->addResourceTypeToCalendar($row, $calendar);
476
-
477
-			if (!isset($calendars[$calendar['id']])) {
478
-				$calendars[$calendar['id']] = $calendar;
479
-			}
480
-		}
481
-		$stmt->closeCursor();
482
-		return array_values($calendars);
483
-	}
484
-
485
-	/**
486
-	 * @return array
487
-	 */
488
-	public function getPublicCalendars() {
489
-		$fields = array_column($this->propertyMap, 0);
490
-		$fields[] = 'a.id';
491
-		$fields[] = 'a.uri';
492
-		$fields[] = 'a.synctoken';
493
-		$fields[] = 'a.components';
494
-		$fields[] = 'a.principaluri';
495
-		$fields[] = 'a.transparent';
496
-		$fields[] = 's.access';
497
-		$fields[] = 's.publicuri';
498
-		$calendars = [];
499
-		$query = $this->db->getQueryBuilder();
500
-		$result = $query->select($fields)
501
-			->from('dav_shares', 's')
502
-			->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
503
-			->where($query->expr()->in('s.access', $query->createNamedParameter(self::ACCESS_PUBLIC)))
504
-			->andWhere($query->expr()->eq('s.type', $query->createNamedParameter('calendar')))
505
-			->executeQuery();
506
-
507
-		while ($row = $result->fetch()) {
508
-			$row['principaluri'] = (string)$row['principaluri'];
509
-			[, $name] = Uri\split($row['principaluri']);
510
-			$row['displayname'] = $row['displayname'] . "($name)";
511
-			$components = [];
512
-			if ($row['components']) {
513
-				$components = explode(',', $row['components']);
514
-			}
515
-			$calendar = [
516
-				'id' => $row['id'],
517
-				'uri' => $row['publicuri'],
518
-				'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
519
-				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ?: '0'),
520
-				'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
521
-				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
522
-				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
523
-				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], $this->legacyEndpoint),
524
-				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ,
525
-				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}public' => (int)$row['access'] === self::ACCESS_PUBLIC,
526
-			];
527
-
528
-			$calendar = $this->rowToCalendar($row, $calendar);
529
-			$calendar = $this->addOwnerPrincipalToCalendar($calendar);
530
-			$calendar = $this->addResourceTypeToCalendar($row, $calendar);
531
-
532
-			if (!isset($calendars[$calendar['id']])) {
533
-				$calendars[$calendar['id']] = $calendar;
534
-			}
535
-		}
536
-		$result->closeCursor();
537
-
538
-		return array_values($calendars);
539
-	}
540
-
541
-	/**
542
-	 * @param string $uri
543
-	 * @return array
544
-	 * @throws NotFound
545
-	 */
546
-	public function getPublicCalendar($uri) {
547
-		$fields = array_column($this->propertyMap, 0);
548
-		$fields[] = 'a.id';
549
-		$fields[] = 'a.uri';
550
-		$fields[] = 'a.synctoken';
551
-		$fields[] = 'a.components';
552
-		$fields[] = 'a.principaluri';
553
-		$fields[] = 'a.transparent';
554
-		$fields[] = 's.access';
555
-		$fields[] = 's.publicuri';
556
-		$query = $this->db->getQueryBuilder();
557
-		$result = $query->select($fields)
558
-			->from('dav_shares', 's')
559
-			->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
560
-			->where($query->expr()->in('s.access', $query->createNamedParameter(self::ACCESS_PUBLIC)))
561
-			->andWhere($query->expr()->eq('s.type', $query->createNamedParameter('calendar')))
562
-			->andWhere($query->expr()->eq('s.publicuri', $query->createNamedParameter($uri)))
563
-			->executeQuery();
564
-
565
-		$row = $result->fetch();
566
-
567
-		$result->closeCursor();
568
-
569
-		if ($row === false) {
570
-			throw new NotFound('Node with name \'' . $uri . '\' could not be found');
571
-		}
572
-
573
-		$row['principaluri'] = (string)$row['principaluri'];
574
-		[, $name] = Uri\split($row['principaluri']);
575
-		$row['displayname'] = $row['displayname'] . ' ' . "($name)";
576
-		$components = [];
577
-		if ($row['components']) {
578
-			$components = explode(',', $row['components']);
579
-		}
580
-		$calendar = [
581
-			'id' => $row['id'],
582
-			'uri' => $row['publicuri'],
583
-			'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
584
-			'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ?: '0'),
585
-			'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
586
-			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
587
-			'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
588
-			'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
589
-			'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ,
590
-			'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}public' => (int)$row['access'] === self::ACCESS_PUBLIC,
591
-		];
592
-
593
-		$calendar = $this->rowToCalendar($row, $calendar);
594
-		$calendar = $this->addOwnerPrincipalToCalendar($calendar);
595
-		$calendar = $this->addResourceTypeToCalendar($row, $calendar);
596
-
597
-		return $calendar;
598
-	}
599
-
600
-	/**
601
-	 * @param string $principal
602
-	 * @param string $uri
603
-	 * @return array|null
604
-	 */
605
-	public function getCalendarByUri($principal, $uri) {
606
-		$fields = array_column($this->propertyMap, 0);
607
-		$fields[] = 'id';
608
-		$fields[] = 'uri';
609
-		$fields[] = 'synctoken';
610
-		$fields[] = 'components';
611
-		$fields[] = 'principaluri';
612
-		$fields[] = 'transparent';
613
-
614
-		// Making fields a comma-delimited list
615
-		$query = $this->db->getQueryBuilder();
616
-		$query->select($fields)->from('calendars')
617
-			->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
618
-			->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($principal)))
619
-			->setMaxResults(1);
620
-		$stmt = $query->executeQuery();
621
-
622
-		$row = $stmt->fetch();
623
-		$stmt->closeCursor();
624
-		if ($row === false) {
625
-			return null;
626
-		}
627
-
628
-		$row['principaluri'] = (string)$row['principaluri'];
629
-		$components = [];
630
-		if ($row['components']) {
631
-			$components = explode(',', $row['components']);
632
-		}
633
-
634
-		$calendar = [
635
-			'id' => $row['id'],
636
-			'uri' => $row['uri'],
637
-			'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
638
-			'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ?: '0'),
639
-			'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
640
-			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
641
-			'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
642
-		];
643
-
644
-		$calendar = $this->rowToCalendar($row, $calendar);
645
-		$calendar = $this->addOwnerPrincipalToCalendar($calendar);
646
-		$calendar = $this->addResourceTypeToCalendar($row, $calendar);
647
-
648
-		return $calendar;
649
-	}
650
-
651
-	/**
652
-	 * @return array{id: int, uri: string, '{http://calendarserver.org/ns/}getctag': string, '{http://sabredav.org/ns}sync-token': int, '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set': SupportedCalendarComponentSet, '{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp': ScheduleCalendarTransp, '{urn:ietf:params:xml:ns:caldav}calendar-timezone': ?string }|null
653
-	 */
654
-	public function getCalendarById(int $calendarId): ?array {
655
-		$fields = array_column($this->propertyMap, 0);
656
-		$fields[] = 'id';
657
-		$fields[] = 'uri';
658
-		$fields[] = 'synctoken';
659
-		$fields[] = 'components';
660
-		$fields[] = 'principaluri';
661
-		$fields[] = 'transparent';
662
-
663
-		// Making fields a comma-delimited list
664
-		$query = $this->db->getQueryBuilder();
665
-		$query->select($fields)->from('calendars')
666
-			->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)))
667
-			->setMaxResults(1);
668
-		$stmt = $query->executeQuery();
669
-
670
-		$row = $stmt->fetch();
671
-		$stmt->closeCursor();
672
-		if ($row === false) {
673
-			return null;
674
-		}
675
-
676
-		$row['principaluri'] = (string)$row['principaluri'];
677
-		$components = [];
678
-		if ($row['components']) {
679
-			$components = explode(',', $row['components']);
680
-		}
681
-
682
-		$calendar = [
683
-			'id' => $row['id'],
684
-			'uri' => $row['uri'],
685
-			'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
686
-			'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ?: '0'),
687
-			'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?? 0,
688
-			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
689
-			'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
690
-		];
691
-
692
-		$calendar = $this->rowToCalendar($row, $calendar);
693
-		$calendar = $this->addOwnerPrincipalToCalendar($calendar);
694
-		$calendar = $this->addResourceTypeToCalendar($row, $calendar);
695
-
696
-		return $calendar;
697
-	}
698
-
699
-	/**
700
-	 * @param $subscriptionId
701
-	 */
702
-	public function getSubscriptionById($subscriptionId) {
703
-		$fields = array_column($this->subscriptionPropertyMap, 0);
704
-		$fields[] = 'id';
705
-		$fields[] = 'uri';
706
-		$fields[] = 'source';
707
-		$fields[] = 'synctoken';
708
-		$fields[] = 'principaluri';
709
-		$fields[] = 'lastmodified';
710
-
711
-		$query = $this->db->getQueryBuilder();
712
-		$query->select($fields)
713
-			->from('calendarsubscriptions')
714
-			->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
715
-			->orderBy('calendarorder', 'asc');
716
-		$stmt = $query->executeQuery();
717
-
718
-		$row = $stmt->fetch();
719
-		$stmt->closeCursor();
720
-		if ($row === false) {
721
-			return null;
722
-		}
723
-
724
-		$row['principaluri'] = (string)$row['principaluri'];
725
-		$subscription = [
726
-			'id' => $row['id'],
727
-			'uri' => $row['uri'],
728
-			'principaluri' => $row['principaluri'],
729
-			'source' => $row['source'],
730
-			'lastmodified' => $row['lastmodified'],
731
-			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
732
-			'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
733
-		];
734
-
735
-		return $this->rowToSubscription($row, $subscription);
736
-	}
737
-
738
-	public function getSubscriptionByUri(string $principal, string $uri): ?array {
739
-		$fields = array_column($this->subscriptionPropertyMap, 0);
740
-		$fields[] = 'id';
741
-		$fields[] = 'uri';
742
-		$fields[] = 'source';
743
-		$fields[] = 'synctoken';
744
-		$fields[] = 'principaluri';
745
-		$fields[] = 'lastmodified';
746
-
747
-		$query = $this->db->getQueryBuilder();
748
-		$query->select($fields)
749
-			->from('calendarsubscriptions')
750
-			->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
751
-			->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($principal)))
752
-			->setMaxResults(1);
753
-		$stmt = $query->executeQuery();
754
-
755
-		$row = $stmt->fetch();
756
-		$stmt->closeCursor();
757
-		if ($row === false) {
758
-			return null;
759
-		}
760
-
761
-		$row['principaluri'] = (string)$row['principaluri'];
762
-		$subscription = [
763
-			'id' => $row['id'],
764
-			'uri' => $row['uri'],
765
-			'principaluri' => $row['principaluri'],
766
-			'source' => $row['source'],
767
-			'lastmodified' => $row['lastmodified'],
768
-			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
769
-			'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
770
-		];
771
-
772
-		return $this->rowToSubscription($row, $subscription);
773
-	}
774
-
775
-	/**
776
-	 * Creates a new calendar for a principal.
777
-	 *
778
-	 * If the creation was a success, an id must be returned that can be used to reference
779
-	 * this calendar in other methods, such as updateCalendar.
780
-	 *
781
-	 * @param string $principalUri
782
-	 * @param string $calendarUri
783
-	 * @param array $properties
784
-	 * @return int
785
-	 *
786
-	 * @throws CalendarException
787
-	 */
788
-	public function createCalendar($principalUri, $calendarUri, array $properties) {
789
-		if (strlen($calendarUri) > 255) {
790
-			throw new CalendarException('URI too long. Calendar not created');
791
-		}
792
-
793
-		$values = [
794
-			'principaluri' => $this->convertPrincipal($principalUri, true),
795
-			'uri' => $calendarUri,
796
-			'synctoken' => 1,
797
-			'transparent' => 0,
798
-			'components' => 'VEVENT,VTODO,VJOURNAL',
799
-			'displayname' => $calendarUri
800
-		];
801
-
802
-		// Default value
803
-		$sccs = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set';
804
-		if (isset($properties[$sccs])) {
805
-			if (!($properties[$sccs] instanceof SupportedCalendarComponentSet)) {
806
-				throw new DAV\Exception('The ' . $sccs . ' property must be of type: \Sabre\CalDAV\Property\SupportedCalendarComponentSet');
807
-			}
808
-			$values['components'] = implode(',', $properties[$sccs]->getValue());
809
-		} elseif (isset($properties['components'])) {
810
-			// Allow to provide components internally without having
811
-			// to create a SupportedCalendarComponentSet object
812
-			$values['components'] = $properties['components'];
813
-		}
814
-
815
-		$transp = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp';
816
-		if (isset($properties[$transp])) {
817
-			$values['transparent'] = (int)($properties[$transp]->getValue() === 'transparent');
818
-		}
819
-
820
-		foreach ($this->propertyMap as $xmlName => [$dbName, $type]) {
821
-			if (isset($properties[$xmlName])) {
822
-				$values[$dbName] = $properties[$xmlName];
823
-			}
824
-		}
825
-
826
-		[$calendarId, $calendarData] = $this->atomic(function () use ($values) {
827
-			$query = $this->db->getQueryBuilder();
828
-			$query->insert('calendars');
829
-			foreach ($values as $column => $value) {
830
-				$query->setValue($column, $query->createNamedParameter($value));
831
-			}
832
-			$query->executeStatement();
833
-			$calendarId = $query->getLastInsertId();
834
-
835
-			$calendarData = $this->getCalendarById($calendarId);
836
-			return [$calendarId, $calendarData];
837
-		}, $this->db);
838
-
839
-		$this->dispatcher->dispatchTyped(new CalendarCreatedEvent((int)$calendarId, $calendarData));
840
-
841
-		return $calendarId;
842
-	}
843
-
844
-	/**
845
-	 * Updates properties for a calendar.
846
-	 *
847
-	 * The list of mutations is stored in a Sabre\DAV\PropPatch object.
848
-	 * To do the actual updates, you must tell this object which properties
849
-	 * you're going to process with the handle() method.
850
-	 *
851
-	 * Calling the handle method is like telling the PropPatch object "I
852
-	 * promise I can handle updating this property".
853
-	 *
854
-	 * Read the PropPatch documentation for more info and examples.
855
-	 *
856
-	 * @param mixed $calendarId
857
-	 * @param PropPatch $propPatch
858
-	 * @return void
859
-	 */
860
-	public function updateCalendar($calendarId, PropPatch $propPatch) {
861
-		$supportedProperties = array_keys($this->propertyMap);
862
-		$supportedProperties[] = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp';
863
-
864
-		$propPatch->handle($supportedProperties, function ($mutations) use ($calendarId) {
865
-			$newValues = [];
866
-			foreach ($mutations as $propertyName => $propertyValue) {
867
-				switch ($propertyName) {
868
-					case '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp':
869
-						$fieldName = 'transparent';
870
-						$newValues[$fieldName] = (int)($propertyValue->getValue() === 'transparent');
871
-						break;
872
-					default:
873
-						$fieldName = $this->propertyMap[$propertyName][0];
874
-						$newValues[$fieldName] = $propertyValue;
875
-						break;
876
-				}
877
-			}
878
-			[$calendarData, $shares] = $this->atomic(function () use ($calendarId, $newValues) {
879
-				$query = $this->db->getQueryBuilder();
880
-				$query->update('calendars');
881
-				foreach ($newValues as $fieldName => $value) {
882
-					$query->set($fieldName, $query->createNamedParameter($value));
883
-				}
884
-				$query->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)));
885
-				$query->executeStatement();
886
-
887
-				$this->addChanges($calendarId, [''], 2);
888
-
889
-				$calendarData = $this->getCalendarById($calendarId);
890
-				$shares = $this->getShares($calendarId);
891
-				return [$calendarData, $shares];
892
-			}, $this->db);
893
-
894
-			$this->dispatcher->dispatchTyped(new CalendarUpdatedEvent($calendarId, $calendarData, $shares, $mutations));
895
-
896
-			return true;
897
-		});
898
-	}
899
-
900
-	/**
901
-	 * Delete a calendar and all it's objects
902
-	 *
903
-	 * @param mixed $calendarId
904
-	 * @return void
905
-	 */
906
-	public function deleteCalendar($calendarId, bool $forceDeletePermanently = false) {
907
-		$this->atomic(function () use ($calendarId, $forceDeletePermanently): void {
908
-			// The calendar is deleted right away if this is either enforced by the caller
909
-			// or the special contacts birthday calendar or when the preference of an empty
910
-			// retention (0 seconds) is set, which signals a disabled trashbin.
911
-			$calendarData = $this->getCalendarById($calendarId);
912
-			$isBirthdayCalendar = isset($calendarData['uri']) && $calendarData['uri'] === BirthdayService::BIRTHDAY_CALENDAR_URI;
913
-			$trashbinDisabled = $this->config->getAppValue(Application::APP_ID, RetentionService::RETENTION_CONFIG_KEY) === '0';
914
-			if ($forceDeletePermanently || $isBirthdayCalendar || $trashbinDisabled) {
915
-				$calendarData = $this->getCalendarById($calendarId);
916
-				$shares = $this->getShares($calendarId);
917
-
918
-				$this->purgeCalendarInvitations($calendarId);
919
-
920
-				$qbDeleteCalendarObjectProps = $this->db->getQueryBuilder();
921
-				$qbDeleteCalendarObjectProps->delete($this->dbObjectPropertiesTable)
922
-					->where($qbDeleteCalendarObjectProps->expr()->eq('calendarid', $qbDeleteCalendarObjectProps->createNamedParameter($calendarId)))
923
-					->andWhere($qbDeleteCalendarObjectProps->expr()->eq('calendartype', $qbDeleteCalendarObjectProps->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)))
924
-					->executeStatement();
925
-
926
-				$qbDeleteCalendarObjects = $this->db->getQueryBuilder();
927
-				$qbDeleteCalendarObjects->delete('calendarobjects')
928
-					->where($qbDeleteCalendarObjects->expr()->eq('calendarid', $qbDeleteCalendarObjects->createNamedParameter($calendarId)))
929
-					->andWhere($qbDeleteCalendarObjects->expr()->eq('calendartype', $qbDeleteCalendarObjects->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)))
930
-					->executeStatement();
931
-
932
-				$qbDeleteCalendarChanges = $this->db->getQueryBuilder();
933
-				$qbDeleteCalendarChanges->delete('calendarchanges')
934
-					->where($qbDeleteCalendarChanges->expr()->eq('calendarid', $qbDeleteCalendarChanges->createNamedParameter($calendarId)))
935
-					->andWhere($qbDeleteCalendarChanges->expr()->eq('calendartype', $qbDeleteCalendarChanges->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)))
936
-					->executeStatement();
937
-
938
-				$this->calendarSharingBackend->deleteAllShares($calendarId);
939
-
940
-				$qbDeleteCalendar = $this->db->getQueryBuilder();
941
-				$qbDeleteCalendar->delete('calendars')
942
-					->where($qbDeleteCalendar->expr()->eq('id', $qbDeleteCalendar->createNamedParameter($calendarId)))
943
-					->executeStatement();
944
-
945
-				// Only dispatch if we actually deleted anything
946
-				if ($calendarData) {
947
-					$this->dispatcher->dispatchTyped(new CalendarDeletedEvent($calendarId, $calendarData, $shares));
948
-				}
949
-			} else {
950
-				$qbMarkCalendarDeleted = $this->db->getQueryBuilder();
951
-				$qbMarkCalendarDeleted->update('calendars')
952
-					->set('deleted_at', $qbMarkCalendarDeleted->createNamedParameter(time()))
953
-					->where($qbMarkCalendarDeleted->expr()->eq('id', $qbMarkCalendarDeleted->createNamedParameter($calendarId)))
954
-					->executeStatement();
955
-
956
-				$calendarData = $this->getCalendarById($calendarId);
957
-				$shares = $this->getShares($calendarId);
958
-				if ($calendarData) {
959
-					$this->dispatcher->dispatchTyped(new CalendarMovedToTrashEvent(
960
-						$calendarId,
961
-						$calendarData,
962
-						$shares
963
-					));
964
-				}
965
-			}
966
-		}, $this->db);
967
-	}
968
-
969
-	public function restoreCalendar(int $id): void {
970
-		$this->atomic(function () use ($id): void {
971
-			$qb = $this->db->getQueryBuilder();
972
-			$update = $qb->update('calendars')
973
-				->set('deleted_at', $qb->createNamedParameter(null))
974
-				->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT));
975
-			$update->executeStatement();
976
-
977
-			$calendarData = $this->getCalendarById($id);
978
-			$shares = $this->getShares($id);
979
-			if ($calendarData === null) {
980
-				throw new RuntimeException('Calendar data that was just written can\'t be read back. Check your database configuration.');
981
-			}
982
-			$this->dispatcher->dispatchTyped(new CalendarRestoredEvent(
983
-				$id,
984
-				$calendarData,
985
-				$shares
986
-			));
987
-		}, $this->db);
988
-	}
989
-
990
-	/**
991
-	 * Returns all calendar objects with limited metadata for a calendar
992
-	 *
993
-	 * Every item contains an array with the following keys:
994
-	 *   * id - the table row id
995
-	 *   * etag - An arbitrary string
996
-	 *   * uri - a unique key which will be used to construct the uri. This can
997
-	 *     be any arbitrary string.
998
-	 *   * calendardata - The iCalendar-compatible calendar data
999
-	 *
1000
-	 * @param mixed $calendarId
1001
-	 * @param int $calendarType
1002
-	 * @return array
1003
-	 */
1004
-	public function getLimitedCalendarObjects(int $calendarId, int $calendarType = self::CALENDAR_TYPE_CALENDAR):array {
1005
-		$query = $this->db->getQueryBuilder();
1006
-		$query->select(['id','uid', 'etag', 'uri', 'calendardata'])
1007
-			->from('calendarobjects')
1008
-			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
1009
-			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)))
1010
-			->andWhere($query->expr()->isNull('deleted_at'));
1011
-		$stmt = $query->executeQuery();
1012
-
1013
-		$result = [];
1014
-		while (($row = $stmt->fetch()) !== false) {
1015
-			$result[$row['uid']] = [
1016
-				'id' => $row['id'],
1017
-				'etag' => $row['etag'],
1018
-				'uri' => $row['uri'],
1019
-				'calendardata' => $row['calendardata'],
1020
-			];
1021
-		}
1022
-		$stmt->closeCursor();
1023
-
1024
-		return $result;
1025
-	}
1026
-
1027
-	/**
1028
-	 * Delete all of an user's shares
1029
-	 *
1030
-	 * @param string $principaluri
1031
-	 * @return void
1032
-	 */
1033
-	public function deleteAllSharesByUser($principaluri) {
1034
-		$this->calendarSharingBackend->deleteAllSharesByUser($principaluri);
1035
-	}
1036
-
1037
-	/**
1038
-	 * Returns all calendar objects within a calendar.
1039
-	 *
1040
-	 * Every item contains an array with the following keys:
1041
-	 *   * calendardata - The iCalendar-compatible calendar data
1042
-	 *   * uri - a unique key which will be used to construct the uri. This can
1043
-	 *     be any arbitrary string, but making sure it ends with '.ics' is a
1044
-	 *     good idea. This is only the basename, or filename, not the full
1045
-	 *     path.
1046
-	 *   * lastmodified - a timestamp of the last modification time
1047
-	 *   * etag - An arbitrary string, surrounded by double-quotes. (e.g.:
1048
-	 *   '"abcdef"')
1049
-	 *   * size - The size of the calendar objects, in bytes.
1050
-	 *   * component - optional, a string containing the type of object, such
1051
-	 *     as 'vevent' or 'vtodo'. If specified, this will be used to populate
1052
-	 *     the Content-Type header.
1053
-	 *
1054
-	 * Note that the etag is optional, but it's highly encouraged to return for
1055
-	 * speed reasons.
1056
-	 *
1057
-	 * The calendardata is also optional. If it's not returned
1058
-	 * 'getCalendarObject' will be called later, which *is* expected to return
1059
-	 * calendardata.
1060
-	 *
1061
-	 * If neither etag or size are specified, the calendardata will be
1062
-	 * used/fetched to determine these numbers. If both are specified the
1063
-	 * amount of times this is needed is reduced by a great degree.
1064
-	 *
1065
-	 * @param mixed $calendarId
1066
-	 * @param int $calendarType
1067
-	 * @return array
1068
-	 */
1069
-	public function getCalendarObjects($calendarId, $calendarType = self::CALENDAR_TYPE_CALENDAR):array {
1070
-		$query = $this->db->getQueryBuilder();
1071
-		$query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'componenttype', 'classification'])
1072
-			->from('calendarobjects')
1073
-			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
1074
-			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)))
1075
-			->andWhere($query->expr()->isNull('deleted_at'));
1076
-		$stmt = $query->executeQuery();
1077
-
1078
-		$result = [];
1079
-		while (($row = $stmt->fetch()) !== false) {
1080
-			$result[] = [
1081
-				'id' => $row['id'],
1082
-				'uri' => $row['uri'],
1083
-				'lastmodified' => $row['lastmodified'],
1084
-				'etag' => '"' . $row['etag'] . '"',
1085
-				'calendarid' => $row['calendarid'],
1086
-				'size' => (int)$row['size'],
1087
-				'component' => strtolower($row['componenttype']),
1088
-				'classification' => (int)$row['classification']
1089
-			];
1090
-		}
1091
-		$stmt->closeCursor();
1092
-
1093
-		return $result;
1094
-	}
1095
-
1096
-	public function getDeletedCalendarObjects(int $deletedBefore): array {
1097
-		$query = $this->db->getQueryBuilder();
1098
-		$query->select(['co.id', 'co.uri', 'co.lastmodified', 'co.etag', 'co.calendarid', 'co.calendartype', 'co.size', 'co.componenttype', 'co.classification', 'co.deleted_at'])
1099
-			->from('calendarobjects', 'co')
1100
-			->join('co', 'calendars', 'c', $query->expr()->eq('c.id', 'co.calendarid', IQueryBuilder::PARAM_INT))
1101
-			->where($query->expr()->isNotNull('co.deleted_at'))
1102
-			->andWhere($query->expr()->lt('co.deleted_at', $query->createNamedParameter($deletedBefore)));
1103
-		$stmt = $query->executeQuery();
1104
-
1105
-		$result = [];
1106
-		while (($row = $stmt->fetch()) !== false) {
1107
-			$result[] = [
1108
-				'id' => $row['id'],
1109
-				'uri' => $row['uri'],
1110
-				'lastmodified' => $row['lastmodified'],
1111
-				'etag' => '"' . $row['etag'] . '"',
1112
-				'calendarid' => (int)$row['calendarid'],
1113
-				'calendartype' => (int)$row['calendartype'],
1114
-				'size' => (int)$row['size'],
1115
-				'component' => strtolower($row['componenttype']),
1116
-				'classification' => (int)$row['classification'],
1117
-				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => $row['deleted_at'] === null ? $row['deleted_at'] : (int)$row['deleted_at'],
1118
-			];
1119
-		}
1120
-		$stmt->closeCursor();
1121
-
1122
-		return $result;
1123
-	}
1124
-
1125
-	/**
1126
-	 * Return all deleted calendar objects by the given principal that are not
1127
-	 * in deleted calendars.
1128
-	 *
1129
-	 * @param string $principalUri
1130
-	 * @return array
1131
-	 * @throws Exception
1132
-	 */
1133
-	public function getDeletedCalendarObjectsByPrincipal(string $principalUri): array {
1134
-		$query = $this->db->getQueryBuilder();
1135
-		$query->select(['co.id', 'co.uri', 'co.lastmodified', 'co.etag', 'co.calendarid', 'co.size', 'co.componenttype', 'co.classification', 'co.deleted_at'])
1136
-			->selectAlias('c.uri', 'calendaruri')
1137
-			->from('calendarobjects', 'co')
1138
-			->join('co', 'calendars', 'c', $query->expr()->eq('c.id', 'co.calendarid', IQueryBuilder::PARAM_INT))
1139
-			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
1140
-			->andWhere($query->expr()->isNotNull('co.deleted_at'))
1141
-			->andWhere($query->expr()->isNull('c.deleted_at'));
1142
-		$stmt = $query->executeQuery();
1143
-
1144
-		$result = [];
1145
-		while ($row = $stmt->fetch()) {
1146
-			$result[] = [
1147
-				'id' => $row['id'],
1148
-				'uri' => $row['uri'],
1149
-				'lastmodified' => $row['lastmodified'],
1150
-				'etag' => '"' . $row['etag'] . '"',
1151
-				'calendarid' => $row['calendarid'],
1152
-				'calendaruri' => $row['calendaruri'],
1153
-				'size' => (int)$row['size'],
1154
-				'component' => strtolower($row['componenttype']),
1155
-				'classification' => (int)$row['classification'],
1156
-				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => $row['deleted_at'] === null ? $row['deleted_at'] : (int)$row['deleted_at'],
1157
-			];
1158
-		}
1159
-		$stmt->closeCursor();
1160
-
1161
-		return $result;
1162
-	}
1163
-
1164
-	/**
1165
-	 * Returns information from a single calendar object, based on it's object
1166
-	 * uri.
1167
-	 *
1168
-	 * The object uri is only the basename, or filename and not a full path.
1169
-	 *
1170
-	 * The returned array must have the same keys as getCalendarObjects. The
1171
-	 * 'calendardata' object is required here though, while it's not required
1172
-	 * for getCalendarObjects.
1173
-	 *
1174
-	 * This method must return null if the object did not exist.
1175
-	 *
1176
-	 * @param mixed $calendarId
1177
-	 * @param string $objectUri
1178
-	 * @param int $calendarType
1179
-	 * @return array|null
1180
-	 */
1181
-	public function getCalendarObject($calendarId, $objectUri, int $calendarType = self::CALENDAR_TYPE_CALENDAR) {
1182
-		$key = $calendarId . '::' . $objectUri . '::' . $calendarType;
1183
-		if (isset($this->cachedObjects[$key])) {
1184
-			return $this->cachedObjects[$key];
1185
-		}
1186
-		$query = $this->db->getQueryBuilder();
1187
-		$query->select(['id', 'uri', 'uid', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification', 'deleted_at'])
1188
-			->from('calendarobjects')
1189
-			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
1190
-			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
1191
-			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)));
1192
-		$stmt = $query->executeQuery();
1193
-		$row = $stmt->fetch();
1194
-		$stmt->closeCursor();
1195
-
1196
-		if (!$row) {
1197
-			return null;
1198
-		}
1199
-
1200
-		$object = $this->rowToCalendarObject($row);
1201
-		$this->cachedObjects[$key] = $object;
1202
-		return $object;
1203
-	}
1204
-
1205
-	private function rowToCalendarObject(array $row): array {
1206
-		return [
1207
-			'id' => $row['id'],
1208
-			'uri' => $row['uri'],
1209
-			'uid' => $row['uid'],
1210
-			'lastmodified' => $row['lastmodified'],
1211
-			'etag' => '"' . $row['etag'] . '"',
1212
-			'calendarid' => $row['calendarid'],
1213
-			'size' => (int)$row['size'],
1214
-			'calendardata' => $this->readBlob($row['calendardata']),
1215
-			'component' => strtolower($row['componenttype']),
1216
-			'classification' => (int)$row['classification'],
1217
-			'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => $row['deleted_at'] === null ? $row['deleted_at'] : (int)$row['deleted_at'],
1218
-		];
1219
-	}
1220
-
1221
-	/**
1222
-	 * Returns a list of calendar objects.
1223
-	 *
1224
-	 * This method should work identical to getCalendarObject, but instead
1225
-	 * return all the calendar objects in the list as an array.
1226
-	 *
1227
-	 * If the backend supports this, it may allow for some speed-ups.
1228
-	 *
1229
-	 * @param mixed $calendarId
1230
-	 * @param string[] $uris
1231
-	 * @param int $calendarType
1232
-	 * @return array
1233
-	 */
1234
-	public function getMultipleCalendarObjects($calendarId, array $uris, $calendarType = self::CALENDAR_TYPE_CALENDAR):array {
1235
-		if (empty($uris)) {
1236
-			return [];
1237
-		}
1238
-
1239
-		$chunks = array_chunk($uris, 100);
1240
-		$objects = [];
1241
-
1242
-		$query = $this->db->getQueryBuilder();
1243
-		$query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification'])
1244
-			->from('calendarobjects')
1245
-			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
1246
-			->andWhere($query->expr()->in('uri', $query->createParameter('uri')))
1247
-			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)))
1248
-			->andWhere($query->expr()->isNull('deleted_at'));
1249
-
1250
-		foreach ($chunks as $uris) {
1251
-			$query->setParameter('uri', $uris, IQueryBuilder::PARAM_STR_ARRAY);
1252
-			$result = $query->executeQuery();
1253
-
1254
-			while ($row = $result->fetch()) {
1255
-				$objects[] = [
1256
-					'id' => $row['id'],
1257
-					'uri' => $row['uri'],
1258
-					'lastmodified' => $row['lastmodified'],
1259
-					'etag' => '"' . $row['etag'] . '"',
1260
-					'calendarid' => $row['calendarid'],
1261
-					'size' => (int)$row['size'],
1262
-					'calendardata' => $this->readBlob($row['calendardata']),
1263
-					'component' => strtolower($row['componenttype']),
1264
-					'classification' => (int)$row['classification']
1265
-				];
1266
-			}
1267
-			$result->closeCursor();
1268
-		}
1269
-
1270
-		return $objects;
1271
-	}
1272
-
1273
-	/**
1274
-	 * Creates a new calendar object.
1275
-	 *
1276
-	 * The object uri is only the basename, or filename and not a full path.
1277
-	 *
1278
-	 * It is possible return an etag from this function, which will be used in
1279
-	 * the response to this PUT request. Note that the ETag must be surrounded
1280
-	 * by double-quotes.
1281
-	 *
1282
-	 * However, you should only really return this ETag if you don't mangle the
1283
-	 * calendar-data. If the result of a subsequent GET to this object is not
1284
-	 * the exact same as this request body, you should omit the ETag.
1285
-	 *
1286
-	 * @param mixed $calendarId
1287
-	 * @param string $objectUri
1288
-	 * @param string $calendarData
1289
-	 * @param int $calendarType
1290
-	 * @return string
1291
-	 */
1292
-	public function createCalendarObject($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
1293
-		$this->cachedObjects = [];
1294
-		$extraData = $this->getDenormalizedData($calendarData);
1295
-
1296
-		return $this->atomic(function () use ($calendarId, $objectUri, $calendarData, $extraData, $calendarType) {
1297
-			// Try to detect duplicates
1298
-			$qb = $this->db->getQueryBuilder();
1299
-			$qb->select($qb->func()->count('*'))
1300
-				->from('calendarobjects')
1301
-				->where($qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)))
1302
-				->andWhere($qb->expr()->eq('uid', $qb->createNamedParameter($extraData['uid'])))
1303
-				->andWhere($qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType)))
1304
-				->andWhere($qb->expr()->isNull('deleted_at'));
1305
-			$result = $qb->executeQuery();
1306
-			$count = (int)$result->fetchOne();
1307
-			$result->closeCursor();
1308
-
1309
-			if ($count !== 0) {
1310
-				throw new BadRequest('Calendar object with uid already exists in this calendar collection.');
1311
-			}
1312
-			// For a more specific error message we also try to explicitly look up the UID but as a deleted entry
1313
-			$qbDel = $this->db->getQueryBuilder();
1314
-			$qbDel->select('*')
1315
-				->from('calendarobjects')
1316
-				->where($qbDel->expr()->eq('calendarid', $qbDel->createNamedParameter($calendarId)))
1317
-				->andWhere($qbDel->expr()->eq('uid', $qbDel->createNamedParameter($extraData['uid'])))
1318
-				->andWhere($qbDel->expr()->eq('calendartype', $qbDel->createNamedParameter($calendarType)))
1319
-				->andWhere($qbDel->expr()->isNotNull('deleted_at'));
1320
-			$result = $qbDel->executeQuery();
1321
-			$found = $result->fetch();
1322
-			$result->closeCursor();
1323
-			if ($found !== false) {
1324
-				// the object existed previously but has been deleted
1325
-				// remove the trashbin entry and continue as if it was a new object
1326
-				$this->deleteCalendarObject($calendarId, $found['uri']);
1327
-			}
1328
-
1329
-			$query = $this->db->getQueryBuilder();
1330
-			$query->insert('calendarobjects')
1331
-				->values([
1332
-					'calendarid' => $query->createNamedParameter($calendarId),
1333
-					'uri' => $query->createNamedParameter($objectUri),
1334
-					'calendardata' => $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB),
1335
-					'lastmodified' => $query->createNamedParameter(time()),
1336
-					'etag' => $query->createNamedParameter($extraData['etag']),
1337
-					'size' => $query->createNamedParameter($extraData['size']),
1338
-					'componenttype' => $query->createNamedParameter($extraData['componentType']),
1339
-					'firstoccurence' => $query->createNamedParameter($extraData['firstOccurence']),
1340
-					'lastoccurence' => $query->createNamedParameter($extraData['lastOccurence']),
1341
-					'classification' => $query->createNamedParameter($extraData['classification']),
1342
-					'uid' => $query->createNamedParameter($extraData['uid']),
1343
-					'calendartype' => $query->createNamedParameter($calendarType),
1344
-				])
1345
-				->executeStatement();
1346
-
1347
-			$this->updateProperties($calendarId, $objectUri, $calendarData, $calendarType);
1348
-			$this->addChanges($calendarId, [$objectUri], 1, $calendarType);
1349
-
1350
-			$objectRow = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
1351
-			assert($objectRow !== null);
1352
-
1353
-			if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
1354
-				$calendarRow = $this->getCalendarById($calendarId);
1355
-				$shares = $this->getShares($calendarId);
1356
-
1357
-				$this->dispatcher->dispatchTyped(new CalendarObjectCreatedEvent($calendarId, $calendarRow, $shares, $objectRow));
1358
-			} else {
1359
-				$subscriptionRow = $this->getSubscriptionById($calendarId);
1360
-
1361
-				$this->dispatcher->dispatchTyped(new CachedCalendarObjectCreatedEvent($calendarId, $subscriptionRow, [], $objectRow));
1362
-			}
1363
-
1364
-			return '"' . $extraData['etag'] . '"';
1365
-		}, $this->db);
1366
-	}
1367
-
1368
-	/**
1369
-	 * Updates an existing calendarobject, based on it's uri.
1370
-	 *
1371
-	 * The object uri is only the basename, or filename and not a full path.
1372
-	 *
1373
-	 * It is possible return an etag from this function, which will be used in
1374
-	 * the response to this PUT request. Note that the ETag must be surrounded
1375
-	 * by double-quotes.
1376
-	 *
1377
-	 * However, you should only really return this ETag if you don't mangle the
1378
-	 * calendar-data. If the result of a subsequent GET to this object is not
1379
-	 * the exact same as this request body, you should omit the ETag.
1380
-	 *
1381
-	 * @param mixed $calendarId
1382
-	 * @param string $objectUri
1383
-	 * @param string $calendarData
1384
-	 * @param int $calendarType
1385
-	 * @return string
1386
-	 */
1387
-	public function updateCalendarObject($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
1388
-		$this->cachedObjects = [];
1389
-		$extraData = $this->getDenormalizedData($calendarData);
1390
-
1391
-		return $this->atomic(function () use ($calendarId, $objectUri, $calendarData, $extraData, $calendarType) {
1392
-			$query = $this->db->getQueryBuilder();
1393
-			$query->update('calendarobjects')
1394
-				->set('calendardata', $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB))
1395
-				->set('lastmodified', $query->createNamedParameter(time()))
1396
-				->set('etag', $query->createNamedParameter($extraData['etag']))
1397
-				->set('size', $query->createNamedParameter($extraData['size']))
1398
-				->set('componenttype', $query->createNamedParameter($extraData['componentType']))
1399
-				->set('firstoccurence', $query->createNamedParameter($extraData['firstOccurence']))
1400
-				->set('lastoccurence', $query->createNamedParameter($extraData['lastOccurence']))
1401
-				->set('classification', $query->createNamedParameter($extraData['classification']))
1402
-				->set('uid', $query->createNamedParameter($extraData['uid']))
1403
-				->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
1404
-				->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
1405
-				->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)))
1406
-				->executeStatement();
1407
-
1408
-			$this->updateProperties($calendarId, $objectUri, $calendarData, $calendarType);
1409
-			$this->addChanges($calendarId, [$objectUri], 2, $calendarType);
1410
-
1411
-			$objectRow = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
1412
-			if (is_array($objectRow)) {
1413
-				if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
1414
-					$calendarRow = $this->getCalendarById($calendarId);
1415
-					$shares = $this->getShares($calendarId);
1416
-
1417
-					$this->dispatcher->dispatchTyped(new CalendarObjectUpdatedEvent($calendarId, $calendarRow, $shares, $objectRow));
1418
-				} else {
1419
-					$subscriptionRow = $this->getSubscriptionById($calendarId);
1420
-
1421
-					$this->dispatcher->dispatchTyped(new CachedCalendarObjectUpdatedEvent($calendarId, $subscriptionRow, [], $objectRow));
1422
-				}
1423
-			}
1424
-
1425
-			return '"' . $extraData['etag'] . '"';
1426
-		}, $this->db);
1427
-	}
1428
-
1429
-	/**
1430
-	 * Moves a calendar object from calendar to calendar.
1431
-	 *
1432
-	 * @param string $sourcePrincipalUri
1433
-	 * @param int $sourceObjectId
1434
-	 * @param string $targetPrincipalUri
1435
-	 * @param int $targetCalendarId
1436
-	 * @param string $tragetObjectUri
1437
-	 * @param int $calendarType
1438
-	 * @return bool
1439
-	 * @throws Exception
1440
-	 */
1441
-	public function moveCalendarObject(string $sourcePrincipalUri, int $sourceObjectId, string $targetPrincipalUri, int $targetCalendarId, string $tragetObjectUri, int $calendarType = self::CALENDAR_TYPE_CALENDAR): bool {
1442
-		$this->cachedObjects = [];
1443
-		return $this->atomic(function () use ($sourcePrincipalUri, $sourceObjectId, $targetPrincipalUri, $targetCalendarId, $tragetObjectUri, $calendarType) {
1444
-			$object = $this->getCalendarObjectById($sourcePrincipalUri, $sourceObjectId);
1445
-			if (empty($object)) {
1446
-				return false;
1447
-			}
1448
-
1449
-			$sourceCalendarId = $object['calendarid'];
1450
-			$sourceObjectUri = $object['uri'];
1451
-
1452
-			$query = $this->db->getQueryBuilder();
1453
-			$query->update('calendarobjects')
1454
-				->set('calendarid', $query->createNamedParameter($targetCalendarId, IQueryBuilder::PARAM_INT))
1455
-				->set('uri', $query->createNamedParameter($tragetObjectUri, IQueryBuilder::PARAM_STR))
1456
-				->where($query->expr()->eq('id', $query->createNamedParameter($sourceObjectId, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT))
1457
-				->executeStatement();
1458
-
1459
-			$this->purgeProperties($sourceCalendarId, $sourceObjectId);
1460
-			$this->updateProperties($targetCalendarId, $tragetObjectUri, $object['calendardata'], $calendarType);
1461
-
1462
-			$this->addChanges($sourceCalendarId, [$sourceObjectUri], 3, $calendarType);
1463
-			$this->addChanges($targetCalendarId, [$tragetObjectUri], 1, $calendarType);
1464
-
1465
-			$object = $this->getCalendarObjectById($targetPrincipalUri, $sourceObjectId);
1466
-			// Calendar Object wasn't found - possibly because it was deleted in the meantime by a different client
1467
-			if (empty($object)) {
1468
-				return false;
1469
-			}
1470
-
1471
-			$targetCalendarRow = $this->getCalendarById($targetCalendarId);
1472
-			// the calendar this event is being moved to does not exist any longer
1473
-			if (empty($targetCalendarRow)) {
1474
-				return false;
1475
-			}
1476
-
1477
-			if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
1478
-				$sourceShares = $this->getShares($sourceCalendarId);
1479
-				$targetShares = $this->getShares($targetCalendarId);
1480
-				$sourceCalendarRow = $this->getCalendarById($sourceCalendarId);
1481
-				$this->dispatcher->dispatchTyped(new CalendarObjectMovedEvent($sourceCalendarId, $sourceCalendarRow, $targetCalendarId, $targetCalendarRow, $sourceShares, $targetShares, $object));
1482
-			}
1483
-			return true;
1484
-		}, $this->db);
1485
-	}
1486
-
1487
-
1488
-	/**
1489
-	 * @param int $calendarObjectId
1490
-	 * @param int $classification
1491
-	 */
1492
-	public function setClassification($calendarObjectId, $classification) {
1493
-		$this->cachedObjects = [];
1494
-		if (!in_array($classification, [
1495
-			self::CLASSIFICATION_PUBLIC, self::CLASSIFICATION_PRIVATE, self::CLASSIFICATION_CONFIDENTIAL
1496
-		])) {
1497
-			throw new \InvalidArgumentException();
1498
-		}
1499
-		$query = $this->db->getQueryBuilder();
1500
-		$query->update('calendarobjects')
1501
-			->set('classification', $query->createNamedParameter($classification))
1502
-			->where($query->expr()->eq('id', $query->createNamedParameter($calendarObjectId)))
1503
-			->executeStatement();
1504
-	}
1505
-
1506
-	/**
1507
-	 * Deletes an existing calendar object.
1508
-	 *
1509
-	 * The object uri is only the basename, or filename and not a full path.
1510
-	 *
1511
-	 * @param mixed $calendarId
1512
-	 * @param string $objectUri
1513
-	 * @param int $calendarType
1514
-	 * @param bool $forceDeletePermanently
1515
-	 * @return void
1516
-	 */
1517
-	public function deleteCalendarObject($calendarId, $objectUri, $calendarType = self::CALENDAR_TYPE_CALENDAR, bool $forceDeletePermanently = false) {
1518
-		$this->cachedObjects = [];
1519
-		$this->atomic(function () use ($calendarId, $objectUri, $calendarType, $forceDeletePermanently): void {
1520
-			$data = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
1521
-
1522
-			if ($data === null) {
1523
-				// Nothing to delete
1524
-				return;
1525
-			}
1526
-
1527
-			if ($forceDeletePermanently || $this->config->getAppValue(Application::APP_ID, RetentionService::RETENTION_CONFIG_KEY) === '0') {
1528
-				$stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ? AND `uri` = ? AND `calendartype` = ?');
1529
-				$stmt->execute([$calendarId, $objectUri, $calendarType]);
1530
-
1531
-				$this->purgeProperties($calendarId, $data['id']);
1532
-
1533
-				$this->purgeObjectInvitations($data['uid']);
1534
-
1535
-				if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
1536
-					$calendarRow = $this->getCalendarById($calendarId);
1537
-					$shares = $this->getShares($calendarId);
1538
-
1539
-					$this->dispatcher->dispatchTyped(new CalendarObjectDeletedEvent($calendarId, $calendarRow, $shares, $data));
1540
-				} else {
1541
-					$subscriptionRow = $this->getSubscriptionById($calendarId);
1542
-
1543
-					$this->dispatcher->dispatchTyped(new CachedCalendarObjectDeletedEvent($calendarId, $subscriptionRow, [], $data));
1544
-				}
1545
-			} else {
1546
-				$pathInfo = pathinfo($data['uri']);
1547
-				if (!empty($pathInfo['extension'])) {
1548
-					// Append a suffix to "free" the old URI for recreation
1549
-					$newUri = sprintf(
1550
-						'%s-deleted.%s',
1551
-						$pathInfo['filename'],
1552
-						$pathInfo['extension']
1553
-					);
1554
-				} else {
1555
-					$newUri = sprintf(
1556
-						'%s-deleted',
1557
-						$pathInfo['filename']
1558
-					);
1559
-				}
1560
-
1561
-				// Try to detect conflicts before the DB does
1562
-				// As unlikely as it seems, this can happen when the user imports, then deletes, imports and deletes again
1563
-				$newObject = $this->getCalendarObject($calendarId, $newUri, $calendarType);
1564
-				if ($newObject !== null) {
1565
-					throw new Forbidden("A calendar object with URI $newUri already exists in calendar $calendarId, therefore this object can't be moved into the trashbin");
1566
-				}
1567
-
1568
-				$qb = $this->db->getQueryBuilder();
1569
-				$markObjectDeletedQuery = $qb->update('calendarobjects')
1570
-					->set('deleted_at', $qb->createNamedParameter(time(), IQueryBuilder::PARAM_INT))
1571
-					->set('uri', $qb->createNamedParameter($newUri))
1572
-					->where(
1573
-						$qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)),
1574
-						$qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT),
1575
-						$qb->expr()->eq('uri', $qb->createNamedParameter($objectUri))
1576
-					);
1577
-				$markObjectDeletedQuery->executeStatement();
1578
-
1579
-				$calendarData = $this->getCalendarById($calendarId);
1580
-				if ($calendarData !== null) {
1581
-					$this->dispatcher->dispatchTyped(
1582
-						new CalendarObjectMovedToTrashEvent(
1583
-							$calendarId,
1584
-							$calendarData,
1585
-							$this->getShares($calendarId),
1586
-							$data
1587
-						)
1588
-					);
1589
-				}
1590
-			}
1591
-
1592
-			$this->addChanges($calendarId, [$objectUri], 3, $calendarType);
1593
-		}, $this->db);
1594
-	}
1595
-
1596
-	/**
1597
-	 * @param mixed $objectData
1598
-	 *
1599
-	 * @throws Forbidden
1600
-	 */
1601
-	public function restoreCalendarObject(array $objectData): void {
1602
-		$this->cachedObjects = [];
1603
-		$this->atomic(function () use ($objectData): void {
1604
-			$id = (int)$objectData['id'];
1605
-			$restoreUri = str_replace('-deleted.ics', '.ics', $objectData['uri']);
1606
-			$targetObject = $this->getCalendarObject(
1607
-				$objectData['calendarid'],
1608
-				$restoreUri
1609
-			);
1610
-			if ($targetObject !== null) {
1611
-				throw new Forbidden("Can not restore calendar $id because a calendar object with the URI $restoreUri already exists");
1612
-			}
1613
-
1614
-			$qb = $this->db->getQueryBuilder();
1615
-			$update = $qb->update('calendarobjects')
1616
-				->set('uri', $qb->createNamedParameter($restoreUri))
1617
-				->set('deleted_at', $qb->createNamedParameter(null))
1618
-				->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT));
1619
-			$update->executeStatement();
1620
-
1621
-			// Make sure this change is tracked in the changes table
1622
-			$qb2 = $this->db->getQueryBuilder();
1623
-			$selectObject = $qb2->select('calendardata', 'uri', 'calendarid', 'calendartype')
1624
-				->selectAlias('componenttype', 'component')
1625
-				->from('calendarobjects')
1626
-				->where($qb2->expr()->eq('id', $qb2->createNamedParameter($id, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT));
1627
-			$result = $selectObject->executeQuery();
1628
-			$row = $result->fetch();
1629
-			$result->closeCursor();
1630
-			if ($row === false) {
1631
-				// Welp, this should possibly not have happened, but let's ignore
1632
-				return;
1633
-			}
1634
-			$this->addChanges($row['calendarid'], [$row['uri']], 1, (int)$row['calendartype']);
1635
-
1636
-			$calendarRow = $this->getCalendarById((int)$row['calendarid']);
1637
-			if ($calendarRow === null) {
1638
-				throw new RuntimeException('Calendar object data that was just written can\'t be read back. Check your database configuration.');
1639
-			}
1640
-			$this->dispatcher->dispatchTyped(
1641
-				new CalendarObjectRestoredEvent(
1642
-					(int)$objectData['calendarid'],
1643
-					$calendarRow,
1644
-					$this->getShares((int)$row['calendarid']),
1645
-					$row
1646
-				)
1647
-			);
1648
-		}, $this->db);
1649
-	}
1650
-
1651
-	/**
1652
-	 * Performs a calendar-query on the contents of this calendar.
1653
-	 *
1654
-	 * The calendar-query is defined in RFC4791 : CalDAV. Using the
1655
-	 * calendar-query it is possible for a client to request a specific set of
1656
-	 * object, based on contents of iCalendar properties, date-ranges and
1657
-	 * iCalendar component types (VTODO, VEVENT).
1658
-	 *
1659
-	 * This method should just return a list of (relative) urls that match this
1660
-	 * query.
1661
-	 *
1662
-	 * The list of filters are specified as an array. The exact array is
1663
-	 * documented by Sabre\CalDAV\CalendarQueryParser.
1664
-	 *
1665
-	 * Note that it is extremely likely that getCalendarObject for every path
1666
-	 * returned from this method will be called almost immediately after. You
1667
-	 * may want to anticipate this to speed up these requests.
1668
-	 *
1669
-	 * This method provides a default implementation, which parses *all* the
1670
-	 * iCalendar objects in the specified calendar.
1671
-	 *
1672
-	 * This default may well be good enough for personal use, and calendars
1673
-	 * that aren't very large. But if you anticipate high usage, big calendars
1674
-	 * or high loads, you are strongly advised to optimize certain paths.
1675
-	 *
1676
-	 * The best way to do so is override this method and to optimize
1677
-	 * specifically for 'common filters'.
1678
-	 *
1679
-	 * Requests that are extremely common are:
1680
-	 *   * requests for just VEVENTS
1681
-	 *   * requests for just VTODO
1682
-	 *   * requests with a time-range-filter on either VEVENT or VTODO.
1683
-	 *
1684
-	 * ..and combinations of these requests. It may not be worth it to try to
1685
-	 * handle every possible situation and just rely on the (relatively
1686
-	 * easy to use) CalendarQueryValidator to handle the rest.
1687
-	 *
1688
-	 * Note that especially time-range-filters may be difficult to parse. A
1689
-	 * time-range filter specified on a VEVENT must for instance also handle
1690
-	 * recurrence rules correctly.
1691
-	 * A good example of how to interpret all these filters can also simply
1692
-	 * be found in Sabre\CalDAV\CalendarQueryFilter. This class is as correct
1693
-	 * as possible, so it gives you a good idea on what type of stuff you need
1694
-	 * to think of.
1695
-	 *
1696
-	 * @param mixed $calendarId
1697
-	 * @param array $filters
1698
-	 * @param int $calendarType
1699
-	 * @return array
1700
-	 */
1701
-	public function calendarQuery($calendarId, array $filters, $calendarType = self::CALENDAR_TYPE_CALENDAR):array {
1702
-		$componentType = null;
1703
-		$requirePostFilter = true;
1704
-		$timeRange = null;
1705
-
1706
-		// if no filters were specified, we don't need to filter after a query
1707
-		if (!$filters['prop-filters'] && !$filters['comp-filters']) {
1708
-			$requirePostFilter = false;
1709
-		}
1710
-
1711
-		// Figuring out if there's a component filter
1712
-		if (count($filters['comp-filters']) > 0 && !$filters['comp-filters'][0]['is-not-defined']) {
1713
-			$componentType = $filters['comp-filters'][0]['name'];
1714
-
1715
-			// Checking if we need post-filters
1716
-			if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['time-range'] && !$filters['comp-filters'][0]['prop-filters']) {
1717
-				$requirePostFilter = false;
1718
-			}
1719
-			// There was a time-range filter
1720
-			if ($componentType === 'VEVENT' && isset($filters['comp-filters'][0]['time-range']) && is_array($filters['comp-filters'][0]['time-range'])) {
1721
-				$timeRange = $filters['comp-filters'][0]['time-range'];
1722
-
1723
-				// If start time OR the end time is not specified, we can do a
1724
-				// 100% accurate mysql query.
1725
-				if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['prop-filters'] && (!$timeRange['start'] || !$timeRange['end'])) {
1726
-					$requirePostFilter = false;
1727
-				}
1728
-			}
1729
-		}
1730
-		$query = $this->db->getQueryBuilder();
1731
-		$query->select(['id', 'uri', 'uid', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification', 'deleted_at'])
1732
-			->from('calendarobjects')
1733
-			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
1734
-			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)))
1735
-			->andWhere($query->expr()->isNull('deleted_at'));
1736
-
1737
-		if ($componentType) {
1738
-			$query->andWhere($query->expr()->eq('componenttype', $query->createNamedParameter($componentType)));
1739
-		}
1740
-
1741
-		if ($timeRange && $timeRange['start']) {
1742
-			$query->andWhere($query->expr()->gt('lastoccurence', $query->createNamedParameter($timeRange['start']->getTimeStamp())));
1743
-		}
1744
-		if ($timeRange && $timeRange['end']) {
1745
-			$query->andWhere($query->expr()->lt('firstoccurence', $query->createNamedParameter($timeRange['end']->getTimeStamp())));
1746
-		}
1747
-
1748
-		$stmt = $query->executeQuery();
1749
-
1750
-		$result = [];
1751
-		while ($row = $stmt->fetch()) {
1752
-			// if we leave it as a blob we can't read it both from the post filter and the rowToCalendarObject
1753
-			if (isset($row['calendardata'])) {
1754
-				$row['calendardata'] = $this->readBlob($row['calendardata']);
1755
-			}
1756
-
1757
-			if ($requirePostFilter) {
1758
-				// validateFilterForObject will parse the calendar data
1759
-				// catch parsing errors
1760
-				try {
1761
-					$matches = $this->validateFilterForObject($row, $filters);
1762
-				} catch (ParseException $ex) {
1763
-					$this->logger->error('Caught parsing exception for calendar data. This usually indicates invalid calendar data. calendar-id:' . $calendarId . ' uri:' . $row['uri'], [
1764
-						'app' => 'dav',
1765
-						'exception' => $ex,
1766
-					]);
1767
-					continue;
1768
-				} catch (InvalidDataException $ex) {
1769
-					$this->logger->error('Caught invalid data exception for calendar data. This usually indicates invalid calendar data. calendar-id:' . $calendarId . ' uri:' . $row['uri'], [
1770
-						'app' => 'dav',
1771
-						'exception' => $ex,
1772
-					]);
1773
-					continue;
1774
-				} catch (MaxInstancesExceededException $ex) {
1775
-					$this->logger->warning('Caught max instances exceeded exception for calendar data. This usually indicates too much recurring (more than 3500) event in calendar data. Object uri: ' . $row['uri'], [
1776
-						'app' => 'dav',
1777
-						'exception' => $ex,
1778
-					]);
1779
-					continue;
1780
-				}
1781
-
1782
-				if (!$matches) {
1783
-					continue;
1784
-				}
1785
-			}
1786
-			$result[] = $row['uri'];
1787
-			$key = $calendarId . '::' . $row['uri'] . '::' . $calendarType;
1788
-			$this->cachedObjects[$key] = $this->rowToCalendarObject($row);
1789
-		}
1790
-
1791
-		return $result;
1792
-	}
1793
-
1794
-	/**
1795
-	 * custom Nextcloud search extension for CalDAV
1796
-	 *
1797
-	 * TODO - this should optionally cover cached calendar objects as well
1798
-	 *
1799
-	 * @param string $principalUri
1800
-	 * @param array $filters
1801
-	 * @param integer|null $limit
1802
-	 * @param integer|null $offset
1803
-	 * @return array
1804
-	 */
1805
-	public function calendarSearch($principalUri, array $filters, $limit = null, $offset = null) {
1806
-		return $this->atomic(function () use ($principalUri, $filters, $limit, $offset) {
1807
-			$calendars = $this->getCalendarsForUser($principalUri);
1808
-			$ownCalendars = [];
1809
-			$sharedCalendars = [];
1810
-
1811
-			$uriMapper = [];
1812
-
1813
-			foreach ($calendars as $calendar) {
1814
-				if ($calendar['{http://owncloud.org/ns}owner-principal'] === $principalUri) {
1815
-					$ownCalendars[] = $calendar['id'];
1816
-				} else {
1817
-					$sharedCalendars[] = $calendar['id'];
1818
-				}
1819
-				$uriMapper[$calendar['id']] = $calendar['uri'];
1820
-			}
1821
-			if (count($ownCalendars) === 0 && count($sharedCalendars) === 0) {
1822
-				return [];
1823
-			}
1824
-
1825
-			$query = $this->db->getQueryBuilder();
1826
-			// Calendar id expressions
1827
-			$calendarExpressions = [];
1828
-			foreach ($ownCalendars as $id) {
1829
-				$calendarExpressions[] = $query->expr()->andX(
1830
-					$query->expr()->eq('c.calendarid',
1831
-						$query->createNamedParameter($id)),
1832
-					$query->expr()->eq('c.calendartype',
1833
-						$query->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
1834
-			}
1835
-			foreach ($sharedCalendars as $id) {
1836
-				$calendarExpressions[] = $query->expr()->andX(
1837
-					$query->expr()->eq('c.calendarid',
1838
-						$query->createNamedParameter($id)),
1839
-					$query->expr()->eq('c.classification',
1840
-						$query->createNamedParameter(self::CLASSIFICATION_PUBLIC)),
1841
-					$query->expr()->eq('c.calendartype',
1842
-						$query->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
1843
-			}
1844
-
1845
-			if (count($calendarExpressions) === 1) {
1846
-				$calExpr = $calendarExpressions[0];
1847
-			} else {
1848
-				$calExpr = call_user_func_array([$query->expr(), 'orX'], $calendarExpressions);
1849
-			}
1850
-
1851
-			// Component expressions
1852
-			$compExpressions = [];
1853
-			foreach ($filters['comps'] as $comp) {
1854
-				$compExpressions[] = $query->expr()
1855
-					->eq('c.componenttype', $query->createNamedParameter($comp));
1856
-			}
1857
-
1858
-			if (count($compExpressions) === 1) {
1859
-				$compExpr = $compExpressions[0];
1860
-			} else {
1861
-				$compExpr = call_user_func_array([$query->expr(), 'orX'], $compExpressions);
1862
-			}
1863
-
1864
-			if (!isset($filters['props'])) {
1865
-				$filters['props'] = [];
1866
-			}
1867
-			if (!isset($filters['params'])) {
1868
-				$filters['params'] = [];
1869
-			}
1870
-
1871
-			$propParamExpressions = [];
1872
-			foreach ($filters['props'] as $prop) {
1873
-				$propParamExpressions[] = $query->expr()->andX(
1874
-					$query->expr()->eq('i.name', $query->createNamedParameter($prop)),
1875
-					$query->expr()->isNull('i.parameter')
1876
-				);
1877
-			}
1878
-			foreach ($filters['params'] as $param) {
1879
-				$propParamExpressions[] = $query->expr()->andX(
1880
-					$query->expr()->eq('i.name', $query->createNamedParameter($param['property'])),
1881
-					$query->expr()->eq('i.parameter', $query->createNamedParameter($param['parameter']))
1882
-				);
1883
-			}
1884
-
1885
-			if (count($propParamExpressions) === 1) {
1886
-				$propParamExpr = $propParamExpressions[0];
1887
-			} else {
1888
-				$propParamExpr = call_user_func_array([$query->expr(), 'orX'], $propParamExpressions);
1889
-			}
1890
-
1891
-			$query->select(['c.calendarid', 'c.uri'])
1892
-				->from($this->dbObjectPropertiesTable, 'i')
1893
-				->join('i', 'calendarobjects', 'c', $query->expr()->eq('i.objectid', 'c.id'))
1894
-				->where($calExpr)
1895
-				->andWhere($compExpr)
1896
-				->andWhere($propParamExpr)
1897
-				->andWhere($query->expr()->iLike('i.value',
1898
-					$query->createNamedParameter('%' . $this->db->escapeLikeParameter($filters['search-term']) . '%')))
1899
-				->andWhere($query->expr()->isNull('deleted_at'));
1900
-
1901
-			if ($offset) {
1902
-				$query->setFirstResult($offset);
1903
-			}
1904
-			if ($limit) {
1905
-				$query->setMaxResults($limit);
1906
-			}
1907
-
1908
-			$stmt = $query->executeQuery();
1909
-
1910
-			$result = [];
1911
-			while ($row = $stmt->fetch()) {
1912
-				$path = $uriMapper[$row['calendarid']] . '/' . $row['uri'];
1913
-				if (!in_array($path, $result)) {
1914
-					$result[] = $path;
1915
-				}
1916
-			}
1917
-
1918
-			return $result;
1919
-		}, $this->db);
1920
-	}
1921
-
1922
-	/**
1923
-	 * used for Nextcloud's calendar API
1924
-	 *
1925
-	 * @param array $calendarInfo
1926
-	 * @param string $pattern
1927
-	 * @param array $searchProperties
1928
-	 * @param array $options
1929
-	 * @param integer|null $limit
1930
-	 * @param integer|null $offset
1931
-	 *
1932
-	 * @return array
1933
-	 */
1934
-	public function search(
1935
-		array $calendarInfo,
1936
-		$pattern,
1937
-		array $searchProperties,
1938
-		array $options,
1939
-		$limit,
1940
-		$offset,
1941
-	) {
1942
-		$outerQuery = $this->db->getQueryBuilder();
1943
-		$innerQuery = $this->db->getQueryBuilder();
1944
-
1945
-		if (isset($calendarInfo['source'])) {
1946
-			$calendarType = self::CALENDAR_TYPE_SUBSCRIPTION;
1947
-		} else {
1948
-			$calendarType = self::CALENDAR_TYPE_CALENDAR;
1949
-		}
1950
-
1951
-		$innerQuery->selectDistinct('op.objectid')
1952
-			->from($this->dbObjectPropertiesTable, 'op')
1953
-			->andWhere($innerQuery->expr()->eq('op.calendarid',
1954
-				$outerQuery->createNamedParameter($calendarInfo['id'])))
1955
-			->andWhere($innerQuery->expr()->eq('op.calendartype',
1956
-				$outerQuery->createNamedParameter($calendarType)));
1957
-
1958
-		$outerQuery->select('c.id', 'c.calendardata', 'c.componenttype', 'c.uid', 'c.uri')
1959
-			->from('calendarobjects', 'c')
1960
-			->where($outerQuery->expr()->isNull('deleted_at'));
1961
-
1962
-		// only return public items for shared calendars for now
1963
-		if (isset($calendarInfo['{http://owncloud.org/ns}owner-principal']) === false || $calendarInfo['principaluri'] !== $calendarInfo['{http://owncloud.org/ns}owner-principal']) {
1964
-			$outerQuery->andWhere($outerQuery->expr()->eq('c.classification',
1965
-				$outerQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
1966
-		}
1967
-
1968
-		if (!empty($searchProperties)) {
1969
-			$or = [];
1970
-			foreach ($searchProperties as $searchProperty) {
1971
-				$or[] = $innerQuery->expr()->eq('op.name',
1972
-					$outerQuery->createNamedParameter($searchProperty));
1973
-			}
1974
-			$innerQuery->andWhere($innerQuery->expr()->orX(...$or));
1975
-		}
1976
-
1977
-		if ($pattern !== '') {
1978
-			$innerQuery->andWhere($innerQuery->expr()->iLike('op.value',
1979
-				$outerQuery->createNamedParameter('%' .
1980
-					$this->db->escapeLikeParameter($pattern) . '%')));
1981
-		}
1982
-
1983
-		$start = null;
1984
-		$end = null;
1985
-
1986
-		$hasLimit = is_int($limit);
1987
-		$hasTimeRange = false;
1988
-
1989
-		if (isset($options['timerange']['start']) && $options['timerange']['start'] instanceof DateTimeInterface) {
1990
-			/** @var DateTimeInterface $start */
1991
-			$start = $options['timerange']['start'];
1992
-			$outerQuery->andWhere(
1993
-				$outerQuery->expr()->gt(
1994
-					'lastoccurence',
1995
-					$outerQuery->createNamedParameter($start->getTimestamp())
1996
-				)
1997
-			);
1998
-			$hasTimeRange = true;
1999
-		}
2000
-
2001
-		if (isset($options['timerange']['end']) && $options['timerange']['end'] instanceof DateTimeInterface) {
2002
-			/** @var DateTimeInterface $end */
2003
-			$end = $options['timerange']['end'];
2004
-			$outerQuery->andWhere(
2005
-				$outerQuery->expr()->lt(
2006
-					'firstoccurence',
2007
-					$outerQuery->createNamedParameter($end->getTimestamp())
2008
-				)
2009
-			);
2010
-			$hasTimeRange = true;
2011
-		}
2012
-
2013
-		if (isset($options['uid'])) {
2014
-			$outerQuery->andWhere($outerQuery->expr()->eq('uid', $outerQuery->createNamedParameter($options['uid'])));
2015
-		}
2016
-
2017
-		if (!empty($options['types'])) {
2018
-			$or = [];
2019
-			foreach ($options['types'] as $type) {
2020
-				$or[] = $outerQuery->expr()->eq('componenttype',
2021
-					$outerQuery->createNamedParameter($type));
2022
-			}
2023
-			$outerQuery->andWhere($outerQuery->expr()->orX(...$or));
2024
-		}
2025
-
2026
-		$outerQuery->andWhere($outerQuery->expr()->in('c.id', $outerQuery->createFunction($innerQuery->getSQL())));
2027
-
2028
-		// Without explicit order by its undefined in which order the SQL server returns the events.
2029
-		// For the pagination with hasLimit and hasTimeRange, a stable ordering is helpful.
2030
-		$outerQuery->addOrderBy('id');
2031
-
2032
-		$offset = (int)$offset;
2033
-		$outerQuery->setFirstResult($offset);
2034
-
2035
-		$calendarObjects = [];
2036
-
2037
-		if ($hasLimit && $hasTimeRange) {
2038
-			/**
2039
-			 * Event recurrences are evaluated at runtime because the database only knows the first and last occurrence.
2040
-			 *
2041
-			 * Given, a user created 8 events with a yearly reoccurrence and two for events tomorrow.
2042
-			 * The upcoming event widget asks the CalDAV backend for 7 events within the next 14 days.
2043
-			 *
2044
-			 * If limit 7 is applied to the SQL query, we find the 7 events with a yearly reoccurrence
2045
-			 * and discard the events after evaluating the reoccurrence rules because they are not due within
2046
-			 * the next 14 days and end up with an empty result even if there are two events to show.
2047
-			 *
2048
-			 * The workaround for search requests with a limit and time range is asking for more row than requested
2049
-			 * and retrying if we have not reached the limit.
2050
-			 *
2051
-			 * 25 rows and 3 retries is entirely arbitrary.
2052
-			 */
2053
-			$maxResults = (int)max($limit, 25);
2054
-			$outerQuery->setMaxResults($maxResults);
2055
-
2056
-			for ($attempt = $objectsCount = 0; $attempt < 3 && $objectsCount < $limit; $attempt++) {
2057
-				$objectsCount = array_push($calendarObjects, ...$this->searchCalendarObjects($outerQuery, $start, $end));
2058
-				$outerQuery->setFirstResult($offset += $maxResults);
2059
-			}
2060
-
2061
-			$calendarObjects = array_slice($calendarObjects, 0, $limit, false);
2062
-		} else {
2063
-			$outerQuery->setMaxResults($limit);
2064
-			$calendarObjects = $this->searchCalendarObjects($outerQuery, $start, $end);
2065
-		}
2066
-
2067
-		$calendarObjects = array_map(function ($o) use ($options) {
2068
-			$calendarData = Reader::read($o['calendardata']);
2069
-
2070
-			// Expand recurrences if an explicit time range is requested
2071
-			if ($calendarData instanceof VCalendar
2072
-				&& isset($options['timerange']['start'], $options['timerange']['end'])) {
2073
-				$calendarData = $calendarData->expand(
2074
-					$options['timerange']['start'],
2075
-					$options['timerange']['end'],
2076
-				);
2077
-			}
2078
-
2079
-			$comps = $calendarData->getComponents();
2080
-			$objects = [];
2081
-			$timezones = [];
2082
-			foreach ($comps as $comp) {
2083
-				if ($comp instanceof VTimeZone) {
2084
-					$timezones[] = $comp;
2085
-				} else {
2086
-					$objects[] = $comp;
2087
-				}
2088
-			}
2089
-
2090
-			return [
2091
-				'id' => $o['id'],
2092
-				'type' => $o['componenttype'],
2093
-				'uid' => $o['uid'],
2094
-				'uri' => $o['uri'],
2095
-				'objects' => array_map(function ($c) {
2096
-					return $this->transformSearchData($c);
2097
-				}, $objects),
2098
-				'timezones' => array_map(function ($c) {
2099
-					return $this->transformSearchData($c);
2100
-				}, $timezones),
2101
-			];
2102
-		}, $calendarObjects);
2103
-
2104
-		usort($calendarObjects, function (array $a, array $b) {
2105
-			/** @var DateTimeImmutable $startA */
2106
-			$startA = $a['objects'][0]['DTSTART'][0] ?? new DateTimeImmutable(self::MAX_DATE);
2107
-			/** @var DateTimeImmutable $startB */
2108
-			$startB = $b['objects'][0]['DTSTART'][0] ?? new DateTimeImmutable(self::MAX_DATE);
2109
-
2110
-			return $startA->getTimestamp() <=> $startB->getTimestamp();
2111
-		});
2112
-
2113
-		return $calendarObjects;
2114
-	}
2115
-
2116
-	private function searchCalendarObjects(IQueryBuilder $query, ?DateTimeInterface $start, ?DateTimeInterface $end): array {
2117
-		$calendarObjects = [];
2118
-		$filterByTimeRange = ($start instanceof DateTimeInterface) || ($end instanceof DateTimeInterface);
2119
-
2120
-		$result = $query->executeQuery();
2121
-
2122
-		while (($row = $result->fetch()) !== false) {
2123
-			if ($filterByTimeRange === false) {
2124
-				// No filter required
2125
-				$calendarObjects[] = $row;
2126
-				continue;
2127
-			}
2128
-
2129
-			try {
2130
-				$isValid = $this->validateFilterForObject($row, [
2131
-					'name' => 'VCALENDAR',
2132
-					'comp-filters' => [
2133
-						[
2134
-							'name' => 'VEVENT',
2135
-							'comp-filters' => [],
2136
-							'prop-filters' => [],
2137
-							'is-not-defined' => false,
2138
-							'time-range' => [
2139
-								'start' => $start,
2140
-								'end' => $end,
2141
-							],
2142
-						],
2143
-					],
2144
-					'prop-filters' => [],
2145
-					'is-not-defined' => false,
2146
-					'time-range' => null,
2147
-				]);
2148
-			} catch (MaxInstancesExceededException $ex) {
2149
-				$this->logger->warning('Caught max instances exceeded exception for calendar data. This usually indicates too much recurring (more than 3500) event in calendar data. Object uri: ' . $row['uri'], [
2150
-					'app' => 'dav',
2151
-					'exception' => $ex,
2152
-				]);
2153
-				continue;
2154
-			}
2155
-
2156
-			if (is_resource($row['calendardata'])) {
2157
-				// Put the stream back to the beginning so it can be read another time
2158
-				rewind($row['calendardata']);
2159
-			}
2160
-
2161
-			if ($isValid) {
2162
-				$calendarObjects[] = $row;
2163
-			}
2164
-		}
2165
-
2166
-		$result->closeCursor();
2167
-
2168
-		return $calendarObjects;
2169
-	}
2170
-
2171
-	/**
2172
-	 * @param Component $comp
2173
-	 * @return array
2174
-	 */
2175
-	private function transformSearchData(Component $comp) {
2176
-		$data = [];
2177
-		/** @var Component[] $subComponents */
2178
-		$subComponents = $comp->getComponents();
2179
-		/** @var Property[] $properties */
2180
-		$properties = array_filter($comp->children(), function ($c) {
2181
-			return $c instanceof Property;
2182
-		});
2183
-		$validationRules = $comp->getValidationRules();
2184
-
2185
-		foreach ($subComponents as $subComponent) {
2186
-			$name = $subComponent->name;
2187
-			if (!isset($data[$name])) {
2188
-				$data[$name] = [];
2189
-			}
2190
-			$data[$name][] = $this->transformSearchData($subComponent);
2191
-		}
2192
-
2193
-		foreach ($properties as $property) {
2194
-			$name = $property->name;
2195
-			if (!isset($validationRules[$name])) {
2196
-				$validationRules[$name] = '*';
2197
-			}
2198
-
2199
-			$rule = $validationRules[$property->name];
2200
-			if ($rule === '+' || $rule === '*') { // multiple
2201
-				if (!isset($data[$name])) {
2202
-					$data[$name] = [];
2203
-				}
2204
-
2205
-				$data[$name][] = $this->transformSearchProperty($property);
2206
-			} else { // once
2207
-				$data[$name] = $this->transformSearchProperty($property);
2208
-			}
2209
-		}
2210
-
2211
-		return $data;
2212
-	}
2213
-
2214
-	/**
2215
-	 * @param Property $prop
2216
-	 * @return array
2217
-	 */
2218
-	private function transformSearchProperty(Property $prop) {
2219
-		// No need to check Date, as it extends DateTime
2220
-		if ($prop instanceof Property\ICalendar\DateTime) {
2221
-			$value = $prop->getDateTime();
2222
-		} else {
2223
-			$value = $prop->getValue();
2224
-		}
2225
-
2226
-		return [
2227
-			$value,
2228
-			$prop->parameters()
2229
-		];
2230
-	}
2231
-
2232
-	/**
2233
-	 * @param string $principalUri
2234
-	 * @param string $pattern
2235
-	 * @param array $componentTypes
2236
-	 * @param array $searchProperties
2237
-	 * @param array $searchParameters
2238
-	 * @param array $options
2239
-	 * @return array
2240
-	 */
2241
-	public function searchPrincipalUri(string $principalUri,
2242
-		string $pattern,
2243
-		array $componentTypes,
2244
-		array $searchProperties,
2245
-		array $searchParameters,
2246
-		array $options = [],
2247
-	): array {
2248
-		return $this->atomic(function () use ($principalUri, $pattern, $componentTypes, $searchProperties, $searchParameters, $options) {
2249
-			$escapePattern = !\array_key_exists('escape_like_param', $options) || $options['escape_like_param'] !== false;
2250
-
2251
-			$calendarObjectIdQuery = $this->db->getQueryBuilder();
2252
-			$calendarOr = [];
2253
-			$searchOr = [];
2254
-
2255
-			// Fetch calendars and subscription
2256
-			$calendars = $this->getCalendarsForUser($principalUri);
2257
-			$subscriptions = $this->getSubscriptionsForUser($principalUri);
2258
-			foreach ($calendars as $calendar) {
2259
-				$calendarAnd = $calendarObjectIdQuery->expr()->andX(
2260
-					$calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int)$calendar['id'])),
2261
-					$calendarObjectIdQuery->expr()->eq('cob.calendartype', $calendarObjectIdQuery->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)),
2262
-				);
2263
-
2264
-				// If it's shared, limit search to public events
2265
-				if (isset($calendar['{http://owncloud.org/ns}owner-principal'])
2266
-					&& $calendar['principaluri'] !== $calendar['{http://owncloud.org/ns}owner-principal']) {
2267
-					$calendarAnd->add($calendarObjectIdQuery->expr()->eq('co.classification', $calendarObjectIdQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
2268
-				}
2269
-
2270
-				$calendarOr[] = $calendarAnd;
2271
-			}
2272
-			foreach ($subscriptions as $subscription) {
2273
-				$subscriptionAnd = $calendarObjectIdQuery->expr()->andX(
2274
-					$calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int)$subscription['id'])),
2275
-					$calendarObjectIdQuery->expr()->eq('cob.calendartype', $calendarObjectIdQuery->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)),
2276
-				);
2277
-
2278
-				// If it's shared, limit search to public events
2279
-				if (isset($subscription['{http://owncloud.org/ns}owner-principal'])
2280
-					&& $subscription['principaluri'] !== $subscription['{http://owncloud.org/ns}owner-principal']) {
2281
-					$subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('co.classification', $calendarObjectIdQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
2282
-				}
2283
-
2284
-				$calendarOr[] = $subscriptionAnd;
2285
-			}
2286
-
2287
-			foreach ($searchProperties as $property) {
2288
-				$propertyAnd = $calendarObjectIdQuery->expr()->andX(
2289
-					$calendarObjectIdQuery->expr()->eq('cob.name', $calendarObjectIdQuery->createNamedParameter($property, IQueryBuilder::PARAM_STR)),
2290
-					$calendarObjectIdQuery->expr()->isNull('cob.parameter'),
2291
-				);
2292
-
2293
-				$searchOr[] = $propertyAnd;
2294
-			}
2295
-			foreach ($searchParameters as $property => $parameter) {
2296
-				$parameterAnd = $calendarObjectIdQuery->expr()->andX(
2297
-					$calendarObjectIdQuery->expr()->eq('cob.name', $calendarObjectIdQuery->createNamedParameter($property, IQueryBuilder::PARAM_STR)),
2298
-					$calendarObjectIdQuery->expr()->eq('cob.parameter', $calendarObjectIdQuery->createNamedParameter($parameter, IQueryBuilder::PARAM_STR_ARRAY)),
2299
-				);
2300
-
2301
-				$searchOr[] = $parameterAnd;
2302
-			}
2303
-
2304
-			if (empty($calendarOr)) {
2305
-				return [];
2306
-			}
2307
-			if (empty($searchOr)) {
2308
-				return [];
2309
-			}
2310
-
2311
-			$calendarObjectIdQuery->selectDistinct('cob.objectid')
2312
-				->from($this->dbObjectPropertiesTable, 'cob')
2313
-				->leftJoin('cob', 'calendarobjects', 'co', $calendarObjectIdQuery->expr()->eq('co.id', 'cob.objectid'))
2314
-				->andWhere($calendarObjectIdQuery->expr()->in('co.componenttype', $calendarObjectIdQuery->createNamedParameter($componentTypes, IQueryBuilder::PARAM_STR_ARRAY)))
2315
-				->andWhere($calendarObjectIdQuery->expr()->orX(...$calendarOr))
2316
-				->andWhere($calendarObjectIdQuery->expr()->orX(...$searchOr))
2317
-				->andWhere($calendarObjectIdQuery->expr()->isNull('deleted_at'));
2318
-
2319
-			if ($pattern !== '') {
2320
-				if (!$escapePattern) {
2321
-					$calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->ilike('cob.value', $calendarObjectIdQuery->createNamedParameter($pattern)));
2322
-				} else {
2323
-					$calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->ilike('cob.value', $calendarObjectIdQuery->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%')));
2324
-				}
2325
-			}
2326
-
2327
-			if (isset($options['limit'])) {
2328
-				$calendarObjectIdQuery->setMaxResults($options['limit']);
2329
-			}
2330
-			if (isset($options['offset'])) {
2331
-				$calendarObjectIdQuery->setFirstResult($options['offset']);
2332
-			}
2333
-			if (isset($options['timerange'])) {
2334
-				if (isset($options['timerange']['start']) && $options['timerange']['start'] instanceof DateTimeInterface) {
2335
-					$calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->gt(
2336
-						'lastoccurence',
2337
-						$calendarObjectIdQuery->createNamedParameter($options['timerange']['start']->getTimeStamp()),
2338
-					));
2339
-				}
2340
-				if (isset($options['timerange']['end']) && $options['timerange']['end'] instanceof DateTimeInterface) {
2341
-					$calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->lt(
2342
-						'firstoccurence',
2343
-						$calendarObjectIdQuery->createNamedParameter($options['timerange']['end']->getTimeStamp()),
2344
-					));
2345
-				}
2346
-			}
2347
-
2348
-			$result = $calendarObjectIdQuery->executeQuery();
2349
-			$matches = [];
2350
-			while (($row = $result->fetch()) !== false) {
2351
-				$matches[] = (int)$row['objectid'];
2352
-			}
2353
-			$result->closeCursor();
2354
-
2355
-			$query = $this->db->getQueryBuilder();
2356
-			$query->select('calendardata', 'uri', 'calendarid', 'calendartype')
2357
-				->from('calendarobjects')
2358
-				->where($query->expr()->in('id', $query->createNamedParameter($matches, IQueryBuilder::PARAM_INT_ARRAY)));
2359
-
2360
-			$result = $query->executeQuery();
2361
-			$calendarObjects = [];
2362
-			while (($array = $result->fetch()) !== false) {
2363
-				$array['calendarid'] = (int)$array['calendarid'];
2364
-				$array['calendartype'] = (int)$array['calendartype'];
2365
-				$array['calendardata'] = $this->readBlob($array['calendardata']);
2366
-
2367
-				$calendarObjects[] = $array;
2368
-			}
2369
-			$result->closeCursor();
2370
-			return $calendarObjects;
2371
-		}, $this->db);
2372
-	}
2373
-
2374
-	/**
2375
-	 * Searches through all of a users calendars and calendar objects to find
2376
-	 * an object with a specific UID.
2377
-	 *
2378
-	 * This method should return the path to this object, relative to the
2379
-	 * calendar home, so this path usually only contains two parts:
2380
-	 *
2381
-	 * calendarpath/objectpath.ics
2382
-	 *
2383
-	 * If the uid is not found, return null.
2384
-	 *
2385
-	 * This method should only consider * objects that the principal owns, so
2386
-	 * any calendars owned by other principals that also appear in this
2387
-	 * collection should be ignored.
2388
-	 *
2389
-	 * @param string $principalUri
2390
-	 * @param string $uid
2391
-	 * @return string|null
2392
-	 */
2393
-	public function getCalendarObjectByUID($principalUri, $uid) {
2394
-		$query = $this->db->getQueryBuilder();
2395
-		$query->selectAlias('c.uri', 'calendaruri')->selectAlias('co.uri', 'objecturi')
2396
-			->from('calendarobjects', 'co')
2397
-			->leftJoin('co', 'calendars', 'c', $query->expr()->eq('co.calendarid', 'c.id'))
2398
-			->where($query->expr()->eq('c.principaluri', $query->createNamedParameter($principalUri)))
2399
-			->andWhere($query->expr()->eq('co.uid', $query->createNamedParameter($uid)))
2400
-			->andWhere($query->expr()->isNull('co.deleted_at'));
2401
-		$stmt = $query->executeQuery();
2402
-		$row = $stmt->fetch();
2403
-		$stmt->closeCursor();
2404
-		if ($row) {
2405
-			return $row['calendaruri'] . '/' . $row['objecturi'];
2406
-		}
2407
-
2408
-		return null;
2409
-	}
2410
-
2411
-	public function getCalendarObjectById(string $principalUri, int $id): ?array {
2412
-		$query = $this->db->getQueryBuilder();
2413
-		$query->select(['co.id', 'co.uri', 'co.lastmodified', 'co.etag', 'co.calendarid', 'co.size', 'co.calendardata', 'co.componenttype', 'co.classification', 'co.deleted_at'])
2414
-			->selectAlias('c.uri', 'calendaruri')
2415
-			->from('calendarobjects', 'co')
2416
-			->join('co', 'calendars', 'c', $query->expr()->eq('c.id', 'co.calendarid', IQueryBuilder::PARAM_INT))
2417
-			->where($query->expr()->eq('c.principaluri', $query->createNamedParameter($principalUri)))
2418
-			->andWhere($query->expr()->eq('co.id', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT));
2419
-		$stmt = $query->executeQuery();
2420
-		$row = $stmt->fetch();
2421
-		$stmt->closeCursor();
2422
-
2423
-		if (!$row) {
2424
-			return null;
2425
-		}
2426
-
2427
-		return [
2428
-			'id' => $row['id'],
2429
-			'uri' => $row['uri'],
2430
-			'lastmodified' => $row['lastmodified'],
2431
-			'etag' => '"' . $row['etag'] . '"',
2432
-			'calendarid' => $row['calendarid'],
2433
-			'calendaruri' => $row['calendaruri'],
2434
-			'size' => (int)$row['size'],
2435
-			'calendardata' => $this->readBlob($row['calendardata']),
2436
-			'component' => strtolower($row['componenttype']),
2437
-			'classification' => (int)$row['classification'],
2438
-			'deleted_at' => isset($row['deleted_at']) ? ((int)$row['deleted_at']) : null,
2439
-		];
2440
-	}
2441
-
2442
-	/**
2443
-	 * The getChanges method returns all the changes that have happened, since
2444
-	 * the specified syncToken in the specified calendar.
2445
-	 *
2446
-	 * This function should return an array, such as the following:
2447
-	 *
2448
-	 * [
2449
-	 *   'syncToken' => 'The current synctoken',
2450
-	 *   'added'   => [
2451
-	 *      'new.txt',
2452
-	 *   ],
2453
-	 *   'modified'   => [
2454
-	 *      'modified.txt',
2455
-	 *   ],
2456
-	 *   'deleted' => [
2457
-	 *      'foo.php.bak',
2458
-	 *      'old.txt'
2459
-	 *   ]
2460
-	 * );
2461
-	 *
2462
-	 * The returned syncToken property should reflect the *current* syncToken
2463
-	 * of the calendar, as reported in the {http://sabredav.org/ns}sync-token
2464
-	 * property This is * needed here too, to ensure the operation is atomic.
2465
-	 *
2466
-	 * If the $syncToken argument is specified as null, this is an initial
2467
-	 * sync, and all members should be reported.
2468
-	 *
2469
-	 * The modified property is an array of nodenames that have changed since
2470
-	 * the last token.
2471
-	 *
2472
-	 * The deleted property is an array with nodenames, that have been deleted
2473
-	 * from collection.
2474
-	 *
2475
-	 * The $syncLevel argument is basically the 'depth' of the report. If it's
2476
-	 * 1, you only have to report changes that happened only directly in
2477
-	 * immediate descendants. If it's 2, it should also include changes from
2478
-	 * the nodes below the child collections. (grandchildren)
2479
-	 *
2480
-	 * The $limit argument allows a client to specify how many results should
2481
-	 * be returned at most. If the limit is not specified, it should be treated
2482
-	 * as infinite.
2483
-	 *
2484
-	 * If the limit (infinite or not) is higher than you're willing to return,
2485
-	 * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
2486
-	 *
2487
-	 * If the syncToken is expired (due to data cleanup) or unknown, you must
2488
-	 * return null.
2489
-	 *
2490
-	 * The limit is 'suggestive'. You are free to ignore it.
2491
-	 *
2492
-	 * @param string $calendarId
2493
-	 * @param string $syncToken
2494
-	 * @param int $syncLevel
2495
-	 * @param int|null $limit
2496
-	 * @param int $calendarType
2497
-	 * @return ?array
2498
-	 */
2499
-	public function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit = null, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
2500
-		$table = $calendarType === self::CALENDAR_TYPE_CALENDAR ? 'calendars': 'calendarsubscriptions';
2501
-
2502
-		return $this->atomic(function () use ($calendarId, $syncToken, $syncLevel, $limit, $calendarType, $table) {
2503
-			// Current synctoken
2504
-			$qb = $this->db->getQueryBuilder();
2505
-			$qb->select('synctoken')
2506
-				->from($table)
2507
-				->where(
2508
-					$qb->expr()->eq('id', $qb->createNamedParameter($calendarId))
2509
-				);
2510
-			$stmt = $qb->executeQuery();
2511
-			$currentToken = $stmt->fetchOne();
2512
-			$initialSync = !is_numeric($syncToken);
2513
-
2514
-			if ($currentToken === false) {
2515
-				return null;
2516
-			}
2517
-
2518
-			// evaluate if this is a initial sync and construct appropriate command
2519
-			if ($initialSync) {
2520
-				$qb = $this->db->getQueryBuilder();
2521
-				$qb->select('uri')
2522
-					->from('calendarobjects')
2523
-					->where($qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)))
2524
-					->andWhere($qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType)))
2525
-					->andWhere($qb->expr()->isNull('deleted_at'));
2526
-			} else {
2527
-				$qb = $this->db->getQueryBuilder();
2528
-				$qb->select('uri', $qb->func()->max('operation'))
2529
-					->from('calendarchanges')
2530
-					->where($qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)))
2531
-					->andWhere($qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType)))
2532
-					->andWhere($qb->expr()->gte('synctoken', $qb->createNamedParameter($syncToken)))
2533
-					->andWhere($qb->expr()->lt('synctoken', $qb->createNamedParameter($currentToken)))
2534
-					->groupBy('uri');
2535
-			}
2536
-			// evaluate if limit exists
2537
-			if (is_numeric($limit)) {
2538
-				$qb->setMaxResults($limit);
2539
-			}
2540
-			// execute command
2541
-			$stmt = $qb->executeQuery();
2542
-			// build results
2543
-			$result = ['syncToken' => $currentToken, 'added' => [], 'modified' => [], 'deleted' => []];
2544
-			// retrieve results
2545
-			if ($initialSync) {
2546
-				$result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
2547
-			} else {
2548
-				// \PDO::FETCH_NUM is needed due to the inconsistent field names
2549
-				// produced by doctrine for MAX() with different databases
2550
-				while ($entry = $stmt->fetch(\PDO::FETCH_NUM)) {
2551
-					// assign uri (column 0) to appropriate mutation based on operation (column 1)
2552
-					// forced (int) is needed as doctrine with OCI returns the operation field as string not integer
2553
-					match ((int)$entry[1]) {
2554
-						1 => $result['added'][] = $entry[0],
2555
-						2 => $result['modified'][] = $entry[0],
2556
-						3 => $result['deleted'][] = $entry[0],
2557
-						default => $this->logger->debug('Unknown calendar change operation detected')
2558
-					};
2559
-				}
2560
-			}
2561
-			$stmt->closeCursor();
2562
-
2563
-			return $result;
2564
-		}, $this->db);
2565
-	}
2566
-
2567
-	/**
2568
-	 * Returns a list of subscriptions for a principal.
2569
-	 *
2570
-	 * Every subscription is an array with the following keys:
2571
-	 *  * id, a unique id that will be used by other functions to modify the
2572
-	 *    subscription. This can be the same as the uri or a database key.
2573
-	 *  * uri. This is just the 'base uri' or 'filename' of the subscription.
2574
-	 *  * principaluri. The owner of the subscription. Almost always the same as
2575
-	 *    principalUri passed to this method.
2576
-	 *
2577
-	 * Furthermore, all the subscription info must be returned too:
2578
-	 *
2579
-	 * 1. {DAV:}displayname
2580
-	 * 2. {http://apple.com/ns/ical/}refreshrate
2581
-	 * 3. {http://calendarserver.org/ns/}subscribed-strip-todos (omit if todos
2582
-	 *    should not be stripped).
2583
-	 * 4. {http://calendarserver.org/ns/}subscribed-strip-alarms (omit if alarms
2584
-	 *    should not be stripped).
2585
-	 * 5. {http://calendarserver.org/ns/}subscribed-strip-attachments (omit if
2586
-	 *    attachments should not be stripped).
2587
-	 * 6. {http://calendarserver.org/ns/}source (Must be a
2588
-	 *     Sabre\DAV\Property\Href).
2589
-	 * 7. {http://apple.com/ns/ical/}calendar-color
2590
-	 * 8. {http://apple.com/ns/ical/}calendar-order
2591
-	 * 9. {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
2592
-	 *    (should just be an instance of
2593
-	 *    Sabre\CalDAV\Property\SupportedCalendarComponentSet, with a bunch of
2594
-	 *    default components).
2595
-	 *
2596
-	 * @param string $principalUri
2597
-	 * @return array
2598
-	 */
2599
-	public function getSubscriptionsForUser($principalUri) {
2600
-		$fields = array_column($this->subscriptionPropertyMap, 0);
2601
-		$fields[] = 'id';
2602
-		$fields[] = 'uri';
2603
-		$fields[] = 'source';
2604
-		$fields[] = 'principaluri';
2605
-		$fields[] = 'lastmodified';
2606
-		$fields[] = 'synctoken';
2607
-
2608
-		$query = $this->db->getQueryBuilder();
2609
-		$query->select($fields)
2610
-			->from('calendarsubscriptions')
2611
-			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
2612
-			->orderBy('calendarorder', 'asc');
2613
-		$stmt = $query->executeQuery();
2614
-
2615
-		$subscriptions = [];
2616
-		while ($row = $stmt->fetch()) {
2617
-			$subscription = [
2618
-				'id' => $row['id'],
2619
-				'uri' => $row['uri'],
2620
-				'principaluri' => $row['principaluri'],
2621
-				'source' => $row['source'],
2622
-				'lastmodified' => $row['lastmodified'],
2623
-
2624
-				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
2625
-				'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
2626
-			];
2627
-
2628
-			$subscriptions[] = $this->rowToSubscription($row, $subscription);
2629
-		}
2630
-
2631
-		return $subscriptions;
2632
-	}
2633
-
2634
-	/**
2635
-	 * Creates a new subscription for a principal.
2636
-	 *
2637
-	 * If the creation was a success, an id must be returned that can be used to reference
2638
-	 * this subscription in other methods, such as updateSubscription.
2639
-	 *
2640
-	 * @param string $principalUri
2641
-	 * @param string $uri
2642
-	 * @param array $properties
2643
-	 * @return mixed
2644
-	 */
2645
-	public function createSubscription($principalUri, $uri, array $properties) {
2646
-		if (!isset($properties['{http://calendarserver.org/ns/}source'])) {
2647
-			throw new Forbidden('The {http://calendarserver.org/ns/}source property is required when creating subscriptions');
2648
-		}
2649
-
2650
-		$values = [
2651
-			'principaluri' => $principalUri,
2652
-			'uri' => $uri,
2653
-			'source' => $properties['{http://calendarserver.org/ns/}source']->getHref(),
2654
-			'lastmodified' => time(),
2655
-		];
2656
-
2657
-		$propertiesBoolean = ['striptodos', 'stripalarms', 'stripattachments'];
2658
-
2659
-		foreach ($this->subscriptionPropertyMap as $xmlName => [$dbName, $type]) {
2660
-			if (array_key_exists($xmlName, $properties)) {
2661
-				$values[$dbName] = $properties[$xmlName];
2662
-				if (in_array($dbName, $propertiesBoolean)) {
2663
-					$values[$dbName] = true;
2664
-				}
2665
-			}
2666
-		}
2667
-
2668
-		[$subscriptionId, $subscriptionRow] = $this->atomic(function () use ($values) {
2669
-			$valuesToInsert = [];
2670
-			$query = $this->db->getQueryBuilder();
2671
-			foreach (array_keys($values) as $name) {
2672
-				$valuesToInsert[$name] = $query->createNamedParameter($values[$name]);
2673
-			}
2674
-			$query->insert('calendarsubscriptions')
2675
-				->values($valuesToInsert)
2676
-				->executeStatement();
2677
-
2678
-			$subscriptionId = $query->getLastInsertId();
2679
-
2680
-			$subscriptionRow = $this->getSubscriptionById($subscriptionId);
2681
-			return [$subscriptionId, $subscriptionRow];
2682
-		}, $this->db);
2683
-
2684
-		$this->dispatcher->dispatchTyped(new SubscriptionCreatedEvent($subscriptionId, $subscriptionRow));
2685
-
2686
-		return $subscriptionId;
2687
-	}
2688
-
2689
-	/**
2690
-	 * Updates a subscription
2691
-	 *
2692
-	 * The list of mutations is stored in a Sabre\DAV\PropPatch object.
2693
-	 * To do the actual updates, you must tell this object which properties
2694
-	 * you're going to process with the handle() method.
2695
-	 *
2696
-	 * Calling the handle method is like telling the PropPatch object "I
2697
-	 * promise I can handle updating this property".
2698
-	 *
2699
-	 * Read the PropPatch documentation for more info and examples.
2700
-	 *
2701
-	 * @param mixed $subscriptionId
2702
-	 * @param PropPatch $propPatch
2703
-	 * @return void
2704
-	 */
2705
-	public function updateSubscription($subscriptionId, PropPatch $propPatch) {
2706
-		$supportedProperties = array_keys($this->subscriptionPropertyMap);
2707
-		$supportedProperties[] = '{http://calendarserver.org/ns/}source';
2708
-
2709
-		$propPatch->handle($supportedProperties, function ($mutations) use ($subscriptionId) {
2710
-			$newValues = [];
2711
-
2712
-			foreach ($mutations as $propertyName => $propertyValue) {
2713
-				if ($propertyName === '{http://calendarserver.org/ns/}source') {
2714
-					$newValues['source'] = $propertyValue->getHref();
2715
-				} else {
2716
-					$fieldName = $this->subscriptionPropertyMap[$propertyName][0];
2717
-					$newValues[$fieldName] = $propertyValue;
2718
-				}
2719
-			}
2720
-
2721
-			$subscriptionRow = $this->atomic(function () use ($subscriptionId, $newValues) {
2722
-				$query = $this->db->getQueryBuilder();
2723
-				$query->update('calendarsubscriptions')
2724
-					->set('lastmodified', $query->createNamedParameter(time()));
2725
-				foreach ($newValues as $fieldName => $value) {
2726
-					$query->set($fieldName, $query->createNamedParameter($value));
2727
-				}
2728
-				$query->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
2729
-					->executeStatement();
2730
-
2731
-				return $this->getSubscriptionById($subscriptionId);
2732
-			}, $this->db);
2733
-
2734
-			$this->dispatcher->dispatchTyped(new SubscriptionUpdatedEvent((int)$subscriptionId, $subscriptionRow, [], $mutations));
2735
-
2736
-			return true;
2737
-		});
2738
-	}
2739
-
2740
-	/**
2741
-	 * Deletes a subscription.
2742
-	 *
2743
-	 * @param mixed $subscriptionId
2744
-	 * @return void
2745
-	 */
2746
-	public function deleteSubscription($subscriptionId) {
2747
-		$this->atomic(function () use ($subscriptionId): void {
2748
-			$subscriptionRow = $this->getSubscriptionById($subscriptionId);
2749
-
2750
-			$query = $this->db->getQueryBuilder();
2751
-			$query->delete('calendarsubscriptions')
2752
-				->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
2753
-				->executeStatement();
2754
-
2755
-			$query = $this->db->getQueryBuilder();
2756
-			$query->delete('calendarobjects')
2757
-				->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2758
-				->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2759
-				->executeStatement();
2760
-
2761
-			$query->delete('calendarchanges')
2762
-				->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2763
-				->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2764
-				->executeStatement();
2765
-
2766
-			$query->delete($this->dbObjectPropertiesTable)
2767
-				->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2768
-				->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2769
-				->executeStatement();
2770
-
2771
-			if ($subscriptionRow) {
2772
-				$this->dispatcher->dispatchTyped(new SubscriptionDeletedEvent((int)$subscriptionId, $subscriptionRow, []));
2773
-			}
2774
-		}, $this->db);
2775
-	}
2776
-
2777
-	/**
2778
-	 * Returns a single scheduling object for the inbox collection.
2779
-	 *
2780
-	 * The returned array should contain the following elements:
2781
-	 *   * uri - A unique basename for the object. This will be used to
2782
-	 *           construct a full uri.
2783
-	 *   * calendardata - The iCalendar object
2784
-	 *   * lastmodified - The last modification date. Can be an int for a unix
2785
-	 *                    timestamp, or a PHP DateTime object.
2786
-	 *   * etag - A unique token that must change if the object changed.
2787
-	 *   * size - The size of the object, in bytes.
2788
-	 *
2789
-	 * @param string $principalUri
2790
-	 * @param string $objectUri
2791
-	 * @return array
2792
-	 */
2793
-	public function getSchedulingObject($principalUri, $objectUri) {
2794
-		$query = $this->db->getQueryBuilder();
2795
-		$stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size'])
2796
-			->from('schedulingobjects')
2797
-			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
2798
-			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
2799
-			->executeQuery();
2800
-
2801
-		$row = $stmt->fetch();
2802
-
2803
-		if (!$row) {
2804
-			return null;
2805
-		}
2806
-
2807
-		return [
2808
-			'uri' => $row['uri'],
2809
-			'calendardata' => $row['calendardata'],
2810
-			'lastmodified' => $row['lastmodified'],
2811
-			'etag' => '"' . $row['etag'] . '"',
2812
-			'size' => (int)$row['size'],
2813
-		];
2814
-	}
2815
-
2816
-	/**
2817
-	 * Returns all scheduling objects for the inbox collection.
2818
-	 *
2819
-	 * These objects should be returned as an array. Every item in the array
2820
-	 * should follow the same structure as returned from getSchedulingObject.
2821
-	 *
2822
-	 * The main difference is that 'calendardata' is optional.
2823
-	 *
2824
-	 * @param string $principalUri
2825
-	 * @return array
2826
-	 */
2827
-	public function getSchedulingObjects($principalUri) {
2828
-		$query = $this->db->getQueryBuilder();
2829
-		$stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size'])
2830
-			->from('schedulingobjects')
2831
-			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
2832
-			->executeQuery();
2833
-
2834
-		$results = [];
2835
-		while (($row = $stmt->fetch()) !== false) {
2836
-			$results[] = [
2837
-				'calendardata' => $row['calendardata'],
2838
-				'uri' => $row['uri'],
2839
-				'lastmodified' => $row['lastmodified'],
2840
-				'etag' => '"' . $row['etag'] . '"',
2841
-				'size' => (int)$row['size'],
2842
-			];
2843
-		}
2844
-		$stmt->closeCursor();
2845
-
2846
-		return $results;
2847
-	}
2848
-
2849
-	/**
2850
-	 * Deletes a scheduling object from the inbox collection.
2851
-	 *
2852
-	 * @param string $principalUri
2853
-	 * @param string $objectUri
2854
-	 * @return void
2855
-	 */
2856
-	public function deleteSchedulingObject($principalUri, $objectUri) {
2857
-		$this->cachedObjects = [];
2858
-		$query = $this->db->getQueryBuilder();
2859
-		$query->delete('schedulingobjects')
2860
-			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
2861
-			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
2862
-			->executeStatement();
2863
-	}
2864
-
2865
-	/**
2866
-	 * Deletes all scheduling objects last modified before $modifiedBefore from the inbox collection.
2867
-	 *
2868
-	 * @param int $modifiedBefore
2869
-	 * @param int $limit
2870
-	 * @return void
2871
-	 */
2872
-	public function deleteOutdatedSchedulingObjects(int $modifiedBefore, int $limit): void {
2873
-		$query = $this->db->getQueryBuilder();
2874
-		$query->select('id')
2875
-			->from('schedulingobjects')
2876
-			->where($query->expr()->lt('lastmodified', $query->createNamedParameter($modifiedBefore)))
2877
-			->setMaxResults($limit);
2878
-		$result = $query->executeQuery();
2879
-		$count = $result->rowCount();
2880
-		if ($count === 0) {
2881
-			return;
2882
-		}
2883
-		$ids = array_map(static function (array $id) {
2884
-			return (int)$id[0];
2885
-		}, $result->fetchAll(\PDO::FETCH_NUM));
2886
-		$result->closeCursor();
2887
-
2888
-		$numDeleted = 0;
2889
-		$deleteQuery = $this->db->getQueryBuilder();
2890
-		$deleteQuery->delete('schedulingobjects')
2891
-			->where($deleteQuery->expr()->in('id', $deleteQuery->createParameter('ids'), IQueryBuilder::PARAM_INT_ARRAY));
2892
-		foreach (array_chunk($ids, 1000) as $chunk) {
2893
-			$deleteQuery->setParameter('ids', $chunk, IQueryBuilder::PARAM_INT_ARRAY);
2894
-			$numDeleted += $deleteQuery->executeStatement();
2895
-		}
2896
-
2897
-		if ($numDeleted === $limit) {
2898
-			$this->logger->info("Deleted $limit scheduling objects, continuing with next batch");
2899
-			$this->deleteOutdatedSchedulingObjects($modifiedBefore, $limit);
2900
-		}
2901
-	}
2902
-
2903
-	/**
2904
-	 * Creates a new scheduling object. This should land in a users' inbox.
2905
-	 *
2906
-	 * @param string $principalUri
2907
-	 * @param string $objectUri
2908
-	 * @param string $objectData
2909
-	 * @return void
2910
-	 */
2911
-	public function createSchedulingObject($principalUri, $objectUri, $objectData) {
2912
-		$this->cachedObjects = [];
2913
-		$query = $this->db->getQueryBuilder();
2914
-		$query->insert('schedulingobjects')
2915
-			->values([
2916
-				'principaluri' => $query->createNamedParameter($principalUri),
2917
-				'calendardata' => $query->createNamedParameter($objectData, IQueryBuilder::PARAM_LOB),
2918
-				'uri' => $query->createNamedParameter($objectUri),
2919
-				'lastmodified' => $query->createNamedParameter(time()),
2920
-				'etag' => $query->createNamedParameter(md5($objectData)),
2921
-				'size' => $query->createNamedParameter(strlen($objectData))
2922
-			])
2923
-			->executeStatement();
2924
-	}
2925
-
2926
-	/**
2927
-	 * Adds a change record to the calendarchanges table.
2928
-	 *
2929
-	 * @param mixed $calendarId
2930
-	 * @param string[] $objectUris
2931
-	 * @param int $operation 1 = add, 2 = modify, 3 = delete.
2932
-	 * @param int $calendarType
2933
-	 * @return void
2934
-	 */
2935
-	protected function addChanges(int $calendarId, array $objectUris, int $operation, int $calendarType = self::CALENDAR_TYPE_CALENDAR): void {
2936
-		$this->cachedObjects = [];
2937
-		$table = $calendarType === self::CALENDAR_TYPE_CALENDAR ? 'calendars': 'calendarsubscriptions';
2938
-
2939
-		$this->atomic(function () use ($calendarId, $objectUris, $operation, $calendarType, $table): void {
2940
-			$query = $this->db->getQueryBuilder();
2941
-			$query->select('synctoken')
2942
-				->from($table)
2943
-				->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)));
2944
-			$result = $query->executeQuery();
2945
-			$syncToken = (int)$result->fetchOne();
2946
-			$result->closeCursor();
2947
-
2948
-			$query = $this->db->getQueryBuilder();
2949
-			$query->insert('calendarchanges')
2950
-				->values([
2951
-					'uri' => $query->createParameter('uri'),
2952
-					'synctoken' => $query->createNamedParameter($syncToken),
2953
-					'calendarid' => $query->createNamedParameter($calendarId),
2954
-					'operation' => $query->createNamedParameter($operation),
2955
-					'calendartype' => $query->createNamedParameter($calendarType),
2956
-					'created_at' => time(),
2957
-				]);
2958
-			foreach ($objectUris as $uri) {
2959
-				$query->setParameter('uri', $uri);
2960
-				$query->executeStatement();
2961
-			}
2962
-
2963
-			$query = $this->db->getQueryBuilder();
2964
-			$query->update($table)
2965
-				->set('synctoken', $query->createNamedParameter($syncToken + 1, IQueryBuilder::PARAM_INT))
2966
-				->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)))
2967
-				->executeStatement();
2968
-		}, $this->db);
2969
-	}
2970
-
2971
-	public function restoreChanges(int $calendarId, int $calendarType = self::CALENDAR_TYPE_CALENDAR): void {
2972
-		$this->cachedObjects = [];
2973
-
2974
-		$this->atomic(function () use ($calendarId, $calendarType): void {
2975
-			$qbAdded = $this->db->getQueryBuilder();
2976
-			$qbAdded->select('uri')
2977
-				->from('calendarobjects')
2978
-				->where(
2979
-					$qbAdded->expr()->andX(
2980
-						$qbAdded->expr()->eq('calendarid', $qbAdded->createNamedParameter($calendarId)),
2981
-						$qbAdded->expr()->eq('calendartype', $qbAdded->createNamedParameter($calendarType)),
2982
-						$qbAdded->expr()->isNull('deleted_at'),
2983
-					)
2984
-				);
2985
-			$resultAdded = $qbAdded->executeQuery();
2986
-			$addedUris = $resultAdded->fetchAll(\PDO::FETCH_COLUMN);
2987
-			$resultAdded->closeCursor();
2988
-			// Track everything as changed
2989
-			// Tracking the creation is not necessary because \OCA\DAV\CalDAV\CalDavBackend::getChangesForCalendar
2990
-			// only returns the last change per object.
2991
-			$this->addChanges($calendarId, $addedUris, 2, $calendarType);
2992
-
2993
-			$qbDeleted = $this->db->getQueryBuilder();
2994
-			$qbDeleted->select('uri')
2995
-				->from('calendarobjects')
2996
-				->where(
2997
-					$qbDeleted->expr()->andX(
2998
-						$qbDeleted->expr()->eq('calendarid', $qbDeleted->createNamedParameter($calendarId)),
2999
-						$qbDeleted->expr()->eq('calendartype', $qbDeleted->createNamedParameter($calendarType)),
3000
-						$qbDeleted->expr()->isNotNull('deleted_at'),
3001
-					)
3002
-				);
3003
-			$resultDeleted = $qbDeleted->executeQuery();
3004
-			$deletedUris = array_map(function (string $uri) {
3005
-				return str_replace('-deleted.ics', '.ics', $uri);
3006
-			}, $resultDeleted->fetchAll(\PDO::FETCH_COLUMN));
3007
-			$resultDeleted->closeCursor();
3008
-			$this->addChanges($calendarId, $deletedUris, 3, $calendarType);
3009
-		}, $this->db);
3010
-	}
3011
-
3012
-	/**
3013
-	 * Parses some information from calendar objects, used for optimized
3014
-	 * calendar-queries.
3015
-	 *
3016
-	 * Returns an array with the following keys:
3017
-	 *   * etag - An md5 checksum of the object without the quotes.
3018
-	 *   * size - Size of the object in bytes
3019
-	 *   * componentType - VEVENT, VTODO or VJOURNAL
3020
-	 *   * firstOccurence
3021
-	 *   * lastOccurence
3022
-	 *   * uid - value of the UID property
3023
-	 *
3024
-	 * @param string $calendarData
3025
-	 * @return array
3026
-	 */
3027
-	public function getDenormalizedData(string $calendarData): array {
3028
-		$vObject = Reader::read($calendarData);
3029
-		$vEvents = [];
3030
-		$componentType = null;
3031
-		$component = null;
3032
-		$firstOccurrence = null;
3033
-		$lastOccurrence = null;
3034
-		$uid = null;
3035
-		$classification = self::CLASSIFICATION_PUBLIC;
3036
-		$hasDTSTART = false;
3037
-		foreach ($vObject->getComponents() as $component) {
3038
-			if ($component->name !== 'VTIMEZONE') {
3039
-				// Finding all VEVENTs, and track them
3040
-				if ($component->name === 'VEVENT') {
3041
-					$vEvents[] = $component;
3042
-					if ($component->DTSTART) {
3043
-						$hasDTSTART = true;
3044
-					}
3045
-				}
3046
-				// Track first component type and uid
3047
-				if ($uid === null) {
3048
-					$componentType = $component->name;
3049
-					$uid = (string)$component->UID;
3050
-				}
3051
-			}
3052
-		}
3053
-		if (!$componentType) {
3054
-			throw new BadRequest('Calendar objects must have a VJOURNAL, VEVENT or VTODO component');
3055
-		}
3056
-
3057
-		if ($hasDTSTART) {
3058
-			$component = $vEvents[0];
3059
-
3060
-			// Finding the last occurrence is a bit harder
3061
-			if (!isset($component->RRULE) && count($vEvents) === 1) {
3062
-				$firstOccurrence = $component->DTSTART->getDateTime()->getTimeStamp();
3063
-				if (isset($component->DTEND)) {
3064
-					$lastOccurrence = $component->DTEND->getDateTime()->getTimeStamp();
3065
-				} elseif (isset($component->DURATION)) {
3066
-					$endDate = clone $component->DTSTART->getDateTime();
3067
-					$endDate->add(DateTimeParser::parse($component->DURATION->getValue()));
3068
-					$lastOccurrence = $endDate->getTimeStamp();
3069
-				} elseif (!$component->DTSTART->hasTime()) {
3070
-					$endDate = clone $component->DTSTART->getDateTime();
3071
-					$endDate->modify('+1 day');
3072
-					$lastOccurrence = $endDate->getTimeStamp();
3073
-				} else {
3074
-					$lastOccurrence = $firstOccurrence;
3075
-				}
3076
-			} else {
3077
-				try {
3078
-					$it = new EventIterator($vEvents);
3079
-				} catch (NoInstancesException $e) {
3080
-					$this->logger->debug('Caught no instance exception for calendar data. This usually indicates invalid calendar data.', [
3081
-						'app' => 'dav',
3082
-						'exception' => $e,
3083
-					]);
3084
-					throw new Forbidden($e->getMessage());
3085
-				}
3086
-				$maxDate = new DateTime(self::MAX_DATE);
3087
-				$firstOccurrence = $it->getDtStart()->getTimestamp();
3088
-				if ($it->isInfinite()) {
3089
-					$lastOccurrence = $maxDate->getTimestamp();
3090
-				} else {
3091
-					$end = $it->getDtEnd();
3092
-					while ($it->valid() && $end < $maxDate) {
3093
-						$end = $it->getDtEnd();
3094
-						$it->next();
3095
-					}
3096
-					$lastOccurrence = $end->getTimestamp();
3097
-				}
3098
-			}
3099
-		}
3100
-
3101
-		if ($component->CLASS) {
3102
-			$classification = CalDavBackend::CLASSIFICATION_PRIVATE;
3103
-			switch ($component->CLASS->getValue()) {
3104
-				case 'PUBLIC':
3105
-					$classification = CalDavBackend::CLASSIFICATION_PUBLIC;
3106
-					break;
3107
-				case 'CONFIDENTIAL':
3108
-					$classification = CalDavBackend::CLASSIFICATION_CONFIDENTIAL;
3109
-					break;
3110
-			}
3111
-		}
3112
-		return [
3113
-			'etag' => md5($calendarData),
3114
-			'size' => strlen($calendarData),
3115
-			'componentType' => $componentType,
3116
-			'firstOccurence' => is_null($firstOccurrence) ? null : max(0, $firstOccurrence),
3117
-			'lastOccurence' => is_null($lastOccurrence) ? null : max(0, $lastOccurrence),
3118
-			'uid' => $uid,
3119
-			'classification' => $classification
3120
-		];
3121
-	}
3122
-
3123
-	/**
3124
-	 * @param $cardData
3125
-	 * @return bool|string
3126
-	 */
3127
-	private function readBlob($cardData) {
3128
-		if (is_resource($cardData)) {
3129
-			return stream_get_contents($cardData);
3130
-		}
3131
-
3132
-		return $cardData;
3133
-	}
3134
-
3135
-	/**
3136
-	 * @param list<array{href: string, commonName: string, readOnly: bool}> $add
3137
-	 * @param list<string> $remove
3138
-	 */
3139
-	public function updateShares(IShareable $shareable, array $add, array $remove): void {
3140
-		$this->atomic(function () use ($shareable, $add, $remove): void {
3141
-			$calendarId = $shareable->getResourceId();
3142
-			$calendarRow = $this->getCalendarById($calendarId);
3143
-			if ($calendarRow === null) {
3144
-				throw new \RuntimeException('Trying to update shares for non-existing calendar: ' . $calendarId);
3145
-			}
3146
-			$oldShares = $this->getShares($calendarId);
3147
-
3148
-			$this->calendarSharingBackend->updateShares($shareable, $add, $remove, $oldShares);
3149
-
3150
-			$this->dispatcher->dispatchTyped(new CalendarShareUpdatedEvent($calendarId, $calendarRow, $oldShares, $add, $remove));
3151
-		}, $this->db);
3152
-	}
3153
-
3154
-	/**
3155
-	 * @return list<array{href: string, commonName: string, status: int, readOnly: bool, '{http://owncloud.org/ns}principal': string, '{http://owncloud.org/ns}group-share': bool}>
3156
-	 */
3157
-	public function getShares(int $resourceId): array {
3158
-		return $this->calendarSharingBackend->getShares($resourceId);
3159
-	}
3160
-
3161
-	public function preloadShares(array $resourceIds): void {
3162
-		$this->calendarSharingBackend->preloadShares($resourceIds);
3163
-	}
3164
-
3165
-	/**
3166
-	 * @param boolean $value
3167
-	 * @param Calendar $calendar
3168
-	 * @return string|null
3169
-	 */
3170
-	public function setPublishStatus($value, $calendar) {
3171
-		return $this->atomic(function () use ($value, $calendar) {
3172
-			$calendarId = $calendar->getResourceId();
3173
-			$calendarData = $this->getCalendarById($calendarId);
3174
-
3175
-			$query = $this->db->getQueryBuilder();
3176
-			if ($value) {
3177
-				$publicUri = $this->random->generate(16, ISecureRandom::CHAR_HUMAN_READABLE);
3178
-				$query->insert('dav_shares')
3179
-					->values([
3180
-						'principaluri' => $query->createNamedParameter($calendar->getPrincipalURI()),
3181
-						'type' => $query->createNamedParameter('calendar'),
3182
-						'access' => $query->createNamedParameter(self::ACCESS_PUBLIC),
3183
-						'resourceid' => $query->createNamedParameter($calendar->getResourceId()),
3184
-						'publicuri' => $query->createNamedParameter($publicUri)
3185
-					]);
3186
-				$query->executeStatement();
3187
-
3188
-				$this->dispatcher->dispatchTyped(new CalendarPublishedEvent($calendarId, $calendarData, $publicUri));
3189
-				return $publicUri;
3190
-			}
3191
-			$query->delete('dav_shares')
3192
-				->where($query->expr()->eq('resourceid', $query->createNamedParameter($calendar->getResourceId())))
3193
-				->andWhere($query->expr()->eq('access', $query->createNamedParameter(self::ACCESS_PUBLIC)));
3194
-			$query->executeStatement();
3195
-
3196
-			$this->dispatcher->dispatchTyped(new CalendarUnpublishedEvent($calendarId, $calendarData));
3197
-			return null;
3198
-		}, $this->db);
3199
-	}
3200
-
3201
-	/**
3202
-	 * @param Calendar $calendar
3203
-	 * @return mixed
3204
-	 */
3205
-	public function getPublishStatus($calendar) {
3206
-		$query = $this->db->getQueryBuilder();
3207
-		$result = $query->select('publicuri')
3208
-			->from('dav_shares')
3209
-			->where($query->expr()->eq('resourceid', $query->createNamedParameter($calendar->getResourceId())))
3210
-			->andWhere($query->expr()->eq('access', $query->createNamedParameter(self::ACCESS_PUBLIC)))
3211
-			->executeQuery();
3212
-
3213
-		$row = $result->fetch();
3214
-		$result->closeCursor();
3215
-		return $row ? reset($row) : false;
3216
-	}
3217
-
3218
-	/**
3219
-	 * @param int $resourceId
3220
-	 * @param list<array{privilege: string, principal: string, protected: bool}> $acl
3221
-	 * @return list<array{privilege: string, principal: string, protected: bool}>
3222
-	 */
3223
-	public function applyShareAcl(int $resourceId, array $acl): array {
3224
-		$shares = $this->calendarSharingBackend->getShares($resourceId);
3225
-		return $this->calendarSharingBackend->applyShareAcl($shares, $acl);
3226
-	}
3227
-
3228
-	/**
3229
-	 * update properties table
3230
-	 *
3231
-	 * @param int $calendarId
3232
-	 * @param string $objectUri
3233
-	 * @param string $calendarData
3234
-	 * @param int $calendarType
3235
-	 */
3236
-	public function updateProperties($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
3237
-		$this->cachedObjects = [];
3238
-		$this->atomic(function () use ($calendarId, $objectUri, $calendarData, $calendarType): void {
3239
-			$objectId = $this->getCalendarObjectId($calendarId, $objectUri, $calendarType);
3240
-
3241
-			try {
3242
-				$vCalendar = $this->readCalendarData($calendarData);
3243
-			} catch (\Exception $ex) {
3244
-				return;
3245
-			}
3246
-
3247
-			$this->purgeProperties($calendarId, $objectId);
3248
-
3249
-			$query = $this->db->getQueryBuilder();
3250
-			$query->insert($this->dbObjectPropertiesTable)
3251
-				->values(
3252
-					[
3253
-						'calendarid' => $query->createNamedParameter($calendarId),
3254
-						'calendartype' => $query->createNamedParameter($calendarType),
3255
-						'objectid' => $query->createNamedParameter($objectId),
3256
-						'name' => $query->createParameter('name'),
3257
-						'parameter' => $query->createParameter('parameter'),
3258
-						'value' => $query->createParameter('value'),
3259
-					]
3260
-				);
3261
-
3262
-			$indexComponents = ['VEVENT', 'VJOURNAL', 'VTODO'];
3263
-			foreach ($vCalendar->getComponents() as $component) {
3264
-				if (!in_array($component->name, $indexComponents)) {
3265
-					continue;
3266
-				}
3267
-
3268
-				foreach ($component->children() as $property) {
3269
-					if (in_array($property->name, self::INDEXED_PROPERTIES, true)) {
3270
-						$value = $property->getValue();
3271
-						// is this a shitty db?
3272
-						if (!$this->db->supports4ByteText()) {
3273
-							$value = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $value);
3274
-						}
3275
-						$value = mb_strcut($value, 0, 254);
3276
-
3277
-						$query->setParameter('name', $property->name);
3278
-						$query->setParameter('parameter', null);
3279
-						$query->setParameter('value', mb_strcut($value, 0, 254));
3280
-						$query->executeStatement();
3281
-					}
3282
-
3283
-					if (array_key_exists($property->name, self::$indexParameters)) {
3284
-						$parameters = $property->parameters();
3285
-						$indexedParametersForProperty = self::$indexParameters[$property->name];
3286
-
3287
-						foreach ($parameters as $key => $value) {
3288
-							if (in_array($key, $indexedParametersForProperty)) {
3289
-								// is this a shitty db?
3290
-								if ($this->db->supports4ByteText()) {
3291
-									$value = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $value);
3292
-								}
3293
-
3294
-								$query->setParameter('name', $property->name);
3295
-								$query->setParameter('parameter', mb_strcut($key, 0, 254));
3296
-								$query->setParameter('value', mb_strcut($value, 0, 254));
3297
-								$query->executeStatement();
3298
-							}
3299
-						}
3300
-					}
3301
-				}
3302
-			}
3303
-		}, $this->db);
3304
-	}
3305
-
3306
-	/**
3307
-	 * deletes all birthday calendars
3308
-	 */
3309
-	public function deleteAllBirthdayCalendars() {
3310
-		$this->atomic(function (): void {
3311
-			$query = $this->db->getQueryBuilder();
3312
-			$result = $query->select(['id'])->from('calendars')
3313
-				->where($query->expr()->eq('uri', $query->createNamedParameter(BirthdayService::BIRTHDAY_CALENDAR_URI)))
3314
-				->executeQuery();
3315
-
3316
-			while (($row = $result->fetch()) !== false) {
3317
-				$this->deleteCalendar(
3318
-					$row['id'],
3319
-					true // No data to keep in the trashbin, if the user re-enables then we regenerate
3320
-				);
3321
-			}
3322
-			$result->closeCursor();
3323
-		}, $this->db);
3324
-	}
3325
-
3326
-	/**
3327
-	 * @param $subscriptionId
3328
-	 */
3329
-	public function purgeAllCachedEventsForSubscription($subscriptionId) {
3330
-		$this->atomic(function () use ($subscriptionId): void {
3331
-			$query = $this->db->getQueryBuilder();
3332
-			$query->select('uri')
3333
-				->from('calendarobjects')
3334
-				->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
3335
-				->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)));
3336
-			$stmt = $query->executeQuery();
3337
-
3338
-			$uris = [];
3339
-			while (($row = $stmt->fetch()) !== false) {
3340
-				$uris[] = $row['uri'];
3341
-			}
3342
-			$stmt->closeCursor();
3343
-
3344
-			$query = $this->db->getQueryBuilder();
3345
-			$query->delete('calendarobjects')
3346
-				->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
3347
-				->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
3348
-				->executeStatement();
3349
-
3350
-			$query = $this->db->getQueryBuilder();
3351
-			$query->delete('calendarchanges')
3352
-				->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
3353
-				->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
3354
-				->executeStatement();
3355
-
3356
-			$query = $this->db->getQueryBuilder();
3357
-			$query->delete($this->dbObjectPropertiesTable)
3358
-				->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
3359
-				->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
3360
-				->executeStatement();
3361
-
3362
-			$this->addChanges($subscriptionId, $uris, 3, self::CALENDAR_TYPE_SUBSCRIPTION);
3363
-		}, $this->db);
3364
-	}
3365
-
3366
-	/**
3367
-	 * @param int $subscriptionId
3368
-	 * @param array<int> $calendarObjectIds
3369
-	 * @param array<string> $calendarObjectUris
3370
-	 */
3371
-	public function purgeCachedEventsForSubscription(int $subscriptionId, array $calendarObjectIds, array $calendarObjectUris): void {
3372
-		if (empty($calendarObjectUris)) {
3373
-			return;
3374
-		}
3375
-
3376
-		$this->atomic(function () use ($subscriptionId, $calendarObjectIds, $calendarObjectUris): void {
3377
-			foreach (array_chunk($calendarObjectIds, 1000) as $chunk) {
3378
-				$query = $this->db->getQueryBuilder();
3379
-				$query->delete($this->dbObjectPropertiesTable)
3380
-					->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
3381
-					->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
3382
-					->andWhere($query->expr()->in('id', $query->createNamedParameter($chunk, IQueryBuilder::PARAM_INT_ARRAY), IQueryBuilder::PARAM_INT_ARRAY))
3383
-					->executeStatement();
3384
-
3385
-				$query = $this->db->getQueryBuilder();
3386
-				$query->delete('calendarobjects')
3387
-					->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
3388
-					->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
3389
-					->andWhere($query->expr()->in('id', $query->createNamedParameter($chunk, IQueryBuilder::PARAM_INT_ARRAY), IQueryBuilder::PARAM_INT_ARRAY))
3390
-					->executeStatement();
3391
-			}
3392
-
3393
-			foreach (array_chunk($calendarObjectUris, 1000) as $chunk) {
3394
-				$query = $this->db->getQueryBuilder();
3395
-				$query->delete('calendarchanges')
3396
-					->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
3397
-					->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
3398
-					->andWhere($query->expr()->in('uri', $query->createNamedParameter($chunk, IQueryBuilder::PARAM_STR_ARRAY), IQueryBuilder::PARAM_STR_ARRAY))
3399
-					->executeStatement();
3400
-			}
3401
-			$this->addChanges($subscriptionId, $calendarObjectUris, 3, self::CALENDAR_TYPE_SUBSCRIPTION);
3402
-		}, $this->db);
3403
-	}
3404
-
3405
-	/**
3406
-	 * Move a calendar from one user to another
3407
-	 *
3408
-	 * @param string $uriName
3409
-	 * @param string $uriOrigin
3410
-	 * @param string $uriDestination
3411
-	 * @param string $newUriName (optional) the new uriName
3412
-	 */
3413
-	public function moveCalendar($uriName, $uriOrigin, $uriDestination, $newUriName = null) {
3414
-		$query = $this->db->getQueryBuilder();
3415
-		$query->update('calendars')
3416
-			->set('principaluri', $query->createNamedParameter($uriDestination))
3417
-			->set('uri', $query->createNamedParameter($newUriName ?: $uriName))
3418
-			->where($query->expr()->eq('principaluri', $query->createNamedParameter($uriOrigin)))
3419
-			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($uriName)))
3420
-			->executeStatement();
3421
-	}
3422
-
3423
-	/**
3424
-	 * read VCalendar data into a VCalendar object
3425
-	 *
3426
-	 * @param string $objectData
3427
-	 * @return VCalendar
3428
-	 */
3429
-	protected function readCalendarData($objectData) {
3430
-		return Reader::read($objectData);
3431
-	}
3432
-
3433
-	/**
3434
-	 * delete all properties from a given calendar object
3435
-	 *
3436
-	 * @param int $calendarId
3437
-	 * @param int $objectId
3438
-	 */
3439
-	protected function purgeProperties($calendarId, $objectId) {
3440
-		$this->cachedObjects = [];
3441
-		$query = $this->db->getQueryBuilder();
3442
-		$query->delete($this->dbObjectPropertiesTable)
3443
-			->where($query->expr()->eq('objectid', $query->createNamedParameter($objectId)))
3444
-			->andWhere($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)));
3445
-		$query->executeStatement();
3446
-	}
3447
-
3448
-	/**
3449
-	 * get ID from a given calendar object
3450
-	 *
3451
-	 * @param int $calendarId
3452
-	 * @param string $uri
3453
-	 * @param int $calendarType
3454
-	 * @return int
3455
-	 */
3456
-	protected function getCalendarObjectId($calendarId, $uri, $calendarType):int {
3457
-		$query = $this->db->getQueryBuilder();
3458
-		$query->select('id')
3459
-			->from('calendarobjects')
3460
-			->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
3461
-			->andWhere($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
3462
-			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)));
3463
-
3464
-		$result = $query->executeQuery();
3465
-		$objectIds = $result->fetch();
3466
-		$result->closeCursor();
3467
-
3468
-		if (!isset($objectIds['id'])) {
3469
-			throw new \InvalidArgumentException('Calendarobject does not exists: ' . $uri);
3470
-		}
3471
-
3472
-		return (int)$objectIds['id'];
3473
-	}
3474
-
3475
-	/**
3476
-	 * @throws \InvalidArgumentException
3477
-	 */
3478
-	public function pruneOutdatedSyncTokens(int $keep, int $retention): int {
3479
-		if ($keep < 0) {
3480
-			throw new \InvalidArgumentException();
3481
-		}
3482
-
3483
-		$query = $this->db->getQueryBuilder();
3484
-		$query->select($query->func()->max('id'))
3485
-			->from('calendarchanges');
3486
-
3487
-		$result = $query->executeQuery();
3488
-		$maxId = (int)$result->fetchOne();
3489
-		$result->closeCursor();
3490
-		if (!$maxId || $maxId < $keep) {
3491
-			return 0;
3492
-		}
3493
-
3494
-		$query = $this->db->getQueryBuilder();
3495
-		$query->delete('calendarchanges')
3496
-			->where(
3497
-				$query->expr()->lte('id', $query->createNamedParameter($maxId - $keep, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT),
3498
-				$query->expr()->lte('created_at', $query->createNamedParameter($retention)),
3499
-			);
3500
-		return $query->executeStatement();
3501
-	}
3502
-
3503
-	/**
3504
-	 * return legacy endpoint principal name to new principal name
3505
-	 *
3506
-	 * @param $principalUri
3507
-	 * @param $toV2
3508
-	 * @return string
3509
-	 */
3510
-	private function convertPrincipal($principalUri, $toV2) {
3511
-		if ($this->principalBackend->getPrincipalPrefix() === 'principals') {
3512
-			[, $name] = Uri\split($principalUri);
3513
-			if ($toV2 === true) {
3514
-				return "principals/users/$name";
3515
-			}
3516
-			return "principals/$name";
3517
-		}
3518
-		return $principalUri;
3519
-	}
3520
-
3521
-	/**
3522
-	 * adds information about an owner to the calendar data
3523
-	 *
3524
-	 */
3525
-	private function addOwnerPrincipalToCalendar(array $calendarInfo): array {
3526
-		$ownerPrincipalKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal';
3527
-		$displaynameKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}owner-displayname';
3528
-		if (isset($calendarInfo[$ownerPrincipalKey])) {
3529
-			$uri = $calendarInfo[$ownerPrincipalKey];
3530
-		} else {
3531
-			$uri = $calendarInfo['principaluri'];
3532
-		}
3533
-
3534
-		$principalInformation = $this->principalBackend->getPrincipalByPath($uri);
3535
-		if (isset($principalInformation['{DAV:}displayname'])) {
3536
-			$calendarInfo[$displaynameKey] = $principalInformation['{DAV:}displayname'];
3537
-		}
3538
-		return $calendarInfo;
3539
-	}
3540
-
3541
-	private function addResourceTypeToCalendar(array $row, array $calendar): array {
3542
-		if (isset($row['deleted_at'])) {
3543
-			// Columns is set and not null -> this is a deleted calendar
3544
-			// we send a custom resourcetype to hide the deleted calendar
3545
-			// from ordinary DAV clients, but the Calendar app will know
3546
-			// how to handle this special resource.
3547
-			$calendar['{DAV:}resourcetype'] = new DAV\Xml\Property\ResourceType([
3548
-				'{DAV:}collection',
3549
-				sprintf('{%s}deleted-calendar', \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD),
3550
-			]);
3551
-		}
3552
-		return $calendar;
3553
-	}
3554
-
3555
-	/**
3556
-	 * Amend the calendar info with database row data
3557
-	 *
3558
-	 * @param array $row
3559
-	 * @param array $calendar
3560
-	 *
3561
-	 * @return array
3562
-	 */
3563
-	private function rowToCalendar($row, array $calendar): array {
3564
-		foreach ($this->propertyMap as $xmlName => [$dbName, $type]) {
3565
-			$value = $row[$dbName];
3566
-			if ($value !== null) {
3567
-				settype($value, $type);
3568
-			}
3569
-			$calendar[$xmlName] = $value;
3570
-		}
3571
-		return $calendar;
3572
-	}
3573
-
3574
-	/**
3575
-	 * Amend the subscription info with database row data
3576
-	 *
3577
-	 * @param array $row
3578
-	 * @param array $subscription
3579
-	 *
3580
-	 * @return array
3581
-	 */
3582
-	private function rowToSubscription($row, array $subscription): array {
3583
-		foreach ($this->subscriptionPropertyMap as $xmlName => [$dbName, $type]) {
3584
-			$value = $row[$dbName];
3585
-			if ($value !== null) {
3586
-				settype($value, $type);
3587
-			}
3588
-			$subscription[$xmlName] = $value;
3589
-		}
3590
-		return $subscription;
3591
-	}
3592
-
3593
-	/**
3594
-	 * delete all invitations from a given calendar
3595
-	 *
3596
-	 * @since 31.0.0
3597
-	 *
3598
-	 * @param int $calendarId
3599
-	 *
3600
-	 * @return void
3601
-	 */
3602
-	protected function purgeCalendarInvitations(int $calendarId): void {
3603
-		// select all calendar object uid's
3604
-		$cmd = $this->db->getQueryBuilder();
3605
-		$cmd->select('uid')
3606
-			->from($this->dbObjectsTable)
3607
-			->where($cmd->expr()->eq('calendarid', $cmd->createNamedParameter($calendarId)));
3608
-		$allIds = $cmd->executeQuery()->fetchAll(\PDO::FETCH_COLUMN);
3609
-		// delete all links that match object uid's
3610
-		$cmd = $this->db->getQueryBuilder();
3611
-		$cmd->delete($this->dbObjectInvitationsTable)
3612
-			->where($cmd->expr()->in('uid', $cmd->createParameter('uids'), IQueryBuilder::PARAM_STR_ARRAY));
3613
-		foreach (array_chunk($allIds, 1000) as $chunkIds) {
3614
-			$cmd->setParameter('uids', $chunkIds, IQueryBuilder::PARAM_STR_ARRAY);
3615
-			$cmd->executeStatement();
3616
-		}
3617
-	}
3618
-
3619
-	/**
3620
-	 * Delete all invitations from a given calendar event
3621
-	 *
3622
-	 * @since 31.0.0
3623
-	 *
3624
-	 * @param string $eventId UID of the event
3625
-	 *
3626
-	 * @return void
3627
-	 */
3628
-	protected function purgeObjectInvitations(string $eventId): void {
3629
-		$cmd = $this->db->getQueryBuilder();
3630
-		$cmd->delete($this->dbObjectInvitationsTable)
3631
-			->where($cmd->expr()->eq('uid', $cmd->createNamedParameter($eventId, IQueryBuilder::PARAM_STR), IQueryBuilder::PARAM_STR));
3632
-		$cmd->executeStatement();
3633
-	}
93
+    use TTransactional;
94
+
95
+    public const CALENDAR_TYPE_CALENDAR = 0;
96
+    public const CALENDAR_TYPE_SUBSCRIPTION = 1;
97
+
98
+    public const PERSONAL_CALENDAR_URI = 'personal';
99
+    public const PERSONAL_CALENDAR_NAME = 'Personal';
100
+
101
+    public const RESOURCE_BOOKING_CALENDAR_URI = 'calendar';
102
+    public const RESOURCE_BOOKING_CALENDAR_NAME = 'Calendar';
103
+
104
+    /**
105
+     * We need to specify a max date, because we need to stop *somewhere*
106
+     *
107
+     * On 32 bit system the maximum for a signed integer is 2147483647, so
108
+     * MAX_DATE cannot be higher than date('Y-m-d', 2147483647) which results
109
+     * in 2038-01-19 to avoid problems when the date is converted
110
+     * to a unix timestamp.
111
+     */
112
+    public const MAX_DATE = '2038-01-01';
113
+
114
+    public const ACCESS_PUBLIC = 4;
115
+    public const CLASSIFICATION_PUBLIC = 0;
116
+    public const CLASSIFICATION_PRIVATE = 1;
117
+    public const CLASSIFICATION_CONFIDENTIAL = 2;
118
+
119
+    /**
120
+     * List of CalDAV properties, and how they map to database field names and their type
121
+     * Add your own properties by simply adding on to this array.
122
+     *
123
+     * @var array
124
+     * @psalm-var array<string, string[]>
125
+     */
126
+    public array $propertyMap = [
127
+        '{DAV:}displayname' => ['displayname', 'string'],
128
+        '{urn:ietf:params:xml:ns:caldav}calendar-description' => ['description', 'string'],
129
+        '{urn:ietf:params:xml:ns:caldav}calendar-timezone' => ['timezone', 'string'],
130
+        '{http://apple.com/ns/ical/}calendar-order' => ['calendarorder', 'int'],
131
+        '{http://apple.com/ns/ical/}calendar-color' => ['calendarcolor', 'string'],
132
+        '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => ['deleted_at', 'int'],
133
+    ];
134
+
135
+    /**
136
+     * List of subscription properties, and how they map to database field names.
137
+     *
138
+     * @var array
139
+     */
140
+    public array $subscriptionPropertyMap = [
141
+        '{DAV:}displayname' => ['displayname', 'string'],
142
+        '{http://apple.com/ns/ical/}refreshrate' => ['refreshrate', 'string'],
143
+        '{http://apple.com/ns/ical/}calendar-order' => ['calendarorder', 'int'],
144
+        '{http://apple.com/ns/ical/}calendar-color' => ['calendarcolor', 'string'],
145
+        '{http://calendarserver.org/ns/}subscribed-strip-todos' => ['striptodos', 'bool'],
146
+        '{http://calendarserver.org/ns/}subscribed-strip-alarms' => ['stripalarms', 'string'],
147
+        '{http://calendarserver.org/ns/}subscribed-strip-attachments' => ['stripattachments', 'string'],
148
+    ];
149
+
150
+    /**
151
+     * properties to index
152
+     *
153
+     * This list has to be kept in sync with ICalendarQuery::SEARCH_PROPERTY_*
154
+     *
155
+     * @see \OCP\Calendar\ICalendarQuery
156
+     */
157
+    private const INDEXED_PROPERTIES = [
158
+        'CATEGORIES',
159
+        'COMMENT',
160
+        'DESCRIPTION',
161
+        'LOCATION',
162
+        'RESOURCES',
163
+        'STATUS',
164
+        'SUMMARY',
165
+        'ATTENDEE',
166
+        'CONTACT',
167
+        'ORGANIZER'
168
+    ];
169
+
170
+    /** @var array parameters to index */
171
+    public static array $indexParameters = [
172
+        'ATTENDEE' => ['CN'],
173
+        'ORGANIZER' => ['CN'],
174
+    ];
175
+
176
+    /**
177
+     * @var string[] Map of uid => display name
178
+     */
179
+    protected array $userDisplayNames;
180
+
181
+    private string $dbObjectsTable = 'calendarobjects';
182
+    private string $dbObjectPropertiesTable = 'calendarobjects_props';
183
+    private string $dbObjectInvitationsTable = 'calendar_invitations';
184
+    private array $cachedObjects = [];
185
+
186
+    public function __construct(
187
+        private IDBConnection $db,
188
+        private Principal $principalBackend,
189
+        private IUserManager $userManager,
190
+        private ISecureRandom $random,
191
+        private LoggerInterface $logger,
192
+        private IEventDispatcher $dispatcher,
193
+        private IConfig $config,
194
+        private Sharing\Backend $calendarSharingBackend,
195
+        private bool $legacyEndpoint = false,
196
+    ) {
197
+    }
198
+
199
+    /**
200
+     * Return the number of calendars for a principal
201
+     *
202
+     * By default this excludes the automatically generated birthday calendar
203
+     *
204
+     * @param $principalUri
205
+     * @param bool $excludeBirthday
206
+     * @return int
207
+     */
208
+    public function getCalendarsForUserCount($principalUri, $excludeBirthday = true) {
209
+        $principalUri = $this->convertPrincipal($principalUri, true);
210
+        $query = $this->db->getQueryBuilder();
211
+        $query->select($query->func()->count('*'))
212
+            ->from('calendars');
213
+
214
+        if ($principalUri === '') {
215
+            $query->where($query->expr()->emptyString('principaluri'));
216
+        } else {
217
+            $query->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
218
+        }
219
+
220
+        if ($excludeBirthday) {
221
+            $query->andWhere($query->expr()->neq('uri', $query->createNamedParameter(BirthdayService::BIRTHDAY_CALENDAR_URI)));
222
+        }
223
+
224
+        $result = $query->executeQuery();
225
+        $column = (int)$result->fetchOne();
226
+        $result->closeCursor();
227
+        return $column;
228
+    }
229
+
230
+    /**
231
+     * Return the number of subscriptions for a principal
232
+     */
233
+    public function getSubscriptionsForUserCount(string $principalUri): int {
234
+        $principalUri = $this->convertPrincipal($principalUri, true);
235
+        $query = $this->db->getQueryBuilder();
236
+        $query->select($query->func()->count('*'))
237
+            ->from('calendarsubscriptions');
238
+
239
+        if ($principalUri === '') {
240
+            $query->where($query->expr()->emptyString('principaluri'));
241
+        } else {
242
+            $query->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
243
+        }
244
+
245
+        $result = $query->executeQuery();
246
+        $column = (int)$result->fetchOne();
247
+        $result->closeCursor();
248
+        return $column;
249
+    }
250
+
251
+    /**
252
+     * @return array{id: int, deleted_at: int}[]
253
+     */
254
+    public function getDeletedCalendars(int $deletedBefore): array {
255
+        $qb = $this->db->getQueryBuilder();
256
+        $qb->select(['id', 'deleted_at'])
257
+            ->from('calendars')
258
+            ->where($qb->expr()->isNotNull('deleted_at'))
259
+            ->andWhere($qb->expr()->lt('deleted_at', $qb->createNamedParameter($deletedBefore)));
260
+        $result = $qb->executeQuery();
261
+        $calendars = [];
262
+        while (($row = $result->fetch()) !== false) {
263
+            $calendars[] = [
264
+                'id' => (int)$row['id'],
265
+                'deleted_at' => (int)$row['deleted_at'],
266
+            ];
267
+        }
268
+        $result->closeCursor();
269
+        return $calendars;
270
+    }
271
+
272
+    /**
273
+     * Returns a list of calendars for a principal.
274
+     *
275
+     * Every project is an array with the following keys:
276
+     *  * id, a unique id that will be used by other functions to modify the
277
+     *    calendar. This can be the same as the uri or a database key.
278
+     *  * uri, which the basename of the uri with which the calendar is
279
+     *    accessed.
280
+     *  * principaluri. The owner of the calendar. Almost always the same as
281
+     *    principalUri passed to this method.
282
+     *
283
+     * Furthermore it can contain webdav properties in clark notation. A very
284
+     * common one is '{DAV:}displayname'.
285
+     *
286
+     * Many clients also require:
287
+     * {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
288
+     * For this property, you can just return an instance of
289
+     * Sabre\CalDAV\Property\SupportedCalendarComponentSet.
290
+     *
291
+     * If you return {http://sabredav.org/ns}read-only and set the value to 1,
292
+     * ACL will automatically be put in read-only mode.
293
+     *
294
+     * @param string $principalUri
295
+     * @return array
296
+     */
297
+    public function getCalendarsForUser($principalUri) {
298
+        return $this->atomic(function () use ($principalUri) {
299
+            $principalUriOriginal = $principalUri;
300
+            $principalUri = $this->convertPrincipal($principalUri, true);
301
+            $fields = array_column($this->propertyMap, 0);
302
+            $fields[] = 'id';
303
+            $fields[] = 'uri';
304
+            $fields[] = 'synctoken';
305
+            $fields[] = 'components';
306
+            $fields[] = 'principaluri';
307
+            $fields[] = 'transparent';
308
+
309
+            // Making fields a comma-delimited list
310
+            $query = $this->db->getQueryBuilder();
311
+            $query->select($fields)
312
+                ->from('calendars')
313
+                ->orderBy('calendarorder', 'ASC');
314
+
315
+            if ($principalUri === '') {
316
+                $query->where($query->expr()->emptyString('principaluri'));
317
+            } else {
318
+                $query->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
319
+            }
320
+
321
+            $result = $query->executeQuery();
322
+
323
+            $calendars = [];
324
+            while ($row = $result->fetch()) {
325
+                $row['principaluri'] = (string)$row['principaluri'];
326
+                $components = [];
327
+                if ($row['components']) {
328
+                    $components = explode(',', $row['components']);
329
+                }
330
+
331
+                $calendar = [
332
+                    'id' => $row['id'],
333
+                    'uri' => $row['uri'],
334
+                    'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
335
+                    '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ?: '0'),
336
+                    '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
337
+                    '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
338
+                    '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
339
+                    '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($principalUri, !$this->legacyEndpoint),
340
+                ];
341
+
342
+                $calendar = $this->rowToCalendar($row, $calendar);
343
+                $calendar = $this->addOwnerPrincipalToCalendar($calendar);
344
+                $calendar = $this->addResourceTypeToCalendar($row, $calendar);
345
+
346
+                if (!isset($calendars[$calendar['id']])) {
347
+                    $calendars[$calendar['id']] = $calendar;
348
+                }
349
+            }
350
+            $result->closeCursor();
351
+
352
+            // query for shared calendars
353
+            $principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true);
354
+            $principals = array_merge($principals, $this->principalBackend->getCircleMembership($principalUriOriginal));
355
+            $principals[] = $principalUri;
356
+
357
+            $fields = array_column($this->propertyMap, 0);
358
+            $fields = array_map(function (string $field) {
359
+                return 'a.' . $field;
360
+            }, $fields);
361
+            $fields[] = 'a.id';
362
+            $fields[] = 'a.uri';
363
+            $fields[] = 'a.synctoken';
364
+            $fields[] = 'a.components';
365
+            $fields[] = 'a.principaluri';
366
+            $fields[] = 'a.transparent';
367
+            $fields[] = 's.access';
368
+
369
+            $select = $this->db->getQueryBuilder();
370
+            $subSelect = $this->db->getQueryBuilder();
371
+
372
+            $subSelect->select('resourceid')
373
+                ->from('dav_shares', 'd')
374
+                ->where($subSelect->expr()->eq('d.access', $select->createNamedParameter(Backend::ACCESS_UNSHARED, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT))
375
+                ->andWhere($subSelect->expr()->in('d.principaluri', $select->createNamedParameter($principals, IQueryBuilder::PARAM_STR_ARRAY), IQueryBuilder::PARAM_STR_ARRAY));
376
+
377
+            $select->select($fields)
378
+                ->from('dav_shares', 's')
379
+                ->join('s', 'calendars', 'a', $select->expr()->eq('s.resourceid', 'a.id', IQueryBuilder::PARAM_INT))
380
+                ->where($select->expr()->in('s.principaluri', $select->createNamedParameter($principals, IQueryBuilder::PARAM_STR_ARRAY), IQueryBuilder::PARAM_STR_ARRAY))
381
+                ->andWhere($select->expr()->eq('s.type', $select->createNamedParameter('calendar', IQueryBuilder::PARAM_STR), IQueryBuilder::PARAM_STR))
382
+                ->andWhere($select->expr()->notIn('a.id', $select->createFunction($subSelect->getSQL()), IQueryBuilder::PARAM_INT_ARRAY));
383
+
384
+            $results = $select->executeQuery();
385
+
386
+            $readOnlyPropertyName = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only';
387
+            while ($row = $results->fetch()) {
388
+                $row['principaluri'] = (string)$row['principaluri'];
389
+                if ($row['principaluri'] === $principalUri) {
390
+                    continue;
391
+                }
392
+
393
+                $readOnly = (int)$row['access'] === Backend::ACCESS_READ;
394
+                if (isset($calendars[$row['id']])) {
395
+                    if ($readOnly) {
396
+                        // New share can not have more permissions than the old one.
397
+                        continue;
398
+                    }
399
+                    if (isset($calendars[$row['id']][$readOnlyPropertyName]) &&
400
+                        $calendars[$row['id']][$readOnlyPropertyName] === 0) {
401
+                        // Old share is already read-write, no more permissions can be gained
402
+                        continue;
403
+                    }
404
+                }
405
+
406
+                [, $name] = Uri\split($row['principaluri']);
407
+                $uri = $row['uri'] . '_shared_by_' . $name;
408
+                $row['displayname'] = $row['displayname'] . ' (' . ($this->userManager->getDisplayName($name) ?? ($name ?? '')) . ')';
409
+                $components = [];
410
+                if ($row['components']) {
411
+                    $components = explode(',', $row['components']);
412
+                }
413
+                $calendar = [
414
+                    'id' => $row['id'],
415
+                    'uri' => $uri,
416
+                    'principaluri' => $this->convertPrincipal($principalUri, !$this->legacyEndpoint),
417
+                    '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ?: '0'),
418
+                    '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
419
+                    '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
420
+                    '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp('transparent'),
421
+                    '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
422
+                    $readOnlyPropertyName => $readOnly,
423
+                ];
424
+
425
+                $calendar = $this->rowToCalendar($row, $calendar);
426
+                $calendar = $this->addOwnerPrincipalToCalendar($calendar);
427
+                $calendar = $this->addResourceTypeToCalendar($row, $calendar);
428
+
429
+                $calendars[$calendar['id']] = $calendar;
430
+            }
431
+            $result->closeCursor();
432
+
433
+            return array_values($calendars);
434
+        }, $this->db);
435
+    }
436
+
437
+    /**
438
+     * @param $principalUri
439
+     * @return array
440
+     */
441
+    public function getUsersOwnCalendars($principalUri) {
442
+        $principalUri = $this->convertPrincipal($principalUri, true);
443
+        $fields = array_column($this->propertyMap, 0);
444
+        $fields[] = 'id';
445
+        $fields[] = 'uri';
446
+        $fields[] = 'synctoken';
447
+        $fields[] = 'components';
448
+        $fields[] = 'principaluri';
449
+        $fields[] = 'transparent';
450
+        // Making fields a comma-delimited list
451
+        $query = $this->db->getQueryBuilder();
452
+        $query->select($fields)->from('calendars')
453
+            ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
454
+            ->orderBy('calendarorder', 'ASC');
455
+        $stmt = $query->executeQuery();
456
+        $calendars = [];
457
+        while ($row = $stmt->fetch()) {
458
+            $row['principaluri'] = (string)$row['principaluri'];
459
+            $components = [];
460
+            if ($row['components']) {
461
+                $components = explode(',', $row['components']);
462
+            }
463
+            $calendar = [
464
+                'id' => $row['id'],
465
+                'uri' => $row['uri'],
466
+                'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
467
+                '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ?: '0'),
468
+                '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
469
+                '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
470
+                '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
471
+            ];
472
+
473
+            $calendar = $this->rowToCalendar($row, $calendar);
474
+            $calendar = $this->addOwnerPrincipalToCalendar($calendar);
475
+            $calendar = $this->addResourceTypeToCalendar($row, $calendar);
476
+
477
+            if (!isset($calendars[$calendar['id']])) {
478
+                $calendars[$calendar['id']] = $calendar;
479
+            }
480
+        }
481
+        $stmt->closeCursor();
482
+        return array_values($calendars);
483
+    }
484
+
485
+    /**
486
+     * @return array
487
+     */
488
+    public function getPublicCalendars() {
489
+        $fields = array_column($this->propertyMap, 0);
490
+        $fields[] = 'a.id';
491
+        $fields[] = 'a.uri';
492
+        $fields[] = 'a.synctoken';
493
+        $fields[] = 'a.components';
494
+        $fields[] = 'a.principaluri';
495
+        $fields[] = 'a.transparent';
496
+        $fields[] = 's.access';
497
+        $fields[] = 's.publicuri';
498
+        $calendars = [];
499
+        $query = $this->db->getQueryBuilder();
500
+        $result = $query->select($fields)
501
+            ->from('dav_shares', 's')
502
+            ->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
503
+            ->where($query->expr()->in('s.access', $query->createNamedParameter(self::ACCESS_PUBLIC)))
504
+            ->andWhere($query->expr()->eq('s.type', $query->createNamedParameter('calendar')))
505
+            ->executeQuery();
506
+
507
+        while ($row = $result->fetch()) {
508
+            $row['principaluri'] = (string)$row['principaluri'];
509
+            [, $name] = Uri\split($row['principaluri']);
510
+            $row['displayname'] = $row['displayname'] . "($name)";
511
+            $components = [];
512
+            if ($row['components']) {
513
+                $components = explode(',', $row['components']);
514
+            }
515
+            $calendar = [
516
+                'id' => $row['id'],
517
+                'uri' => $row['publicuri'],
518
+                'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
519
+                '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ?: '0'),
520
+                '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
521
+                '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
522
+                '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
523
+                '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], $this->legacyEndpoint),
524
+                '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ,
525
+                '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}public' => (int)$row['access'] === self::ACCESS_PUBLIC,
526
+            ];
527
+
528
+            $calendar = $this->rowToCalendar($row, $calendar);
529
+            $calendar = $this->addOwnerPrincipalToCalendar($calendar);
530
+            $calendar = $this->addResourceTypeToCalendar($row, $calendar);
531
+
532
+            if (!isset($calendars[$calendar['id']])) {
533
+                $calendars[$calendar['id']] = $calendar;
534
+            }
535
+        }
536
+        $result->closeCursor();
537
+
538
+        return array_values($calendars);
539
+    }
540
+
541
+    /**
542
+     * @param string $uri
543
+     * @return array
544
+     * @throws NotFound
545
+     */
546
+    public function getPublicCalendar($uri) {
547
+        $fields = array_column($this->propertyMap, 0);
548
+        $fields[] = 'a.id';
549
+        $fields[] = 'a.uri';
550
+        $fields[] = 'a.synctoken';
551
+        $fields[] = 'a.components';
552
+        $fields[] = 'a.principaluri';
553
+        $fields[] = 'a.transparent';
554
+        $fields[] = 's.access';
555
+        $fields[] = 's.publicuri';
556
+        $query = $this->db->getQueryBuilder();
557
+        $result = $query->select($fields)
558
+            ->from('dav_shares', 's')
559
+            ->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
560
+            ->where($query->expr()->in('s.access', $query->createNamedParameter(self::ACCESS_PUBLIC)))
561
+            ->andWhere($query->expr()->eq('s.type', $query->createNamedParameter('calendar')))
562
+            ->andWhere($query->expr()->eq('s.publicuri', $query->createNamedParameter($uri)))
563
+            ->executeQuery();
564
+
565
+        $row = $result->fetch();
566
+
567
+        $result->closeCursor();
568
+
569
+        if ($row === false) {
570
+            throw new NotFound('Node with name \'' . $uri . '\' could not be found');
571
+        }
572
+
573
+        $row['principaluri'] = (string)$row['principaluri'];
574
+        [, $name] = Uri\split($row['principaluri']);
575
+        $row['displayname'] = $row['displayname'] . ' ' . "($name)";
576
+        $components = [];
577
+        if ($row['components']) {
578
+            $components = explode(',', $row['components']);
579
+        }
580
+        $calendar = [
581
+            'id' => $row['id'],
582
+            'uri' => $row['publicuri'],
583
+            'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
584
+            '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ?: '0'),
585
+            '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
586
+            '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
587
+            '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
588
+            '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
589
+            '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ,
590
+            '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}public' => (int)$row['access'] === self::ACCESS_PUBLIC,
591
+        ];
592
+
593
+        $calendar = $this->rowToCalendar($row, $calendar);
594
+        $calendar = $this->addOwnerPrincipalToCalendar($calendar);
595
+        $calendar = $this->addResourceTypeToCalendar($row, $calendar);
596
+
597
+        return $calendar;
598
+    }
599
+
600
+    /**
601
+     * @param string $principal
602
+     * @param string $uri
603
+     * @return array|null
604
+     */
605
+    public function getCalendarByUri($principal, $uri) {
606
+        $fields = array_column($this->propertyMap, 0);
607
+        $fields[] = 'id';
608
+        $fields[] = 'uri';
609
+        $fields[] = 'synctoken';
610
+        $fields[] = 'components';
611
+        $fields[] = 'principaluri';
612
+        $fields[] = 'transparent';
613
+
614
+        // Making fields a comma-delimited list
615
+        $query = $this->db->getQueryBuilder();
616
+        $query->select($fields)->from('calendars')
617
+            ->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
618
+            ->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($principal)))
619
+            ->setMaxResults(1);
620
+        $stmt = $query->executeQuery();
621
+
622
+        $row = $stmt->fetch();
623
+        $stmt->closeCursor();
624
+        if ($row === false) {
625
+            return null;
626
+        }
627
+
628
+        $row['principaluri'] = (string)$row['principaluri'];
629
+        $components = [];
630
+        if ($row['components']) {
631
+            $components = explode(',', $row['components']);
632
+        }
633
+
634
+        $calendar = [
635
+            'id' => $row['id'],
636
+            'uri' => $row['uri'],
637
+            'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
638
+            '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ?: '0'),
639
+            '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
640
+            '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
641
+            '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
642
+        ];
643
+
644
+        $calendar = $this->rowToCalendar($row, $calendar);
645
+        $calendar = $this->addOwnerPrincipalToCalendar($calendar);
646
+        $calendar = $this->addResourceTypeToCalendar($row, $calendar);
647
+
648
+        return $calendar;
649
+    }
650
+
651
+    /**
652
+     * @return array{id: int, uri: string, '{http://calendarserver.org/ns/}getctag': string, '{http://sabredav.org/ns}sync-token': int, '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set': SupportedCalendarComponentSet, '{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp': ScheduleCalendarTransp, '{urn:ietf:params:xml:ns:caldav}calendar-timezone': ?string }|null
653
+     */
654
+    public function getCalendarById(int $calendarId): ?array {
655
+        $fields = array_column($this->propertyMap, 0);
656
+        $fields[] = 'id';
657
+        $fields[] = 'uri';
658
+        $fields[] = 'synctoken';
659
+        $fields[] = 'components';
660
+        $fields[] = 'principaluri';
661
+        $fields[] = 'transparent';
662
+
663
+        // Making fields a comma-delimited list
664
+        $query = $this->db->getQueryBuilder();
665
+        $query->select($fields)->from('calendars')
666
+            ->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)))
667
+            ->setMaxResults(1);
668
+        $stmt = $query->executeQuery();
669
+
670
+        $row = $stmt->fetch();
671
+        $stmt->closeCursor();
672
+        if ($row === false) {
673
+            return null;
674
+        }
675
+
676
+        $row['principaluri'] = (string)$row['principaluri'];
677
+        $components = [];
678
+        if ($row['components']) {
679
+            $components = explode(',', $row['components']);
680
+        }
681
+
682
+        $calendar = [
683
+            'id' => $row['id'],
684
+            'uri' => $row['uri'],
685
+            'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
686
+            '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ?: '0'),
687
+            '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?? 0,
688
+            '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
689
+            '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
690
+        ];
691
+
692
+        $calendar = $this->rowToCalendar($row, $calendar);
693
+        $calendar = $this->addOwnerPrincipalToCalendar($calendar);
694
+        $calendar = $this->addResourceTypeToCalendar($row, $calendar);
695
+
696
+        return $calendar;
697
+    }
698
+
699
+    /**
700
+     * @param $subscriptionId
701
+     */
702
+    public function getSubscriptionById($subscriptionId) {
703
+        $fields = array_column($this->subscriptionPropertyMap, 0);
704
+        $fields[] = 'id';
705
+        $fields[] = 'uri';
706
+        $fields[] = 'source';
707
+        $fields[] = 'synctoken';
708
+        $fields[] = 'principaluri';
709
+        $fields[] = 'lastmodified';
710
+
711
+        $query = $this->db->getQueryBuilder();
712
+        $query->select($fields)
713
+            ->from('calendarsubscriptions')
714
+            ->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
715
+            ->orderBy('calendarorder', 'asc');
716
+        $stmt = $query->executeQuery();
717
+
718
+        $row = $stmt->fetch();
719
+        $stmt->closeCursor();
720
+        if ($row === false) {
721
+            return null;
722
+        }
723
+
724
+        $row['principaluri'] = (string)$row['principaluri'];
725
+        $subscription = [
726
+            'id' => $row['id'],
727
+            'uri' => $row['uri'],
728
+            'principaluri' => $row['principaluri'],
729
+            'source' => $row['source'],
730
+            'lastmodified' => $row['lastmodified'],
731
+            '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
732
+            '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
733
+        ];
734
+
735
+        return $this->rowToSubscription($row, $subscription);
736
+    }
737
+
738
+    public function getSubscriptionByUri(string $principal, string $uri): ?array {
739
+        $fields = array_column($this->subscriptionPropertyMap, 0);
740
+        $fields[] = 'id';
741
+        $fields[] = 'uri';
742
+        $fields[] = 'source';
743
+        $fields[] = 'synctoken';
744
+        $fields[] = 'principaluri';
745
+        $fields[] = 'lastmodified';
746
+
747
+        $query = $this->db->getQueryBuilder();
748
+        $query->select($fields)
749
+            ->from('calendarsubscriptions')
750
+            ->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
751
+            ->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($principal)))
752
+            ->setMaxResults(1);
753
+        $stmt = $query->executeQuery();
754
+
755
+        $row = $stmt->fetch();
756
+        $stmt->closeCursor();
757
+        if ($row === false) {
758
+            return null;
759
+        }
760
+
761
+        $row['principaluri'] = (string)$row['principaluri'];
762
+        $subscription = [
763
+            'id' => $row['id'],
764
+            'uri' => $row['uri'],
765
+            'principaluri' => $row['principaluri'],
766
+            'source' => $row['source'],
767
+            'lastmodified' => $row['lastmodified'],
768
+            '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
769
+            '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
770
+        ];
771
+
772
+        return $this->rowToSubscription($row, $subscription);
773
+    }
774
+
775
+    /**
776
+     * Creates a new calendar for a principal.
777
+     *
778
+     * If the creation was a success, an id must be returned that can be used to reference
779
+     * this calendar in other methods, such as updateCalendar.
780
+     *
781
+     * @param string $principalUri
782
+     * @param string $calendarUri
783
+     * @param array $properties
784
+     * @return int
785
+     *
786
+     * @throws CalendarException
787
+     */
788
+    public function createCalendar($principalUri, $calendarUri, array $properties) {
789
+        if (strlen($calendarUri) > 255) {
790
+            throw new CalendarException('URI too long. Calendar not created');
791
+        }
792
+
793
+        $values = [
794
+            'principaluri' => $this->convertPrincipal($principalUri, true),
795
+            'uri' => $calendarUri,
796
+            'synctoken' => 1,
797
+            'transparent' => 0,
798
+            'components' => 'VEVENT,VTODO,VJOURNAL',
799
+            'displayname' => $calendarUri
800
+        ];
801
+
802
+        // Default value
803
+        $sccs = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set';
804
+        if (isset($properties[$sccs])) {
805
+            if (!($properties[$sccs] instanceof SupportedCalendarComponentSet)) {
806
+                throw new DAV\Exception('The ' . $sccs . ' property must be of type: \Sabre\CalDAV\Property\SupportedCalendarComponentSet');
807
+            }
808
+            $values['components'] = implode(',', $properties[$sccs]->getValue());
809
+        } elseif (isset($properties['components'])) {
810
+            // Allow to provide components internally without having
811
+            // to create a SupportedCalendarComponentSet object
812
+            $values['components'] = $properties['components'];
813
+        }
814
+
815
+        $transp = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp';
816
+        if (isset($properties[$transp])) {
817
+            $values['transparent'] = (int)($properties[$transp]->getValue() === 'transparent');
818
+        }
819
+
820
+        foreach ($this->propertyMap as $xmlName => [$dbName, $type]) {
821
+            if (isset($properties[$xmlName])) {
822
+                $values[$dbName] = $properties[$xmlName];
823
+            }
824
+        }
825
+
826
+        [$calendarId, $calendarData] = $this->atomic(function () use ($values) {
827
+            $query = $this->db->getQueryBuilder();
828
+            $query->insert('calendars');
829
+            foreach ($values as $column => $value) {
830
+                $query->setValue($column, $query->createNamedParameter($value));
831
+            }
832
+            $query->executeStatement();
833
+            $calendarId = $query->getLastInsertId();
834
+
835
+            $calendarData = $this->getCalendarById($calendarId);
836
+            return [$calendarId, $calendarData];
837
+        }, $this->db);
838
+
839
+        $this->dispatcher->dispatchTyped(new CalendarCreatedEvent((int)$calendarId, $calendarData));
840
+
841
+        return $calendarId;
842
+    }
843
+
844
+    /**
845
+     * Updates properties for a calendar.
846
+     *
847
+     * The list of mutations is stored in a Sabre\DAV\PropPatch object.
848
+     * To do the actual updates, you must tell this object which properties
849
+     * you're going to process with the handle() method.
850
+     *
851
+     * Calling the handle method is like telling the PropPatch object "I
852
+     * promise I can handle updating this property".
853
+     *
854
+     * Read the PropPatch documentation for more info and examples.
855
+     *
856
+     * @param mixed $calendarId
857
+     * @param PropPatch $propPatch
858
+     * @return void
859
+     */
860
+    public function updateCalendar($calendarId, PropPatch $propPatch) {
861
+        $supportedProperties = array_keys($this->propertyMap);
862
+        $supportedProperties[] = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp';
863
+
864
+        $propPatch->handle($supportedProperties, function ($mutations) use ($calendarId) {
865
+            $newValues = [];
866
+            foreach ($mutations as $propertyName => $propertyValue) {
867
+                switch ($propertyName) {
868
+                    case '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp':
869
+                        $fieldName = 'transparent';
870
+                        $newValues[$fieldName] = (int)($propertyValue->getValue() === 'transparent');
871
+                        break;
872
+                    default:
873
+                        $fieldName = $this->propertyMap[$propertyName][0];
874
+                        $newValues[$fieldName] = $propertyValue;
875
+                        break;
876
+                }
877
+            }
878
+            [$calendarData, $shares] = $this->atomic(function () use ($calendarId, $newValues) {
879
+                $query = $this->db->getQueryBuilder();
880
+                $query->update('calendars');
881
+                foreach ($newValues as $fieldName => $value) {
882
+                    $query->set($fieldName, $query->createNamedParameter($value));
883
+                }
884
+                $query->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)));
885
+                $query->executeStatement();
886
+
887
+                $this->addChanges($calendarId, [''], 2);
888
+
889
+                $calendarData = $this->getCalendarById($calendarId);
890
+                $shares = $this->getShares($calendarId);
891
+                return [$calendarData, $shares];
892
+            }, $this->db);
893
+
894
+            $this->dispatcher->dispatchTyped(new CalendarUpdatedEvent($calendarId, $calendarData, $shares, $mutations));
895
+
896
+            return true;
897
+        });
898
+    }
899
+
900
+    /**
901
+     * Delete a calendar and all it's objects
902
+     *
903
+     * @param mixed $calendarId
904
+     * @return void
905
+     */
906
+    public function deleteCalendar($calendarId, bool $forceDeletePermanently = false) {
907
+        $this->atomic(function () use ($calendarId, $forceDeletePermanently): void {
908
+            // The calendar is deleted right away if this is either enforced by the caller
909
+            // or the special contacts birthday calendar or when the preference of an empty
910
+            // retention (0 seconds) is set, which signals a disabled trashbin.
911
+            $calendarData = $this->getCalendarById($calendarId);
912
+            $isBirthdayCalendar = isset($calendarData['uri']) && $calendarData['uri'] === BirthdayService::BIRTHDAY_CALENDAR_URI;
913
+            $trashbinDisabled = $this->config->getAppValue(Application::APP_ID, RetentionService::RETENTION_CONFIG_KEY) === '0';
914
+            if ($forceDeletePermanently || $isBirthdayCalendar || $trashbinDisabled) {
915
+                $calendarData = $this->getCalendarById($calendarId);
916
+                $shares = $this->getShares($calendarId);
917
+
918
+                $this->purgeCalendarInvitations($calendarId);
919
+
920
+                $qbDeleteCalendarObjectProps = $this->db->getQueryBuilder();
921
+                $qbDeleteCalendarObjectProps->delete($this->dbObjectPropertiesTable)
922
+                    ->where($qbDeleteCalendarObjectProps->expr()->eq('calendarid', $qbDeleteCalendarObjectProps->createNamedParameter($calendarId)))
923
+                    ->andWhere($qbDeleteCalendarObjectProps->expr()->eq('calendartype', $qbDeleteCalendarObjectProps->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)))
924
+                    ->executeStatement();
925
+
926
+                $qbDeleteCalendarObjects = $this->db->getQueryBuilder();
927
+                $qbDeleteCalendarObjects->delete('calendarobjects')
928
+                    ->where($qbDeleteCalendarObjects->expr()->eq('calendarid', $qbDeleteCalendarObjects->createNamedParameter($calendarId)))
929
+                    ->andWhere($qbDeleteCalendarObjects->expr()->eq('calendartype', $qbDeleteCalendarObjects->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)))
930
+                    ->executeStatement();
931
+
932
+                $qbDeleteCalendarChanges = $this->db->getQueryBuilder();
933
+                $qbDeleteCalendarChanges->delete('calendarchanges')
934
+                    ->where($qbDeleteCalendarChanges->expr()->eq('calendarid', $qbDeleteCalendarChanges->createNamedParameter($calendarId)))
935
+                    ->andWhere($qbDeleteCalendarChanges->expr()->eq('calendartype', $qbDeleteCalendarChanges->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)))
936
+                    ->executeStatement();
937
+
938
+                $this->calendarSharingBackend->deleteAllShares($calendarId);
939
+
940
+                $qbDeleteCalendar = $this->db->getQueryBuilder();
941
+                $qbDeleteCalendar->delete('calendars')
942
+                    ->where($qbDeleteCalendar->expr()->eq('id', $qbDeleteCalendar->createNamedParameter($calendarId)))
943
+                    ->executeStatement();
944
+
945
+                // Only dispatch if we actually deleted anything
946
+                if ($calendarData) {
947
+                    $this->dispatcher->dispatchTyped(new CalendarDeletedEvent($calendarId, $calendarData, $shares));
948
+                }
949
+            } else {
950
+                $qbMarkCalendarDeleted = $this->db->getQueryBuilder();
951
+                $qbMarkCalendarDeleted->update('calendars')
952
+                    ->set('deleted_at', $qbMarkCalendarDeleted->createNamedParameter(time()))
953
+                    ->where($qbMarkCalendarDeleted->expr()->eq('id', $qbMarkCalendarDeleted->createNamedParameter($calendarId)))
954
+                    ->executeStatement();
955
+
956
+                $calendarData = $this->getCalendarById($calendarId);
957
+                $shares = $this->getShares($calendarId);
958
+                if ($calendarData) {
959
+                    $this->dispatcher->dispatchTyped(new CalendarMovedToTrashEvent(
960
+                        $calendarId,
961
+                        $calendarData,
962
+                        $shares
963
+                    ));
964
+                }
965
+            }
966
+        }, $this->db);
967
+    }
968
+
969
+    public function restoreCalendar(int $id): void {
970
+        $this->atomic(function () use ($id): void {
971
+            $qb = $this->db->getQueryBuilder();
972
+            $update = $qb->update('calendars')
973
+                ->set('deleted_at', $qb->createNamedParameter(null))
974
+                ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT));
975
+            $update->executeStatement();
976
+
977
+            $calendarData = $this->getCalendarById($id);
978
+            $shares = $this->getShares($id);
979
+            if ($calendarData === null) {
980
+                throw new RuntimeException('Calendar data that was just written can\'t be read back. Check your database configuration.');
981
+            }
982
+            $this->dispatcher->dispatchTyped(new CalendarRestoredEvent(
983
+                $id,
984
+                $calendarData,
985
+                $shares
986
+            ));
987
+        }, $this->db);
988
+    }
989
+
990
+    /**
991
+     * Returns all calendar objects with limited metadata for a calendar
992
+     *
993
+     * Every item contains an array with the following keys:
994
+     *   * id - the table row id
995
+     *   * etag - An arbitrary string
996
+     *   * uri - a unique key which will be used to construct the uri. This can
997
+     *     be any arbitrary string.
998
+     *   * calendardata - The iCalendar-compatible calendar data
999
+     *
1000
+     * @param mixed $calendarId
1001
+     * @param int $calendarType
1002
+     * @return array
1003
+     */
1004
+    public function getLimitedCalendarObjects(int $calendarId, int $calendarType = self::CALENDAR_TYPE_CALENDAR):array {
1005
+        $query = $this->db->getQueryBuilder();
1006
+        $query->select(['id','uid', 'etag', 'uri', 'calendardata'])
1007
+            ->from('calendarobjects')
1008
+            ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
1009
+            ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)))
1010
+            ->andWhere($query->expr()->isNull('deleted_at'));
1011
+        $stmt = $query->executeQuery();
1012
+
1013
+        $result = [];
1014
+        while (($row = $stmt->fetch()) !== false) {
1015
+            $result[$row['uid']] = [
1016
+                'id' => $row['id'],
1017
+                'etag' => $row['etag'],
1018
+                'uri' => $row['uri'],
1019
+                'calendardata' => $row['calendardata'],
1020
+            ];
1021
+        }
1022
+        $stmt->closeCursor();
1023
+
1024
+        return $result;
1025
+    }
1026
+
1027
+    /**
1028
+     * Delete all of an user's shares
1029
+     *
1030
+     * @param string $principaluri
1031
+     * @return void
1032
+     */
1033
+    public function deleteAllSharesByUser($principaluri) {
1034
+        $this->calendarSharingBackend->deleteAllSharesByUser($principaluri);
1035
+    }
1036
+
1037
+    /**
1038
+     * Returns all calendar objects within a calendar.
1039
+     *
1040
+     * Every item contains an array with the following keys:
1041
+     *   * calendardata - The iCalendar-compatible calendar data
1042
+     *   * uri - a unique key which will be used to construct the uri. This can
1043
+     *     be any arbitrary string, but making sure it ends with '.ics' is a
1044
+     *     good idea. This is only the basename, or filename, not the full
1045
+     *     path.
1046
+     *   * lastmodified - a timestamp of the last modification time
1047
+     *   * etag - An arbitrary string, surrounded by double-quotes. (e.g.:
1048
+     *   '"abcdef"')
1049
+     *   * size - The size of the calendar objects, in bytes.
1050
+     *   * component - optional, a string containing the type of object, such
1051
+     *     as 'vevent' or 'vtodo'. If specified, this will be used to populate
1052
+     *     the Content-Type header.
1053
+     *
1054
+     * Note that the etag is optional, but it's highly encouraged to return for
1055
+     * speed reasons.
1056
+     *
1057
+     * The calendardata is also optional. If it's not returned
1058
+     * 'getCalendarObject' will be called later, which *is* expected to return
1059
+     * calendardata.
1060
+     *
1061
+     * If neither etag or size are specified, the calendardata will be
1062
+     * used/fetched to determine these numbers. If both are specified the
1063
+     * amount of times this is needed is reduced by a great degree.
1064
+     *
1065
+     * @param mixed $calendarId
1066
+     * @param int $calendarType
1067
+     * @return array
1068
+     */
1069
+    public function getCalendarObjects($calendarId, $calendarType = self::CALENDAR_TYPE_CALENDAR):array {
1070
+        $query = $this->db->getQueryBuilder();
1071
+        $query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'componenttype', 'classification'])
1072
+            ->from('calendarobjects')
1073
+            ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
1074
+            ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)))
1075
+            ->andWhere($query->expr()->isNull('deleted_at'));
1076
+        $stmt = $query->executeQuery();
1077
+
1078
+        $result = [];
1079
+        while (($row = $stmt->fetch()) !== false) {
1080
+            $result[] = [
1081
+                'id' => $row['id'],
1082
+                'uri' => $row['uri'],
1083
+                'lastmodified' => $row['lastmodified'],
1084
+                'etag' => '"' . $row['etag'] . '"',
1085
+                'calendarid' => $row['calendarid'],
1086
+                'size' => (int)$row['size'],
1087
+                'component' => strtolower($row['componenttype']),
1088
+                'classification' => (int)$row['classification']
1089
+            ];
1090
+        }
1091
+        $stmt->closeCursor();
1092
+
1093
+        return $result;
1094
+    }
1095
+
1096
+    public function getDeletedCalendarObjects(int $deletedBefore): array {
1097
+        $query = $this->db->getQueryBuilder();
1098
+        $query->select(['co.id', 'co.uri', 'co.lastmodified', 'co.etag', 'co.calendarid', 'co.calendartype', 'co.size', 'co.componenttype', 'co.classification', 'co.deleted_at'])
1099
+            ->from('calendarobjects', 'co')
1100
+            ->join('co', 'calendars', 'c', $query->expr()->eq('c.id', 'co.calendarid', IQueryBuilder::PARAM_INT))
1101
+            ->where($query->expr()->isNotNull('co.deleted_at'))
1102
+            ->andWhere($query->expr()->lt('co.deleted_at', $query->createNamedParameter($deletedBefore)));
1103
+        $stmt = $query->executeQuery();
1104
+
1105
+        $result = [];
1106
+        while (($row = $stmt->fetch()) !== false) {
1107
+            $result[] = [
1108
+                'id' => $row['id'],
1109
+                'uri' => $row['uri'],
1110
+                'lastmodified' => $row['lastmodified'],
1111
+                'etag' => '"' . $row['etag'] . '"',
1112
+                'calendarid' => (int)$row['calendarid'],
1113
+                'calendartype' => (int)$row['calendartype'],
1114
+                'size' => (int)$row['size'],
1115
+                'component' => strtolower($row['componenttype']),
1116
+                'classification' => (int)$row['classification'],
1117
+                '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => $row['deleted_at'] === null ? $row['deleted_at'] : (int)$row['deleted_at'],
1118
+            ];
1119
+        }
1120
+        $stmt->closeCursor();
1121
+
1122
+        return $result;
1123
+    }
1124
+
1125
+    /**
1126
+     * Return all deleted calendar objects by the given principal that are not
1127
+     * in deleted calendars.
1128
+     *
1129
+     * @param string $principalUri
1130
+     * @return array
1131
+     * @throws Exception
1132
+     */
1133
+    public function getDeletedCalendarObjectsByPrincipal(string $principalUri): array {
1134
+        $query = $this->db->getQueryBuilder();
1135
+        $query->select(['co.id', 'co.uri', 'co.lastmodified', 'co.etag', 'co.calendarid', 'co.size', 'co.componenttype', 'co.classification', 'co.deleted_at'])
1136
+            ->selectAlias('c.uri', 'calendaruri')
1137
+            ->from('calendarobjects', 'co')
1138
+            ->join('co', 'calendars', 'c', $query->expr()->eq('c.id', 'co.calendarid', IQueryBuilder::PARAM_INT))
1139
+            ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
1140
+            ->andWhere($query->expr()->isNotNull('co.deleted_at'))
1141
+            ->andWhere($query->expr()->isNull('c.deleted_at'));
1142
+        $stmt = $query->executeQuery();
1143
+
1144
+        $result = [];
1145
+        while ($row = $stmt->fetch()) {
1146
+            $result[] = [
1147
+                'id' => $row['id'],
1148
+                'uri' => $row['uri'],
1149
+                'lastmodified' => $row['lastmodified'],
1150
+                'etag' => '"' . $row['etag'] . '"',
1151
+                'calendarid' => $row['calendarid'],
1152
+                'calendaruri' => $row['calendaruri'],
1153
+                'size' => (int)$row['size'],
1154
+                'component' => strtolower($row['componenttype']),
1155
+                'classification' => (int)$row['classification'],
1156
+                '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => $row['deleted_at'] === null ? $row['deleted_at'] : (int)$row['deleted_at'],
1157
+            ];
1158
+        }
1159
+        $stmt->closeCursor();
1160
+
1161
+        return $result;
1162
+    }
1163
+
1164
+    /**
1165
+     * Returns information from a single calendar object, based on it's object
1166
+     * uri.
1167
+     *
1168
+     * The object uri is only the basename, or filename and not a full path.
1169
+     *
1170
+     * The returned array must have the same keys as getCalendarObjects. The
1171
+     * 'calendardata' object is required here though, while it's not required
1172
+     * for getCalendarObjects.
1173
+     *
1174
+     * This method must return null if the object did not exist.
1175
+     *
1176
+     * @param mixed $calendarId
1177
+     * @param string $objectUri
1178
+     * @param int $calendarType
1179
+     * @return array|null
1180
+     */
1181
+    public function getCalendarObject($calendarId, $objectUri, int $calendarType = self::CALENDAR_TYPE_CALENDAR) {
1182
+        $key = $calendarId . '::' . $objectUri . '::' . $calendarType;
1183
+        if (isset($this->cachedObjects[$key])) {
1184
+            return $this->cachedObjects[$key];
1185
+        }
1186
+        $query = $this->db->getQueryBuilder();
1187
+        $query->select(['id', 'uri', 'uid', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification', 'deleted_at'])
1188
+            ->from('calendarobjects')
1189
+            ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
1190
+            ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
1191
+            ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)));
1192
+        $stmt = $query->executeQuery();
1193
+        $row = $stmt->fetch();
1194
+        $stmt->closeCursor();
1195
+
1196
+        if (!$row) {
1197
+            return null;
1198
+        }
1199
+
1200
+        $object = $this->rowToCalendarObject($row);
1201
+        $this->cachedObjects[$key] = $object;
1202
+        return $object;
1203
+    }
1204
+
1205
+    private function rowToCalendarObject(array $row): array {
1206
+        return [
1207
+            'id' => $row['id'],
1208
+            'uri' => $row['uri'],
1209
+            'uid' => $row['uid'],
1210
+            'lastmodified' => $row['lastmodified'],
1211
+            'etag' => '"' . $row['etag'] . '"',
1212
+            'calendarid' => $row['calendarid'],
1213
+            'size' => (int)$row['size'],
1214
+            'calendardata' => $this->readBlob($row['calendardata']),
1215
+            'component' => strtolower($row['componenttype']),
1216
+            'classification' => (int)$row['classification'],
1217
+            '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => $row['deleted_at'] === null ? $row['deleted_at'] : (int)$row['deleted_at'],
1218
+        ];
1219
+    }
1220
+
1221
+    /**
1222
+     * Returns a list of calendar objects.
1223
+     *
1224
+     * This method should work identical to getCalendarObject, but instead
1225
+     * return all the calendar objects in the list as an array.
1226
+     *
1227
+     * If the backend supports this, it may allow for some speed-ups.
1228
+     *
1229
+     * @param mixed $calendarId
1230
+     * @param string[] $uris
1231
+     * @param int $calendarType
1232
+     * @return array
1233
+     */
1234
+    public function getMultipleCalendarObjects($calendarId, array $uris, $calendarType = self::CALENDAR_TYPE_CALENDAR):array {
1235
+        if (empty($uris)) {
1236
+            return [];
1237
+        }
1238
+
1239
+        $chunks = array_chunk($uris, 100);
1240
+        $objects = [];
1241
+
1242
+        $query = $this->db->getQueryBuilder();
1243
+        $query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification'])
1244
+            ->from('calendarobjects')
1245
+            ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
1246
+            ->andWhere($query->expr()->in('uri', $query->createParameter('uri')))
1247
+            ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)))
1248
+            ->andWhere($query->expr()->isNull('deleted_at'));
1249
+
1250
+        foreach ($chunks as $uris) {
1251
+            $query->setParameter('uri', $uris, IQueryBuilder::PARAM_STR_ARRAY);
1252
+            $result = $query->executeQuery();
1253
+
1254
+            while ($row = $result->fetch()) {
1255
+                $objects[] = [
1256
+                    'id' => $row['id'],
1257
+                    'uri' => $row['uri'],
1258
+                    'lastmodified' => $row['lastmodified'],
1259
+                    'etag' => '"' . $row['etag'] . '"',
1260
+                    'calendarid' => $row['calendarid'],
1261
+                    'size' => (int)$row['size'],
1262
+                    'calendardata' => $this->readBlob($row['calendardata']),
1263
+                    'component' => strtolower($row['componenttype']),
1264
+                    'classification' => (int)$row['classification']
1265
+                ];
1266
+            }
1267
+            $result->closeCursor();
1268
+        }
1269
+
1270
+        return $objects;
1271
+    }
1272
+
1273
+    /**
1274
+     * Creates a new calendar object.
1275
+     *
1276
+     * The object uri is only the basename, or filename and not a full path.
1277
+     *
1278
+     * It is possible return an etag from this function, which will be used in
1279
+     * the response to this PUT request. Note that the ETag must be surrounded
1280
+     * by double-quotes.
1281
+     *
1282
+     * However, you should only really return this ETag if you don't mangle the
1283
+     * calendar-data. If the result of a subsequent GET to this object is not
1284
+     * the exact same as this request body, you should omit the ETag.
1285
+     *
1286
+     * @param mixed $calendarId
1287
+     * @param string $objectUri
1288
+     * @param string $calendarData
1289
+     * @param int $calendarType
1290
+     * @return string
1291
+     */
1292
+    public function createCalendarObject($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
1293
+        $this->cachedObjects = [];
1294
+        $extraData = $this->getDenormalizedData($calendarData);
1295
+
1296
+        return $this->atomic(function () use ($calendarId, $objectUri, $calendarData, $extraData, $calendarType) {
1297
+            // Try to detect duplicates
1298
+            $qb = $this->db->getQueryBuilder();
1299
+            $qb->select($qb->func()->count('*'))
1300
+                ->from('calendarobjects')
1301
+                ->where($qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)))
1302
+                ->andWhere($qb->expr()->eq('uid', $qb->createNamedParameter($extraData['uid'])))
1303
+                ->andWhere($qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType)))
1304
+                ->andWhere($qb->expr()->isNull('deleted_at'));
1305
+            $result = $qb->executeQuery();
1306
+            $count = (int)$result->fetchOne();
1307
+            $result->closeCursor();
1308
+
1309
+            if ($count !== 0) {
1310
+                throw new BadRequest('Calendar object with uid already exists in this calendar collection.');
1311
+            }
1312
+            // For a more specific error message we also try to explicitly look up the UID but as a deleted entry
1313
+            $qbDel = $this->db->getQueryBuilder();
1314
+            $qbDel->select('*')
1315
+                ->from('calendarobjects')
1316
+                ->where($qbDel->expr()->eq('calendarid', $qbDel->createNamedParameter($calendarId)))
1317
+                ->andWhere($qbDel->expr()->eq('uid', $qbDel->createNamedParameter($extraData['uid'])))
1318
+                ->andWhere($qbDel->expr()->eq('calendartype', $qbDel->createNamedParameter($calendarType)))
1319
+                ->andWhere($qbDel->expr()->isNotNull('deleted_at'));
1320
+            $result = $qbDel->executeQuery();
1321
+            $found = $result->fetch();
1322
+            $result->closeCursor();
1323
+            if ($found !== false) {
1324
+                // the object existed previously but has been deleted
1325
+                // remove the trashbin entry and continue as if it was a new object
1326
+                $this->deleteCalendarObject($calendarId, $found['uri']);
1327
+            }
1328
+
1329
+            $query = $this->db->getQueryBuilder();
1330
+            $query->insert('calendarobjects')
1331
+                ->values([
1332
+                    'calendarid' => $query->createNamedParameter($calendarId),
1333
+                    'uri' => $query->createNamedParameter($objectUri),
1334
+                    'calendardata' => $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB),
1335
+                    'lastmodified' => $query->createNamedParameter(time()),
1336
+                    'etag' => $query->createNamedParameter($extraData['etag']),
1337
+                    'size' => $query->createNamedParameter($extraData['size']),
1338
+                    'componenttype' => $query->createNamedParameter($extraData['componentType']),
1339
+                    'firstoccurence' => $query->createNamedParameter($extraData['firstOccurence']),
1340
+                    'lastoccurence' => $query->createNamedParameter($extraData['lastOccurence']),
1341
+                    'classification' => $query->createNamedParameter($extraData['classification']),
1342
+                    'uid' => $query->createNamedParameter($extraData['uid']),
1343
+                    'calendartype' => $query->createNamedParameter($calendarType),
1344
+                ])
1345
+                ->executeStatement();
1346
+
1347
+            $this->updateProperties($calendarId, $objectUri, $calendarData, $calendarType);
1348
+            $this->addChanges($calendarId, [$objectUri], 1, $calendarType);
1349
+
1350
+            $objectRow = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
1351
+            assert($objectRow !== null);
1352
+
1353
+            if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
1354
+                $calendarRow = $this->getCalendarById($calendarId);
1355
+                $shares = $this->getShares($calendarId);
1356
+
1357
+                $this->dispatcher->dispatchTyped(new CalendarObjectCreatedEvent($calendarId, $calendarRow, $shares, $objectRow));
1358
+            } else {
1359
+                $subscriptionRow = $this->getSubscriptionById($calendarId);
1360
+
1361
+                $this->dispatcher->dispatchTyped(new CachedCalendarObjectCreatedEvent($calendarId, $subscriptionRow, [], $objectRow));
1362
+            }
1363
+
1364
+            return '"' . $extraData['etag'] . '"';
1365
+        }, $this->db);
1366
+    }
1367
+
1368
+    /**
1369
+     * Updates an existing calendarobject, based on it's uri.
1370
+     *
1371
+     * The object uri is only the basename, or filename and not a full path.
1372
+     *
1373
+     * It is possible return an etag from this function, which will be used in
1374
+     * the response to this PUT request. Note that the ETag must be surrounded
1375
+     * by double-quotes.
1376
+     *
1377
+     * However, you should only really return this ETag if you don't mangle the
1378
+     * calendar-data. If the result of a subsequent GET to this object is not
1379
+     * the exact same as this request body, you should omit the ETag.
1380
+     *
1381
+     * @param mixed $calendarId
1382
+     * @param string $objectUri
1383
+     * @param string $calendarData
1384
+     * @param int $calendarType
1385
+     * @return string
1386
+     */
1387
+    public function updateCalendarObject($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
1388
+        $this->cachedObjects = [];
1389
+        $extraData = $this->getDenormalizedData($calendarData);
1390
+
1391
+        return $this->atomic(function () use ($calendarId, $objectUri, $calendarData, $extraData, $calendarType) {
1392
+            $query = $this->db->getQueryBuilder();
1393
+            $query->update('calendarobjects')
1394
+                ->set('calendardata', $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB))
1395
+                ->set('lastmodified', $query->createNamedParameter(time()))
1396
+                ->set('etag', $query->createNamedParameter($extraData['etag']))
1397
+                ->set('size', $query->createNamedParameter($extraData['size']))
1398
+                ->set('componenttype', $query->createNamedParameter($extraData['componentType']))
1399
+                ->set('firstoccurence', $query->createNamedParameter($extraData['firstOccurence']))
1400
+                ->set('lastoccurence', $query->createNamedParameter($extraData['lastOccurence']))
1401
+                ->set('classification', $query->createNamedParameter($extraData['classification']))
1402
+                ->set('uid', $query->createNamedParameter($extraData['uid']))
1403
+                ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
1404
+                ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
1405
+                ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)))
1406
+                ->executeStatement();
1407
+
1408
+            $this->updateProperties($calendarId, $objectUri, $calendarData, $calendarType);
1409
+            $this->addChanges($calendarId, [$objectUri], 2, $calendarType);
1410
+
1411
+            $objectRow = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
1412
+            if (is_array($objectRow)) {
1413
+                if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
1414
+                    $calendarRow = $this->getCalendarById($calendarId);
1415
+                    $shares = $this->getShares($calendarId);
1416
+
1417
+                    $this->dispatcher->dispatchTyped(new CalendarObjectUpdatedEvent($calendarId, $calendarRow, $shares, $objectRow));
1418
+                } else {
1419
+                    $subscriptionRow = $this->getSubscriptionById($calendarId);
1420
+
1421
+                    $this->dispatcher->dispatchTyped(new CachedCalendarObjectUpdatedEvent($calendarId, $subscriptionRow, [], $objectRow));
1422
+                }
1423
+            }
1424
+
1425
+            return '"' . $extraData['etag'] . '"';
1426
+        }, $this->db);
1427
+    }
1428
+
1429
+    /**
1430
+     * Moves a calendar object from calendar to calendar.
1431
+     *
1432
+     * @param string $sourcePrincipalUri
1433
+     * @param int $sourceObjectId
1434
+     * @param string $targetPrincipalUri
1435
+     * @param int $targetCalendarId
1436
+     * @param string $tragetObjectUri
1437
+     * @param int $calendarType
1438
+     * @return bool
1439
+     * @throws Exception
1440
+     */
1441
+    public function moveCalendarObject(string $sourcePrincipalUri, int $sourceObjectId, string $targetPrincipalUri, int $targetCalendarId, string $tragetObjectUri, int $calendarType = self::CALENDAR_TYPE_CALENDAR): bool {
1442
+        $this->cachedObjects = [];
1443
+        return $this->atomic(function () use ($sourcePrincipalUri, $sourceObjectId, $targetPrincipalUri, $targetCalendarId, $tragetObjectUri, $calendarType) {
1444
+            $object = $this->getCalendarObjectById($sourcePrincipalUri, $sourceObjectId);
1445
+            if (empty($object)) {
1446
+                return false;
1447
+            }
1448
+
1449
+            $sourceCalendarId = $object['calendarid'];
1450
+            $sourceObjectUri = $object['uri'];
1451
+
1452
+            $query = $this->db->getQueryBuilder();
1453
+            $query->update('calendarobjects')
1454
+                ->set('calendarid', $query->createNamedParameter($targetCalendarId, IQueryBuilder::PARAM_INT))
1455
+                ->set('uri', $query->createNamedParameter($tragetObjectUri, IQueryBuilder::PARAM_STR))
1456
+                ->where($query->expr()->eq('id', $query->createNamedParameter($sourceObjectId, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT))
1457
+                ->executeStatement();
1458
+
1459
+            $this->purgeProperties($sourceCalendarId, $sourceObjectId);
1460
+            $this->updateProperties($targetCalendarId, $tragetObjectUri, $object['calendardata'], $calendarType);
1461
+
1462
+            $this->addChanges($sourceCalendarId, [$sourceObjectUri], 3, $calendarType);
1463
+            $this->addChanges($targetCalendarId, [$tragetObjectUri], 1, $calendarType);
1464
+
1465
+            $object = $this->getCalendarObjectById($targetPrincipalUri, $sourceObjectId);
1466
+            // Calendar Object wasn't found - possibly because it was deleted in the meantime by a different client
1467
+            if (empty($object)) {
1468
+                return false;
1469
+            }
1470
+
1471
+            $targetCalendarRow = $this->getCalendarById($targetCalendarId);
1472
+            // the calendar this event is being moved to does not exist any longer
1473
+            if (empty($targetCalendarRow)) {
1474
+                return false;
1475
+            }
1476
+
1477
+            if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
1478
+                $sourceShares = $this->getShares($sourceCalendarId);
1479
+                $targetShares = $this->getShares($targetCalendarId);
1480
+                $sourceCalendarRow = $this->getCalendarById($sourceCalendarId);
1481
+                $this->dispatcher->dispatchTyped(new CalendarObjectMovedEvent($sourceCalendarId, $sourceCalendarRow, $targetCalendarId, $targetCalendarRow, $sourceShares, $targetShares, $object));
1482
+            }
1483
+            return true;
1484
+        }, $this->db);
1485
+    }
1486
+
1487
+
1488
+    /**
1489
+     * @param int $calendarObjectId
1490
+     * @param int $classification
1491
+     */
1492
+    public function setClassification($calendarObjectId, $classification) {
1493
+        $this->cachedObjects = [];
1494
+        if (!in_array($classification, [
1495
+            self::CLASSIFICATION_PUBLIC, self::CLASSIFICATION_PRIVATE, self::CLASSIFICATION_CONFIDENTIAL
1496
+        ])) {
1497
+            throw new \InvalidArgumentException();
1498
+        }
1499
+        $query = $this->db->getQueryBuilder();
1500
+        $query->update('calendarobjects')
1501
+            ->set('classification', $query->createNamedParameter($classification))
1502
+            ->where($query->expr()->eq('id', $query->createNamedParameter($calendarObjectId)))
1503
+            ->executeStatement();
1504
+    }
1505
+
1506
+    /**
1507
+     * Deletes an existing calendar object.
1508
+     *
1509
+     * The object uri is only the basename, or filename and not a full path.
1510
+     *
1511
+     * @param mixed $calendarId
1512
+     * @param string $objectUri
1513
+     * @param int $calendarType
1514
+     * @param bool $forceDeletePermanently
1515
+     * @return void
1516
+     */
1517
+    public function deleteCalendarObject($calendarId, $objectUri, $calendarType = self::CALENDAR_TYPE_CALENDAR, bool $forceDeletePermanently = false) {
1518
+        $this->cachedObjects = [];
1519
+        $this->atomic(function () use ($calendarId, $objectUri, $calendarType, $forceDeletePermanently): void {
1520
+            $data = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
1521
+
1522
+            if ($data === null) {
1523
+                // Nothing to delete
1524
+                return;
1525
+            }
1526
+
1527
+            if ($forceDeletePermanently || $this->config->getAppValue(Application::APP_ID, RetentionService::RETENTION_CONFIG_KEY) === '0') {
1528
+                $stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ? AND `uri` = ? AND `calendartype` = ?');
1529
+                $stmt->execute([$calendarId, $objectUri, $calendarType]);
1530
+
1531
+                $this->purgeProperties($calendarId, $data['id']);
1532
+
1533
+                $this->purgeObjectInvitations($data['uid']);
1534
+
1535
+                if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
1536
+                    $calendarRow = $this->getCalendarById($calendarId);
1537
+                    $shares = $this->getShares($calendarId);
1538
+
1539
+                    $this->dispatcher->dispatchTyped(new CalendarObjectDeletedEvent($calendarId, $calendarRow, $shares, $data));
1540
+                } else {
1541
+                    $subscriptionRow = $this->getSubscriptionById($calendarId);
1542
+
1543
+                    $this->dispatcher->dispatchTyped(new CachedCalendarObjectDeletedEvent($calendarId, $subscriptionRow, [], $data));
1544
+                }
1545
+            } else {
1546
+                $pathInfo = pathinfo($data['uri']);
1547
+                if (!empty($pathInfo['extension'])) {
1548
+                    // Append a suffix to "free" the old URI for recreation
1549
+                    $newUri = sprintf(
1550
+                        '%s-deleted.%s',
1551
+                        $pathInfo['filename'],
1552
+                        $pathInfo['extension']
1553
+                    );
1554
+                } else {
1555
+                    $newUri = sprintf(
1556
+                        '%s-deleted',
1557
+                        $pathInfo['filename']
1558
+                    );
1559
+                }
1560
+
1561
+                // Try to detect conflicts before the DB does
1562
+                // As unlikely as it seems, this can happen when the user imports, then deletes, imports and deletes again
1563
+                $newObject = $this->getCalendarObject($calendarId, $newUri, $calendarType);
1564
+                if ($newObject !== null) {
1565
+                    throw new Forbidden("A calendar object with URI $newUri already exists in calendar $calendarId, therefore this object can't be moved into the trashbin");
1566
+                }
1567
+
1568
+                $qb = $this->db->getQueryBuilder();
1569
+                $markObjectDeletedQuery = $qb->update('calendarobjects')
1570
+                    ->set('deleted_at', $qb->createNamedParameter(time(), IQueryBuilder::PARAM_INT))
1571
+                    ->set('uri', $qb->createNamedParameter($newUri))
1572
+                    ->where(
1573
+                        $qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)),
1574
+                        $qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT),
1575
+                        $qb->expr()->eq('uri', $qb->createNamedParameter($objectUri))
1576
+                    );
1577
+                $markObjectDeletedQuery->executeStatement();
1578
+
1579
+                $calendarData = $this->getCalendarById($calendarId);
1580
+                if ($calendarData !== null) {
1581
+                    $this->dispatcher->dispatchTyped(
1582
+                        new CalendarObjectMovedToTrashEvent(
1583
+                            $calendarId,
1584
+                            $calendarData,
1585
+                            $this->getShares($calendarId),
1586
+                            $data
1587
+                        )
1588
+                    );
1589
+                }
1590
+            }
1591
+
1592
+            $this->addChanges($calendarId, [$objectUri], 3, $calendarType);
1593
+        }, $this->db);
1594
+    }
1595
+
1596
+    /**
1597
+     * @param mixed $objectData
1598
+     *
1599
+     * @throws Forbidden
1600
+     */
1601
+    public function restoreCalendarObject(array $objectData): void {
1602
+        $this->cachedObjects = [];
1603
+        $this->atomic(function () use ($objectData): void {
1604
+            $id = (int)$objectData['id'];
1605
+            $restoreUri = str_replace('-deleted.ics', '.ics', $objectData['uri']);
1606
+            $targetObject = $this->getCalendarObject(
1607
+                $objectData['calendarid'],
1608
+                $restoreUri
1609
+            );
1610
+            if ($targetObject !== null) {
1611
+                throw new Forbidden("Can not restore calendar $id because a calendar object with the URI $restoreUri already exists");
1612
+            }
1613
+
1614
+            $qb = $this->db->getQueryBuilder();
1615
+            $update = $qb->update('calendarobjects')
1616
+                ->set('uri', $qb->createNamedParameter($restoreUri))
1617
+                ->set('deleted_at', $qb->createNamedParameter(null))
1618
+                ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT));
1619
+            $update->executeStatement();
1620
+
1621
+            // Make sure this change is tracked in the changes table
1622
+            $qb2 = $this->db->getQueryBuilder();
1623
+            $selectObject = $qb2->select('calendardata', 'uri', 'calendarid', 'calendartype')
1624
+                ->selectAlias('componenttype', 'component')
1625
+                ->from('calendarobjects')
1626
+                ->where($qb2->expr()->eq('id', $qb2->createNamedParameter($id, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT));
1627
+            $result = $selectObject->executeQuery();
1628
+            $row = $result->fetch();
1629
+            $result->closeCursor();
1630
+            if ($row === false) {
1631
+                // Welp, this should possibly not have happened, but let's ignore
1632
+                return;
1633
+            }
1634
+            $this->addChanges($row['calendarid'], [$row['uri']], 1, (int)$row['calendartype']);
1635
+
1636
+            $calendarRow = $this->getCalendarById((int)$row['calendarid']);
1637
+            if ($calendarRow === null) {
1638
+                throw new RuntimeException('Calendar object data that was just written can\'t be read back. Check your database configuration.');
1639
+            }
1640
+            $this->dispatcher->dispatchTyped(
1641
+                new CalendarObjectRestoredEvent(
1642
+                    (int)$objectData['calendarid'],
1643
+                    $calendarRow,
1644
+                    $this->getShares((int)$row['calendarid']),
1645
+                    $row
1646
+                )
1647
+            );
1648
+        }, $this->db);
1649
+    }
1650
+
1651
+    /**
1652
+     * Performs a calendar-query on the contents of this calendar.
1653
+     *
1654
+     * The calendar-query is defined in RFC4791 : CalDAV. Using the
1655
+     * calendar-query it is possible for a client to request a specific set of
1656
+     * object, based on contents of iCalendar properties, date-ranges and
1657
+     * iCalendar component types (VTODO, VEVENT).
1658
+     *
1659
+     * This method should just return a list of (relative) urls that match this
1660
+     * query.
1661
+     *
1662
+     * The list of filters are specified as an array. The exact array is
1663
+     * documented by Sabre\CalDAV\CalendarQueryParser.
1664
+     *
1665
+     * Note that it is extremely likely that getCalendarObject for every path
1666
+     * returned from this method will be called almost immediately after. You
1667
+     * may want to anticipate this to speed up these requests.
1668
+     *
1669
+     * This method provides a default implementation, which parses *all* the
1670
+     * iCalendar objects in the specified calendar.
1671
+     *
1672
+     * This default may well be good enough for personal use, and calendars
1673
+     * that aren't very large. But if you anticipate high usage, big calendars
1674
+     * or high loads, you are strongly advised to optimize certain paths.
1675
+     *
1676
+     * The best way to do so is override this method and to optimize
1677
+     * specifically for 'common filters'.
1678
+     *
1679
+     * Requests that are extremely common are:
1680
+     *   * requests for just VEVENTS
1681
+     *   * requests for just VTODO
1682
+     *   * requests with a time-range-filter on either VEVENT or VTODO.
1683
+     *
1684
+     * ..and combinations of these requests. It may not be worth it to try to
1685
+     * handle every possible situation and just rely on the (relatively
1686
+     * easy to use) CalendarQueryValidator to handle the rest.
1687
+     *
1688
+     * Note that especially time-range-filters may be difficult to parse. A
1689
+     * time-range filter specified on a VEVENT must for instance also handle
1690
+     * recurrence rules correctly.
1691
+     * A good example of how to interpret all these filters can also simply
1692
+     * be found in Sabre\CalDAV\CalendarQueryFilter. This class is as correct
1693
+     * as possible, so it gives you a good idea on what type of stuff you need
1694
+     * to think of.
1695
+     *
1696
+     * @param mixed $calendarId
1697
+     * @param array $filters
1698
+     * @param int $calendarType
1699
+     * @return array
1700
+     */
1701
+    public function calendarQuery($calendarId, array $filters, $calendarType = self::CALENDAR_TYPE_CALENDAR):array {
1702
+        $componentType = null;
1703
+        $requirePostFilter = true;
1704
+        $timeRange = null;
1705
+
1706
+        // if no filters were specified, we don't need to filter after a query
1707
+        if (!$filters['prop-filters'] && !$filters['comp-filters']) {
1708
+            $requirePostFilter = false;
1709
+        }
1710
+
1711
+        // Figuring out if there's a component filter
1712
+        if (count($filters['comp-filters']) > 0 && !$filters['comp-filters'][0]['is-not-defined']) {
1713
+            $componentType = $filters['comp-filters'][0]['name'];
1714
+
1715
+            // Checking if we need post-filters
1716
+            if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['time-range'] && !$filters['comp-filters'][0]['prop-filters']) {
1717
+                $requirePostFilter = false;
1718
+            }
1719
+            // There was a time-range filter
1720
+            if ($componentType === 'VEVENT' && isset($filters['comp-filters'][0]['time-range']) && is_array($filters['comp-filters'][0]['time-range'])) {
1721
+                $timeRange = $filters['comp-filters'][0]['time-range'];
1722
+
1723
+                // If start time OR the end time is not specified, we can do a
1724
+                // 100% accurate mysql query.
1725
+                if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['prop-filters'] && (!$timeRange['start'] || !$timeRange['end'])) {
1726
+                    $requirePostFilter = false;
1727
+                }
1728
+            }
1729
+        }
1730
+        $query = $this->db->getQueryBuilder();
1731
+        $query->select(['id', 'uri', 'uid', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification', 'deleted_at'])
1732
+            ->from('calendarobjects')
1733
+            ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
1734
+            ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)))
1735
+            ->andWhere($query->expr()->isNull('deleted_at'));
1736
+
1737
+        if ($componentType) {
1738
+            $query->andWhere($query->expr()->eq('componenttype', $query->createNamedParameter($componentType)));
1739
+        }
1740
+
1741
+        if ($timeRange && $timeRange['start']) {
1742
+            $query->andWhere($query->expr()->gt('lastoccurence', $query->createNamedParameter($timeRange['start']->getTimeStamp())));
1743
+        }
1744
+        if ($timeRange && $timeRange['end']) {
1745
+            $query->andWhere($query->expr()->lt('firstoccurence', $query->createNamedParameter($timeRange['end']->getTimeStamp())));
1746
+        }
1747
+
1748
+        $stmt = $query->executeQuery();
1749
+
1750
+        $result = [];
1751
+        while ($row = $stmt->fetch()) {
1752
+            // if we leave it as a blob we can't read it both from the post filter and the rowToCalendarObject
1753
+            if (isset($row['calendardata'])) {
1754
+                $row['calendardata'] = $this->readBlob($row['calendardata']);
1755
+            }
1756
+
1757
+            if ($requirePostFilter) {
1758
+                // validateFilterForObject will parse the calendar data
1759
+                // catch parsing errors
1760
+                try {
1761
+                    $matches = $this->validateFilterForObject($row, $filters);
1762
+                } catch (ParseException $ex) {
1763
+                    $this->logger->error('Caught parsing exception for calendar data. This usually indicates invalid calendar data. calendar-id:' . $calendarId . ' uri:' . $row['uri'], [
1764
+                        'app' => 'dav',
1765
+                        'exception' => $ex,
1766
+                    ]);
1767
+                    continue;
1768
+                } catch (InvalidDataException $ex) {
1769
+                    $this->logger->error('Caught invalid data exception for calendar data. This usually indicates invalid calendar data. calendar-id:' . $calendarId . ' uri:' . $row['uri'], [
1770
+                        'app' => 'dav',
1771
+                        'exception' => $ex,
1772
+                    ]);
1773
+                    continue;
1774
+                } catch (MaxInstancesExceededException $ex) {
1775
+                    $this->logger->warning('Caught max instances exceeded exception for calendar data. This usually indicates too much recurring (more than 3500) event in calendar data. Object uri: ' . $row['uri'], [
1776
+                        'app' => 'dav',
1777
+                        'exception' => $ex,
1778
+                    ]);
1779
+                    continue;
1780
+                }
1781
+
1782
+                if (!$matches) {
1783
+                    continue;
1784
+                }
1785
+            }
1786
+            $result[] = $row['uri'];
1787
+            $key = $calendarId . '::' . $row['uri'] . '::' . $calendarType;
1788
+            $this->cachedObjects[$key] = $this->rowToCalendarObject($row);
1789
+        }
1790
+
1791
+        return $result;
1792
+    }
1793
+
1794
+    /**
1795
+     * custom Nextcloud search extension for CalDAV
1796
+     *
1797
+     * TODO - this should optionally cover cached calendar objects as well
1798
+     *
1799
+     * @param string $principalUri
1800
+     * @param array $filters
1801
+     * @param integer|null $limit
1802
+     * @param integer|null $offset
1803
+     * @return array
1804
+     */
1805
+    public function calendarSearch($principalUri, array $filters, $limit = null, $offset = null) {
1806
+        return $this->atomic(function () use ($principalUri, $filters, $limit, $offset) {
1807
+            $calendars = $this->getCalendarsForUser($principalUri);
1808
+            $ownCalendars = [];
1809
+            $sharedCalendars = [];
1810
+
1811
+            $uriMapper = [];
1812
+
1813
+            foreach ($calendars as $calendar) {
1814
+                if ($calendar['{http://owncloud.org/ns}owner-principal'] === $principalUri) {
1815
+                    $ownCalendars[] = $calendar['id'];
1816
+                } else {
1817
+                    $sharedCalendars[] = $calendar['id'];
1818
+                }
1819
+                $uriMapper[$calendar['id']] = $calendar['uri'];
1820
+            }
1821
+            if (count($ownCalendars) === 0 && count($sharedCalendars) === 0) {
1822
+                return [];
1823
+            }
1824
+
1825
+            $query = $this->db->getQueryBuilder();
1826
+            // Calendar id expressions
1827
+            $calendarExpressions = [];
1828
+            foreach ($ownCalendars as $id) {
1829
+                $calendarExpressions[] = $query->expr()->andX(
1830
+                    $query->expr()->eq('c.calendarid',
1831
+                        $query->createNamedParameter($id)),
1832
+                    $query->expr()->eq('c.calendartype',
1833
+                        $query->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
1834
+            }
1835
+            foreach ($sharedCalendars as $id) {
1836
+                $calendarExpressions[] = $query->expr()->andX(
1837
+                    $query->expr()->eq('c.calendarid',
1838
+                        $query->createNamedParameter($id)),
1839
+                    $query->expr()->eq('c.classification',
1840
+                        $query->createNamedParameter(self::CLASSIFICATION_PUBLIC)),
1841
+                    $query->expr()->eq('c.calendartype',
1842
+                        $query->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
1843
+            }
1844
+
1845
+            if (count($calendarExpressions) === 1) {
1846
+                $calExpr = $calendarExpressions[0];
1847
+            } else {
1848
+                $calExpr = call_user_func_array([$query->expr(), 'orX'], $calendarExpressions);
1849
+            }
1850
+
1851
+            // Component expressions
1852
+            $compExpressions = [];
1853
+            foreach ($filters['comps'] as $comp) {
1854
+                $compExpressions[] = $query->expr()
1855
+                    ->eq('c.componenttype', $query->createNamedParameter($comp));
1856
+            }
1857
+
1858
+            if (count($compExpressions) === 1) {
1859
+                $compExpr = $compExpressions[0];
1860
+            } else {
1861
+                $compExpr = call_user_func_array([$query->expr(), 'orX'], $compExpressions);
1862
+            }
1863
+
1864
+            if (!isset($filters['props'])) {
1865
+                $filters['props'] = [];
1866
+            }
1867
+            if (!isset($filters['params'])) {
1868
+                $filters['params'] = [];
1869
+            }
1870
+
1871
+            $propParamExpressions = [];
1872
+            foreach ($filters['props'] as $prop) {
1873
+                $propParamExpressions[] = $query->expr()->andX(
1874
+                    $query->expr()->eq('i.name', $query->createNamedParameter($prop)),
1875
+                    $query->expr()->isNull('i.parameter')
1876
+                );
1877
+            }
1878
+            foreach ($filters['params'] as $param) {
1879
+                $propParamExpressions[] = $query->expr()->andX(
1880
+                    $query->expr()->eq('i.name', $query->createNamedParameter($param['property'])),
1881
+                    $query->expr()->eq('i.parameter', $query->createNamedParameter($param['parameter']))
1882
+                );
1883
+            }
1884
+
1885
+            if (count($propParamExpressions) === 1) {
1886
+                $propParamExpr = $propParamExpressions[0];
1887
+            } else {
1888
+                $propParamExpr = call_user_func_array([$query->expr(), 'orX'], $propParamExpressions);
1889
+            }
1890
+
1891
+            $query->select(['c.calendarid', 'c.uri'])
1892
+                ->from($this->dbObjectPropertiesTable, 'i')
1893
+                ->join('i', 'calendarobjects', 'c', $query->expr()->eq('i.objectid', 'c.id'))
1894
+                ->where($calExpr)
1895
+                ->andWhere($compExpr)
1896
+                ->andWhere($propParamExpr)
1897
+                ->andWhere($query->expr()->iLike('i.value',
1898
+                    $query->createNamedParameter('%' . $this->db->escapeLikeParameter($filters['search-term']) . '%')))
1899
+                ->andWhere($query->expr()->isNull('deleted_at'));
1900
+
1901
+            if ($offset) {
1902
+                $query->setFirstResult($offset);
1903
+            }
1904
+            if ($limit) {
1905
+                $query->setMaxResults($limit);
1906
+            }
1907
+
1908
+            $stmt = $query->executeQuery();
1909
+
1910
+            $result = [];
1911
+            while ($row = $stmt->fetch()) {
1912
+                $path = $uriMapper[$row['calendarid']] . '/' . $row['uri'];
1913
+                if (!in_array($path, $result)) {
1914
+                    $result[] = $path;
1915
+                }
1916
+            }
1917
+
1918
+            return $result;
1919
+        }, $this->db);
1920
+    }
1921
+
1922
+    /**
1923
+     * used for Nextcloud's calendar API
1924
+     *
1925
+     * @param array $calendarInfo
1926
+     * @param string $pattern
1927
+     * @param array $searchProperties
1928
+     * @param array $options
1929
+     * @param integer|null $limit
1930
+     * @param integer|null $offset
1931
+     *
1932
+     * @return array
1933
+     */
1934
+    public function search(
1935
+        array $calendarInfo,
1936
+        $pattern,
1937
+        array $searchProperties,
1938
+        array $options,
1939
+        $limit,
1940
+        $offset,
1941
+    ) {
1942
+        $outerQuery = $this->db->getQueryBuilder();
1943
+        $innerQuery = $this->db->getQueryBuilder();
1944
+
1945
+        if (isset($calendarInfo['source'])) {
1946
+            $calendarType = self::CALENDAR_TYPE_SUBSCRIPTION;
1947
+        } else {
1948
+            $calendarType = self::CALENDAR_TYPE_CALENDAR;
1949
+        }
1950
+
1951
+        $innerQuery->selectDistinct('op.objectid')
1952
+            ->from($this->dbObjectPropertiesTable, 'op')
1953
+            ->andWhere($innerQuery->expr()->eq('op.calendarid',
1954
+                $outerQuery->createNamedParameter($calendarInfo['id'])))
1955
+            ->andWhere($innerQuery->expr()->eq('op.calendartype',
1956
+                $outerQuery->createNamedParameter($calendarType)));
1957
+
1958
+        $outerQuery->select('c.id', 'c.calendardata', 'c.componenttype', 'c.uid', 'c.uri')
1959
+            ->from('calendarobjects', 'c')
1960
+            ->where($outerQuery->expr()->isNull('deleted_at'));
1961
+
1962
+        // only return public items for shared calendars for now
1963
+        if (isset($calendarInfo['{http://owncloud.org/ns}owner-principal']) === false || $calendarInfo['principaluri'] !== $calendarInfo['{http://owncloud.org/ns}owner-principal']) {
1964
+            $outerQuery->andWhere($outerQuery->expr()->eq('c.classification',
1965
+                $outerQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
1966
+        }
1967
+
1968
+        if (!empty($searchProperties)) {
1969
+            $or = [];
1970
+            foreach ($searchProperties as $searchProperty) {
1971
+                $or[] = $innerQuery->expr()->eq('op.name',
1972
+                    $outerQuery->createNamedParameter($searchProperty));
1973
+            }
1974
+            $innerQuery->andWhere($innerQuery->expr()->orX(...$or));
1975
+        }
1976
+
1977
+        if ($pattern !== '') {
1978
+            $innerQuery->andWhere($innerQuery->expr()->iLike('op.value',
1979
+                $outerQuery->createNamedParameter('%' .
1980
+                    $this->db->escapeLikeParameter($pattern) . '%')));
1981
+        }
1982
+
1983
+        $start = null;
1984
+        $end = null;
1985
+
1986
+        $hasLimit = is_int($limit);
1987
+        $hasTimeRange = false;
1988
+
1989
+        if (isset($options['timerange']['start']) && $options['timerange']['start'] instanceof DateTimeInterface) {
1990
+            /** @var DateTimeInterface $start */
1991
+            $start = $options['timerange']['start'];
1992
+            $outerQuery->andWhere(
1993
+                $outerQuery->expr()->gt(
1994
+                    'lastoccurence',
1995
+                    $outerQuery->createNamedParameter($start->getTimestamp())
1996
+                )
1997
+            );
1998
+            $hasTimeRange = true;
1999
+        }
2000
+
2001
+        if (isset($options['timerange']['end']) && $options['timerange']['end'] instanceof DateTimeInterface) {
2002
+            /** @var DateTimeInterface $end */
2003
+            $end = $options['timerange']['end'];
2004
+            $outerQuery->andWhere(
2005
+                $outerQuery->expr()->lt(
2006
+                    'firstoccurence',
2007
+                    $outerQuery->createNamedParameter($end->getTimestamp())
2008
+                )
2009
+            );
2010
+            $hasTimeRange = true;
2011
+        }
2012
+
2013
+        if (isset($options['uid'])) {
2014
+            $outerQuery->andWhere($outerQuery->expr()->eq('uid', $outerQuery->createNamedParameter($options['uid'])));
2015
+        }
2016
+
2017
+        if (!empty($options['types'])) {
2018
+            $or = [];
2019
+            foreach ($options['types'] as $type) {
2020
+                $or[] = $outerQuery->expr()->eq('componenttype',
2021
+                    $outerQuery->createNamedParameter($type));
2022
+            }
2023
+            $outerQuery->andWhere($outerQuery->expr()->orX(...$or));
2024
+        }
2025
+
2026
+        $outerQuery->andWhere($outerQuery->expr()->in('c.id', $outerQuery->createFunction($innerQuery->getSQL())));
2027
+
2028
+        // Without explicit order by its undefined in which order the SQL server returns the events.
2029
+        // For the pagination with hasLimit and hasTimeRange, a stable ordering is helpful.
2030
+        $outerQuery->addOrderBy('id');
2031
+
2032
+        $offset = (int)$offset;
2033
+        $outerQuery->setFirstResult($offset);
2034
+
2035
+        $calendarObjects = [];
2036
+
2037
+        if ($hasLimit && $hasTimeRange) {
2038
+            /**
2039
+             * Event recurrences are evaluated at runtime because the database only knows the first and last occurrence.
2040
+             *
2041
+             * Given, a user created 8 events with a yearly reoccurrence and two for events tomorrow.
2042
+             * The upcoming event widget asks the CalDAV backend for 7 events within the next 14 days.
2043
+             *
2044
+             * If limit 7 is applied to the SQL query, we find the 7 events with a yearly reoccurrence
2045
+             * and discard the events after evaluating the reoccurrence rules because they are not due within
2046
+             * the next 14 days and end up with an empty result even if there are two events to show.
2047
+             *
2048
+             * The workaround for search requests with a limit and time range is asking for more row than requested
2049
+             * and retrying if we have not reached the limit.
2050
+             *
2051
+             * 25 rows and 3 retries is entirely arbitrary.
2052
+             */
2053
+            $maxResults = (int)max($limit, 25);
2054
+            $outerQuery->setMaxResults($maxResults);
2055
+
2056
+            for ($attempt = $objectsCount = 0; $attempt < 3 && $objectsCount < $limit; $attempt++) {
2057
+                $objectsCount = array_push($calendarObjects, ...$this->searchCalendarObjects($outerQuery, $start, $end));
2058
+                $outerQuery->setFirstResult($offset += $maxResults);
2059
+            }
2060
+
2061
+            $calendarObjects = array_slice($calendarObjects, 0, $limit, false);
2062
+        } else {
2063
+            $outerQuery->setMaxResults($limit);
2064
+            $calendarObjects = $this->searchCalendarObjects($outerQuery, $start, $end);
2065
+        }
2066
+
2067
+        $calendarObjects = array_map(function ($o) use ($options) {
2068
+            $calendarData = Reader::read($o['calendardata']);
2069
+
2070
+            // Expand recurrences if an explicit time range is requested
2071
+            if ($calendarData instanceof VCalendar
2072
+                && isset($options['timerange']['start'], $options['timerange']['end'])) {
2073
+                $calendarData = $calendarData->expand(
2074
+                    $options['timerange']['start'],
2075
+                    $options['timerange']['end'],
2076
+                );
2077
+            }
2078
+
2079
+            $comps = $calendarData->getComponents();
2080
+            $objects = [];
2081
+            $timezones = [];
2082
+            foreach ($comps as $comp) {
2083
+                if ($comp instanceof VTimeZone) {
2084
+                    $timezones[] = $comp;
2085
+                } else {
2086
+                    $objects[] = $comp;
2087
+                }
2088
+            }
2089
+
2090
+            return [
2091
+                'id' => $o['id'],
2092
+                'type' => $o['componenttype'],
2093
+                'uid' => $o['uid'],
2094
+                'uri' => $o['uri'],
2095
+                'objects' => array_map(function ($c) {
2096
+                    return $this->transformSearchData($c);
2097
+                }, $objects),
2098
+                'timezones' => array_map(function ($c) {
2099
+                    return $this->transformSearchData($c);
2100
+                }, $timezones),
2101
+            ];
2102
+        }, $calendarObjects);
2103
+
2104
+        usort($calendarObjects, function (array $a, array $b) {
2105
+            /** @var DateTimeImmutable $startA */
2106
+            $startA = $a['objects'][0]['DTSTART'][0] ?? new DateTimeImmutable(self::MAX_DATE);
2107
+            /** @var DateTimeImmutable $startB */
2108
+            $startB = $b['objects'][0]['DTSTART'][0] ?? new DateTimeImmutable(self::MAX_DATE);
2109
+
2110
+            return $startA->getTimestamp() <=> $startB->getTimestamp();
2111
+        });
2112
+
2113
+        return $calendarObjects;
2114
+    }
2115
+
2116
+    private function searchCalendarObjects(IQueryBuilder $query, ?DateTimeInterface $start, ?DateTimeInterface $end): array {
2117
+        $calendarObjects = [];
2118
+        $filterByTimeRange = ($start instanceof DateTimeInterface) || ($end instanceof DateTimeInterface);
2119
+
2120
+        $result = $query->executeQuery();
2121
+
2122
+        while (($row = $result->fetch()) !== false) {
2123
+            if ($filterByTimeRange === false) {
2124
+                // No filter required
2125
+                $calendarObjects[] = $row;
2126
+                continue;
2127
+            }
2128
+
2129
+            try {
2130
+                $isValid = $this->validateFilterForObject($row, [
2131
+                    'name' => 'VCALENDAR',
2132
+                    'comp-filters' => [
2133
+                        [
2134
+                            'name' => 'VEVENT',
2135
+                            'comp-filters' => [],
2136
+                            'prop-filters' => [],
2137
+                            'is-not-defined' => false,
2138
+                            'time-range' => [
2139
+                                'start' => $start,
2140
+                                'end' => $end,
2141
+                            ],
2142
+                        ],
2143
+                    ],
2144
+                    'prop-filters' => [],
2145
+                    'is-not-defined' => false,
2146
+                    'time-range' => null,
2147
+                ]);
2148
+            } catch (MaxInstancesExceededException $ex) {
2149
+                $this->logger->warning('Caught max instances exceeded exception for calendar data. This usually indicates too much recurring (more than 3500) event in calendar data. Object uri: ' . $row['uri'], [
2150
+                    'app' => 'dav',
2151
+                    'exception' => $ex,
2152
+                ]);
2153
+                continue;
2154
+            }
2155
+
2156
+            if (is_resource($row['calendardata'])) {
2157
+                // Put the stream back to the beginning so it can be read another time
2158
+                rewind($row['calendardata']);
2159
+            }
2160
+
2161
+            if ($isValid) {
2162
+                $calendarObjects[] = $row;
2163
+            }
2164
+        }
2165
+
2166
+        $result->closeCursor();
2167
+
2168
+        return $calendarObjects;
2169
+    }
2170
+
2171
+    /**
2172
+     * @param Component $comp
2173
+     * @return array
2174
+     */
2175
+    private function transformSearchData(Component $comp) {
2176
+        $data = [];
2177
+        /** @var Component[] $subComponents */
2178
+        $subComponents = $comp->getComponents();
2179
+        /** @var Property[] $properties */
2180
+        $properties = array_filter($comp->children(), function ($c) {
2181
+            return $c instanceof Property;
2182
+        });
2183
+        $validationRules = $comp->getValidationRules();
2184
+
2185
+        foreach ($subComponents as $subComponent) {
2186
+            $name = $subComponent->name;
2187
+            if (!isset($data[$name])) {
2188
+                $data[$name] = [];
2189
+            }
2190
+            $data[$name][] = $this->transformSearchData($subComponent);
2191
+        }
2192
+
2193
+        foreach ($properties as $property) {
2194
+            $name = $property->name;
2195
+            if (!isset($validationRules[$name])) {
2196
+                $validationRules[$name] = '*';
2197
+            }
2198
+
2199
+            $rule = $validationRules[$property->name];
2200
+            if ($rule === '+' || $rule === '*') { // multiple
2201
+                if (!isset($data[$name])) {
2202
+                    $data[$name] = [];
2203
+                }
2204
+
2205
+                $data[$name][] = $this->transformSearchProperty($property);
2206
+            } else { // once
2207
+                $data[$name] = $this->transformSearchProperty($property);
2208
+            }
2209
+        }
2210
+
2211
+        return $data;
2212
+    }
2213
+
2214
+    /**
2215
+     * @param Property $prop
2216
+     * @return array
2217
+     */
2218
+    private function transformSearchProperty(Property $prop) {
2219
+        // No need to check Date, as it extends DateTime
2220
+        if ($prop instanceof Property\ICalendar\DateTime) {
2221
+            $value = $prop->getDateTime();
2222
+        } else {
2223
+            $value = $prop->getValue();
2224
+        }
2225
+
2226
+        return [
2227
+            $value,
2228
+            $prop->parameters()
2229
+        ];
2230
+    }
2231
+
2232
+    /**
2233
+     * @param string $principalUri
2234
+     * @param string $pattern
2235
+     * @param array $componentTypes
2236
+     * @param array $searchProperties
2237
+     * @param array $searchParameters
2238
+     * @param array $options
2239
+     * @return array
2240
+     */
2241
+    public function searchPrincipalUri(string $principalUri,
2242
+        string $pattern,
2243
+        array $componentTypes,
2244
+        array $searchProperties,
2245
+        array $searchParameters,
2246
+        array $options = [],
2247
+    ): array {
2248
+        return $this->atomic(function () use ($principalUri, $pattern, $componentTypes, $searchProperties, $searchParameters, $options) {
2249
+            $escapePattern = !\array_key_exists('escape_like_param', $options) || $options['escape_like_param'] !== false;
2250
+
2251
+            $calendarObjectIdQuery = $this->db->getQueryBuilder();
2252
+            $calendarOr = [];
2253
+            $searchOr = [];
2254
+
2255
+            // Fetch calendars and subscription
2256
+            $calendars = $this->getCalendarsForUser($principalUri);
2257
+            $subscriptions = $this->getSubscriptionsForUser($principalUri);
2258
+            foreach ($calendars as $calendar) {
2259
+                $calendarAnd = $calendarObjectIdQuery->expr()->andX(
2260
+                    $calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int)$calendar['id'])),
2261
+                    $calendarObjectIdQuery->expr()->eq('cob.calendartype', $calendarObjectIdQuery->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)),
2262
+                );
2263
+
2264
+                // If it's shared, limit search to public events
2265
+                if (isset($calendar['{http://owncloud.org/ns}owner-principal'])
2266
+                    && $calendar['principaluri'] !== $calendar['{http://owncloud.org/ns}owner-principal']) {
2267
+                    $calendarAnd->add($calendarObjectIdQuery->expr()->eq('co.classification', $calendarObjectIdQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
2268
+                }
2269
+
2270
+                $calendarOr[] = $calendarAnd;
2271
+            }
2272
+            foreach ($subscriptions as $subscription) {
2273
+                $subscriptionAnd = $calendarObjectIdQuery->expr()->andX(
2274
+                    $calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int)$subscription['id'])),
2275
+                    $calendarObjectIdQuery->expr()->eq('cob.calendartype', $calendarObjectIdQuery->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)),
2276
+                );
2277
+
2278
+                // If it's shared, limit search to public events
2279
+                if (isset($subscription['{http://owncloud.org/ns}owner-principal'])
2280
+                    && $subscription['principaluri'] !== $subscription['{http://owncloud.org/ns}owner-principal']) {
2281
+                    $subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('co.classification', $calendarObjectIdQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
2282
+                }
2283
+
2284
+                $calendarOr[] = $subscriptionAnd;
2285
+            }
2286
+
2287
+            foreach ($searchProperties as $property) {
2288
+                $propertyAnd = $calendarObjectIdQuery->expr()->andX(
2289
+                    $calendarObjectIdQuery->expr()->eq('cob.name', $calendarObjectIdQuery->createNamedParameter($property, IQueryBuilder::PARAM_STR)),
2290
+                    $calendarObjectIdQuery->expr()->isNull('cob.parameter'),
2291
+                );
2292
+
2293
+                $searchOr[] = $propertyAnd;
2294
+            }
2295
+            foreach ($searchParameters as $property => $parameter) {
2296
+                $parameterAnd = $calendarObjectIdQuery->expr()->andX(
2297
+                    $calendarObjectIdQuery->expr()->eq('cob.name', $calendarObjectIdQuery->createNamedParameter($property, IQueryBuilder::PARAM_STR)),
2298
+                    $calendarObjectIdQuery->expr()->eq('cob.parameter', $calendarObjectIdQuery->createNamedParameter($parameter, IQueryBuilder::PARAM_STR_ARRAY)),
2299
+                );
2300
+
2301
+                $searchOr[] = $parameterAnd;
2302
+            }
2303
+
2304
+            if (empty($calendarOr)) {
2305
+                return [];
2306
+            }
2307
+            if (empty($searchOr)) {
2308
+                return [];
2309
+            }
2310
+
2311
+            $calendarObjectIdQuery->selectDistinct('cob.objectid')
2312
+                ->from($this->dbObjectPropertiesTable, 'cob')
2313
+                ->leftJoin('cob', 'calendarobjects', 'co', $calendarObjectIdQuery->expr()->eq('co.id', 'cob.objectid'))
2314
+                ->andWhere($calendarObjectIdQuery->expr()->in('co.componenttype', $calendarObjectIdQuery->createNamedParameter($componentTypes, IQueryBuilder::PARAM_STR_ARRAY)))
2315
+                ->andWhere($calendarObjectIdQuery->expr()->orX(...$calendarOr))
2316
+                ->andWhere($calendarObjectIdQuery->expr()->orX(...$searchOr))
2317
+                ->andWhere($calendarObjectIdQuery->expr()->isNull('deleted_at'));
2318
+
2319
+            if ($pattern !== '') {
2320
+                if (!$escapePattern) {
2321
+                    $calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->ilike('cob.value', $calendarObjectIdQuery->createNamedParameter($pattern)));
2322
+                } else {
2323
+                    $calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->ilike('cob.value', $calendarObjectIdQuery->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%')));
2324
+                }
2325
+            }
2326
+
2327
+            if (isset($options['limit'])) {
2328
+                $calendarObjectIdQuery->setMaxResults($options['limit']);
2329
+            }
2330
+            if (isset($options['offset'])) {
2331
+                $calendarObjectIdQuery->setFirstResult($options['offset']);
2332
+            }
2333
+            if (isset($options['timerange'])) {
2334
+                if (isset($options['timerange']['start']) && $options['timerange']['start'] instanceof DateTimeInterface) {
2335
+                    $calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->gt(
2336
+                        'lastoccurence',
2337
+                        $calendarObjectIdQuery->createNamedParameter($options['timerange']['start']->getTimeStamp()),
2338
+                    ));
2339
+                }
2340
+                if (isset($options['timerange']['end']) && $options['timerange']['end'] instanceof DateTimeInterface) {
2341
+                    $calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->lt(
2342
+                        'firstoccurence',
2343
+                        $calendarObjectIdQuery->createNamedParameter($options['timerange']['end']->getTimeStamp()),
2344
+                    ));
2345
+                }
2346
+            }
2347
+
2348
+            $result = $calendarObjectIdQuery->executeQuery();
2349
+            $matches = [];
2350
+            while (($row = $result->fetch()) !== false) {
2351
+                $matches[] = (int)$row['objectid'];
2352
+            }
2353
+            $result->closeCursor();
2354
+
2355
+            $query = $this->db->getQueryBuilder();
2356
+            $query->select('calendardata', 'uri', 'calendarid', 'calendartype')
2357
+                ->from('calendarobjects')
2358
+                ->where($query->expr()->in('id', $query->createNamedParameter($matches, IQueryBuilder::PARAM_INT_ARRAY)));
2359
+
2360
+            $result = $query->executeQuery();
2361
+            $calendarObjects = [];
2362
+            while (($array = $result->fetch()) !== false) {
2363
+                $array['calendarid'] = (int)$array['calendarid'];
2364
+                $array['calendartype'] = (int)$array['calendartype'];
2365
+                $array['calendardata'] = $this->readBlob($array['calendardata']);
2366
+
2367
+                $calendarObjects[] = $array;
2368
+            }
2369
+            $result->closeCursor();
2370
+            return $calendarObjects;
2371
+        }, $this->db);
2372
+    }
2373
+
2374
+    /**
2375
+     * Searches through all of a users calendars and calendar objects to find
2376
+     * an object with a specific UID.
2377
+     *
2378
+     * This method should return the path to this object, relative to the
2379
+     * calendar home, so this path usually only contains two parts:
2380
+     *
2381
+     * calendarpath/objectpath.ics
2382
+     *
2383
+     * If the uid is not found, return null.
2384
+     *
2385
+     * This method should only consider * objects that the principal owns, so
2386
+     * any calendars owned by other principals that also appear in this
2387
+     * collection should be ignored.
2388
+     *
2389
+     * @param string $principalUri
2390
+     * @param string $uid
2391
+     * @return string|null
2392
+     */
2393
+    public function getCalendarObjectByUID($principalUri, $uid) {
2394
+        $query = $this->db->getQueryBuilder();
2395
+        $query->selectAlias('c.uri', 'calendaruri')->selectAlias('co.uri', 'objecturi')
2396
+            ->from('calendarobjects', 'co')
2397
+            ->leftJoin('co', 'calendars', 'c', $query->expr()->eq('co.calendarid', 'c.id'))
2398
+            ->where($query->expr()->eq('c.principaluri', $query->createNamedParameter($principalUri)))
2399
+            ->andWhere($query->expr()->eq('co.uid', $query->createNamedParameter($uid)))
2400
+            ->andWhere($query->expr()->isNull('co.deleted_at'));
2401
+        $stmt = $query->executeQuery();
2402
+        $row = $stmt->fetch();
2403
+        $stmt->closeCursor();
2404
+        if ($row) {
2405
+            return $row['calendaruri'] . '/' . $row['objecturi'];
2406
+        }
2407
+
2408
+        return null;
2409
+    }
2410
+
2411
+    public function getCalendarObjectById(string $principalUri, int $id): ?array {
2412
+        $query = $this->db->getQueryBuilder();
2413
+        $query->select(['co.id', 'co.uri', 'co.lastmodified', 'co.etag', 'co.calendarid', 'co.size', 'co.calendardata', 'co.componenttype', 'co.classification', 'co.deleted_at'])
2414
+            ->selectAlias('c.uri', 'calendaruri')
2415
+            ->from('calendarobjects', 'co')
2416
+            ->join('co', 'calendars', 'c', $query->expr()->eq('c.id', 'co.calendarid', IQueryBuilder::PARAM_INT))
2417
+            ->where($query->expr()->eq('c.principaluri', $query->createNamedParameter($principalUri)))
2418
+            ->andWhere($query->expr()->eq('co.id', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT));
2419
+        $stmt = $query->executeQuery();
2420
+        $row = $stmt->fetch();
2421
+        $stmt->closeCursor();
2422
+
2423
+        if (!$row) {
2424
+            return null;
2425
+        }
2426
+
2427
+        return [
2428
+            'id' => $row['id'],
2429
+            'uri' => $row['uri'],
2430
+            'lastmodified' => $row['lastmodified'],
2431
+            'etag' => '"' . $row['etag'] . '"',
2432
+            'calendarid' => $row['calendarid'],
2433
+            'calendaruri' => $row['calendaruri'],
2434
+            'size' => (int)$row['size'],
2435
+            'calendardata' => $this->readBlob($row['calendardata']),
2436
+            'component' => strtolower($row['componenttype']),
2437
+            'classification' => (int)$row['classification'],
2438
+            'deleted_at' => isset($row['deleted_at']) ? ((int)$row['deleted_at']) : null,
2439
+        ];
2440
+    }
2441
+
2442
+    /**
2443
+     * The getChanges method returns all the changes that have happened, since
2444
+     * the specified syncToken in the specified calendar.
2445
+     *
2446
+     * This function should return an array, such as the following:
2447
+     *
2448
+     * [
2449
+     *   'syncToken' => 'The current synctoken',
2450
+     *   'added'   => [
2451
+     *      'new.txt',
2452
+     *   ],
2453
+     *   'modified'   => [
2454
+     *      'modified.txt',
2455
+     *   ],
2456
+     *   'deleted' => [
2457
+     *      'foo.php.bak',
2458
+     *      'old.txt'
2459
+     *   ]
2460
+     * );
2461
+     *
2462
+     * The returned syncToken property should reflect the *current* syncToken
2463
+     * of the calendar, as reported in the {http://sabredav.org/ns}sync-token
2464
+     * property This is * needed here too, to ensure the operation is atomic.
2465
+     *
2466
+     * If the $syncToken argument is specified as null, this is an initial
2467
+     * sync, and all members should be reported.
2468
+     *
2469
+     * The modified property is an array of nodenames that have changed since
2470
+     * the last token.
2471
+     *
2472
+     * The deleted property is an array with nodenames, that have been deleted
2473
+     * from collection.
2474
+     *
2475
+     * The $syncLevel argument is basically the 'depth' of the report. If it's
2476
+     * 1, you only have to report changes that happened only directly in
2477
+     * immediate descendants. If it's 2, it should also include changes from
2478
+     * the nodes below the child collections. (grandchildren)
2479
+     *
2480
+     * The $limit argument allows a client to specify how many results should
2481
+     * be returned at most. If the limit is not specified, it should be treated
2482
+     * as infinite.
2483
+     *
2484
+     * If the limit (infinite or not) is higher than you're willing to return,
2485
+     * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
2486
+     *
2487
+     * If the syncToken is expired (due to data cleanup) or unknown, you must
2488
+     * return null.
2489
+     *
2490
+     * The limit is 'suggestive'. You are free to ignore it.
2491
+     *
2492
+     * @param string $calendarId
2493
+     * @param string $syncToken
2494
+     * @param int $syncLevel
2495
+     * @param int|null $limit
2496
+     * @param int $calendarType
2497
+     * @return ?array
2498
+     */
2499
+    public function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit = null, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
2500
+        $table = $calendarType === self::CALENDAR_TYPE_CALENDAR ? 'calendars': 'calendarsubscriptions';
2501
+
2502
+        return $this->atomic(function () use ($calendarId, $syncToken, $syncLevel, $limit, $calendarType, $table) {
2503
+            // Current synctoken
2504
+            $qb = $this->db->getQueryBuilder();
2505
+            $qb->select('synctoken')
2506
+                ->from($table)
2507
+                ->where(
2508
+                    $qb->expr()->eq('id', $qb->createNamedParameter($calendarId))
2509
+                );
2510
+            $stmt = $qb->executeQuery();
2511
+            $currentToken = $stmt->fetchOne();
2512
+            $initialSync = !is_numeric($syncToken);
2513
+
2514
+            if ($currentToken === false) {
2515
+                return null;
2516
+            }
2517
+
2518
+            // evaluate if this is a initial sync and construct appropriate command
2519
+            if ($initialSync) {
2520
+                $qb = $this->db->getQueryBuilder();
2521
+                $qb->select('uri')
2522
+                    ->from('calendarobjects')
2523
+                    ->where($qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)))
2524
+                    ->andWhere($qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType)))
2525
+                    ->andWhere($qb->expr()->isNull('deleted_at'));
2526
+            } else {
2527
+                $qb = $this->db->getQueryBuilder();
2528
+                $qb->select('uri', $qb->func()->max('operation'))
2529
+                    ->from('calendarchanges')
2530
+                    ->where($qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)))
2531
+                    ->andWhere($qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType)))
2532
+                    ->andWhere($qb->expr()->gte('synctoken', $qb->createNamedParameter($syncToken)))
2533
+                    ->andWhere($qb->expr()->lt('synctoken', $qb->createNamedParameter($currentToken)))
2534
+                    ->groupBy('uri');
2535
+            }
2536
+            // evaluate if limit exists
2537
+            if (is_numeric($limit)) {
2538
+                $qb->setMaxResults($limit);
2539
+            }
2540
+            // execute command
2541
+            $stmt = $qb->executeQuery();
2542
+            // build results
2543
+            $result = ['syncToken' => $currentToken, 'added' => [], 'modified' => [], 'deleted' => []];
2544
+            // retrieve results
2545
+            if ($initialSync) {
2546
+                $result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
2547
+            } else {
2548
+                // \PDO::FETCH_NUM is needed due to the inconsistent field names
2549
+                // produced by doctrine for MAX() with different databases
2550
+                while ($entry = $stmt->fetch(\PDO::FETCH_NUM)) {
2551
+                    // assign uri (column 0) to appropriate mutation based on operation (column 1)
2552
+                    // forced (int) is needed as doctrine with OCI returns the operation field as string not integer
2553
+                    match ((int)$entry[1]) {
2554
+                        1 => $result['added'][] = $entry[0],
2555
+                        2 => $result['modified'][] = $entry[0],
2556
+                        3 => $result['deleted'][] = $entry[0],
2557
+                        default => $this->logger->debug('Unknown calendar change operation detected')
2558
+                    };
2559
+                }
2560
+            }
2561
+            $stmt->closeCursor();
2562
+
2563
+            return $result;
2564
+        }, $this->db);
2565
+    }
2566
+
2567
+    /**
2568
+     * Returns a list of subscriptions for a principal.
2569
+     *
2570
+     * Every subscription is an array with the following keys:
2571
+     *  * id, a unique id that will be used by other functions to modify the
2572
+     *    subscription. This can be the same as the uri or a database key.
2573
+     *  * uri. This is just the 'base uri' or 'filename' of the subscription.
2574
+     *  * principaluri. The owner of the subscription. Almost always the same as
2575
+     *    principalUri passed to this method.
2576
+     *
2577
+     * Furthermore, all the subscription info must be returned too:
2578
+     *
2579
+     * 1. {DAV:}displayname
2580
+     * 2. {http://apple.com/ns/ical/}refreshrate
2581
+     * 3. {http://calendarserver.org/ns/}subscribed-strip-todos (omit if todos
2582
+     *    should not be stripped).
2583
+     * 4. {http://calendarserver.org/ns/}subscribed-strip-alarms (omit if alarms
2584
+     *    should not be stripped).
2585
+     * 5. {http://calendarserver.org/ns/}subscribed-strip-attachments (omit if
2586
+     *    attachments should not be stripped).
2587
+     * 6. {http://calendarserver.org/ns/}source (Must be a
2588
+     *     Sabre\DAV\Property\Href).
2589
+     * 7. {http://apple.com/ns/ical/}calendar-color
2590
+     * 8. {http://apple.com/ns/ical/}calendar-order
2591
+     * 9. {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
2592
+     *    (should just be an instance of
2593
+     *    Sabre\CalDAV\Property\SupportedCalendarComponentSet, with a bunch of
2594
+     *    default components).
2595
+     *
2596
+     * @param string $principalUri
2597
+     * @return array
2598
+     */
2599
+    public function getSubscriptionsForUser($principalUri) {
2600
+        $fields = array_column($this->subscriptionPropertyMap, 0);
2601
+        $fields[] = 'id';
2602
+        $fields[] = 'uri';
2603
+        $fields[] = 'source';
2604
+        $fields[] = 'principaluri';
2605
+        $fields[] = 'lastmodified';
2606
+        $fields[] = 'synctoken';
2607
+
2608
+        $query = $this->db->getQueryBuilder();
2609
+        $query->select($fields)
2610
+            ->from('calendarsubscriptions')
2611
+            ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
2612
+            ->orderBy('calendarorder', 'asc');
2613
+        $stmt = $query->executeQuery();
2614
+
2615
+        $subscriptions = [];
2616
+        while ($row = $stmt->fetch()) {
2617
+            $subscription = [
2618
+                'id' => $row['id'],
2619
+                'uri' => $row['uri'],
2620
+                'principaluri' => $row['principaluri'],
2621
+                'source' => $row['source'],
2622
+                'lastmodified' => $row['lastmodified'],
2623
+
2624
+                '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
2625
+                '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
2626
+            ];
2627
+
2628
+            $subscriptions[] = $this->rowToSubscription($row, $subscription);
2629
+        }
2630
+
2631
+        return $subscriptions;
2632
+    }
2633
+
2634
+    /**
2635
+     * Creates a new subscription for a principal.
2636
+     *
2637
+     * If the creation was a success, an id must be returned that can be used to reference
2638
+     * this subscription in other methods, such as updateSubscription.
2639
+     *
2640
+     * @param string $principalUri
2641
+     * @param string $uri
2642
+     * @param array $properties
2643
+     * @return mixed
2644
+     */
2645
+    public function createSubscription($principalUri, $uri, array $properties) {
2646
+        if (!isset($properties['{http://calendarserver.org/ns/}source'])) {
2647
+            throw new Forbidden('The {http://calendarserver.org/ns/}source property is required when creating subscriptions');
2648
+        }
2649
+
2650
+        $values = [
2651
+            'principaluri' => $principalUri,
2652
+            'uri' => $uri,
2653
+            'source' => $properties['{http://calendarserver.org/ns/}source']->getHref(),
2654
+            'lastmodified' => time(),
2655
+        ];
2656
+
2657
+        $propertiesBoolean = ['striptodos', 'stripalarms', 'stripattachments'];
2658
+
2659
+        foreach ($this->subscriptionPropertyMap as $xmlName => [$dbName, $type]) {
2660
+            if (array_key_exists($xmlName, $properties)) {
2661
+                $values[$dbName] = $properties[$xmlName];
2662
+                if (in_array($dbName, $propertiesBoolean)) {
2663
+                    $values[$dbName] = true;
2664
+                }
2665
+            }
2666
+        }
2667
+
2668
+        [$subscriptionId, $subscriptionRow] = $this->atomic(function () use ($values) {
2669
+            $valuesToInsert = [];
2670
+            $query = $this->db->getQueryBuilder();
2671
+            foreach (array_keys($values) as $name) {
2672
+                $valuesToInsert[$name] = $query->createNamedParameter($values[$name]);
2673
+            }
2674
+            $query->insert('calendarsubscriptions')
2675
+                ->values($valuesToInsert)
2676
+                ->executeStatement();
2677
+
2678
+            $subscriptionId = $query->getLastInsertId();
2679
+
2680
+            $subscriptionRow = $this->getSubscriptionById($subscriptionId);
2681
+            return [$subscriptionId, $subscriptionRow];
2682
+        }, $this->db);
2683
+
2684
+        $this->dispatcher->dispatchTyped(new SubscriptionCreatedEvent($subscriptionId, $subscriptionRow));
2685
+
2686
+        return $subscriptionId;
2687
+    }
2688
+
2689
+    /**
2690
+     * Updates a subscription
2691
+     *
2692
+     * The list of mutations is stored in a Sabre\DAV\PropPatch object.
2693
+     * To do the actual updates, you must tell this object which properties
2694
+     * you're going to process with the handle() method.
2695
+     *
2696
+     * Calling the handle method is like telling the PropPatch object "I
2697
+     * promise I can handle updating this property".
2698
+     *
2699
+     * Read the PropPatch documentation for more info and examples.
2700
+     *
2701
+     * @param mixed $subscriptionId
2702
+     * @param PropPatch $propPatch
2703
+     * @return void
2704
+     */
2705
+    public function updateSubscription($subscriptionId, PropPatch $propPatch) {
2706
+        $supportedProperties = array_keys($this->subscriptionPropertyMap);
2707
+        $supportedProperties[] = '{http://calendarserver.org/ns/}source';
2708
+
2709
+        $propPatch->handle($supportedProperties, function ($mutations) use ($subscriptionId) {
2710
+            $newValues = [];
2711
+
2712
+            foreach ($mutations as $propertyName => $propertyValue) {
2713
+                if ($propertyName === '{http://calendarserver.org/ns/}source') {
2714
+                    $newValues['source'] = $propertyValue->getHref();
2715
+                } else {
2716
+                    $fieldName = $this->subscriptionPropertyMap[$propertyName][0];
2717
+                    $newValues[$fieldName] = $propertyValue;
2718
+                }
2719
+            }
2720
+
2721
+            $subscriptionRow = $this->atomic(function () use ($subscriptionId, $newValues) {
2722
+                $query = $this->db->getQueryBuilder();
2723
+                $query->update('calendarsubscriptions')
2724
+                    ->set('lastmodified', $query->createNamedParameter(time()));
2725
+                foreach ($newValues as $fieldName => $value) {
2726
+                    $query->set($fieldName, $query->createNamedParameter($value));
2727
+                }
2728
+                $query->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
2729
+                    ->executeStatement();
2730
+
2731
+                return $this->getSubscriptionById($subscriptionId);
2732
+            }, $this->db);
2733
+
2734
+            $this->dispatcher->dispatchTyped(new SubscriptionUpdatedEvent((int)$subscriptionId, $subscriptionRow, [], $mutations));
2735
+
2736
+            return true;
2737
+        });
2738
+    }
2739
+
2740
+    /**
2741
+     * Deletes a subscription.
2742
+     *
2743
+     * @param mixed $subscriptionId
2744
+     * @return void
2745
+     */
2746
+    public function deleteSubscription($subscriptionId) {
2747
+        $this->atomic(function () use ($subscriptionId): void {
2748
+            $subscriptionRow = $this->getSubscriptionById($subscriptionId);
2749
+
2750
+            $query = $this->db->getQueryBuilder();
2751
+            $query->delete('calendarsubscriptions')
2752
+                ->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
2753
+                ->executeStatement();
2754
+
2755
+            $query = $this->db->getQueryBuilder();
2756
+            $query->delete('calendarobjects')
2757
+                ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2758
+                ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2759
+                ->executeStatement();
2760
+
2761
+            $query->delete('calendarchanges')
2762
+                ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2763
+                ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2764
+                ->executeStatement();
2765
+
2766
+            $query->delete($this->dbObjectPropertiesTable)
2767
+                ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2768
+                ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2769
+                ->executeStatement();
2770
+
2771
+            if ($subscriptionRow) {
2772
+                $this->dispatcher->dispatchTyped(new SubscriptionDeletedEvent((int)$subscriptionId, $subscriptionRow, []));
2773
+            }
2774
+        }, $this->db);
2775
+    }
2776
+
2777
+    /**
2778
+     * Returns a single scheduling object for the inbox collection.
2779
+     *
2780
+     * The returned array should contain the following elements:
2781
+     *   * uri - A unique basename for the object. This will be used to
2782
+     *           construct a full uri.
2783
+     *   * calendardata - The iCalendar object
2784
+     *   * lastmodified - The last modification date. Can be an int for a unix
2785
+     *                    timestamp, or a PHP DateTime object.
2786
+     *   * etag - A unique token that must change if the object changed.
2787
+     *   * size - The size of the object, in bytes.
2788
+     *
2789
+     * @param string $principalUri
2790
+     * @param string $objectUri
2791
+     * @return array
2792
+     */
2793
+    public function getSchedulingObject($principalUri, $objectUri) {
2794
+        $query = $this->db->getQueryBuilder();
2795
+        $stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size'])
2796
+            ->from('schedulingobjects')
2797
+            ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
2798
+            ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
2799
+            ->executeQuery();
2800
+
2801
+        $row = $stmt->fetch();
2802
+
2803
+        if (!$row) {
2804
+            return null;
2805
+        }
2806
+
2807
+        return [
2808
+            'uri' => $row['uri'],
2809
+            'calendardata' => $row['calendardata'],
2810
+            'lastmodified' => $row['lastmodified'],
2811
+            'etag' => '"' . $row['etag'] . '"',
2812
+            'size' => (int)$row['size'],
2813
+        ];
2814
+    }
2815
+
2816
+    /**
2817
+     * Returns all scheduling objects for the inbox collection.
2818
+     *
2819
+     * These objects should be returned as an array. Every item in the array
2820
+     * should follow the same structure as returned from getSchedulingObject.
2821
+     *
2822
+     * The main difference is that 'calendardata' is optional.
2823
+     *
2824
+     * @param string $principalUri
2825
+     * @return array
2826
+     */
2827
+    public function getSchedulingObjects($principalUri) {
2828
+        $query = $this->db->getQueryBuilder();
2829
+        $stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size'])
2830
+            ->from('schedulingobjects')
2831
+            ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
2832
+            ->executeQuery();
2833
+
2834
+        $results = [];
2835
+        while (($row = $stmt->fetch()) !== false) {
2836
+            $results[] = [
2837
+                'calendardata' => $row['calendardata'],
2838
+                'uri' => $row['uri'],
2839
+                'lastmodified' => $row['lastmodified'],
2840
+                'etag' => '"' . $row['etag'] . '"',
2841
+                'size' => (int)$row['size'],
2842
+            ];
2843
+        }
2844
+        $stmt->closeCursor();
2845
+
2846
+        return $results;
2847
+    }
2848
+
2849
+    /**
2850
+     * Deletes a scheduling object from the inbox collection.
2851
+     *
2852
+     * @param string $principalUri
2853
+     * @param string $objectUri
2854
+     * @return void
2855
+     */
2856
+    public function deleteSchedulingObject($principalUri, $objectUri) {
2857
+        $this->cachedObjects = [];
2858
+        $query = $this->db->getQueryBuilder();
2859
+        $query->delete('schedulingobjects')
2860
+            ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
2861
+            ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
2862
+            ->executeStatement();
2863
+    }
2864
+
2865
+    /**
2866
+     * Deletes all scheduling objects last modified before $modifiedBefore from the inbox collection.
2867
+     *
2868
+     * @param int $modifiedBefore
2869
+     * @param int $limit
2870
+     * @return void
2871
+     */
2872
+    public function deleteOutdatedSchedulingObjects(int $modifiedBefore, int $limit): void {
2873
+        $query = $this->db->getQueryBuilder();
2874
+        $query->select('id')
2875
+            ->from('schedulingobjects')
2876
+            ->where($query->expr()->lt('lastmodified', $query->createNamedParameter($modifiedBefore)))
2877
+            ->setMaxResults($limit);
2878
+        $result = $query->executeQuery();
2879
+        $count = $result->rowCount();
2880
+        if ($count === 0) {
2881
+            return;
2882
+        }
2883
+        $ids = array_map(static function (array $id) {
2884
+            return (int)$id[0];
2885
+        }, $result->fetchAll(\PDO::FETCH_NUM));
2886
+        $result->closeCursor();
2887
+
2888
+        $numDeleted = 0;
2889
+        $deleteQuery = $this->db->getQueryBuilder();
2890
+        $deleteQuery->delete('schedulingobjects')
2891
+            ->where($deleteQuery->expr()->in('id', $deleteQuery->createParameter('ids'), IQueryBuilder::PARAM_INT_ARRAY));
2892
+        foreach (array_chunk($ids, 1000) as $chunk) {
2893
+            $deleteQuery->setParameter('ids', $chunk, IQueryBuilder::PARAM_INT_ARRAY);
2894
+            $numDeleted += $deleteQuery->executeStatement();
2895
+        }
2896
+
2897
+        if ($numDeleted === $limit) {
2898
+            $this->logger->info("Deleted $limit scheduling objects, continuing with next batch");
2899
+            $this->deleteOutdatedSchedulingObjects($modifiedBefore, $limit);
2900
+        }
2901
+    }
2902
+
2903
+    /**
2904
+     * Creates a new scheduling object. This should land in a users' inbox.
2905
+     *
2906
+     * @param string $principalUri
2907
+     * @param string $objectUri
2908
+     * @param string $objectData
2909
+     * @return void
2910
+     */
2911
+    public function createSchedulingObject($principalUri, $objectUri, $objectData) {
2912
+        $this->cachedObjects = [];
2913
+        $query = $this->db->getQueryBuilder();
2914
+        $query->insert('schedulingobjects')
2915
+            ->values([
2916
+                'principaluri' => $query->createNamedParameter($principalUri),
2917
+                'calendardata' => $query->createNamedParameter($objectData, IQueryBuilder::PARAM_LOB),
2918
+                'uri' => $query->createNamedParameter($objectUri),
2919
+                'lastmodified' => $query->createNamedParameter(time()),
2920
+                'etag' => $query->createNamedParameter(md5($objectData)),
2921
+                'size' => $query->createNamedParameter(strlen($objectData))
2922
+            ])
2923
+            ->executeStatement();
2924
+    }
2925
+
2926
+    /**
2927
+     * Adds a change record to the calendarchanges table.
2928
+     *
2929
+     * @param mixed $calendarId
2930
+     * @param string[] $objectUris
2931
+     * @param int $operation 1 = add, 2 = modify, 3 = delete.
2932
+     * @param int $calendarType
2933
+     * @return void
2934
+     */
2935
+    protected function addChanges(int $calendarId, array $objectUris, int $operation, int $calendarType = self::CALENDAR_TYPE_CALENDAR): void {
2936
+        $this->cachedObjects = [];
2937
+        $table = $calendarType === self::CALENDAR_TYPE_CALENDAR ? 'calendars': 'calendarsubscriptions';
2938
+
2939
+        $this->atomic(function () use ($calendarId, $objectUris, $operation, $calendarType, $table): void {
2940
+            $query = $this->db->getQueryBuilder();
2941
+            $query->select('synctoken')
2942
+                ->from($table)
2943
+                ->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)));
2944
+            $result = $query->executeQuery();
2945
+            $syncToken = (int)$result->fetchOne();
2946
+            $result->closeCursor();
2947
+
2948
+            $query = $this->db->getQueryBuilder();
2949
+            $query->insert('calendarchanges')
2950
+                ->values([
2951
+                    'uri' => $query->createParameter('uri'),
2952
+                    'synctoken' => $query->createNamedParameter($syncToken),
2953
+                    'calendarid' => $query->createNamedParameter($calendarId),
2954
+                    'operation' => $query->createNamedParameter($operation),
2955
+                    'calendartype' => $query->createNamedParameter($calendarType),
2956
+                    'created_at' => time(),
2957
+                ]);
2958
+            foreach ($objectUris as $uri) {
2959
+                $query->setParameter('uri', $uri);
2960
+                $query->executeStatement();
2961
+            }
2962
+
2963
+            $query = $this->db->getQueryBuilder();
2964
+            $query->update($table)
2965
+                ->set('synctoken', $query->createNamedParameter($syncToken + 1, IQueryBuilder::PARAM_INT))
2966
+                ->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)))
2967
+                ->executeStatement();
2968
+        }, $this->db);
2969
+    }
2970
+
2971
+    public function restoreChanges(int $calendarId, int $calendarType = self::CALENDAR_TYPE_CALENDAR): void {
2972
+        $this->cachedObjects = [];
2973
+
2974
+        $this->atomic(function () use ($calendarId, $calendarType): void {
2975
+            $qbAdded = $this->db->getQueryBuilder();
2976
+            $qbAdded->select('uri')
2977
+                ->from('calendarobjects')
2978
+                ->where(
2979
+                    $qbAdded->expr()->andX(
2980
+                        $qbAdded->expr()->eq('calendarid', $qbAdded->createNamedParameter($calendarId)),
2981
+                        $qbAdded->expr()->eq('calendartype', $qbAdded->createNamedParameter($calendarType)),
2982
+                        $qbAdded->expr()->isNull('deleted_at'),
2983
+                    )
2984
+                );
2985
+            $resultAdded = $qbAdded->executeQuery();
2986
+            $addedUris = $resultAdded->fetchAll(\PDO::FETCH_COLUMN);
2987
+            $resultAdded->closeCursor();
2988
+            // Track everything as changed
2989
+            // Tracking the creation is not necessary because \OCA\DAV\CalDAV\CalDavBackend::getChangesForCalendar
2990
+            // only returns the last change per object.
2991
+            $this->addChanges($calendarId, $addedUris, 2, $calendarType);
2992
+
2993
+            $qbDeleted = $this->db->getQueryBuilder();
2994
+            $qbDeleted->select('uri')
2995
+                ->from('calendarobjects')
2996
+                ->where(
2997
+                    $qbDeleted->expr()->andX(
2998
+                        $qbDeleted->expr()->eq('calendarid', $qbDeleted->createNamedParameter($calendarId)),
2999
+                        $qbDeleted->expr()->eq('calendartype', $qbDeleted->createNamedParameter($calendarType)),
3000
+                        $qbDeleted->expr()->isNotNull('deleted_at'),
3001
+                    )
3002
+                );
3003
+            $resultDeleted = $qbDeleted->executeQuery();
3004
+            $deletedUris = array_map(function (string $uri) {
3005
+                return str_replace('-deleted.ics', '.ics', $uri);
3006
+            }, $resultDeleted->fetchAll(\PDO::FETCH_COLUMN));
3007
+            $resultDeleted->closeCursor();
3008
+            $this->addChanges($calendarId, $deletedUris, 3, $calendarType);
3009
+        }, $this->db);
3010
+    }
3011
+
3012
+    /**
3013
+     * Parses some information from calendar objects, used for optimized
3014
+     * calendar-queries.
3015
+     *
3016
+     * Returns an array with the following keys:
3017
+     *   * etag - An md5 checksum of the object without the quotes.
3018
+     *   * size - Size of the object in bytes
3019
+     *   * componentType - VEVENT, VTODO or VJOURNAL
3020
+     *   * firstOccurence
3021
+     *   * lastOccurence
3022
+     *   * uid - value of the UID property
3023
+     *
3024
+     * @param string $calendarData
3025
+     * @return array
3026
+     */
3027
+    public function getDenormalizedData(string $calendarData): array {
3028
+        $vObject = Reader::read($calendarData);
3029
+        $vEvents = [];
3030
+        $componentType = null;
3031
+        $component = null;
3032
+        $firstOccurrence = null;
3033
+        $lastOccurrence = null;
3034
+        $uid = null;
3035
+        $classification = self::CLASSIFICATION_PUBLIC;
3036
+        $hasDTSTART = false;
3037
+        foreach ($vObject->getComponents() as $component) {
3038
+            if ($component->name !== 'VTIMEZONE') {
3039
+                // Finding all VEVENTs, and track them
3040
+                if ($component->name === 'VEVENT') {
3041
+                    $vEvents[] = $component;
3042
+                    if ($component->DTSTART) {
3043
+                        $hasDTSTART = true;
3044
+                    }
3045
+                }
3046
+                // Track first component type and uid
3047
+                if ($uid === null) {
3048
+                    $componentType = $component->name;
3049
+                    $uid = (string)$component->UID;
3050
+                }
3051
+            }
3052
+        }
3053
+        if (!$componentType) {
3054
+            throw new BadRequest('Calendar objects must have a VJOURNAL, VEVENT or VTODO component');
3055
+        }
3056
+
3057
+        if ($hasDTSTART) {
3058
+            $component = $vEvents[0];
3059
+
3060
+            // Finding the last occurrence is a bit harder
3061
+            if (!isset($component->RRULE) && count($vEvents) === 1) {
3062
+                $firstOccurrence = $component->DTSTART->getDateTime()->getTimeStamp();
3063
+                if (isset($component->DTEND)) {
3064
+                    $lastOccurrence = $component->DTEND->getDateTime()->getTimeStamp();
3065
+                } elseif (isset($component->DURATION)) {
3066
+                    $endDate = clone $component->DTSTART->getDateTime();
3067
+                    $endDate->add(DateTimeParser::parse($component->DURATION->getValue()));
3068
+                    $lastOccurrence = $endDate->getTimeStamp();
3069
+                } elseif (!$component->DTSTART->hasTime()) {
3070
+                    $endDate = clone $component->DTSTART->getDateTime();
3071
+                    $endDate->modify('+1 day');
3072
+                    $lastOccurrence = $endDate->getTimeStamp();
3073
+                } else {
3074
+                    $lastOccurrence = $firstOccurrence;
3075
+                }
3076
+            } else {
3077
+                try {
3078
+                    $it = new EventIterator($vEvents);
3079
+                } catch (NoInstancesException $e) {
3080
+                    $this->logger->debug('Caught no instance exception for calendar data. This usually indicates invalid calendar data.', [
3081
+                        'app' => 'dav',
3082
+                        'exception' => $e,
3083
+                    ]);
3084
+                    throw new Forbidden($e->getMessage());
3085
+                }
3086
+                $maxDate = new DateTime(self::MAX_DATE);
3087
+                $firstOccurrence = $it->getDtStart()->getTimestamp();
3088
+                if ($it->isInfinite()) {
3089
+                    $lastOccurrence = $maxDate->getTimestamp();
3090
+                } else {
3091
+                    $end = $it->getDtEnd();
3092
+                    while ($it->valid() && $end < $maxDate) {
3093
+                        $end = $it->getDtEnd();
3094
+                        $it->next();
3095
+                    }
3096
+                    $lastOccurrence = $end->getTimestamp();
3097
+                }
3098
+            }
3099
+        }
3100
+
3101
+        if ($component->CLASS) {
3102
+            $classification = CalDavBackend::CLASSIFICATION_PRIVATE;
3103
+            switch ($component->CLASS->getValue()) {
3104
+                case 'PUBLIC':
3105
+                    $classification = CalDavBackend::CLASSIFICATION_PUBLIC;
3106
+                    break;
3107
+                case 'CONFIDENTIAL':
3108
+                    $classification = CalDavBackend::CLASSIFICATION_CONFIDENTIAL;
3109
+                    break;
3110
+            }
3111
+        }
3112
+        return [
3113
+            'etag' => md5($calendarData),
3114
+            'size' => strlen($calendarData),
3115
+            'componentType' => $componentType,
3116
+            'firstOccurence' => is_null($firstOccurrence) ? null : max(0, $firstOccurrence),
3117
+            'lastOccurence' => is_null($lastOccurrence) ? null : max(0, $lastOccurrence),
3118
+            'uid' => $uid,
3119
+            'classification' => $classification
3120
+        ];
3121
+    }
3122
+
3123
+    /**
3124
+     * @param $cardData
3125
+     * @return bool|string
3126
+     */
3127
+    private function readBlob($cardData) {
3128
+        if (is_resource($cardData)) {
3129
+            return stream_get_contents($cardData);
3130
+        }
3131
+
3132
+        return $cardData;
3133
+    }
3134
+
3135
+    /**
3136
+     * @param list<array{href: string, commonName: string, readOnly: bool}> $add
3137
+     * @param list<string> $remove
3138
+     */
3139
+    public function updateShares(IShareable $shareable, array $add, array $remove): void {
3140
+        $this->atomic(function () use ($shareable, $add, $remove): void {
3141
+            $calendarId = $shareable->getResourceId();
3142
+            $calendarRow = $this->getCalendarById($calendarId);
3143
+            if ($calendarRow === null) {
3144
+                throw new \RuntimeException('Trying to update shares for non-existing calendar: ' . $calendarId);
3145
+            }
3146
+            $oldShares = $this->getShares($calendarId);
3147
+
3148
+            $this->calendarSharingBackend->updateShares($shareable, $add, $remove, $oldShares);
3149
+
3150
+            $this->dispatcher->dispatchTyped(new CalendarShareUpdatedEvent($calendarId, $calendarRow, $oldShares, $add, $remove));
3151
+        }, $this->db);
3152
+    }
3153
+
3154
+    /**
3155
+     * @return list<array{href: string, commonName: string, status: int, readOnly: bool, '{http://owncloud.org/ns}principal': string, '{http://owncloud.org/ns}group-share': bool}>
3156
+     */
3157
+    public function getShares(int $resourceId): array {
3158
+        return $this->calendarSharingBackend->getShares($resourceId);
3159
+    }
3160
+
3161
+    public function preloadShares(array $resourceIds): void {
3162
+        $this->calendarSharingBackend->preloadShares($resourceIds);
3163
+    }
3164
+
3165
+    /**
3166
+     * @param boolean $value
3167
+     * @param Calendar $calendar
3168
+     * @return string|null
3169
+     */
3170
+    public function setPublishStatus($value, $calendar) {
3171
+        return $this->atomic(function () use ($value, $calendar) {
3172
+            $calendarId = $calendar->getResourceId();
3173
+            $calendarData = $this->getCalendarById($calendarId);
3174
+
3175
+            $query = $this->db->getQueryBuilder();
3176
+            if ($value) {
3177
+                $publicUri = $this->random->generate(16, ISecureRandom::CHAR_HUMAN_READABLE);
3178
+                $query->insert('dav_shares')
3179
+                    ->values([
3180
+                        'principaluri' => $query->createNamedParameter($calendar->getPrincipalURI()),
3181
+                        'type' => $query->createNamedParameter('calendar'),
3182
+                        'access' => $query->createNamedParameter(self::ACCESS_PUBLIC),
3183
+                        'resourceid' => $query->createNamedParameter($calendar->getResourceId()),
3184
+                        'publicuri' => $query->createNamedParameter($publicUri)
3185
+                    ]);
3186
+                $query->executeStatement();
3187
+
3188
+                $this->dispatcher->dispatchTyped(new CalendarPublishedEvent($calendarId, $calendarData, $publicUri));
3189
+                return $publicUri;
3190
+            }
3191
+            $query->delete('dav_shares')
3192
+                ->where($query->expr()->eq('resourceid', $query->createNamedParameter($calendar->getResourceId())))
3193
+                ->andWhere($query->expr()->eq('access', $query->createNamedParameter(self::ACCESS_PUBLIC)));
3194
+            $query->executeStatement();
3195
+
3196
+            $this->dispatcher->dispatchTyped(new CalendarUnpublishedEvent($calendarId, $calendarData));
3197
+            return null;
3198
+        }, $this->db);
3199
+    }
3200
+
3201
+    /**
3202
+     * @param Calendar $calendar
3203
+     * @return mixed
3204
+     */
3205
+    public function getPublishStatus($calendar) {
3206
+        $query = $this->db->getQueryBuilder();
3207
+        $result = $query->select('publicuri')
3208
+            ->from('dav_shares')
3209
+            ->where($query->expr()->eq('resourceid', $query->createNamedParameter($calendar->getResourceId())))
3210
+            ->andWhere($query->expr()->eq('access', $query->createNamedParameter(self::ACCESS_PUBLIC)))
3211
+            ->executeQuery();
3212
+
3213
+        $row = $result->fetch();
3214
+        $result->closeCursor();
3215
+        return $row ? reset($row) : false;
3216
+    }
3217
+
3218
+    /**
3219
+     * @param int $resourceId
3220
+     * @param list<array{privilege: string, principal: string, protected: bool}> $acl
3221
+     * @return list<array{privilege: string, principal: string, protected: bool}>
3222
+     */
3223
+    public function applyShareAcl(int $resourceId, array $acl): array {
3224
+        $shares = $this->calendarSharingBackend->getShares($resourceId);
3225
+        return $this->calendarSharingBackend->applyShareAcl($shares, $acl);
3226
+    }
3227
+
3228
+    /**
3229
+     * update properties table
3230
+     *
3231
+     * @param int $calendarId
3232
+     * @param string $objectUri
3233
+     * @param string $calendarData
3234
+     * @param int $calendarType
3235
+     */
3236
+    public function updateProperties($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
3237
+        $this->cachedObjects = [];
3238
+        $this->atomic(function () use ($calendarId, $objectUri, $calendarData, $calendarType): void {
3239
+            $objectId = $this->getCalendarObjectId($calendarId, $objectUri, $calendarType);
3240
+
3241
+            try {
3242
+                $vCalendar = $this->readCalendarData($calendarData);
3243
+            } catch (\Exception $ex) {
3244
+                return;
3245
+            }
3246
+
3247
+            $this->purgeProperties($calendarId, $objectId);
3248
+
3249
+            $query = $this->db->getQueryBuilder();
3250
+            $query->insert($this->dbObjectPropertiesTable)
3251
+                ->values(
3252
+                    [
3253
+                        'calendarid' => $query->createNamedParameter($calendarId),
3254
+                        'calendartype' => $query->createNamedParameter($calendarType),
3255
+                        'objectid' => $query->createNamedParameter($objectId),
3256
+                        'name' => $query->createParameter('name'),
3257
+                        'parameter' => $query->createParameter('parameter'),
3258
+                        'value' => $query->createParameter('value'),
3259
+                    ]
3260
+                );
3261
+
3262
+            $indexComponents = ['VEVENT', 'VJOURNAL', 'VTODO'];
3263
+            foreach ($vCalendar->getComponents() as $component) {
3264
+                if (!in_array($component->name, $indexComponents)) {
3265
+                    continue;
3266
+                }
3267
+
3268
+                foreach ($component->children() as $property) {
3269
+                    if (in_array($property->name, self::INDEXED_PROPERTIES, true)) {
3270
+                        $value = $property->getValue();
3271
+                        // is this a shitty db?
3272
+                        if (!$this->db->supports4ByteText()) {
3273
+                            $value = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $value);
3274
+                        }
3275
+                        $value = mb_strcut($value, 0, 254);
3276
+
3277
+                        $query->setParameter('name', $property->name);
3278
+                        $query->setParameter('parameter', null);
3279
+                        $query->setParameter('value', mb_strcut($value, 0, 254));
3280
+                        $query->executeStatement();
3281
+                    }
3282
+
3283
+                    if (array_key_exists($property->name, self::$indexParameters)) {
3284
+                        $parameters = $property->parameters();
3285
+                        $indexedParametersForProperty = self::$indexParameters[$property->name];
3286
+
3287
+                        foreach ($parameters as $key => $value) {
3288
+                            if (in_array($key, $indexedParametersForProperty)) {
3289
+                                // is this a shitty db?
3290
+                                if ($this->db->supports4ByteText()) {
3291
+                                    $value = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $value);
3292
+                                }
3293
+
3294
+                                $query->setParameter('name', $property->name);
3295
+                                $query->setParameter('parameter', mb_strcut($key, 0, 254));
3296
+                                $query->setParameter('value', mb_strcut($value, 0, 254));
3297
+                                $query->executeStatement();
3298
+                            }
3299
+                        }
3300
+                    }
3301
+                }
3302
+            }
3303
+        }, $this->db);
3304
+    }
3305
+
3306
+    /**
3307
+     * deletes all birthday calendars
3308
+     */
3309
+    public function deleteAllBirthdayCalendars() {
3310
+        $this->atomic(function (): void {
3311
+            $query = $this->db->getQueryBuilder();
3312
+            $result = $query->select(['id'])->from('calendars')
3313
+                ->where($query->expr()->eq('uri', $query->createNamedParameter(BirthdayService::BIRTHDAY_CALENDAR_URI)))
3314
+                ->executeQuery();
3315
+
3316
+            while (($row = $result->fetch()) !== false) {
3317
+                $this->deleteCalendar(
3318
+                    $row['id'],
3319
+                    true // No data to keep in the trashbin, if the user re-enables then we regenerate
3320
+                );
3321
+            }
3322
+            $result->closeCursor();
3323
+        }, $this->db);
3324
+    }
3325
+
3326
+    /**
3327
+     * @param $subscriptionId
3328
+     */
3329
+    public function purgeAllCachedEventsForSubscription($subscriptionId) {
3330
+        $this->atomic(function () use ($subscriptionId): void {
3331
+            $query = $this->db->getQueryBuilder();
3332
+            $query->select('uri')
3333
+                ->from('calendarobjects')
3334
+                ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
3335
+                ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)));
3336
+            $stmt = $query->executeQuery();
3337
+
3338
+            $uris = [];
3339
+            while (($row = $stmt->fetch()) !== false) {
3340
+                $uris[] = $row['uri'];
3341
+            }
3342
+            $stmt->closeCursor();
3343
+
3344
+            $query = $this->db->getQueryBuilder();
3345
+            $query->delete('calendarobjects')
3346
+                ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
3347
+                ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
3348
+                ->executeStatement();
3349
+
3350
+            $query = $this->db->getQueryBuilder();
3351
+            $query->delete('calendarchanges')
3352
+                ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
3353
+                ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
3354
+                ->executeStatement();
3355
+
3356
+            $query = $this->db->getQueryBuilder();
3357
+            $query->delete($this->dbObjectPropertiesTable)
3358
+                ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
3359
+                ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
3360
+                ->executeStatement();
3361
+
3362
+            $this->addChanges($subscriptionId, $uris, 3, self::CALENDAR_TYPE_SUBSCRIPTION);
3363
+        }, $this->db);
3364
+    }
3365
+
3366
+    /**
3367
+     * @param int $subscriptionId
3368
+     * @param array<int> $calendarObjectIds
3369
+     * @param array<string> $calendarObjectUris
3370
+     */
3371
+    public function purgeCachedEventsForSubscription(int $subscriptionId, array $calendarObjectIds, array $calendarObjectUris): void {
3372
+        if (empty($calendarObjectUris)) {
3373
+            return;
3374
+        }
3375
+
3376
+        $this->atomic(function () use ($subscriptionId, $calendarObjectIds, $calendarObjectUris): void {
3377
+            foreach (array_chunk($calendarObjectIds, 1000) as $chunk) {
3378
+                $query = $this->db->getQueryBuilder();
3379
+                $query->delete($this->dbObjectPropertiesTable)
3380
+                    ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
3381
+                    ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
3382
+                    ->andWhere($query->expr()->in('id', $query->createNamedParameter($chunk, IQueryBuilder::PARAM_INT_ARRAY), IQueryBuilder::PARAM_INT_ARRAY))
3383
+                    ->executeStatement();
3384
+
3385
+                $query = $this->db->getQueryBuilder();
3386
+                $query->delete('calendarobjects')
3387
+                    ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
3388
+                    ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
3389
+                    ->andWhere($query->expr()->in('id', $query->createNamedParameter($chunk, IQueryBuilder::PARAM_INT_ARRAY), IQueryBuilder::PARAM_INT_ARRAY))
3390
+                    ->executeStatement();
3391
+            }
3392
+
3393
+            foreach (array_chunk($calendarObjectUris, 1000) as $chunk) {
3394
+                $query = $this->db->getQueryBuilder();
3395
+                $query->delete('calendarchanges')
3396
+                    ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
3397
+                    ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
3398
+                    ->andWhere($query->expr()->in('uri', $query->createNamedParameter($chunk, IQueryBuilder::PARAM_STR_ARRAY), IQueryBuilder::PARAM_STR_ARRAY))
3399
+                    ->executeStatement();
3400
+            }
3401
+            $this->addChanges($subscriptionId, $calendarObjectUris, 3, self::CALENDAR_TYPE_SUBSCRIPTION);
3402
+        }, $this->db);
3403
+    }
3404
+
3405
+    /**
3406
+     * Move a calendar from one user to another
3407
+     *
3408
+     * @param string $uriName
3409
+     * @param string $uriOrigin
3410
+     * @param string $uriDestination
3411
+     * @param string $newUriName (optional) the new uriName
3412
+     */
3413
+    public function moveCalendar($uriName, $uriOrigin, $uriDestination, $newUriName = null) {
3414
+        $query = $this->db->getQueryBuilder();
3415
+        $query->update('calendars')
3416
+            ->set('principaluri', $query->createNamedParameter($uriDestination))
3417
+            ->set('uri', $query->createNamedParameter($newUriName ?: $uriName))
3418
+            ->where($query->expr()->eq('principaluri', $query->createNamedParameter($uriOrigin)))
3419
+            ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($uriName)))
3420
+            ->executeStatement();
3421
+    }
3422
+
3423
+    /**
3424
+     * read VCalendar data into a VCalendar object
3425
+     *
3426
+     * @param string $objectData
3427
+     * @return VCalendar
3428
+     */
3429
+    protected function readCalendarData($objectData) {
3430
+        return Reader::read($objectData);
3431
+    }
3432
+
3433
+    /**
3434
+     * delete all properties from a given calendar object
3435
+     *
3436
+     * @param int $calendarId
3437
+     * @param int $objectId
3438
+     */
3439
+    protected function purgeProperties($calendarId, $objectId) {
3440
+        $this->cachedObjects = [];
3441
+        $query = $this->db->getQueryBuilder();
3442
+        $query->delete($this->dbObjectPropertiesTable)
3443
+            ->where($query->expr()->eq('objectid', $query->createNamedParameter($objectId)))
3444
+            ->andWhere($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)));
3445
+        $query->executeStatement();
3446
+    }
3447
+
3448
+    /**
3449
+     * get ID from a given calendar object
3450
+     *
3451
+     * @param int $calendarId
3452
+     * @param string $uri
3453
+     * @param int $calendarType
3454
+     * @return int
3455
+     */
3456
+    protected function getCalendarObjectId($calendarId, $uri, $calendarType):int {
3457
+        $query = $this->db->getQueryBuilder();
3458
+        $query->select('id')
3459
+            ->from('calendarobjects')
3460
+            ->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
3461
+            ->andWhere($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
3462
+            ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)));
3463
+
3464
+        $result = $query->executeQuery();
3465
+        $objectIds = $result->fetch();
3466
+        $result->closeCursor();
3467
+
3468
+        if (!isset($objectIds['id'])) {
3469
+            throw new \InvalidArgumentException('Calendarobject does not exists: ' . $uri);
3470
+        }
3471
+
3472
+        return (int)$objectIds['id'];
3473
+    }
3474
+
3475
+    /**
3476
+     * @throws \InvalidArgumentException
3477
+     */
3478
+    public function pruneOutdatedSyncTokens(int $keep, int $retention): int {
3479
+        if ($keep < 0) {
3480
+            throw new \InvalidArgumentException();
3481
+        }
3482
+
3483
+        $query = $this->db->getQueryBuilder();
3484
+        $query->select($query->func()->max('id'))
3485
+            ->from('calendarchanges');
3486
+
3487
+        $result = $query->executeQuery();
3488
+        $maxId = (int)$result->fetchOne();
3489
+        $result->closeCursor();
3490
+        if (!$maxId || $maxId < $keep) {
3491
+            return 0;
3492
+        }
3493
+
3494
+        $query = $this->db->getQueryBuilder();
3495
+        $query->delete('calendarchanges')
3496
+            ->where(
3497
+                $query->expr()->lte('id', $query->createNamedParameter($maxId - $keep, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT),
3498
+                $query->expr()->lte('created_at', $query->createNamedParameter($retention)),
3499
+            );
3500
+        return $query->executeStatement();
3501
+    }
3502
+
3503
+    /**
3504
+     * return legacy endpoint principal name to new principal name
3505
+     *
3506
+     * @param $principalUri
3507
+     * @param $toV2
3508
+     * @return string
3509
+     */
3510
+    private function convertPrincipal($principalUri, $toV2) {
3511
+        if ($this->principalBackend->getPrincipalPrefix() === 'principals') {
3512
+            [, $name] = Uri\split($principalUri);
3513
+            if ($toV2 === true) {
3514
+                return "principals/users/$name";
3515
+            }
3516
+            return "principals/$name";
3517
+        }
3518
+        return $principalUri;
3519
+    }
3520
+
3521
+    /**
3522
+     * adds information about an owner to the calendar data
3523
+     *
3524
+     */
3525
+    private function addOwnerPrincipalToCalendar(array $calendarInfo): array {
3526
+        $ownerPrincipalKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal';
3527
+        $displaynameKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}owner-displayname';
3528
+        if (isset($calendarInfo[$ownerPrincipalKey])) {
3529
+            $uri = $calendarInfo[$ownerPrincipalKey];
3530
+        } else {
3531
+            $uri = $calendarInfo['principaluri'];
3532
+        }
3533
+
3534
+        $principalInformation = $this->principalBackend->getPrincipalByPath($uri);
3535
+        if (isset($principalInformation['{DAV:}displayname'])) {
3536
+            $calendarInfo[$displaynameKey] = $principalInformation['{DAV:}displayname'];
3537
+        }
3538
+        return $calendarInfo;
3539
+    }
3540
+
3541
+    private function addResourceTypeToCalendar(array $row, array $calendar): array {
3542
+        if (isset($row['deleted_at'])) {
3543
+            // Columns is set and not null -> this is a deleted calendar
3544
+            // we send a custom resourcetype to hide the deleted calendar
3545
+            // from ordinary DAV clients, but the Calendar app will know
3546
+            // how to handle this special resource.
3547
+            $calendar['{DAV:}resourcetype'] = new DAV\Xml\Property\ResourceType([
3548
+                '{DAV:}collection',
3549
+                sprintf('{%s}deleted-calendar', \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD),
3550
+            ]);
3551
+        }
3552
+        return $calendar;
3553
+    }
3554
+
3555
+    /**
3556
+     * Amend the calendar info with database row data
3557
+     *
3558
+     * @param array $row
3559
+     * @param array $calendar
3560
+     *
3561
+     * @return array
3562
+     */
3563
+    private function rowToCalendar($row, array $calendar): array {
3564
+        foreach ($this->propertyMap as $xmlName => [$dbName, $type]) {
3565
+            $value = $row[$dbName];
3566
+            if ($value !== null) {
3567
+                settype($value, $type);
3568
+            }
3569
+            $calendar[$xmlName] = $value;
3570
+        }
3571
+        return $calendar;
3572
+    }
3573
+
3574
+    /**
3575
+     * Amend the subscription info with database row data
3576
+     *
3577
+     * @param array $row
3578
+     * @param array $subscription
3579
+     *
3580
+     * @return array
3581
+     */
3582
+    private function rowToSubscription($row, array $subscription): array {
3583
+        foreach ($this->subscriptionPropertyMap as $xmlName => [$dbName, $type]) {
3584
+            $value = $row[$dbName];
3585
+            if ($value !== null) {
3586
+                settype($value, $type);
3587
+            }
3588
+            $subscription[$xmlName] = $value;
3589
+        }
3590
+        return $subscription;
3591
+    }
3592
+
3593
+    /**
3594
+     * delete all invitations from a given calendar
3595
+     *
3596
+     * @since 31.0.0
3597
+     *
3598
+     * @param int $calendarId
3599
+     *
3600
+     * @return void
3601
+     */
3602
+    protected function purgeCalendarInvitations(int $calendarId): void {
3603
+        // select all calendar object uid's
3604
+        $cmd = $this->db->getQueryBuilder();
3605
+        $cmd->select('uid')
3606
+            ->from($this->dbObjectsTable)
3607
+            ->where($cmd->expr()->eq('calendarid', $cmd->createNamedParameter($calendarId)));
3608
+        $allIds = $cmd->executeQuery()->fetchAll(\PDO::FETCH_COLUMN);
3609
+        // delete all links that match object uid's
3610
+        $cmd = $this->db->getQueryBuilder();
3611
+        $cmd->delete($this->dbObjectInvitationsTable)
3612
+            ->where($cmd->expr()->in('uid', $cmd->createParameter('uids'), IQueryBuilder::PARAM_STR_ARRAY));
3613
+        foreach (array_chunk($allIds, 1000) as $chunkIds) {
3614
+            $cmd->setParameter('uids', $chunkIds, IQueryBuilder::PARAM_STR_ARRAY);
3615
+            $cmd->executeStatement();
3616
+        }
3617
+    }
3618
+
3619
+    /**
3620
+     * Delete all invitations from a given calendar event
3621
+     *
3622
+     * @since 31.0.0
3623
+     *
3624
+     * @param string $eventId UID of the event
3625
+     *
3626
+     * @return void
3627
+     */
3628
+    protected function purgeObjectInvitations(string $eventId): void {
3629
+        $cmd = $this->db->getQueryBuilder();
3630
+        $cmd->delete($this->dbObjectInvitationsTable)
3631
+            ->where($cmd->expr()->eq('uid', $cmd->createNamedParameter($eventId, IQueryBuilder::PARAM_STR), IQueryBuilder::PARAM_STR));
3632
+        $cmd->executeStatement();
3633
+    }
3634 3634
 }
Please login to merge, or discard this patch.
Spacing   +165 added lines, -165 removed lines patch added patch discarded remove patch
@@ -129,7 +129,7 @@  discard block
 block discarded – undo
129 129
 		'{urn:ietf:params:xml:ns:caldav}calendar-timezone' => ['timezone', 'string'],
130 130
 		'{http://apple.com/ns/ical/}calendar-order' => ['calendarorder', 'int'],
131 131
 		'{http://apple.com/ns/ical/}calendar-color' => ['calendarcolor', 'string'],
132
-		'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => ['deleted_at', 'int'],
132
+		'{'.\OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD.'}deleted-at' => ['deleted_at', 'int'],
133 133
 	];
134 134
 
135 135
 	/**
@@ -222,7 +222,7 @@  discard block
 block discarded – undo
222 222
 		}
223 223
 
224 224
 		$result = $query->executeQuery();
225
-		$column = (int)$result->fetchOne();
225
+		$column = (int) $result->fetchOne();
226 226
 		$result->closeCursor();
227 227
 		return $column;
228 228
 	}
@@ -243,7 +243,7 @@  discard block
 block discarded – undo
243 243
 		}
244 244
 
245 245
 		$result = $query->executeQuery();
246
-		$column = (int)$result->fetchOne();
246
+		$column = (int) $result->fetchOne();
247 247
 		$result->closeCursor();
248 248
 		return $column;
249 249
 	}
@@ -261,8 +261,8 @@  discard block
 block discarded – undo
261 261
 		$calendars = [];
262 262
 		while (($row = $result->fetch()) !== false) {
263 263
 			$calendars[] = [
264
-				'id' => (int)$row['id'],
265
-				'deleted_at' => (int)$row['deleted_at'],
264
+				'id' => (int) $row['id'],
265
+				'deleted_at' => (int) $row['deleted_at'],
266 266
 			];
267 267
 		}
268 268
 		$result->closeCursor();
@@ -295,7 +295,7 @@  discard block
 block discarded – undo
295 295
 	 * @return array
296 296
 	 */
297 297
 	public function getCalendarsForUser($principalUri) {
298
-		return $this->atomic(function () use ($principalUri) {
298
+		return $this->atomic(function() use ($principalUri) {
299 299
 			$principalUriOriginal = $principalUri;
300 300
 			$principalUri = $this->convertPrincipal($principalUri, true);
301 301
 			$fields = array_column($this->propertyMap, 0);
@@ -322,7 +322,7 @@  discard block
 block discarded – undo
322 322
 
323 323
 			$calendars = [];
324 324
 			while ($row = $result->fetch()) {
325
-				$row['principaluri'] = (string)$row['principaluri'];
325
+				$row['principaluri'] = (string) $row['principaluri'];
326 326
 				$components = [];
327 327
 				if ($row['components']) {
328 328
 					$components = explode(',', $row['components']);
@@ -332,11 +332,11 @@  discard block
 block discarded – undo
332 332
 					'id' => $row['id'],
333 333
 					'uri' => $row['uri'],
334 334
 					'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
335
-					'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ?: '0'),
335
+					'{'.Plugin::NS_CALENDARSERVER.'}getctag' => 'http://sabre.io/ns/sync/'.($row['synctoken'] ?: '0'),
336 336
 					'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
337
-					'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
338
-					'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
339
-					'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($principalUri, !$this->legacyEndpoint),
337
+					'{'.Plugin::NS_CALDAV.'}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
338
+					'{'.Plugin::NS_CALDAV.'}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent'] ? 'transparent' : 'opaque'),
339
+					'{'.\OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD.'}owner-principal' => $this->convertPrincipal($principalUri, !$this->legacyEndpoint),
340 340
 				];
341 341
 
342 342
 				$calendar = $this->rowToCalendar($row, $calendar);
@@ -355,8 +355,8 @@  discard block
 block discarded – undo
355 355
 			$principals[] = $principalUri;
356 356
 
357 357
 			$fields = array_column($this->propertyMap, 0);
358
-			$fields = array_map(function (string $field) {
359
-				return 'a.' . $field;
358
+			$fields = array_map(function(string $field) {
359
+				return 'a.'.$field;
360 360
 			}, $fields);
361 361
 			$fields[] = 'a.id';
362 362
 			$fields[] = 'a.uri';
@@ -383,14 +383,14 @@  discard block
 block discarded – undo
383 383
 
384 384
 			$results = $select->executeQuery();
385 385
 
386
-			$readOnlyPropertyName = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only';
386
+			$readOnlyPropertyName = '{'.\OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD.'}read-only';
387 387
 			while ($row = $results->fetch()) {
388
-				$row['principaluri'] = (string)$row['principaluri'];
388
+				$row['principaluri'] = (string) $row['principaluri'];
389 389
 				if ($row['principaluri'] === $principalUri) {
390 390
 					continue;
391 391
 				}
392 392
 
393
-				$readOnly = (int)$row['access'] === Backend::ACCESS_READ;
393
+				$readOnly = (int) $row['access'] === Backend::ACCESS_READ;
394 394
 				if (isset($calendars[$row['id']])) {
395 395
 					if ($readOnly) {
396 396
 						// New share can not have more permissions than the old one.
@@ -404,8 +404,8 @@  discard block
 block discarded – undo
404 404
 				}
405 405
 
406 406
 				[, $name] = Uri\split($row['principaluri']);
407
-				$uri = $row['uri'] . '_shared_by_' . $name;
408
-				$row['displayname'] = $row['displayname'] . ' (' . ($this->userManager->getDisplayName($name) ?? ($name ?? '')) . ')';
407
+				$uri = $row['uri'].'_shared_by_'.$name;
408
+				$row['displayname'] = $row['displayname'].' ('.($this->userManager->getDisplayName($name) ?? ($name ?? '')).')';
409 409
 				$components = [];
410 410
 				if ($row['components']) {
411 411
 					$components = explode(',', $row['components']);
@@ -414,11 +414,11 @@  discard block
 block discarded – undo
414 414
 					'id' => $row['id'],
415 415
 					'uri' => $uri,
416 416
 					'principaluri' => $this->convertPrincipal($principalUri, !$this->legacyEndpoint),
417
-					'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ?: '0'),
417
+					'{'.Plugin::NS_CALENDARSERVER.'}getctag' => 'http://sabre.io/ns/sync/'.($row['synctoken'] ?: '0'),
418 418
 					'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
419
-					'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
420
-					'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp('transparent'),
421
-					'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
419
+					'{'.Plugin::NS_CALDAV.'}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
420
+					'{'.Plugin::NS_CALDAV.'}schedule-calendar-transp' => new ScheduleCalendarTransp('transparent'),
421
+					'{'.\OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD.'}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
422 422
 					$readOnlyPropertyName => $readOnly,
423 423
 				];
424 424
 
@@ -455,7 +455,7 @@  discard block
 block discarded – undo
455 455
 		$stmt = $query->executeQuery();
456 456
 		$calendars = [];
457 457
 		while ($row = $stmt->fetch()) {
458
-			$row['principaluri'] = (string)$row['principaluri'];
458
+			$row['principaluri'] = (string) $row['principaluri'];
459 459
 			$components = [];
460 460
 			if ($row['components']) {
461 461
 				$components = explode(',', $row['components']);
@@ -464,10 +464,10 @@  discard block
 block discarded – undo
464 464
 				'id' => $row['id'],
465 465
 				'uri' => $row['uri'],
466 466
 				'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
467
-				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ?: '0'),
467
+				'{'.Plugin::NS_CALENDARSERVER.'}getctag' => 'http://sabre.io/ns/sync/'.($row['synctoken'] ?: '0'),
468 468
 				'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
469
-				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
470
-				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
469
+				'{'.Plugin::NS_CALDAV.'}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
470
+				'{'.Plugin::NS_CALDAV.'}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent'] ? 'transparent' : 'opaque'),
471 471
 			];
472 472
 
473 473
 			$calendar = $this->rowToCalendar($row, $calendar);
@@ -505,9 +505,9 @@  discard block
 block discarded – undo
505 505
 			->executeQuery();
506 506
 
507 507
 		while ($row = $result->fetch()) {
508
-			$row['principaluri'] = (string)$row['principaluri'];
508
+			$row['principaluri'] = (string) $row['principaluri'];
509 509
 			[, $name] = Uri\split($row['principaluri']);
510
-			$row['displayname'] = $row['displayname'] . "($name)";
510
+			$row['displayname'] = $row['displayname']."($name)";
511 511
 			$components = [];
512 512
 			if ($row['components']) {
513 513
 				$components = explode(',', $row['components']);
@@ -516,13 +516,13 @@  discard block
 block discarded – undo
516 516
 				'id' => $row['id'],
517 517
 				'uri' => $row['publicuri'],
518 518
 				'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
519
-				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ?: '0'),
519
+				'{'.Plugin::NS_CALENDARSERVER.'}getctag' => 'http://sabre.io/ns/sync/'.($row['synctoken'] ?: '0'),
520 520
 				'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
521
-				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
522
-				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
523
-				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], $this->legacyEndpoint),
524
-				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ,
525
-				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}public' => (int)$row['access'] === self::ACCESS_PUBLIC,
521
+				'{'.Plugin::NS_CALDAV.'}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
522
+				'{'.Plugin::NS_CALDAV.'}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent'] ? 'transparent' : 'opaque'),
523
+				'{'.\OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD.'}owner-principal' => $this->convertPrincipal($row['principaluri'], $this->legacyEndpoint),
524
+				'{'.\OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD.'}read-only' => (int) $row['access'] === Backend::ACCESS_READ,
525
+				'{'.\OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD.'}public' => (int) $row['access'] === self::ACCESS_PUBLIC,
526 526
 			];
527 527
 
528 528
 			$calendar = $this->rowToCalendar($row, $calendar);
@@ -567,12 +567,12 @@  discard block
 block discarded – undo
567 567
 		$result->closeCursor();
568 568
 
569 569
 		if ($row === false) {
570
-			throw new NotFound('Node with name \'' . $uri . '\' could not be found');
570
+			throw new NotFound('Node with name \''.$uri.'\' could not be found');
571 571
 		}
572 572
 
573
-		$row['principaluri'] = (string)$row['principaluri'];
573
+		$row['principaluri'] = (string) $row['principaluri'];
574 574
 		[, $name] = Uri\split($row['principaluri']);
575
-		$row['displayname'] = $row['displayname'] . ' ' . "($name)";
575
+		$row['displayname'] = $row['displayname'].' '."($name)";
576 576
 		$components = [];
577 577
 		if ($row['components']) {
578 578
 			$components = explode(',', $row['components']);
@@ -581,13 +581,13 @@  discard block
 block discarded – undo
581 581
 			'id' => $row['id'],
582 582
 			'uri' => $row['publicuri'],
583 583
 			'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
584
-			'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ?: '0'),
584
+			'{'.Plugin::NS_CALENDARSERVER.'}getctag' => 'http://sabre.io/ns/sync/'.($row['synctoken'] ?: '0'),
585 585
 			'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
586
-			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
587
-			'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
588
-			'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
589
-			'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ,
590
-			'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}public' => (int)$row['access'] === self::ACCESS_PUBLIC,
586
+			'{'.Plugin::NS_CALDAV.'}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
587
+			'{'.Plugin::NS_CALDAV.'}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent'] ? 'transparent' : 'opaque'),
588
+			'{'.\OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD.'}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
589
+			'{'.\OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD.'}read-only' => (int) $row['access'] === Backend::ACCESS_READ,
590
+			'{'.\OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD.'}public' => (int) $row['access'] === self::ACCESS_PUBLIC,
591 591
 		];
592 592
 
593 593
 		$calendar = $this->rowToCalendar($row, $calendar);
@@ -625,7 +625,7 @@  discard block
 block discarded – undo
625 625
 			return null;
626 626
 		}
627 627
 
628
-		$row['principaluri'] = (string)$row['principaluri'];
628
+		$row['principaluri'] = (string) $row['principaluri'];
629 629
 		$components = [];
630 630
 		if ($row['components']) {
631 631
 			$components = explode(',', $row['components']);
@@ -635,10 +635,10 @@  discard block
 block discarded – undo
635 635
 			'id' => $row['id'],
636 636
 			'uri' => $row['uri'],
637 637
 			'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
638
-			'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ?: '0'),
638
+			'{'.Plugin::NS_CALENDARSERVER.'}getctag' => 'http://sabre.io/ns/sync/'.($row['synctoken'] ?: '0'),
639 639
 			'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
640
-			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
641
-			'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
640
+			'{'.Plugin::NS_CALDAV.'}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
641
+			'{'.Plugin::NS_CALDAV.'}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent'] ? 'transparent' : 'opaque'),
642 642
 		];
643 643
 
644 644
 		$calendar = $this->rowToCalendar($row, $calendar);
@@ -673,7 +673,7 @@  discard block
 block discarded – undo
673 673
 			return null;
674 674
 		}
675 675
 
676
-		$row['principaluri'] = (string)$row['principaluri'];
676
+		$row['principaluri'] = (string) $row['principaluri'];
677 677
 		$components = [];
678 678
 		if ($row['components']) {
679 679
 			$components = explode(',', $row['components']);
@@ -683,10 +683,10 @@  discard block
 block discarded – undo
683 683
 			'id' => $row['id'],
684 684
 			'uri' => $row['uri'],
685 685
 			'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
686
-			'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ?: '0'),
686
+			'{'.Plugin::NS_CALENDARSERVER.'}getctag' => 'http://sabre.io/ns/sync/'.($row['synctoken'] ?: '0'),
687 687
 			'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?? 0,
688
-			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
689
-			'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
688
+			'{'.Plugin::NS_CALDAV.'}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
689
+			'{'.Plugin::NS_CALDAV.'}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent'] ? 'transparent' : 'opaque'),
690 690
 		];
691 691
 
692 692
 		$calendar = $this->rowToCalendar($row, $calendar);
@@ -721,14 +721,14 @@  discard block
 block discarded – undo
721 721
 			return null;
722 722
 		}
723 723
 
724
-		$row['principaluri'] = (string)$row['principaluri'];
724
+		$row['principaluri'] = (string) $row['principaluri'];
725 725
 		$subscription = [
726 726
 			'id' => $row['id'],
727 727
 			'uri' => $row['uri'],
728 728
 			'principaluri' => $row['principaluri'],
729 729
 			'source' => $row['source'],
730 730
 			'lastmodified' => $row['lastmodified'],
731
-			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
731
+			'{'.Plugin::NS_CALDAV.'}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
732 732
 			'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
733 733
 		];
734 734
 
@@ -758,14 +758,14 @@  discard block
 block discarded – undo
758 758
 			return null;
759 759
 		}
760 760
 
761
-		$row['principaluri'] = (string)$row['principaluri'];
761
+		$row['principaluri'] = (string) $row['principaluri'];
762 762
 		$subscription = [
763 763
 			'id' => $row['id'],
764 764
 			'uri' => $row['uri'],
765 765
 			'principaluri' => $row['principaluri'],
766 766
 			'source' => $row['source'],
767 767
 			'lastmodified' => $row['lastmodified'],
768
-			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
768
+			'{'.Plugin::NS_CALDAV.'}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
769 769
 			'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
770 770
 		];
771 771
 
@@ -803,7 +803,7 @@  discard block
 block discarded – undo
803 803
 		$sccs = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set';
804 804
 		if (isset($properties[$sccs])) {
805 805
 			if (!($properties[$sccs] instanceof SupportedCalendarComponentSet)) {
806
-				throw new DAV\Exception('The ' . $sccs . ' property must be of type: \Sabre\CalDAV\Property\SupportedCalendarComponentSet');
806
+				throw new DAV\Exception('The '.$sccs.' property must be of type: \Sabre\CalDAV\Property\SupportedCalendarComponentSet');
807 807
 			}
808 808
 			$values['components'] = implode(',', $properties[$sccs]->getValue());
809 809
 		} elseif (isset($properties['components'])) {
@@ -812,9 +812,9 @@  discard block
 block discarded – undo
812 812
 			$values['components'] = $properties['components'];
813 813
 		}
814 814
 
815
-		$transp = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp';
815
+		$transp = '{'.Plugin::NS_CALDAV.'}schedule-calendar-transp';
816 816
 		if (isset($properties[$transp])) {
817
-			$values['transparent'] = (int)($properties[$transp]->getValue() === 'transparent');
817
+			$values['transparent'] = (int) ($properties[$transp]->getValue() === 'transparent');
818 818
 		}
819 819
 
820 820
 		foreach ($this->propertyMap as $xmlName => [$dbName, $type]) {
@@ -823,7 +823,7 @@  discard block
 block discarded – undo
823 823
 			}
824 824
 		}
825 825
 
826
-		[$calendarId, $calendarData] = $this->atomic(function () use ($values) {
826
+		[$calendarId, $calendarData] = $this->atomic(function() use ($values) {
827 827
 			$query = $this->db->getQueryBuilder();
828 828
 			$query->insert('calendars');
829 829
 			foreach ($values as $column => $value) {
@@ -836,7 +836,7 @@  discard block
 block discarded – undo
836 836
 			return [$calendarId, $calendarData];
837 837
 		}, $this->db);
838 838
 
839
-		$this->dispatcher->dispatchTyped(new CalendarCreatedEvent((int)$calendarId, $calendarData));
839
+		$this->dispatcher->dispatchTyped(new CalendarCreatedEvent((int) $calendarId, $calendarData));
840 840
 
841 841
 		return $calendarId;
842 842
 	}
@@ -859,15 +859,15 @@  discard block
 block discarded – undo
859 859
 	 */
860 860
 	public function updateCalendar($calendarId, PropPatch $propPatch) {
861 861
 		$supportedProperties = array_keys($this->propertyMap);
862
-		$supportedProperties[] = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp';
862
+		$supportedProperties[] = '{'.Plugin::NS_CALDAV.'}schedule-calendar-transp';
863 863
 
864
-		$propPatch->handle($supportedProperties, function ($mutations) use ($calendarId) {
864
+		$propPatch->handle($supportedProperties, function($mutations) use ($calendarId) {
865 865
 			$newValues = [];
866 866
 			foreach ($mutations as $propertyName => $propertyValue) {
867 867
 				switch ($propertyName) {
868
-					case '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp':
868
+					case '{'.Plugin::NS_CALDAV.'}schedule-calendar-transp':
869 869
 						$fieldName = 'transparent';
870
-						$newValues[$fieldName] = (int)($propertyValue->getValue() === 'transparent');
870
+						$newValues[$fieldName] = (int) ($propertyValue->getValue() === 'transparent');
871 871
 						break;
872 872
 					default:
873 873
 						$fieldName = $this->propertyMap[$propertyName][0];
@@ -875,7 +875,7 @@  discard block
 block discarded – undo
875 875
 						break;
876 876
 				}
877 877
 			}
878
-			[$calendarData, $shares] = $this->atomic(function () use ($calendarId, $newValues) {
878
+			[$calendarData, $shares] = $this->atomic(function() use ($calendarId, $newValues) {
879 879
 				$query = $this->db->getQueryBuilder();
880 880
 				$query->update('calendars');
881 881
 				foreach ($newValues as $fieldName => $value) {
@@ -904,7 +904,7 @@  discard block
 block discarded – undo
904 904
 	 * @return void
905 905
 	 */
906 906
 	public function deleteCalendar($calendarId, bool $forceDeletePermanently = false) {
907
-		$this->atomic(function () use ($calendarId, $forceDeletePermanently): void {
907
+		$this->atomic(function() use ($calendarId, $forceDeletePermanently): void {
908 908
 			// The calendar is deleted right away if this is either enforced by the caller
909 909
 			// or the special contacts birthday calendar or when the preference of an empty
910 910
 			// retention (0 seconds) is set, which signals a disabled trashbin.
@@ -967,7 +967,7 @@  discard block
 block discarded – undo
967 967
 	}
968 968
 
969 969
 	public function restoreCalendar(int $id): void {
970
-		$this->atomic(function () use ($id): void {
970
+		$this->atomic(function() use ($id): void {
971 971
 			$qb = $this->db->getQueryBuilder();
972 972
 			$update = $qb->update('calendars')
973 973
 				->set('deleted_at', $qb->createNamedParameter(null))
@@ -1003,7 +1003,7 @@  discard block
 block discarded – undo
1003 1003
 	 */
1004 1004
 	public function getLimitedCalendarObjects(int $calendarId, int $calendarType = self::CALENDAR_TYPE_CALENDAR):array {
1005 1005
 		$query = $this->db->getQueryBuilder();
1006
-		$query->select(['id','uid', 'etag', 'uri', 'calendardata'])
1006
+		$query->select(['id', 'uid', 'etag', 'uri', 'calendardata'])
1007 1007
 			->from('calendarobjects')
1008 1008
 			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
1009 1009
 			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)))
@@ -1081,11 +1081,11 @@  discard block
 block discarded – undo
1081 1081
 				'id' => $row['id'],
1082 1082
 				'uri' => $row['uri'],
1083 1083
 				'lastmodified' => $row['lastmodified'],
1084
-				'etag' => '"' . $row['etag'] . '"',
1084
+				'etag' => '"'.$row['etag'].'"',
1085 1085
 				'calendarid' => $row['calendarid'],
1086
-				'size' => (int)$row['size'],
1086
+				'size' => (int) $row['size'],
1087 1087
 				'component' => strtolower($row['componenttype']),
1088
-				'classification' => (int)$row['classification']
1088
+				'classification' => (int) $row['classification']
1089 1089
 			];
1090 1090
 		}
1091 1091
 		$stmt->closeCursor();
@@ -1108,13 +1108,13 @@  discard block
 block discarded – undo
1108 1108
 				'id' => $row['id'],
1109 1109
 				'uri' => $row['uri'],
1110 1110
 				'lastmodified' => $row['lastmodified'],
1111
-				'etag' => '"' . $row['etag'] . '"',
1112
-				'calendarid' => (int)$row['calendarid'],
1113
-				'calendartype' => (int)$row['calendartype'],
1114
-				'size' => (int)$row['size'],
1111
+				'etag' => '"'.$row['etag'].'"',
1112
+				'calendarid' => (int) $row['calendarid'],
1113
+				'calendartype' => (int) $row['calendartype'],
1114
+				'size' => (int) $row['size'],
1115 1115
 				'component' => strtolower($row['componenttype']),
1116
-				'classification' => (int)$row['classification'],
1117
-				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => $row['deleted_at'] === null ? $row['deleted_at'] : (int)$row['deleted_at'],
1116
+				'classification' => (int) $row['classification'],
1117
+				'{'.\OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD.'}deleted-at' => $row['deleted_at'] === null ? $row['deleted_at'] : (int) $row['deleted_at'],
1118 1118
 			];
1119 1119
 		}
1120 1120
 		$stmt->closeCursor();
@@ -1147,13 +1147,13 @@  discard block
 block discarded – undo
1147 1147
 				'id' => $row['id'],
1148 1148
 				'uri' => $row['uri'],
1149 1149
 				'lastmodified' => $row['lastmodified'],
1150
-				'etag' => '"' . $row['etag'] . '"',
1150
+				'etag' => '"'.$row['etag'].'"',
1151 1151
 				'calendarid' => $row['calendarid'],
1152 1152
 				'calendaruri' => $row['calendaruri'],
1153
-				'size' => (int)$row['size'],
1153
+				'size' => (int) $row['size'],
1154 1154
 				'component' => strtolower($row['componenttype']),
1155
-				'classification' => (int)$row['classification'],
1156
-				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => $row['deleted_at'] === null ? $row['deleted_at'] : (int)$row['deleted_at'],
1155
+				'classification' => (int) $row['classification'],
1156
+				'{'.\OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD.'}deleted-at' => $row['deleted_at'] === null ? $row['deleted_at'] : (int) $row['deleted_at'],
1157 1157
 			];
1158 1158
 		}
1159 1159
 		$stmt->closeCursor();
@@ -1179,7 +1179,7 @@  discard block
 block discarded – undo
1179 1179
 	 * @return array|null
1180 1180
 	 */
1181 1181
 	public function getCalendarObject($calendarId, $objectUri, int $calendarType = self::CALENDAR_TYPE_CALENDAR) {
1182
-		$key = $calendarId . '::' . $objectUri . '::' . $calendarType;
1182
+		$key = $calendarId.'::'.$objectUri.'::'.$calendarType;
1183 1183
 		if (isset($this->cachedObjects[$key])) {
1184 1184
 			return $this->cachedObjects[$key];
1185 1185
 		}
@@ -1208,13 +1208,13 @@  discard block
 block discarded – undo
1208 1208
 			'uri' => $row['uri'],
1209 1209
 			'uid' => $row['uid'],
1210 1210
 			'lastmodified' => $row['lastmodified'],
1211
-			'etag' => '"' . $row['etag'] . '"',
1211
+			'etag' => '"'.$row['etag'].'"',
1212 1212
 			'calendarid' => $row['calendarid'],
1213
-			'size' => (int)$row['size'],
1213
+			'size' => (int) $row['size'],
1214 1214
 			'calendardata' => $this->readBlob($row['calendardata']),
1215 1215
 			'component' => strtolower($row['componenttype']),
1216
-			'classification' => (int)$row['classification'],
1217
-			'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => $row['deleted_at'] === null ? $row['deleted_at'] : (int)$row['deleted_at'],
1216
+			'classification' => (int) $row['classification'],
1217
+			'{'.\OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD.'}deleted-at' => $row['deleted_at'] === null ? $row['deleted_at'] : (int) $row['deleted_at'],
1218 1218
 		];
1219 1219
 	}
1220 1220
 
@@ -1256,12 +1256,12 @@  discard block
 block discarded – undo
1256 1256
 					'id' => $row['id'],
1257 1257
 					'uri' => $row['uri'],
1258 1258
 					'lastmodified' => $row['lastmodified'],
1259
-					'etag' => '"' . $row['etag'] . '"',
1259
+					'etag' => '"'.$row['etag'].'"',
1260 1260
 					'calendarid' => $row['calendarid'],
1261
-					'size' => (int)$row['size'],
1261
+					'size' => (int) $row['size'],
1262 1262
 					'calendardata' => $this->readBlob($row['calendardata']),
1263 1263
 					'component' => strtolower($row['componenttype']),
1264
-					'classification' => (int)$row['classification']
1264
+					'classification' => (int) $row['classification']
1265 1265
 				];
1266 1266
 			}
1267 1267
 			$result->closeCursor();
@@ -1293,7 +1293,7 @@  discard block
 block discarded – undo
1293 1293
 		$this->cachedObjects = [];
1294 1294
 		$extraData = $this->getDenormalizedData($calendarData);
1295 1295
 
1296
-		return $this->atomic(function () use ($calendarId, $objectUri, $calendarData, $extraData, $calendarType) {
1296
+		return $this->atomic(function() use ($calendarId, $objectUri, $calendarData, $extraData, $calendarType) {
1297 1297
 			// Try to detect duplicates
1298 1298
 			$qb = $this->db->getQueryBuilder();
1299 1299
 			$qb->select($qb->func()->count('*'))
@@ -1303,7 +1303,7 @@  discard block
 block discarded – undo
1303 1303
 				->andWhere($qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType)))
1304 1304
 				->andWhere($qb->expr()->isNull('deleted_at'));
1305 1305
 			$result = $qb->executeQuery();
1306
-			$count = (int)$result->fetchOne();
1306
+			$count = (int) $result->fetchOne();
1307 1307
 			$result->closeCursor();
1308 1308
 
1309 1309
 			if ($count !== 0) {
@@ -1361,7 +1361,7 @@  discard block
 block discarded – undo
1361 1361
 				$this->dispatcher->dispatchTyped(new CachedCalendarObjectCreatedEvent($calendarId, $subscriptionRow, [], $objectRow));
1362 1362
 			}
1363 1363
 
1364
-			return '"' . $extraData['etag'] . '"';
1364
+			return '"'.$extraData['etag'].'"';
1365 1365
 		}, $this->db);
1366 1366
 	}
1367 1367
 
@@ -1388,7 +1388,7 @@  discard block
 block discarded – undo
1388 1388
 		$this->cachedObjects = [];
1389 1389
 		$extraData = $this->getDenormalizedData($calendarData);
1390 1390
 
1391
-		return $this->atomic(function () use ($calendarId, $objectUri, $calendarData, $extraData, $calendarType) {
1391
+		return $this->atomic(function() use ($calendarId, $objectUri, $calendarData, $extraData, $calendarType) {
1392 1392
 			$query = $this->db->getQueryBuilder();
1393 1393
 			$query->update('calendarobjects')
1394 1394
 				->set('calendardata', $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB))
@@ -1422,7 +1422,7 @@  discard block
 block discarded – undo
1422 1422
 				}
1423 1423
 			}
1424 1424
 
1425
-			return '"' . $extraData['etag'] . '"';
1425
+			return '"'.$extraData['etag'].'"';
1426 1426
 		}, $this->db);
1427 1427
 	}
1428 1428
 
@@ -1440,7 +1440,7 @@  discard block
 block discarded – undo
1440 1440
 	 */
1441 1441
 	public function moveCalendarObject(string $sourcePrincipalUri, int $sourceObjectId, string $targetPrincipalUri, int $targetCalendarId, string $tragetObjectUri, int $calendarType = self::CALENDAR_TYPE_CALENDAR): bool {
1442 1442
 		$this->cachedObjects = [];
1443
-		return $this->atomic(function () use ($sourcePrincipalUri, $sourceObjectId, $targetPrincipalUri, $targetCalendarId, $tragetObjectUri, $calendarType) {
1443
+		return $this->atomic(function() use ($sourcePrincipalUri, $sourceObjectId, $targetPrincipalUri, $targetCalendarId, $tragetObjectUri, $calendarType) {
1444 1444
 			$object = $this->getCalendarObjectById($sourcePrincipalUri, $sourceObjectId);
1445 1445
 			if (empty($object)) {
1446 1446
 				return false;
@@ -1516,7 +1516,7 @@  discard block
 block discarded – undo
1516 1516
 	 */
1517 1517
 	public function deleteCalendarObject($calendarId, $objectUri, $calendarType = self::CALENDAR_TYPE_CALENDAR, bool $forceDeletePermanently = false) {
1518 1518
 		$this->cachedObjects = [];
1519
-		$this->atomic(function () use ($calendarId, $objectUri, $calendarType, $forceDeletePermanently): void {
1519
+		$this->atomic(function() use ($calendarId, $objectUri, $calendarType, $forceDeletePermanently): void {
1520 1520
 			$data = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
1521 1521
 
1522 1522
 			if ($data === null) {
@@ -1600,8 +1600,8 @@  discard block
 block discarded – undo
1600 1600
 	 */
1601 1601
 	public function restoreCalendarObject(array $objectData): void {
1602 1602
 		$this->cachedObjects = [];
1603
-		$this->atomic(function () use ($objectData): void {
1604
-			$id = (int)$objectData['id'];
1603
+		$this->atomic(function() use ($objectData): void {
1604
+			$id = (int) $objectData['id'];
1605 1605
 			$restoreUri = str_replace('-deleted.ics', '.ics', $objectData['uri']);
1606 1606
 			$targetObject = $this->getCalendarObject(
1607 1607
 				$objectData['calendarid'],
@@ -1631,17 +1631,17 @@  discard block
 block discarded – undo
1631 1631
 				// Welp, this should possibly not have happened, but let's ignore
1632 1632
 				return;
1633 1633
 			}
1634
-			$this->addChanges($row['calendarid'], [$row['uri']], 1, (int)$row['calendartype']);
1634
+			$this->addChanges($row['calendarid'], [$row['uri']], 1, (int) $row['calendartype']);
1635 1635
 
1636
-			$calendarRow = $this->getCalendarById((int)$row['calendarid']);
1636
+			$calendarRow = $this->getCalendarById((int) $row['calendarid']);
1637 1637
 			if ($calendarRow === null) {
1638 1638
 				throw new RuntimeException('Calendar object data that was just written can\'t be read back. Check your database configuration.');
1639 1639
 			}
1640 1640
 			$this->dispatcher->dispatchTyped(
1641 1641
 				new CalendarObjectRestoredEvent(
1642
-					(int)$objectData['calendarid'],
1642
+					(int) $objectData['calendarid'],
1643 1643
 					$calendarRow,
1644
-					$this->getShares((int)$row['calendarid']),
1644
+					$this->getShares((int) $row['calendarid']),
1645 1645
 					$row
1646 1646
 				)
1647 1647
 			);
@@ -1760,19 +1760,19 @@  discard block
 block discarded – undo
1760 1760
 				try {
1761 1761
 					$matches = $this->validateFilterForObject($row, $filters);
1762 1762
 				} catch (ParseException $ex) {
1763
-					$this->logger->error('Caught parsing exception for calendar data. This usually indicates invalid calendar data. calendar-id:' . $calendarId . ' uri:' . $row['uri'], [
1763
+					$this->logger->error('Caught parsing exception for calendar data. This usually indicates invalid calendar data. calendar-id:'.$calendarId.' uri:'.$row['uri'], [
1764 1764
 						'app' => 'dav',
1765 1765
 						'exception' => $ex,
1766 1766
 					]);
1767 1767
 					continue;
1768 1768
 				} catch (InvalidDataException $ex) {
1769
-					$this->logger->error('Caught invalid data exception for calendar data. This usually indicates invalid calendar data. calendar-id:' . $calendarId . ' uri:' . $row['uri'], [
1769
+					$this->logger->error('Caught invalid data exception for calendar data. This usually indicates invalid calendar data. calendar-id:'.$calendarId.' uri:'.$row['uri'], [
1770 1770
 						'app' => 'dav',
1771 1771
 						'exception' => $ex,
1772 1772
 					]);
1773 1773
 					continue;
1774 1774
 				} catch (MaxInstancesExceededException $ex) {
1775
-					$this->logger->warning('Caught max instances exceeded exception for calendar data. This usually indicates too much recurring (more than 3500) event in calendar data. Object uri: ' . $row['uri'], [
1775
+					$this->logger->warning('Caught max instances exceeded exception for calendar data. This usually indicates too much recurring (more than 3500) event in calendar data. Object uri: '.$row['uri'], [
1776 1776
 						'app' => 'dav',
1777 1777
 						'exception' => $ex,
1778 1778
 					]);
@@ -1784,7 +1784,7 @@  discard block
 block discarded – undo
1784 1784
 				}
1785 1785
 			}
1786 1786
 			$result[] = $row['uri'];
1787
-			$key = $calendarId . '::' . $row['uri'] . '::' . $calendarType;
1787
+			$key = $calendarId.'::'.$row['uri'].'::'.$calendarType;
1788 1788
 			$this->cachedObjects[$key] = $this->rowToCalendarObject($row);
1789 1789
 		}
1790 1790
 
@@ -1803,7 +1803,7 @@  discard block
 block discarded – undo
1803 1803
 	 * @return array
1804 1804
 	 */
1805 1805
 	public function calendarSearch($principalUri, array $filters, $limit = null, $offset = null) {
1806
-		return $this->atomic(function () use ($principalUri, $filters, $limit, $offset) {
1806
+		return $this->atomic(function() use ($principalUri, $filters, $limit, $offset) {
1807 1807
 			$calendars = $this->getCalendarsForUser($principalUri);
1808 1808
 			$ownCalendars = [];
1809 1809
 			$sharedCalendars = [];
@@ -1895,7 +1895,7 @@  discard block
 block discarded – undo
1895 1895
 				->andWhere($compExpr)
1896 1896
 				->andWhere($propParamExpr)
1897 1897
 				->andWhere($query->expr()->iLike('i.value',
1898
-					$query->createNamedParameter('%' . $this->db->escapeLikeParameter($filters['search-term']) . '%')))
1898
+					$query->createNamedParameter('%'.$this->db->escapeLikeParameter($filters['search-term']).'%')))
1899 1899
 				->andWhere($query->expr()->isNull('deleted_at'));
1900 1900
 
1901 1901
 			if ($offset) {
@@ -1909,7 +1909,7 @@  discard block
 block discarded – undo
1909 1909
 
1910 1910
 			$result = [];
1911 1911
 			while ($row = $stmt->fetch()) {
1912
-				$path = $uriMapper[$row['calendarid']] . '/' . $row['uri'];
1912
+				$path = $uriMapper[$row['calendarid']].'/'.$row['uri'];
1913 1913
 				if (!in_array($path, $result)) {
1914 1914
 					$result[] = $path;
1915 1915
 				}
@@ -1976,8 +1976,8 @@  discard block
 block discarded – undo
1976 1976
 
1977 1977
 		if ($pattern !== '') {
1978 1978
 			$innerQuery->andWhere($innerQuery->expr()->iLike('op.value',
1979
-				$outerQuery->createNamedParameter('%' .
1980
-					$this->db->escapeLikeParameter($pattern) . '%')));
1979
+				$outerQuery->createNamedParameter('%'.
1980
+					$this->db->escapeLikeParameter($pattern).'%')));
1981 1981
 		}
1982 1982
 
1983 1983
 		$start = null;
@@ -2029,7 +2029,7 @@  discard block
 block discarded – undo
2029 2029
 		// For the pagination with hasLimit and hasTimeRange, a stable ordering is helpful.
2030 2030
 		$outerQuery->addOrderBy('id');
2031 2031
 
2032
-		$offset = (int)$offset;
2032
+		$offset = (int) $offset;
2033 2033
 		$outerQuery->setFirstResult($offset);
2034 2034
 
2035 2035
 		$calendarObjects = [];
@@ -2050,7 +2050,7 @@  discard block
 block discarded – undo
2050 2050
 			 *
2051 2051
 			 * 25 rows and 3 retries is entirely arbitrary.
2052 2052
 			 */
2053
-			$maxResults = (int)max($limit, 25);
2053
+			$maxResults = (int) max($limit, 25);
2054 2054
 			$outerQuery->setMaxResults($maxResults);
2055 2055
 
2056 2056
 			for ($attempt = $objectsCount = 0; $attempt < 3 && $objectsCount < $limit; $attempt++) {
@@ -2064,7 +2064,7 @@  discard block
 block discarded – undo
2064 2064
 			$calendarObjects = $this->searchCalendarObjects($outerQuery, $start, $end);
2065 2065
 		}
2066 2066
 
2067
-		$calendarObjects = array_map(function ($o) use ($options) {
2067
+		$calendarObjects = array_map(function($o) use ($options) {
2068 2068
 			$calendarData = Reader::read($o['calendardata']);
2069 2069
 
2070 2070
 			// Expand recurrences if an explicit time range is requested
@@ -2092,16 +2092,16 @@  discard block
 block discarded – undo
2092 2092
 				'type' => $o['componenttype'],
2093 2093
 				'uid' => $o['uid'],
2094 2094
 				'uri' => $o['uri'],
2095
-				'objects' => array_map(function ($c) {
2095
+				'objects' => array_map(function($c) {
2096 2096
 					return $this->transformSearchData($c);
2097 2097
 				}, $objects),
2098
-				'timezones' => array_map(function ($c) {
2098
+				'timezones' => array_map(function($c) {
2099 2099
 					return $this->transformSearchData($c);
2100 2100
 				}, $timezones),
2101 2101
 			];
2102 2102
 		}, $calendarObjects);
2103 2103
 
2104
-		usort($calendarObjects, function (array $a, array $b) {
2104
+		usort($calendarObjects, function(array $a, array $b) {
2105 2105
 			/** @var DateTimeImmutable $startA */
2106 2106
 			$startA = $a['objects'][0]['DTSTART'][0] ?? new DateTimeImmutable(self::MAX_DATE);
2107 2107
 			/** @var DateTimeImmutable $startB */
@@ -2146,7 +2146,7 @@  discard block
 block discarded – undo
2146 2146
 					'time-range' => null,
2147 2147
 				]);
2148 2148
 			} catch (MaxInstancesExceededException $ex) {
2149
-				$this->logger->warning('Caught max instances exceeded exception for calendar data. This usually indicates too much recurring (more than 3500) event in calendar data. Object uri: ' . $row['uri'], [
2149
+				$this->logger->warning('Caught max instances exceeded exception for calendar data. This usually indicates too much recurring (more than 3500) event in calendar data. Object uri: '.$row['uri'], [
2150 2150
 					'app' => 'dav',
2151 2151
 					'exception' => $ex,
2152 2152
 				]);
@@ -2177,7 +2177,7 @@  discard block
 block discarded – undo
2177 2177
 		/** @var Component[] $subComponents */
2178 2178
 		$subComponents = $comp->getComponents();
2179 2179
 		/** @var Property[] $properties */
2180
-		$properties = array_filter($comp->children(), function ($c) {
2180
+		$properties = array_filter($comp->children(), function($c) {
2181 2181
 			return $c instanceof Property;
2182 2182
 		});
2183 2183
 		$validationRules = $comp->getValidationRules();
@@ -2245,7 +2245,7 @@  discard block
 block discarded – undo
2245 2245
 		array $searchParameters,
2246 2246
 		array $options = [],
2247 2247
 	): array {
2248
-		return $this->atomic(function () use ($principalUri, $pattern, $componentTypes, $searchProperties, $searchParameters, $options) {
2248
+		return $this->atomic(function() use ($principalUri, $pattern, $componentTypes, $searchProperties, $searchParameters, $options) {
2249 2249
 			$escapePattern = !\array_key_exists('escape_like_param', $options) || $options['escape_like_param'] !== false;
2250 2250
 
2251 2251
 			$calendarObjectIdQuery = $this->db->getQueryBuilder();
@@ -2257,7 +2257,7 @@  discard block
 block discarded – undo
2257 2257
 			$subscriptions = $this->getSubscriptionsForUser($principalUri);
2258 2258
 			foreach ($calendars as $calendar) {
2259 2259
 				$calendarAnd = $calendarObjectIdQuery->expr()->andX(
2260
-					$calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int)$calendar['id'])),
2260
+					$calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int) $calendar['id'])),
2261 2261
 					$calendarObjectIdQuery->expr()->eq('cob.calendartype', $calendarObjectIdQuery->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)),
2262 2262
 				);
2263 2263
 
@@ -2271,7 +2271,7 @@  discard block
 block discarded – undo
2271 2271
 			}
2272 2272
 			foreach ($subscriptions as $subscription) {
2273 2273
 				$subscriptionAnd = $calendarObjectIdQuery->expr()->andX(
2274
-					$calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int)$subscription['id'])),
2274
+					$calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int) $subscription['id'])),
2275 2275
 					$calendarObjectIdQuery->expr()->eq('cob.calendartype', $calendarObjectIdQuery->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)),
2276 2276
 				);
2277 2277
 
@@ -2320,7 +2320,7 @@  discard block
 block discarded – undo
2320 2320
 				if (!$escapePattern) {
2321 2321
 					$calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->ilike('cob.value', $calendarObjectIdQuery->createNamedParameter($pattern)));
2322 2322
 				} else {
2323
-					$calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->ilike('cob.value', $calendarObjectIdQuery->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%')));
2323
+					$calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->ilike('cob.value', $calendarObjectIdQuery->createNamedParameter('%'.$this->db->escapeLikeParameter($pattern).'%')));
2324 2324
 				}
2325 2325
 			}
2326 2326
 
@@ -2348,7 +2348,7 @@  discard block
 block discarded – undo
2348 2348
 			$result = $calendarObjectIdQuery->executeQuery();
2349 2349
 			$matches = [];
2350 2350
 			while (($row = $result->fetch()) !== false) {
2351
-				$matches[] = (int)$row['objectid'];
2351
+				$matches[] = (int) $row['objectid'];
2352 2352
 			}
2353 2353
 			$result->closeCursor();
2354 2354
 
@@ -2360,8 +2360,8 @@  discard block
 block discarded – undo
2360 2360
 			$result = $query->executeQuery();
2361 2361
 			$calendarObjects = [];
2362 2362
 			while (($array = $result->fetch()) !== false) {
2363
-				$array['calendarid'] = (int)$array['calendarid'];
2364
-				$array['calendartype'] = (int)$array['calendartype'];
2363
+				$array['calendarid'] = (int) $array['calendarid'];
2364
+				$array['calendartype'] = (int) $array['calendartype'];
2365 2365
 				$array['calendardata'] = $this->readBlob($array['calendardata']);
2366 2366
 
2367 2367
 				$calendarObjects[] = $array;
@@ -2402,7 +2402,7 @@  discard block
 block discarded – undo
2402 2402
 		$row = $stmt->fetch();
2403 2403
 		$stmt->closeCursor();
2404 2404
 		if ($row) {
2405
-			return $row['calendaruri'] . '/' . $row['objecturi'];
2405
+			return $row['calendaruri'].'/'.$row['objecturi'];
2406 2406
 		}
2407 2407
 
2408 2408
 		return null;
@@ -2428,14 +2428,14 @@  discard block
 block discarded – undo
2428 2428
 			'id' => $row['id'],
2429 2429
 			'uri' => $row['uri'],
2430 2430
 			'lastmodified' => $row['lastmodified'],
2431
-			'etag' => '"' . $row['etag'] . '"',
2431
+			'etag' => '"'.$row['etag'].'"',
2432 2432
 			'calendarid' => $row['calendarid'],
2433 2433
 			'calendaruri' => $row['calendaruri'],
2434
-			'size' => (int)$row['size'],
2434
+			'size' => (int) $row['size'],
2435 2435
 			'calendardata' => $this->readBlob($row['calendardata']),
2436 2436
 			'component' => strtolower($row['componenttype']),
2437
-			'classification' => (int)$row['classification'],
2438
-			'deleted_at' => isset($row['deleted_at']) ? ((int)$row['deleted_at']) : null,
2437
+			'classification' => (int) $row['classification'],
2438
+			'deleted_at' => isset($row['deleted_at']) ? ((int) $row['deleted_at']) : null,
2439 2439
 		];
2440 2440
 	}
2441 2441
 
@@ -2497,9 +2497,9 @@  discard block
 block discarded – undo
2497 2497
 	 * @return ?array
2498 2498
 	 */
2499 2499
 	public function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit = null, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
2500
-		$table = $calendarType === self::CALENDAR_TYPE_CALENDAR ? 'calendars': 'calendarsubscriptions';
2500
+		$table = $calendarType === self::CALENDAR_TYPE_CALENDAR ? 'calendars' : 'calendarsubscriptions';
2501 2501
 
2502
-		return $this->atomic(function () use ($calendarId, $syncToken, $syncLevel, $limit, $calendarType, $table) {
2502
+		return $this->atomic(function() use ($calendarId, $syncToken, $syncLevel, $limit, $calendarType, $table) {
2503 2503
 			// Current synctoken
2504 2504
 			$qb = $this->db->getQueryBuilder();
2505 2505
 			$qb->select('synctoken')
@@ -2550,7 +2550,7 @@  discard block
 block discarded – undo
2550 2550
 				while ($entry = $stmt->fetch(\PDO::FETCH_NUM)) {
2551 2551
 					// assign uri (column 0) to appropriate mutation based on operation (column 1)
2552 2552
 					// forced (int) is needed as doctrine with OCI returns the operation field as string not integer
2553
-					match ((int)$entry[1]) {
2553
+					match ((int) $entry[1]) {
2554 2554
 						1 => $result['added'][] = $entry[0],
2555 2555
 						2 => $result['modified'][] = $entry[0],
2556 2556
 						3 => $result['deleted'][] = $entry[0],
@@ -2621,7 +2621,7 @@  discard block
 block discarded – undo
2621 2621
 				'source' => $row['source'],
2622 2622
 				'lastmodified' => $row['lastmodified'],
2623 2623
 
2624
-				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
2624
+				'{'.Plugin::NS_CALDAV.'}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
2625 2625
 				'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
2626 2626
 			];
2627 2627
 
@@ -2665,7 +2665,7 @@  discard block
 block discarded – undo
2665 2665
 			}
2666 2666
 		}
2667 2667
 
2668
-		[$subscriptionId, $subscriptionRow] = $this->atomic(function () use ($values) {
2668
+		[$subscriptionId, $subscriptionRow] = $this->atomic(function() use ($values) {
2669 2669
 			$valuesToInsert = [];
2670 2670
 			$query = $this->db->getQueryBuilder();
2671 2671
 			foreach (array_keys($values) as $name) {
@@ -2706,7 +2706,7 @@  discard block
 block discarded – undo
2706 2706
 		$supportedProperties = array_keys($this->subscriptionPropertyMap);
2707 2707
 		$supportedProperties[] = '{http://calendarserver.org/ns/}source';
2708 2708
 
2709
-		$propPatch->handle($supportedProperties, function ($mutations) use ($subscriptionId) {
2709
+		$propPatch->handle($supportedProperties, function($mutations) use ($subscriptionId) {
2710 2710
 			$newValues = [];
2711 2711
 
2712 2712
 			foreach ($mutations as $propertyName => $propertyValue) {
@@ -2718,7 +2718,7 @@  discard block
 block discarded – undo
2718 2718
 				}
2719 2719
 			}
2720 2720
 
2721
-			$subscriptionRow = $this->atomic(function () use ($subscriptionId, $newValues) {
2721
+			$subscriptionRow = $this->atomic(function() use ($subscriptionId, $newValues) {
2722 2722
 				$query = $this->db->getQueryBuilder();
2723 2723
 				$query->update('calendarsubscriptions')
2724 2724
 					->set('lastmodified', $query->createNamedParameter(time()));
@@ -2731,7 +2731,7 @@  discard block
 block discarded – undo
2731 2731
 				return $this->getSubscriptionById($subscriptionId);
2732 2732
 			}, $this->db);
2733 2733
 
2734
-			$this->dispatcher->dispatchTyped(new SubscriptionUpdatedEvent((int)$subscriptionId, $subscriptionRow, [], $mutations));
2734
+			$this->dispatcher->dispatchTyped(new SubscriptionUpdatedEvent((int) $subscriptionId, $subscriptionRow, [], $mutations));
2735 2735
 
2736 2736
 			return true;
2737 2737
 		});
@@ -2744,7 +2744,7 @@  discard block
 block discarded – undo
2744 2744
 	 * @return void
2745 2745
 	 */
2746 2746
 	public function deleteSubscription($subscriptionId) {
2747
-		$this->atomic(function () use ($subscriptionId): void {
2747
+		$this->atomic(function() use ($subscriptionId): void {
2748 2748
 			$subscriptionRow = $this->getSubscriptionById($subscriptionId);
2749 2749
 
2750 2750
 			$query = $this->db->getQueryBuilder();
@@ -2769,7 +2769,7 @@  discard block
 block discarded – undo
2769 2769
 				->executeStatement();
2770 2770
 
2771 2771
 			if ($subscriptionRow) {
2772
-				$this->dispatcher->dispatchTyped(new SubscriptionDeletedEvent((int)$subscriptionId, $subscriptionRow, []));
2772
+				$this->dispatcher->dispatchTyped(new SubscriptionDeletedEvent((int) $subscriptionId, $subscriptionRow, []));
2773 2773
 			}
2774 2774
 		}, $this->db);
2775 2775
 	}
@@ -2808,8 +2808,8 @@  discard block
 block discarded – undo
2808 2808
 			'uri' => $row['uri'],
2809 2809
 			'calendardata' => $row['calendardata'],
2810 2810
 			'lastmodified' => $row['lastmodified'],
2811
-			'etag' => '"' . $row['etag'] . '"',
2812
-			'size' => (int)$row['size'],
2811
+			'etag' => '"'.$row['etag'].'"',
2812
+			'size' => (int) $row['size'],
2813 2813
 		];
2814 2814
 	}
2815 2815
 
@@ -2837,8 +2837,8 @@  discard block
 block discarded – undo
2837 2837
 				'calendardata' => $row['calendardata'],
2838 2838
 				'uri' => $row['uri'],
2839 2839
 				'lastmodified' => $row['lastmodified'],
2840
-				'etag' => '"' . $row['etag'] . '"',
2841
-				'size' => (int)$row['size'],
2840
+				'etag' => '"'.$row['etag'].'"',
2841
+				'size' => (int) $row['size'],
2842 2842
 			];
2843 2843
 		}
2844 2844
 		$stmt->closeCursor();
@@ -2880,8 +2880,8 @@  discard block
 block discarded – undo
2880 2880
 		if ($count === 0) {
2881 2881
 			return;
2882 2882
 		}
2883
-		$ids = array_map(static function (array $id) {
2884
-			return (int)$id[0];
2883
+		$ids = array_map(static function(array $id) {
2884
+			return (int) $id[0];
2885 2885
 		}, $result->fetchAll(\PDO::FETCH_NUM));
2886 2886
 		$result->closeCursor();
2887 2887
 
@@ -2934,15 +2934,15 @@  discard block
 block discarded – undo
2934 2934
 	 */
2935 2935
 	protected function addChanges(int $calendarId, array $objectUris, int $operation, int $calendarType = self::CALENDAR_TYPE_CALENDAR): void {
2936 2936
 		$this->cachedObjects = [];
2937
-		$table = $calendarType === self::CALENDAR_TYPE_CALENDAR ? 'calendars': 'calendarsubscriptions';
2937
+		$table = $calendarType === self::CALENDAR_TYPE_CALENDAR ? 'calendars' : 'calendarsubscriptions';
2938 2938
 
2939
-		$this->atomic(function () use ($calendarId, $objectUris, $operation, $calendarType, $table): void {
2939
+		$this->atomic(function() use ($calendarId, $objectUris, $operation, $calendarType, $table): void {
2940 2940
 			$query = $this->db->getQueryBuilder();
2941 2941
 			$query->select('synctoken')
2942 2942
 				->from($table)
2943 2943
 				->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)));
2944 2944
 			$result = $query->executeQuery();
2945
-			$syncToken = (int)$result->fetchOne();
2945
+			$syncToken = (int) $result->fetchOne();
2946 2946
 			$result->closeCursor();
2947 2947
 
2948 2948
 			$query = $this->db->getQueryBuilder();
@@ -2971,7 +2971,7 @@  discard block
 block discarded – undo
2971 2971
 	public function restoreChanges(int $calendarId, int $calendarType = self::CALENDAR_TYPE_CALENDAR): void {
2972 2972
 		$this->cachedObjects = [];
2973 2973
 
2974
-		$this->atomic(function () use ($calendarId, $calendarType): void {
2974
+		$this->atomic(function() use ($calendarId, $calendarType): void {
2975 2975
 			$qbAdded = $this->db->getQueryBuilder();
2976 2976
 			$qbAdded->select('uri')
2977 2977
 				->from('calendarobjects')
@@ -3001,7 +3001,7 @@  discard block
 block discarded – undo
3001 3001
 					)
3002 3002
 				);
3003 3003
 			$resultDeleted = $qbDeleted->executeQuery();
3004
-			$deletedUris = array_map(function (string $uri) {
3004
+			$deletedUris = array_map(function(string $uri) {
3005 3005
 				return str_replace('-deleted.ics', '.ics', $uri);
3006 3006
 			}, $resultDeleted->fetchAll(\PDO::FETCH_COLUMN));
3007 3007
 			$resultDeleted->closeCursor();
@@ -3046,7 +3046,7 @@  discard block
 block discarded – undo
3046 3046
 				// Track first component type and uid
3047 3047
 				if ($uid === null) {
3048 3048
 					$componentType = $component->name;
3049
-					$uid = (string)$component->UID;
3049
+					$uid = (string) $component->UID;
3050 3050
 				}
3051 3051
 			}
3052 3052
 		}
@@ -3137,11 +3137,11 @@  discard block
 block discarded – undo
3137 3137
 	 * @param list<string> $remove
3138 3138
 	 */
3139 3139
 	public function updateShares(IShareable $shareable, array $add, array $remove): void {
3140
-		$this->atomic(function () use ($shareable, $add, $remove): void {
3140
+		$this->atomic(function() use ($shareable, $add, $remove): void {
3141 3141
 			$calendarId = $shareable->getResourceId();
3142 3142
 			$calendarRow = $this->getCalendarById($calendarId);
3143 3143
 			if ($calendarRow === null) {
3144
-				throw new \RuntimeException('Trying to update shares for non-existing calendar: ' . $calendarId);
3144
+				throw new \RuntimeException('Trying to update shares for non-existing calendar: '.$calendarId);
3145 3145
 			}
3146 3146
 			$oldShares = $this->getShares($calendarId);
3147 3147
 
@@ -3168,7 +3168,7 @@  discard block
 block discarded – undo
3168 3168
 	 * @return string|null
3169 3169
 	 */
3170 3170
 	public function setPublishStatus($value, $calendar) {
3171
-		return $this->atomic(function () use ($value, $calendar) {
3171
+		return $this->atomic(function() use ($value, $calendar) {
3172 3172
 			$calendarId = $calendar->getResourceId();
3173 3173
 			$calendarData = $this->getCalendarById($calendarId);
3174 3174
 
@@ -3235,7 +3235,7 @@  discard block
 block discarded – undo
3235 3235
 	 */
3236 3236
 	public function updateProperties($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
3237 3237
 		$this->cachedObjects = [];
3238
-		$this->atomic(function () use ($calendarId, $objectUri, $calendarData, $calendarType): void {
3238
+		$this->atomic(function() use ($calendarId, $objectUri, $calendarData, $calendarType): void {
3239 3239
 			$objectId = $this->getCalendarObjectId($calendarId, $objectUri, $calendarType);
3240 3240
 
3241 3241
 			try {
@@ -3307,7 +3307,7 @@  discard block
 block discarded – undo
3307 3307
 	 * deletes all birthday calendars
3308 3308
 	 */
3309 3309
 	public function deleteAllBirthdayCalendars() {
3310
-		$this->atomic(function (): void {
3310
+		$this->atomic(function(): void {
3311 3311
 			$query = $this->db->getQueryBuilder();
3312 3312
 			$result = $query->select(['id'])->from('calendars')
3313 3313
 				->where($query->expr()->eq('uri', $query->createNamedParameter(BirthdayService::BIRTHDAY_CALENDAR_URI)))
@@ -3327,7 +3327,7 @@  discard block
 block discarded – undo
3327 3327
 	 * @param $subscriptionId
3328 3328
 	 */
3329 3329
 	public function purgeAllCachedEventsForSubscription($subscriptionId) {
3330
-		$this->atomic(function () use ($subscriptionId): void {
3330
+		$this->atomic(function() use ($subscriptionId): void {
3331 3331
 			$query = $this->db->getQueryBuilder();
3332 3332
 			$query->select('uri')
3333 3333
 				->from('calendarobjects')
@@ -3373,7 +3373,7 @@  discard block
 block discarded – undo
3373 3373
 			return;
3374 3374
 		}
3375 3375
 
3376
-		$this->atomic(function () use ($subscriptionId, $calendarObjectIds, $calendarObjectUris): void {
3376
+		$this->atomic(function() use ($subscriptionId, $calendarObjectIds, $calendarObjectUris): void {
3377 3377
 			foreach (array_chunk($calendarObjectIds, 1000) as $chunk) {
3378 3378
 				$query = $this->db->getQueryBuilder();
3379 3379
 				$query->delete($this->dbObjectPropertiesTable)
@@ -3466,10 +3466,10 @@  discard block
 block discarded – undo
3466 3466
 		$result->closeCursor();
3467 3467
 
3468 3468
 		if (!isset($objectIds['id'])) {
3469
-			throw new \InvalidArgumentException('Calendarobject does not exists: ' . $uri);
3469
+			throw new \InvalidArgumentException('Calendarobject does not exists: '.$uri);
3470 3470
 		}
3471 3471
 
3472
-		return (int)$objectIds['id'];
3472
+		return (int) $objectIds['id'];
3473 3473
 	}
3474 3474
 
3475 3475
 	/**
@@ -3485,7 +3485,7 @@  discard block
 block discarded – undo
3485 3485
 			->from('calendarchanges');
3486 3486
 
3487 3487
 		$result = $query->executeQuery();
3488
-		$maxId = (int)$result->fetchOne();
3488
+		$maxId = (int) $result->fetchOne();
3489 3489
 		$result->closeCursor();
3490 3490
 		if (!$maxId || $maxId < $keep) {
3491 3491
 			return 0;
@@ -3523,8 +3523,8 @@  discard block
 block discarded – undo
3523 3523
 	 *
3524 3524
 	 */
3525 3525
 	private function addOwnerPrincipalToCalendar(array $calendarInfo): array {
3526
-		$ownerPrincipalKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal';
3527
-		$displaynameKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}owner-displayname';
3526
+		$ownerPrincipalKey = '{'.\OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD.'}owner-principal';
3527
+		$displaynameKey = '{'.\OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD.'}owner-displayname';
3528 3528
 		if (isset($calendarInfo[$ownerPrincipalKey])) {
3529 3529
 			$uri = $calendarInfo[$ownerPrincipalKey];
3530 3530
 		} else {
Please login to merge, or discard this patch.
apps/dav/lib/CalDAV/Calendar.php 2 patches
Indentation   +373 added lines, -373 removed lines patch added patch discarded remove patch
@@ -31,377 +31,377 @@
 block discarded – undo
31 31
  * @property CalDavBackend $caldavBackend
32 32
  */
33 33
 class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable, IMoveTarget {
34
-	protected IL10N $l10n;
35
-	private bool $useTrashbin = true;
36
-
37
-	public function __construct(
38
-		BackendInterface $caldavBackend,
39
-		$calendarInfo,
40
-		IL10N $l10n,
41
-		private IConfig $config,
42
-		private LoggerInterface $logger,
43
-	) {
44
-		// Convert deletion date to ISO8601 string
45
-		if (isset($calendarInfo[TrashbinPlugin::PROPERTY_DELETED_AT])) {
46
-			$calendarInfo[TrashbinPlugin::PROPERTY_DELETED_AT] = (new DateTimeImmutable())
47
-				->setTimestamp($calendarInfo[TrashbinPlugin::PROPERTY_DELETED_AT])
48
-				->format(DateTimeInterface::ATOM);
49
-		}
50
-
51
-		parent::__construct($caldavBackend, $calendarInfo);
52
-
53
-		if ($this->getName() === BirthdayService::BIRTHDAY_CALENDAR_URI && strcasecmp($this->calendarInfo['{DAV:}displayname'], 'Contact birthdays') === 0) {
54
-			$this->calendarInfo['{DAV:}displayname'] = $l10n->t('Contact birthdays');
55
-		}
56
-		if ($this->getName() === CalDavBackend::PERSONAL_CALENDAR_URI &&
57
-			$this->calendarInfo['{DAV:}displayname'] === CalDavBackend::PERSONAL_CALENDAR_NAME) {
58
-			$this->calendarInfo['{DAV:}displayname'] = $l10n->t('Personal');
59
-		}
60
-		$this->l10n = $l10n;
61
-	}
62
-
63
-	/**
64
-	 * {@inheritdoc}
65
-	 * @throws Forbidden
66
-	 */
67
-	public function updateShares(array $add, array $remove): void {
68
-		if ($this->isShared()) {
69
-			throw new Forbidden();
70
-		}
71
-		$this->caldavBackend->updateShares($this, $add, $remove);
72
-	}
73
-
74
-	/**
75
-	 * Returns the list of people whom this resource is shared with.
76
-	 *
77
-	 * Every element in this array should have the following properties:
78
-	 *   * href - Often a mailto: address
79
-	 *   * commonName - Optional, for example a first + last name
80
-	 *   * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants.
81
-	 *   * readOnly - boolean
82
-	 *   * summary - Optional, a description for the share
83
-	 *
84
-	 * @return list<array{href: string, commonName: string, status: int, readOnly: bool, '{http://owncloud.org/ns}principal': string, '{http://owncloud.org/ns}group-share': bool}>
85
-	 */
86
-	public function getShares(): array {
87
-		if ($this->isShared()) {
88
-			return [];
89
-		}
90
-		return $this->caldavBackend->getShares($this->getResourceId());
91
-	}
92
-
93
-	public function getResourceId(): int {
94
-		return $this->calendarInfo['id'];
95
-	}
96
-
97
-	/**
98
-	 * @return string
99
-	 */
100
-	public function getPrincipalURI() {
101
-		return $this->calendarInfo['principaluri'];
102
-	}
103
-
104
-	/**
105
-	 * @param int $resourceId
106
-	 * @param list<array{privilege: string, principal: string, protected: bool}> $acl
107
-	 * @return list<array{privilege: string, principal: ?string, protected: bool}>
108
-	 */
109
-	public function getACL() {
110
-		$acl = [
111
-			[
112
-				'privilege' => '{DAV:}read',
113
-				'principal' => $this->getOwner(),
114
-				'protected' => true,
115
-			],
116
-			[
117
-				'privilege' => '{DAV:}read',
118
-				'principal' => $this->getOwner() . '/calendar-proxy-write',
119
-				'protected' => true,
120
-			],
121
-			[
122
-				'privilege' => '{DAV:}read',
123
-				'principal' => $this->getOwner() . '/calendar-proxy-read',
124
-				'protected' => true,
125
-			],
126
-		];
127
-
128
-		if ($this->getName() !== BirthdayService::BIRTHDAY_CALENDAR_URI) {
129
-			$acl[] = [
130
-				'privilege' => '{DAV:}write',
131
-				'principal' => $this->getOwner(),
132
-				'protected' => true,
133
-			];
134
-			$acl[] = [
135
-				'privilege' => '{DAV:}write',
136
-				'principal' => $this->getOwner() . '/calendar-proxy-write',
137
-				'protected' => true,
138
-			];
139
-		} else {
140
-			$acl[] = [
141
-				'privilege' => '{DAV:}write-properties',
142
-				'principal' => $this->getOwner(),
143
-				'protected' => true,
144
-			];
145
-			$acl[] = [
146
-				'privilege' => '{DAV:}write-properties',
147
-				'principal' => $this->getOwner() . '/calendar-proxy-write',
148
-				'protected' => true,
149
-			];
150
-		}
151
-
152
-		$acl[] = [
153
-			'privilege' => '{DAV:}write-properties',
154
-			'principal' => $this->getOwner() . '/calendar-proxy-read',
155
-			'protected' => true,
156
-		];
157
-
158
-		if (!$this->isShared()) {
159
-			return $acl;
160
-		}
161
-
162
-		if ($this->getOwner() !== parent::getOwner()) {
163
-			$acl[] = [
164
-				'privilege' => '{DAV:}read',
165
-				'principal' => parent::getOwner(),
166
-				'protected' => true,
167
-			];
168
-			if ($this->canWrite()) {
169
-				$acl[] = [
170
-					'privilege' => '{DAV:}write',
171
-					'principal' => parent::getOwner(),
172
-					'protected' => true,
173
-				];
174
-			} else {
175
-				$acl[] = [
176
-					'privilege' => '{DAV:}write-properties',
177
-					'principal' => parent::getOwner(),
178
-					'protected' => true,
179
-				];
180
-			}
181
-		}
182
-		if ($this->isPublic()) {
183
-			$acl[] = [
184
-				'privilege' => '{DAV:}read',
185
-				'principal' => 'principals/system/public',
186
-				'protected' => true,
187
-			];
188
-		}
189
-
190
-		$acl = $this->caldavBackend->applyShareAcl($this->getResourceId(), $acl);
191
-		$allowedPrincipals = [
192
-			$this->getOwner(),
193
-			$this->getOwner() . '/calendar-proxy-read',
194
-			$this->getOwner() . '/calendar-proxy-write',
195
-			parent::getOwner(),
196
-			'principals/system/public'
197
-		];
198
-		/** @var list<array{privilege: string, principal: string, protected: bool}> $acl */
199
-		$acl = array_filter($acl, function (array $rule) use ($allowedPrincipals): bool {
200
-			return \in_array($rule['principal'], $allowedPrincipals, true);
201
-		});
202
-		return $acl;
203
-	}
204
-
205
-	public function getChildACL() {
206
-		return $this->getACL();
207
-	}
208
-
209
-	public function getOwner(): ?string {
210
-		if (isset($this->calendarInfo['{http://owncloud.org/ns}owner-principal'])) {
211
-			return $this->calendarInfo['{http://owncloud.org/ns}owner-principal'];
212
-		}
213
-		return parent::getOwner();
214
-	}
215
-
216
-	public function delete() {
217
-		if (isset($this->calendarInfo['{http://owncloud.org/ns}owner-principal']) &&
218
-			$this->calendarInfo['{http://owncloud.org/ns}owner-principal'] !== $this->calendarInfo['principaluri']) {
219
-			$principal = 'principal:' . parent::getOwner();
220
-			$this->caldavBackend->updateShares($this, [], [
221
-				$principal
222
-			]);
223
-			return;
224
-		}
225
-
226
-		// Remember when a user deleted their birthday calendar
227
-		// in order to not regenerate it on the next contacts change
228
-		if ($this->getName() === BirthdayService::BIRTHDAY_CALENDAR_URI) {
229
-			$principalURI = $this->getPrincipalURI();
230
-			$userId = substr($principalURI, 17);
231
-
232
-			$this->config->setUserValue($userId, 'dav', 'generateBirthdayCalendar', 'no');
233
-		}
234
-
235
-		$this->caldavBackend->deleteCalendar(
236
-			$this->calendarInfo['id'],
237
-			!$this->useTrashbin
238
-		);
239
-	}
240
-
241
-	public function propPatch(PropPatch $propPatch) {
242
-		// parent::propPatch will only update calendars table
243
-		// if calendar is shared, changes have to be made to the properties table
244
-		if (!$this->isShared()) {
245
-			parent::propPatch($propPatch);
246
-		}
247
-	}
248
-
249
-	public function getChild($name) {
250
-		$obj = $this->caldavBackend->getCalendarObject($this->calendarInfo['id'], $name);
251
-
252
-		if (!$obj) {
253
-			throw new NotFound('Calendar object not found');
254
-		}
255
-
256
-		if ($obj['classification'] === CalDavBackend::CLASSIFICATION_PRIVATE && $this->isShared()) {
257
-			throw new NotFound('Calendar object not found');
258
-		}
259
-
260
-		$obj['acl'] = $this->getChildACL();
261
-
262
-		return new CalendarObject($this->caldavBackend, $this->l10n, $this->calendarInfo, $obj);
263
-	}
264
-
265
-	public function getChildren() {
266
-		$objs = $this->caldavBackend->getCalendarObjects($this->calendarInfo['id']);
267
-		$children = [];
268
-		foreach ($objs as $obj) {
269
-			if ($obj['classification'] === CalDavBackend::CLASSIFICATION_PRIVATE && $this->isShared()) {
270
-				continue;
271
-			}
272
-			$obj['acl'] = $this->getChildACL();
273
-			$children[] = new CalendarObject($this->caldavBackend, $this->l10n, $this->calendarInfo, $obj);
274
-		}
275
-		return $children;
276
-	}
277
-
278
-	public function getMultipleChildren(array $paths) {
279
-		$objs = $this->caldavBackend->getMultipleCalendarObjects($this->calendarInfo['id'], $paths);
280
-		$children = [];
281
-		foreach ($objs as $obj) {
282
-			if ($obj['classification'] === CalDavBackend::CLASSIFICATION_PRIVATE && $this->isShared()) {
283
-				continue;
284
-			}
285
-			$obj['acl'] = $this->getChildACL();
286
-			$children[] = new CalendarObject($this->caldavBackend, $this->l10n, $this->calendarInfo, $obj);
287
-		}
288
-		return $children;
289
-	}
290
-
291
-	public function childExists($name) {
292
-		$obj = $this->caldavBackend->getCalendarObject($this->calendarInfo['id'], $name);
293
-		if (!$obj) {
294
-			return false;
295
-		}
296
-		if ($obj['classification'] === CalDavBackend::CLASSIFICATION_PRIVATE && $this->isShared()) {
297
-			return false;
298
-		}
299
-
300
-		return true;
301
-	}
302
-
303
-	public function calendarQuery(array $filters) {
304
-		$uris = $this->caldavBackend->calendarQuery($this->calendarInfo['id'], $filters);
305
-		if ($this->isShared()) {
306
-			return array_filter($uris, function ($uri) {
307
-				return $this->childExists($uri);
308
-			});
309
-		}
310
-
311
-		return $uris;
312
-	}
313
-
314
-	/**
315
-	 * @param boolean $value
316
-	 * @return string|null
317
-	 */
318
-	public function setPublishStatus($value) {
319
-		$publicUri = $this->caldavBackend->setPublishStatus($value, $this);
320
-		$this->calendarInfo['publicuri'] = $publicUri;
321
-		return $publicUri;
322
-	}
323
-
324
-	/**
325
-	 * @return mixed $value
326
-	 */
327
-	public function getPublishStatus() {
328
-		return $this->caldavBackend->getPublishStatus($this);
329
-	}
330
-
331
-	public function canWrite() {
332
-		if ($this->getName() === BirthdayService::BIRTHDAY_CALENDAR_URI) {
333
-			return false;
334
-		}
335
-
336
-		if (isset($this->calendarInfo['{http://owncloud.org/ns}read-only'])) {
337
-			return !$this->calendarInfo['{http://owncloud.org/ns}read-only'];
338
-		}
339
-		return true;
340
-	}
341
-
342
-	private function isPublic() {
343
-		return isset($this->calendarInfo['{http://owncloud.org/ns}public']);
344
-	}
345
-
346
-	public function isShared() {
347
-		if (!isset($this->calendarInfo['{http://owncloud.org/ns}owner-principal'])) {
348
-			return false;
349
-		}
350
-
351
-		return $this->calendarInfo['{http://owncloud.org/ns}owner-principal'] !== $this->calendarInfo['principaluri'];
352
-	}
353
-
354
-	public function isSubscription() {
355
-		return isset($this->calendarInfo['{http://calendarserver.org/ns/}source']);
356
-	}
357
-
358
-	public function isDeleted(): bool {
359
-		if (!isset($this->calendarInfo[TrashbinPlugin::PROPERTY_DELETED_AT])) {
360
-			return false;
361
-		}
362
-		return $this->calendarInfo[TrashbinPlugin::PROPERTY_DELETED_AT] !== null;
363
-	}
364
-
365
-	/**
366
-	 * @inheritDoc
367
-	 */
368
-	public function getChanges($syncToken, $syncLevel, $limit = null) {
369
-		if (!$syncToken && $limit) {
370
-			throw new UnsupportedLimitOnInitialSyncException();
371
-		}
372
-
373
-		return parent::getChanges($syncToken, $syncLevel, $limit);
374
-	}
375
-
376
-	/**
377
-	 * @inheritDoc
378
-	 */
379
-	public function restore(): void {
380
-		$this->caldavBackend->restoreCalendar((int)$this->calendarInfo['id']);
381
-	}
382
-
383
-	public function disableTrashbin(): void {
384
-		$this->useTrashbin = false;
385
-	}
386
-
387
-	/**
388
-	 * @inheritDoc
389
-	 */
390
-	public function moveInto($targetName, $sourcePath, INode $sourceNode) {
391
-		if (!($sourceNode instanceof CalendarObject)) {
392
-			return false;
393
-		}
394
-		try {
395
-			return $this->caldavBackend->moveCalendarObject(
396
-				$sourceNode->getOwner(),
397
-				$sourceNode->getId(),
398
-				$this->getOwner(),
399
-				$this->getResourceId(),
400
-				$targetName,
401
-			);
402
-		} catch (Exception $e) {
403
-			$this->logger->error('Could not move calendar object: ' . $e->getMessage(), ['exception' => $e]);
404
-			return false;
405
-		}
406
-	}
34
+    protected IL10N $l10n;
35
+    private bool $useTrashbin = true;
36
+
37
+    public function __construct(
38
+        BackendInterface $caldavBackend,
39
+        $calendarInfo,
40
+        IL10N $l10n,
41
+        private IConfig $config,
42
+        private LoggerInterface $logger,
43
+    ) {
44
+        // Convert deletion date to ISO8601 string
45
+        if (isset($calendarInfo[TrashbinPlugin::PROPERTY_DELETED_AT])) {
46
+            $calendarInfo[TrashbinPlugin::PROPERTY_DELETED_AT] = (new DateTimeImmutable())
47
+                ->setTimestamp($calendarInfo[TrashbinPlugin::PROPERTY_DELETED_AT])
48
+                ->format(DateTimeInterface::ATOM);
49
+        }
50
+
51
+        parent::__construct($caldavBackend, $calendarInfo);
52
+
53
+        if ($this->getName() === BirthdayService::BIRTHDAY_CALENDAR_URI && strcasecmp($this->calendarInfo['{DAV:}displayname'], 'Contact birthdays') === 0) {
54
+            $this->calendarInfo['{DAV:}displayname'] = $l10n->t('Contact birthdays');
55
+        }
56
+        if ($this->getName() === CalDavBackend::PERSONAL_CALENDAR_URI &&
57
+            $this->calendarInfo['{DAV:}displayname'] === CalDavBackend::PERSONAL_CALENDAR_NAME) {
58
+            $this->calendarInfo['{DAV:}displayname'] = $l10n->t('Personal');
59
+        }
60
+        $this->l10n = $l10n;
61
+    }
62
+
63
+    /**
64
+     * {@inheritdoc}
65
+     * @throws Forbidden
66
+     */
67
+    public function updateShares(array $add, array $remove): void {
68
+        if ($this->isShared()) {
69
+            throw new Forbidden();
70
+        }
71
+        $this->caldavBackend->updateShares($this, $add, $remove);
72
+    }
73
+
74
+    /**
75
+     * Returns the list of people whom this resource is shared with.
76
+     *
77
+     * Every element in this array should have the following properties:
78
+     *   * href - Often a mailto: address
79
+     *   * commonName - Optional, for example a first + last name
80
+     *   * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants.
81
+     *   * readOnly - boolean
82
+     *   * summary - Optional, a description for the share
83
+     *
84
+     * @return list<array{href: string, commonName: string, status: int, readOnly: bool, '{http://owncloud.org/ns}principal': string, '{http://owncloud.org/ns}group-share': bool}>
85
+     */
86
+    public function getShares(): array {
87
+        if ($this->isShared()) {
88
+            return [];
89
+        }
90
+        return $this->caldavBackend->getShares($this->getResourceId());
91
+    }
92
+
93
+    public function getResourceId(): int {
94
+        return $this->calendarInfo['id'];
95
+    }
96
+
97
+    /**
98
+     * @return string
99
+     */
100
+    public function getPrincipalURI() {
101
+        return $this->calendarInfo['principaluri'];
102
+    }
103
+
104
+    /**
105
+     * @param int $resourceId
106
+     * @param list<array{privilege: string, principal: string, protected: bool}> $acl
107
+     * @return list<array{privilege: string, principal: ?string, protected: bool}>
108
+     */
109
+    public function getACL() {
110
+        $acl = [
111
+            [
112
+                'privilege' => '{DAV:}read',
113
+                'principal' => $this->getOwner(),
114
+                'protected' => true,
115
+            ],
116
+            [
117
+                'privilege' => '{DAV:}read',
118
+                'principal' => $this->getOwner() . '/calendar-proxy-write',
119
+                'protected' => true,
120
+            ],
121
+            [
122
+                'privilege' => '{DAV:}read',
123
+                'principal' => $this->getOwner() . '/calendar-proxy-read',
124
+                'protected' => true,
125
+            ],
126
+        ];
127
+
128
+        if ($this->getName() !== BirthdayService::BIRTHDAY_CALENDAR_URI) {
129
+            $acl[] = [
130
+                'privilege' => '{DAV:}write',
131
+                'principal' => $this->getOwner(),
132
+                'protected' => true,
133
+            ];
134
+            $acl[] = [
135
+                'privilege' => '{DAV:}write',
136
+                'principal' => $this->getOwner() . '/calendar-proxy-write',
137
+                'protected' => true,
138
+            ];
139
+        } else {
140
+            $acl[] = [
141
+                'privilege' => '{DAV:}write-properties',
142
+                'principal' => $this->getOwner(),
143
+                'protected' => true,
144
+            ];
145
+            $acl[] = [
146
+                'privilege' => '{DAV:}write-properties',
147
+                'principal' => $this->getOwner() . '/calendar-proxy-write',
148
+                'protected' => true,
149
+            ];
150
+        }
151
+
152
+        $acl[] = [
153
+            'privilege' => '{DAV:}write-properties',
154
+            'principal' => $this->getOwner() . '/calendar-proxy-read',
155
+            'protected' => true,
156
+        ];
157
+
158
+        if (!$this->isShared()) {
159
+            return $acl;
160
+        }
161
+
162
+        if ($this->getOwner() !== parent::getOwner()) {
163
+            $acl[] = [
164
+                'privilege' => '{DAV:}read',
165
+                'principal' => parent::getOwner(),
166
+                'protected' => true,
167
+            ];
168
+            if ($this->canWrite()) {
169
+                $acl[] = [
170
+                    'privilege' => '{DAV:}write',
171
+                    'principal' => parent::getOwner(),
172
+                    'protected' => true,
173
+                ];
174
+            } else {
175
+                $acl[] = [
176
+                    'privilege' => '{DAV:}write-properties',
177
+                    'principal' => parent::getOwner(),
178
+                    'protected' => true,
179
+                ];
180
+            }
181
+        }
182
+        if ($this->isPublic()) {
183
+            $acl[] = [
184
+                'privilege' => '{DAV:}read',
185
+                'principal' => 'principals/system/public',
186
+                'protected' => true,
187
+            ];
188
+        }
189
+
190
+        $acl = $this->caldavBackend->applyShareAcl($this->getResourceId(), $acl);
191
+        $allowedPrincipals = [
192
+            $this->getOwner(),
193
+            $this->getOwner() . '/calendar-proxy-read',
194
+            $this->getOwner() . '/calendar-proxy-write',
195
+            parent::getOwner(),
196
+            'principals/system/public'
197
+        ];
198
+        /** @var list<array{privilege: string, principal: string, protected: bool}> $acl */
199
+        $acl = array_filter($acl, function (array $rule) use ($allowedPrincipals): bool {
200
+            return \in_array($rule['principal'], $allowedPrincipals, true);
201
+        });
202
+        return $acl;
203
+    }
204
+
205
+    public function getChildACL() {
206
+        return $this->getACL();
207
+    }
208
+
209
+    public function getOwner(): ?string {
210
+        if (isset($this->calendarInfo['{http://owncloud.org/ns}owner-principal'])) {
211
+            return $this->calendarInfo['{http://owncloud.org/ns}owner-principal'];
212
+        }
213
+        return parent::getOwner();
214
+    }
215
+
216
+    public function delete() {
217
+        if (isset($this->calendarInfo['{http://owncloud.org/ns}owner-principal']) &&
218
+            $this->calendarInfo['{http://owncloud.org/ns}owner-principal'] !== $this->calendarInfo['principaluri']) {
219
+            $principal = 'principal:' . parent::getOwner();
220
+            $this->caldavBackend->updateShares($this, [], [
221
+                $principal
222
+            ]);
223
+            return;
224
+        }
225
+
226
+        // Remember when a user deleted their birthday calendar
227
+        // in order to not regenerate it on the next contacts change
228
+        if ($this->getName() === BirthdayService::BIRTHDAY_CALENDAR_URI) {
229
+            $principalURI = $this->getPrincipalURI();
230
+            $userId = substr($principalURI, 17);
231
+
232
+            $this->config->setUserValue($userId, 'dav', 'generateBirthdayCalendar', 'no');
233
+        }
234
+
235
+        $this->caldavBackend->deleteCalendar(
236
+            $this->calendarInfo['id'],
237
+            !$this->useTrashbin
238
+        );
239
+    }
240
+
241
+    public function propPatch(PropPatch $propPatch) {
242
+        // parent::propPatch will only update calendars table
243
+        // if calendar is shared, changes have to be made to the properties table
244
+        if (!$this->isShared()) {
245
+            parent::propPatch($propPatch);
246
+        }
247
+    }
248
+
249
+    public function getChild($name) {
250
+        $obj = $this->caldavBackend->getCalendarObject($this->calendarInfo['id'], $name);
251
+
252
+        if (!$obj) {
253
+            throw new NotFound('Calendar object not found');
254
+        }
255
+
256
+        if ($obj['classification'] === CalDavBackend::CLASSIFICATION_PRIVATE && $this->isShared()) {
257
+            throw new NotFound('Calendar object not found');
258
+        }
259
+
260
+        $obj['acl'] = $this->getChildACL();
261
+
262
+        return new CalendarObject($this->caldavBackend, $this->l10n, $this->calendarInfo, $obj);
263
+    }
264
+
265
+    public function getChildren() {
266
+        $objs = $this->caldavBackend->getCalendarObjects($this->calendarInfo['id']);
267
+        $children = [];
268
+        foreach ($objs as $obj) {
269
+            if ($obj['classification'] === CalDavBackend::CLASSIFICATION_PRIVATE && $this->isShared()) {
270
+                continue;
271
+            }
272
+            $obj['acl'] = $this->getChildACL();
273
+            $children[] = new CalendarObject($this->caldavBackend, $this->l10n, $this->calendarInfo, $obj);
274
+        }
275
+        return $children;
276
+    }
277
+
278
+    public function getMultipleChildren(array $paths) {
279
+        $objs = $this->caldavBackend->getMultipleCalendarObjects($this->calendarInfo['id'], $paths);
280
+        $children = [];
281
+        foreach ($objs as $obj) {
282
+            if ($obj['classification'] === CalDavBackend::CLASSIFICATION_PRIVATE && $this->isShared()) {
283
+                continue;
284
+            }
285
+            $obj['acl'] = $this->getChildACL();
286
+            $children[] = new CalendarObject($this->caldavBackend, $this->l10n, $this->calendarInfo, $obj);
287
+        }
288
+        return $children;
289
+    }
290
+
291
+    public function childExists($name) {
292
+        $obj = $this->caldavBackend->getCalendarObject($this->calendarInfo['id'], $name);
293
+        if (!$obj) {
294
+            return false;
295
+        }
296
+        if ($obj['classification'] === CalDavBackend::CLASSIFICATION_PRIVATE && $this->isShared()) {
297
+            return false;
298
+        }
299
+
300
+        return true;
301
+    }
302
+
303
+    public function calendarQuery(array $filters) {
304
+        $uris = $this->caldavBackend->calendarQuery($this->calendarInfo['id'], $filters);
305
+        if ($this->isShared()) {
306
+            return array_filter($uris, function ($uri) {
307
+                return $this->childExists($uri);
308
+            });
309
+        }
310
+
311
+        return $uris;
312
+    }
313
+
314
+    /**
315
+     * @param boolean $value
316
+     * @return string|null
317
+     */
318
+    public function setPublishStatus($value) {
319
+        $publicUri = $this->caldavBackend->setPublishStatus($value, $this);
320
+        $this->calendarInfo['publicuri'] = $publicUri;
321
+        return $publicUri;
322
+    }
323
+
324
+    /**
325
+     * @return mixed $value
326
+     */
327
+    public function getPublishStatus() {
328
+        return $this->caldavBackend->getPublishStatus($this);
329
+    }
330
+
331
+    public function canWrite() {
332
+        if ($this->getName() === BirthdayService::BIRTHDAY_CALENDAR_URI) {
333
+            return false;
334
+        }
335
+
336
+        if (isset($this->calendarInfo['{http://owncloud.org/ns}read-only'])) {
337
+            return !$this->calendarInfo['{http://owncloud.org/ns}read-only'];
338
+        }
339
+        return true;
340
+    }
341
+
342
+    private function isPublic() {
343
+        return isset($this->calendarInfo['{http://owncloud.org/ns}public']);
344
+    }
345
+
346
+    public function isShared() {
347
+        if (!isset($this->calendarInfo['{http://owncloud.org/ns}owner-principal'])) {
348
+            return false;
349
+        }
350
+
351
+        return $this->calendarInfo['{http://owncloud.org/ns}owner-principal'] !== $this->calendarInfo['principaluri'];
352
+    }
353
+
354
+    public function isSubscription() {
355
+        return isset($this->calendarInfo['{http://calendarserver.org/ns/}source']);
356
+    }
357
+
358
+    public function isDeleted(): bool {
359
+        if (!isset($this->calendarInfo[TrashbinPlugin::PROPERTY_DELETED_AT])) {
360
+            return false;
361
+        }
362
+        return $this->calendarInfo[TrashbinPlugin::PROPERTY_DELETED_AT] !== null;
363
+    }
364
+
365
+    /**
366
+     * @inheritDoc
367
+     */
368
+    public function getChanges($syncToken, $syncLevel, $limit = null) {
369
+        if (!$syncToken && $limit) {
370
+            throw new UnsupportedLimitOnInitialSyncException();
371
+        }
372
+
373
+        return parent::getChanges($syncToken, $syncLevel, $limit);
374
+    }
375
+
376
+    /**
377
+     * @inheritDoc
378
+     */
379
+    public function restore(): void {
380
+        $this->caldavBackend->restoreCalendar((int)$this->calendarInfo['id']);
381
+    }
382
+
383
+    public function disableTrashbin(): void {
384
+        $this->useTrashbin = false;
385
+    }
386
+
387
+    /**
388
+     * @inheritDoc
389
+     */
390
+    public function moveInto($targetName, $sourcePath, INode $sourceNode) {
391
+        if (!($sourceNode instanceof CalendarObject)) {
392
+            return false;
393
+        }
394
+        try {
395
+            return $this->caldavBackend->moveCalendarObject(
396
+                $sourceNode->getOwner(),
397
+                $sourceNode->getId(),
398
+                $this->getOwner(),
399
+                $this->getResourceId(),
400
+                $targetName,
401
+            );
402
+        } catch (Exception $e) {
403
+            $this->logger->error('Could not move calendar object: ' . $e->getMessage(), ['exception' => $e]);
404
+            return false;
405
+        }
406
+    }
407 407
 }
Please login to merge, or discard this patch.
Spacing   +12 added lines, -12 removed lines patch added patch discarded remove patch
@@ -115,12 +115,12 @@  discard block
 block discarded – undo
115 115
 			],
116 116
 			[
117 117
 				'privilege' => '{DAV:}read',
118
-				'principal' => $this->getOwner() . '/calendar-proxy-write',
118
+				'principal' => $this->getOwner().'/calendar-proxy-write',
119 119
 				'protected' => true,
120 120
 			],
121 121
 			[
122 122
 				'privilege' => '{DAV:}read',
123
-				'principal' => $this->getOwner() . '/calendar-proxy-read',
123
+				'principal' => $this->getOwner().'/calendar-proxy-read',
124 124
 				'protected' => true,
125 125
 			],
126 126
 		];
@@ -133,7 +133,7 @@  discard block
 block discarded – undo
133 133
 			];
134 134
 			$acl[] = [
135 135
 				'privilege' => '{DAV:}write',
136
-				'principal' => $this->getOwner() . '/calendar-proxy-write',
136
+				'principal' => $this->getOwner().'/calendar-proxy-write',
137 137
 				'protected' => true,
138 138
 			];
139 139
 		} else {
@@ -144,14 +144,14 @@  discard block
 block discarded – undo
144 144
 			];
145 145
 			$acl[] = [
146 146
 				'privilege' => '{DAV:}write-properties',
147
-				'principal' => $this->getOwner() . '/calendar-proxy-write',
147
+				'principal' => $this->getOwner().'/calendar-proxy-write',
148 148
 				'protected' => true,
149 149
 			];
150 150
 		}
151 151
 
152 152
 		$acl[] = [
153 153
 			'privilege' => '{DAV:}write-properties',
154
-			'principal' => $this->getOwner() . '/calendar-proxy-read',
154
+			'principal' => $this->getOwner().'/calendar-proxy-read',
155 155
 			'protected' => true,
156 156
 		];
157 157
 
@@ -190,13 +190,13 @@  discard block
 block discarded – undo
190 190
 		$acl = $this->caldavBackend->applyShareAcl($this->getResourceId(), $acl);
191 191
 		$allowedPrincipals = [
192 192
 			$this->getOwner(),
193
-			$this->getOwner() . '/calendar-proxy-read',
194
-			$this->getOwner() . '/calendar-proxy-write',
193
+			$this->getOwner().'/calendar-proxy-read',
194
+			$this->getOwner().'/calendar-proxy-write',
195 195
 			parent::getOwner(),
196 196
 			'principals/system/public'
197 197
 		];
198 198
 		/** @var list<array{privilege: string, principal: string, protected: bool}> $acl */
199
-		$acl = array_filter($acl, function (array $rule) use ($allowedPrincipals): bool {
199
+		$acl = array_filter($acl, function(array $rule) use ($allowedPrincipals): bool {
200 200
 			return \in_array($rule['principal'], $allowedPrincipals, true);
201 201
 		});
202 202
 		return $acl;
@@ -216,7 +216,7 @@  discard block
 block discarded – undo
216 216
 	public function delete() {
217 217
 		if (isset($this->calendarInfo['{http://owncloud.org/ns}owner-principal']) &&
218 218
 			$this->calendarInfo['{http://owncloud.org/ns}owner-principal'] !== $this->calendarInfo['principaluri']) {
219
-			$principal = 'principal:' . parent::getOwner();
219
+			$principal = 'principal:'.parent::getOwner();
220 220
 			$this->caldavBackend->updateShares($this, [], [
221 221
 				$principal
222 222
 			]);
@@ -303,7 +303,7 @@  discard block
 block discarded – undo
303 303
 	public function calendarQuery(array $filters) {
304 304
 		$uris = $this->caldavBackend->calendarQuery($this->calendarInfo['id'], $filters);
305 305
 		if ($this->isShared()) {
306
-			return array_filter($uris, function ($uri) {
306
+			return array_filter($uris, function($uri) {
307 307
 				return $this->childExists($uri);
308 308
 			});
309 309
 		}
@@ -377,7 +377,7 @@  discard block
 block discarded – undo
377 377
 	 * @inheritDoc
378 378
 	 */
379 379
 	public function restore(): void {
380
-		$this->caldavBackend->restoreCalendar((int)$this->calendarInfo['id']);
380
+		$this->caldavBackend->restoreCalendar((int) $this->calendarInfo['id']);
381 381
 	}
382 382
 
383 383
 	public function disableTrashbin(): void {
@@ -400,7 +400,7 @@  discard block
 block discarded – undo
400 400
 				$targetName,
401 401
 			);
402 402
 		} catch (Exception $e) {
403
-			$this->logger->error('Could not move calendar object: ' . $e->getMessage(), ['exception' => $e]);
403
+			$this->logger->error('Could not move calendar object: '.$e->getMessage(), ['exception' => $e]);
404 404
 			return false;
405 405
 		}
406 406
 	}
Please login to merge, or discard this patch.