Passed
Push — master ( d3d534...5f2afa )
by Christoph
14:45 queued 25s
created
apps/dav/lib/CalDAV/CalDavBackend.php 2 patches
Indentation   +3151 added lines, -3151 removed lines patch added patch discarded remove patch
@@ -119,3155 +119,3155 @@
 block discarded – undo
119 119
  * @package OCA\DAV\CalDAV
120 120
  */
121 121
 class CalDavBackend extends AbstractBackend implements SyncSupport, SubscriptionSupport, SchedulingSupport {
122
-	public const CALENDAR_TYPE_CALENDAR = 0;
123
-	public const CALENDAR_TYPE_SUBSCRIPTION = 1;
124
-
125
-	public const PERSONAL_CALENDAR_URI = 'personal';
126
-	public const PERSONAL_CALENDAR_NAME = 'Personal';
127
-
128
-	public const RESOURCE_BOOKING_CALENDAR_URI = 'calendar';
129
-	public const RESOURCE_BOOKING_CALENDAR_NAME = 'Calendar';
130
-
131
-	/**
132
-	 * We need to specify a max date, because we need to stop *somewhere*
133
-	 *
134
-	 * On 32 bit system the maximum for a signed integer is 2147483647, so
135
-	 * MAX_DATE cannot be higher than date('Y-m-d', 2147483647) which results
136
-	 * in 2038-01-19 to avoid problems when the date is converted
137
-	 * to a unix timestamp.
138
-	 */
139
-	public const MAX_DATE = '2038-01-01';
140
-
141
-	public const ACCESS_PUBLIC = 4;
142
-	public const CLASSIFICATION_PUBLIC = 0;
143
-	public const CLASSIFICATION_PRIVATE = 1;
144
-	public const CLASSIFICATION_CONFIDENTIAL = 2;
145
-
146
-	/**
147
-	 * List of CalDAV properties, and how they map to database field names and their type
148
-	 * Add your own properties by simply adding on to this array.
149
-	 *
150
-	 * @var array
151
-	 * @psalm-var array<string, string[]>
152
-	 */
153
-	public $propertyMap = [
154
-		'{DAV:}displayname' => ['displayname', 'string'],
155
-		'{urn:ietf:params:xml:ns:caldav}calendar-description' => ['description', 'string'],
156
-		'{urn:ietf:params:xml:ns:caldav}calendar-timezone' => ['timezone', 'string'],
157
-		'{http://apple.com/ns/ical/}calendar-order' => ['calendarorder', 'int'],
158
-		'{http://apple.com/ns/ical/}calendar-color' => ['calendarcolor', 'string'],
159
-		'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => ['deleted_at', 'int'],
160
-	];
161
-
162
-	/**
163
-	 * List of subscription properties, and how they map to database field names.
164
-	 *
165
-	 * @var array
166
-	 */
167
-	public $subscriptionPropertyMap = [
168
-		'{DAV:}displayname' => ['displayname', 'string'],
169
-		'{http://apple.com/ns/ical/}refreshrate' => ['refreshrate', 'string'],
170
-		'{http://apple.com/ns/ical/}calendar-order' => ['calendarorder', 'int'],
171
-		'{http://apple.com/ns/ical/}calendar-color' => ['calendarcolor', 'string'],
172
-		'{http://calendarserver.org/ns/}subscribed-strip-todos' => ['striptodos', 'bool'],
173
-		'{http://calendarserver.org/ns/}subscribed-strip-alarms' => ['stripalarms', 'string'],
174
-		'{http://calendarserver.org/ns/}subscribed-strip-attachments' => ['stripattachments', 'string'],
175
-	];
176
-
177
-	/**
178
-	 * properties to index
179
-	 *
180
-	 * This list has to be kept in sync with ICalendarQuery::SEARCH_PROPERTY_*
181
-	 *
182
-	 * @see \OCP\Calendar\ICalendarQuery
183
-	 */
184
-	private const INDEXED_PROPERTIES = [
185
-		'CATEGORIES',
186
-		'COMMENT',
187
-		'DESCRIPTION',
188
-		'LOCATION',
189
-		'RESOURCES',
190
-		'STATUS',
191
-		'SUMMARY',
192
-		'ATTENDEE',
193
-		'CONTACT',
194
-		'ORGANIZER'
195
-	];
196
-
197
-	/** @var array parameters to index */
198
-	public static $indexParameters = [
199
-		'ATTENDEE' => ['CN'],
200
-		'ORGANIZER' => ['CN'],
201
-	];
202
-
203
-	/**
204
-	 * @var string[] Map of uid => display name
205
-	 */
206
-	protected $userDisplayNames;
207
-
208
-	/** @var IDBConnection */
209
-	private $db;
210
-
211
-	/** @var Backend */
212
-	private $calendarSharingBackend;
213
-
214
-	/** @var Principal */
215
-	private $principalBackend;
216
-
217
-	/** @var IUserManager */
218
-	private $userManager;
219
-
220
-	/** @var ISecureRandom */
221
-	private $random;
222
-
223
-	/** @var ILogger */
224
-	private $logger;
225
-
226
-	/** @var IEventDispatcher */
227
-	private $dispatcher;
228
-
229
-	/** @var EventDispatcherInterface */
230
-	private $legacyDispatcher;
231
-
232
-	/** @var IConfig */
233
-	private $config;
234
-
235
-	/** @var bool */
236
-	private $legacyEndpoint;
237
-
238
-	/** @var string */
239
-	private $dbObjectPropertiesTable = 'calendarobjects_props';
240
-
241
-	/**
242
-	 * CalDavBackend constructor.
243
-	 *
244
-	 * @param IDBConnection $db
245
-	 * @param Principal $principalBackend
246
-	 * @param IUserManager $userManager
247
-	 * @param IGroupManager $groupManager
248
-	 * @param ISecureRandom $random
249
-	 * @param ILogger $logger
250
-	 * @param IEventDispatcher $dispatcher
251
-	 * @param EventDispatcherInterface $legacyDispatcher
252
-	 * @param bool $legacyEndpoint
253
-	 */
254
-	public function __construct(IDBConnection $db,
255
-								Principal $principalBackend,
256
-								IUserManager $userManager,
257
-								IGroupManager $groupManager,
258
-								ISecureRandom $random,
259
-								ILogger $logger,
260
-								IEventDispatcher $dispatcher,
261
-								EventDispatcherInterface $legacyDispatcher,
262
-								IConfig $config,
263
-								bool $legacyEndpoint = false) {
264
-		$this->db = $db;
265
-		$this->principalBackend = $principalBackend;
266
-		$this->userManager = $userManager;
267
-		$this->calendarSharingBackend = new Backend($this->db, $this->userManager, $groupManager, $principalBackend, 'calendar');
268
-		$this->random = $random;
269
-		$this->logger = $logger;
270
-		$this->dispatcher = $dispatcher;
271
-		$this->legacyDispatcher = $legacyDispatcher;
272
-		$this->config = $config;
273
-		$this->legacyEndpoint = $legacyEndpoint;
274
-	}
275
-
276
-	/**
277
-	 * Return the number of calendars for a principal
278
-	 *
279
-	 * By default this excludes the automatically generated birthday calendar
280
-	 *
281
-	 * @param $principalUri
282
-	 * @param bool $excludeBirthday
283
-	 * @return int
284
-	 */
285
-	public function getCalendarsForUserCount($principalUri, $excludeBirthday = true) {
286
-		$principalUri = $this->convertPrincipal($principalUri, true);
287
-		$query = $this->db->getQueryBuilder();
288
-		$query->select($query->func()->count('*'))
289
-			->from('calendars');
290
-
291
-		if ($principalUri === '') {
292
-			$query->where($query->expr()->emptyString('principaluri'));
293
-		} else {
294
-			$query->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
295
-		}
296
-
297
-		if ($excludeBirthday) {
298
-			$query->andWhere($query->expr()->neq('uri', $query->createNamedParameter(BirthdayService::BIRTHDAY_CALENDAR_URI)));
299
-		}
300
-
301
-		$result = $query->executeQuery();
302
-		$column = (int)$result->fetchOne();
303
-		$result->closeCursor();
304
-		return $column;
305
-	}
306
-
307
-	/**
308
-	 * @return array{id: int, deleted_at: int}[]
309
-	 */
310
-	public function getDeletedCalendars(int $deletedBefore): array {
311
-		$qb = $this->db->getQueryBuilder();
312
-		$qb->select(['id', 'deleted_at'])
313
-			->from('calendars')
314
-			->where($qb->expr()->isNotNull('deleted_at'))
315
-			->andWhere($qb->expr()->lt('deleted_at', $qb->createNamedParameter($deletedBefore)));
316
-		$result = $qb->executeQuery();
317
-		$raw = $result->fetchAll();
318
-		$result->closeCursor();
319
-		return array_map(function ($row) {
320
-			return [
321
-				'id' => (int) $row['id'],
322
-				'deleted_at' => (int) $row['deleted_at'],
323
-			];
324
-		}, $raw);
325
-	}
326
-
327
-	/**
328
-	 * Returns a list of calendars for a principal.
329
-	 *
330
-	 * Every project is an array with the following keys:
331
-	 *  * id, a unique id that will be used by other functions to modify the
332
-	 *    calendar. This can be the same as the uri or a database key.
333
-	 *  * uri, which the basename of the uri with which the calendar is
334
-	 *    accessed.
335
-	 *  * principaluri. The owner of the calendar. Almost always the same as
336
-	 *    principalUri passed to this method.
337
-	 *
338
-	 * Furthermore it can contain webdav properties in clark notation. A very
339
-	 * common one is '{DAV:}displayname'.
340
-	 *
341
-	 * Many clients also require:
342
-	 * {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
343
-	 * For this property, you can just return an instance of
344
-	 * Sabre\CalDAV\Property\SupportedCalendarComponentSet.
345
-	 *
346
-	 * If you return {http://sabredav.org/ns}read-only and set the value to 1,
347
-	 * ACL will automatically be put in read-only mode.
348
-	 *
349
-	 * @param string $principalUri
350
-	 * @return array
351
-	 */
352
-	public function getCalendarsForUser($principalUri) {
353
-		$principalUriOriginal = $principalUri;
354
-		$principalUri = $this->convertPrincipal($principalUri, true);
355
-		$fields = array_column($this->propertyMap, 0);
356
-		$fields[] = 'id';
357
-		$fields[] = 'uri';
358
-		$fields[] = 'synctoken';
359
-		$fields[] = 'components';
360
-		$fields[] = 'principaluri';
361
-		$fields[] = 'transparent';
362
-
363
-		// Making fields a comma-delimited list
364
-		$query = $this->db->getQueryBuilder();
365
-		$query->select($fields)
366
-			->from('calendars')
367
-			->orderBy('calendarorder', 'ASC');
368
-
369
-		if ($principalUri === '') {
370
-			$query->where($query->expr()->emptyString('principaluri'));
371
-		} else {
372
-			$query->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
373
-		}
374
-
375
-		$result = $query->executeQuery();
376
-
377
-		$calendars = [];
378
-		while ($row = $result->fetch()) {
379
-			$row['principaluri'] = (string) $row['principaluri'];
380
-			$components = [];
381
-			if ($row['components']) {
382
-				$components = explode(',',$row['components']);
383
-			}
384
-
385
-			$calendar = [
386
-				'id' => $row['id'],
387
-				'uri' => $row['uri'],
388
-				'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
389
-				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
390
-				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
391
-				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
392
-				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
393
-				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($principalUri, !$this->legacyEndpoint),
394
-			];
395
-
396
-			$calendar = $this->rowToCalendar($row, $calendar);
397
-			$calendar = $this->addOwnerPrincipalToCalendar($calendar);
398
-			$calendar = $this->addResourceTypeToCalendar($row, $calendar);
399
-
400
-			if (!isset($calendars[$calendar['id']])) {
401
-				$calendars[$calendar['id']] = $calendar;
402
-			}
403
-		}
404
-		$result->closeCursor();
405
-
406
-		// query for shared calendars
407
-		$principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true);
408
-		$principals = array_merge($principals, $this->principalBackend->getCircleMembership($principalUriOriginal));
409
-
410
-		$principals[] = $principalUri;
411
-
412
-		$fields = array_column($this->propertyMap, 0);
413
-		$fields[] = 'a.id';
414
-		$fields[] = 'a.uri';
415
-		$fields[] = 'a.synctoken';
416
-		$fields[] = 'a.components';
417
-		$fields[] = 'a.principaluri';
418
-		$fields[] = 'a.transparent';
419
-		$fields[] = 's.access';
420
-		$query = $this->db->getQueryBuilder();
421
-		$query->select($fields)
422
-			->from('dav_shares', 's')
423
-			->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
424
-			->where($query->expr()->in('s.principaluri', $query->createParameter('principaluri')))
425
-			->andWhere($query->expr()->eq('s.type', $query->createParameter('type')))
426
-			->setParameter('type', 'calendar')
427
-			->setParameter('principaluri', $principals, \Doctrine\DBAL\Connection::PARAM_STR_ARRAY);
428
-
429
-		$result = $query->executeQuery();
430
-
431
-		$readOnlyPropertyName = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only';
432
-		while ($row = $result->fetch()) {
433
-			$row['principaluri'] = (string) $row['principaluri'];
434
-			if ($row['principaluri'] === $principalUri) {
435
-				continue;
436
-			}
437
-
438
-			$readOnly = (int) $row['access'] === Backend::ACCESS_READ;
439
-			if (isset($calendars[$row['id']])) {
440
-				if ($readOnly) {
441
-					// New share can not have more permissions then the old one.
442
-					continue;
443
-				}
444
-				if (isset($calendars[$row['id']][$readOnlyPropertyName]) &&
445
-					$calendars[$row['id']][$readOnlyPropertyName] === 0) {
446
-					// Old share is already read-write, no more permissions can be gained
447
-					continue;
448
-				}
449
-			}
450
-
451
-			[, $name] = Uri\split($row['principaluri']);
452
-			$uri = $row['uri'] . '_shared_by_' . $name;
453
-			$row['displayname'] = $row['displayname'] . ' (' . $this->getUserDisplayName($name) . ')';
454
-			$components = [];
455
-			if ($row['components']) {
456
-				$components = explode(',',$row['components']);
457
-			}
458
-			$calendar = [
459
-				'id' => $row['id'],
460
-				'uri' => $uri,
461
-				'principaluri' => $this->convertPrincipal($principalUri, !$this->legacyEndpoint),
462
-				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
463
-				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
464
-				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
465
-				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp('transparent'),
466
-				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
467
-				$readOnlyPropertyName => $readOnly,
468
-			];
469
-
470
-			$calendar = $this->rowToCalendar($row, $calendar);
471
-			$calendar = $this->addOwnerPrincipalToCalendar($calendar);
472
-			$calendar = $this->addResourceTypeToCalendar($row, $calendar);
473
-
474
-			$calendars[$calendar['id']] = $calendar;
475
-		}
476
-		$result->closeCursor();
477
-
478
-		return array_values($calendars);
479
-	}
480
-
481
-	/**
482
-	 * @param $principalUri
483
-	 * @return array
484
-	 */
485
-	public function getUsersOwnCalendars($principalUri) {
486
-		$principalUri = $this->convertPrincipal($principalUri, true);
487
-		$fields = array_column($this->propertyMap, 0);
488
-		$fields[] = 'id';
489
-		$fields[] = 'uri';
490
-		$fields[] = 'synctoken';
491
-		$fields[] = 'components';
492
-		$fields[] = 'principaluri';
493
-		$fields[] = 'transparent';
494
-		// Making fields a comma-delimited list
495
-		$query = $this->db->getQueryBuilder();
496
-		$query->select($fields)->from('calendars')
497
-			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
498
-			->orderBy('calendarorder', 'ASC');
499
-		$stmt = $query->executeQuery();
500
-		$calendars = [];
501
-		while ($row = $stmt->fetch()) {
502
-			$row['principaluri'] = (string) $row['principaluri'];
503
-			$components = [];
504
-			if ($row['components']) {
505
-				$components = explode(',',$row['components']);
506
-			}
507
-			$calendar = [
508
-				'id' => $row['id'],
509
-				'uri' => $row['uri'],
510
-				'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
511
-				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
512
-				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
513
-				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
514
-				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
515
-			];
516
-
517
-			$calendar = $this->rowToCalendar($row, $calendar);
518
-			$calendar = $this->addOwnerPrincipalToCalendar($calendar);
519
-			$calendar = $this->addResourceTypeToCalendar($row, $calendar);
520
-
521
-			if (!isset($calendars[$calendar['id']])) {
522
-				$calendars[$calendar['id']] = $calendar;
523
-			}
524
-		}
525
-		$stmt->closeCursor();
526
-		return array_values($calendars);
527
-	}
528
-
529
-
530
-	/**
531
-	 * @param $uid
532
-	 * @return string
533
-	 */
534
-	private function getUserDisplayName($uid) {
535
-		if (!isset($this->userDisplayNames[$uid])) {
536
-			$user = $this->userManager->get($uid);
537
-
538
-			if ($user instanceof IUser) {
539
-				$this->userDisplayNames[$uid] = $user->getDisplayName();
540
-			} else {
541
-				$this->userDisplayNames[$uid] = $uid;
542
-			}
543
-		}
544
-
545
-		return $this->userDisplayNames[$uid];
546
-	}
547
-
548
-	/**
549
-	 * @return array
550
-	 */
551
-	public function getPublicCalendars() {
552
-		$fields = array_column($this->propertyMap, 0);
553
-		$fields[] = 'a.id';
554
-		$fields[] = 'a.uri';
555
-		$fields[] = 'a.synctoken';
556
-		$fields[] = 'a.components';
557
-		$fields[] = 'a.principaluri';
558
-		$fields[] = 'a.transparent';
559
-		$fields[] = 's.access';
560
-		$fields[] = 's.publicuri';
561
-		$calendars = [];
562
-		$query = $this->db->getQueryBuilder();
563
-		$result = $query->select($fields)
564
-			->from('dav_shares', 's')
565
-			->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
566
-			->where($query->expr()->in('s.access', $query->createNamedParameter(self::ACCESS_PUBLIC)))
567
-			->andWhere($query->expr()->eq('s.type', $query->createNamedParameter('calendar')))
568
-			->executeQuery();
569
-
570
-		while ($row = $result->fetch()) {
571
-			$row['principaluri'] = (string) $row['principaluri'];
572
-			[, $name] = Uri\split($row['principaluri']);
573
-			$row['displayname'] = $row['displayname'] . "($name)";
574
-			$components = [];
575
-			if ($row['components']) {
576
-				$components = explode(',',$row['components']);
577
-			}
578
-			$calendar = [
579
-				'id' => $row['id'],
580
-				'uri' => $row['publicuri'],
581
-				'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
582
-				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
583
-				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
584
-				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
585
-				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
586
-				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], $this->legacyEndpoint),
587
-				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ,
588
-				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}public' => (int)$row['access'] === self::ACCESS_PUBLIC,
589
-			];
590
-
591
-			$calendar = $this->rowToCalendar($row, $calendar);
592
-			$calendar = $this->addOwnerPrincipalToCalendar($calendar);
593
-			$calendar = $this->addResourceTypeToCalendar($row, $calendar);
594
-
595
-			if (!isset($calendars[$calendar['id']])) {
596
-				$calendars[$calendar['id']] = $calendar;
597
-			}
598
-		}
599
-		$result->closeCursor();
600
-
601
-		return array_values($calendars);
602
-	}
603
-
604
-	/**
605
-	 * @param string $uri
606
-	 * @return array
607
-	 * @throws NotFound
608
-	 */
609
-	public function getPublicCalendar($uri) {
610
-		$fields = array_column($this->propertyMap, 0);
611
-		$fields[] = 'a.id';
612
-		$fields[] = 'a.uri';
613
-		$fields[] = 'a.synctoken';
614
-		$fields[] = 'a.components';
615
-		$fields[] = 'a.principaluri';
616
-		$fields[] = 'a.transparent';
617
-		$fields[] = 's.access';
618
-		$fields[] = 's.publicuri';
619
-		$query = $this->db->getQueryBuilder();
620
-		$result = $query->select($fields)
621
-			->from('dav_shares', 's')
622
-			->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
623
-			->where($query->expr()->in('s.access', $query->createNamedParameter(self::ACCESS_PUBLIC)))
624
-			->andWhere($query->expr()->eq('s.type', $query->createNamedParameter('calendar')))
625
-			->andWhere($query->expr()->eq('s.publicuri', $query->createNamedParameter($uri)))
626
-			->executeQuery();
627
-
628
-		$row = $result->fetch();
629
-
630
-		$result->closeCursor();
631
-
632
-		if ($row === false) {
633
-			throw new NotFound('Node with name \'' . $uri . '\' could not be found');
634
-		}
635
-
636
-		$row['principaluri'] = (string) $row['principaluri'];
637
-		[, $name] = Uri\split($row['principaluri']);
638
-		$row['displayname'] = $row['displayname'] . ' ' . "($name)";
639
-		$components = [];
640
-		if ($row['components']) {
641
-			$components = explode(',',$row['components']);
642
-		}
643
-		$calendar = [
644
-			'id' => $row['id'],
645
-			'uri' => $row['publicuri'],
646
-			'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
647
-			'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
648
-			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
649
-			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
650
-			'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
651
-			'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
652
-			'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ,
653
-			'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}public' => (int)$row['access'] === self::ACCESS_PUBLIC,
654
-		];
655
-
656
-		$calendar = $this->rowToCalendar($row, $calendar);
657
-		$calendar = $this->addOwnerPrincipalToCalendar($calendar);
658
-		$calendar = $this->addResourceTypeToCalendar($row, $calendar);
659
-
660
-		return $calendar;
661
-	}
662
-
663
-	/**
664
-	 * @param string $principal
665
-	 * @param string $uri
666
-	 * @return array|null
667
-	 */
668
-	public function getCalendarByUri($principal, $uri) {
669
-		$fields = array_column($this->propertyMap, 0);
670
-		$fields[] = 'id';
671
-		$fields[] = 'uri';
672
-		$fields[] = 'synctoken';
673
-		$fields[] = 'components';
674
-		$fields[] = 'principaluri';
675
-		$fields[] = 'transparent';
676
-
677
-		// Making fields a comma-delimited list
678
-		$query = $this->db->getQueryBuilder();
679
-		$query->select($fields)->from('calendars')
680
-			->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
681
-			->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($principal)))
682
-			->setMaxResults(1);
683
-		$stmt = $query->executeQuery();
684
-
685
-		$row = $stmt->fetch();
686
-		$stmt->closeCursor();
687
-		if ($row === false) {
688
-			return null;
689
-		}
690
-
691
-		$row['principaluri'] = (string) $row['principaluri'];
692
-		$components = [];
693
-		if ($row['components']) {
694
-			$components = explode(',',$row['components']);
695
-		}
696
-
697
-		$calendar = [
698
-			'id' => $row['id'],
699
-			'uri' => $row['uri'],
700
-			'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
701
-			'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
702
-			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
703
-			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
704
-			'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
705
-		];
706
-
707
-		$calendar = $this->rowToCalendar($row, $calendar);
708
-		$calendar = $this->addOwnerPrincipalToCalendar($calendar);
709
-		$calendar = $this->addResourceTypeToCalendar($row, $calendar);
710
-
711
-		return $calendar;
712
-	}
713
-
714
-	/**
715
-	 * @param $calendarId
716
-	 * @return array|null
717
-	 */
718
-	public function getCalendarById($calendarId) {
719
-		$fields = array_column($this->propertyMap, 0);
720
-		$fields[] = 'id';
721
-		$fields[] = 'uri';
722
-		$fields[] = 'synctoken';
723
-		$fields[] = 'components';
724
-		$fields[] = 'principaluri';
725
-		$fields[] = 'transparent';
726
-
727
-		// Making fields a comma-delimited list
728
-		$query = $this->db->getQueryBuilder();
729
-		$query->select($fields)->from('calendars')
730
-			->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)))
731
-			->setMaxResults(1);
732
-		$stmt = $query->executeQuery();
733
-
734
-		$row = $stmt->fetch();
735
-		$stmt->closeCursor();
736
-		if ($row === false) {
737
-			return null;
738
-		}
739
-
740
-		$row['principaluri'] = (string) $row['principaluri'];
741
-		$components = [];
742
-		if ($row['components']) {
743
-			$components = explode(',',$row['components']);
744
-		}
745
-
746
-		$calendar = [
747
-			'id' => $row['id'],
748
-			'uri' => $row['uri'],
749
-			'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
750
-			'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
751
-			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
752
-			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
753
-			'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
754
-		];
755
-
756
-		$calendar = $this->rowToCalendar($row, $calendar);
757
-		$calendar = $this->addOwnerPrincipalToCalendar($calendar);
758
-		$calendar = $this->addResourceTypeToCalendar($row, $calendar);
759
-
760
-		return $calendar;
761
-	}
762
-
763
-	/**
764
-	 * @param $subscriptionId
765
-	 */
766
-	public function getSubscriptionById($subscriptionId) {
767
-		$fields = array_column($this->subscriptionPropertyMap, 0);
768
-		$fields[] = 'id';
769
-		$fields[] = 'uri';
770
-		$fields[] = 'source';
771
-		$fields[] = 'synctoken';
772
-		$fields[] = 'principaluri';
773
-		$fields[] = 'lastmodified';
774
-
775
-		$query = $this->db->getQueryBuilder();
776
-		$query->select($fields)
777
-			->from('calendarsubscriptions')
778
-			->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
779
-			->orderBy('calendarorder', 'asc');
780
-		$stmt = $query->executeQuery();
781
-
782
-		$row = $stmt->fetch();
783
-		$stmt->closeCursor();
784
-		if ($row === false) {
785
-			return null;
786
-		}
787
-
788
-		$row['principaluri'] = (string) $row['principaluri'];
789
-		$subscription = [
790
-			'id' => $row['id'],
791
-			'uri' => $row['uri'],
792
-			'principaluri' => $row['principaluri'],
793
-			'source' => $row['source'],
794
-			'lastmodified' => $row['lastmodified'],
795
-			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
796
-			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
797
-		];
798
-
799
-		return $this->rowToSubscription($row, $subscription);
800
-	}
801
-
802
-	/**
803
-	 * Creates a new calendar for a principal.
804
-	 *
805
-	 * If the creation was a success, an id must be returned that can be used to reference
806
-	 * this calendar in other methods, such as updateCalendar.
807
-	 *
808
-	 * @param string $principalUri
809
-	 * @param string $calendarUri
810
-	 * @param array $properties
811
-	 * @return int
812
-	 */
813
-	public function createCalendar($principalUri, $calendarUri, array $properties) {
814
-		$values = [
815
-			'principaluri' => $this->convertPrincipal($principalUri, true),
816
-			'uri' => $calendarUri,
817
-			'synctoken' => 1,
818
-			'transparent' => 0,
819
-			'components' => 'VEVENT,VTODO',
820
-			'displayname' => $calendarUri
821
-		];
822
-
823
-		// Default value
824
-		$sccs = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set';
825
-		if (isset($properties[$sccs])) {
826
-			if (!($properties[$sccs] instanceof SupportedCalendarComponentSet)) {
827
-				throw new DAV\Exception('The ' . $sccs . ' property must be of type: \Sabre\CalDAV\Property\SupportedCalendarComponentSet');
828
-			}
829
-			$values['components'] = implode(',',$properties[$sccs]->getValue());
830
-		} elseif (isset($properties['components'])) {
831
-			// Allow to provide components internally without having
832
-			// to create a SupportedCalendarComponentSet object
833
-			$values['components'] = $properties['components'];
834
-		}
835
-
836
-		$transp = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp';
837
-		if (isset($properties[$transp])) {
838
-			$values['transparent'] = (int) ($properties[$transp]->getValue() === 'transparent');
839
-		}
840
-
841
-		foreach ($this->propertyMap as $xmlName => [$dbName, $type]) {
842
-			if (isset($properties[$xmlName])) {
843
-				$values[$dbName] = $properties[$xmlName];
844
-			}
845
-		}
846
-
847
-		$query = $this->db->getQueryBuilder();
848
-		$query->insert('calendars');
849
-		foreach ($values as $column => $value) {
850
-			$query->setValue($column, $query->createNamedParameter($value));
851
-		}
852
-		$query->executeStatement();
853
-		$calendarId = $query->getLastInsertId();
854
-
855
-		$calendarData = $this->getCalendarById($calendarId);
856
-		$this->dispatcher->dispatchTyped(new CalendarCreatedEvent((int)$calendarId, $calendarData));
857
-
858
-		return $calendarId;
859
-	}
860
-
861
-	/**
862
-	 * Updates properties for a calendar.
863
-	 *
864
-	 * The list of mutations is stored in a Sabre\DAV\PropPatch object.
865
-	 * To do the actual updates, you must tell this object which properties
866
-	 * you're going to process with the handle() method.
867
-	 *
868
-	 * Calling the handle method is like telling the PropPatch object "I
869
-	 * promise I can handle updating this property".
870
-	 *
871
-	 * Read the PropPatch documentation for more info and examples.
872
-	 *
873
-	 * @param mixed $calendarId
874
-	 * @param PropPatch $propPatch
875
-	 * @return void
876
-	 */
877
-	public function updateCalendar($calendarId, PropPatch $propPatch) {
878
-		$supportedProperties = array_keys($this->propertyMap);
879
-		$supportedProperties[] = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp';
880
-
881
-		$propPatch->handle($supportedProperties, function ($mutations) use ($calendarId) {
882
-			$newValues = [];
883
-			foreach ($mutations as $propertyName => $propertyValue) {
884
-				switch ($propertyName) {
885
-					case '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp':
886
-						$fieldName = 'transparent';
887
-						$newValues[$fieldName] = (int) ($propertyValue->getValue() === 'transparent');
888
-						break;
889
-					default:
890
-						$fieldName = $this->propertyMap[$propertyName][0];
891
-						$newValues[$fieldName] = $propertyValue;
892
-						break;
893
-				}
894
-			}
895
-			$query = $this->db->getQueryBuilder();
896
-			$query->update('calendars');
897
-			foreach ($newValues as $fieldName => $value) {
898
-				$query->set($fieldName, $query->createNamedParameter($value));
899
-			}
900
-			$query->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)));
901
-			$query->executeStatement();
902
-
903
-			$this->addChange($calendarId, "", 2);
904
-
905
-			$calendarData = $this->getCalendarById($calendarId);
906
-			$shares = $this->getShares($calendarId);
907
-			$this->dispatcher->dispatchTyped(new CalendarUpdatedEvent((int)$calendarId, $calendarData, $shares, $mutations));
908
-
909
-			return true;
910
-		});
911
-	}
912
-
913
-	/**
914
-	 * Delete a calendar and all it's objects
915
-	 *
916
-	 * @param mixed $calendarId
917
-	 * @return void
918
-	 */
919
-	public function deleteCalendar($calendarId, bool $forceDeletePermanently = false) {
920
-		// The calendar is deleted right away if this is either enforced by the caller
921
-		// or the special contacts birthday calendar or when the preference of an empty
922
-		// retention (0 seconds) is set, which signals a disabled trashbin.
923
-		$calendarData = $this->getCalendarById($calendarId);
924
-		$isBirthdayCalendar = isset($calendarData['uri']) && $calendarData['uri'] === BirthdayService::BIRTHDAY_CALENDAR_URI;
925
-		$trashbinDisabled = $this->config->getAppValue(Application::APP_ID, RetentionService::RETENTION_CONFIG_KEY) === '0';
926
-		if ($forceDeletePermanently || $isBirthdayCalendar || $trashbinDisabled) {
927
-			$calendarData = $this->getCalendarById($calendarId);
928
-			$shares = $this->getShares($calendarId);
929
-
930
-			$qbDeleteCalendarObjectProps = $this->db->getQueryBuilder();
931
-			$qbDeleteCalendarObjectProps->delete($this->dbObjectPropertiesTable)
932
-				->where($qbDeleteCalendarObjectProps->expr()->eq('calendarid', $qbDeleteCalendarObjectProps->createNamedParameter($calendarId)))
933
-				->andWhere($qbDeleteCalendarObjectProps->expr()->eq('calendartype', $qbDeleteCalendarObjectProps->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)))
934
-				->executeStatement();
935
-
936
-			$qbDeleteCalendarObjects = $this->db->getQueryBuilder();
937
-			$qbDeleteCalendarObjects->delete('calendarobjects')
938
-				->where($qbDeleteCalendarObjects->expr()->eq('calendarid', $qbDeleteCalendarObjects->createNamedParameter($calendarId)))
939
-				->andWhere($qbDeleteCalendarObjects->expr()->eq('calendartype', $qbDeleteCalendarObjects->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)))
940
-				->executeStatement();
941
-
942
-			$qbDeleteCalendarChanges = $this->db->getQueryBuilder();
943
-			$qbDeleteCalendarObjects->delete('calendarchanges')
944
-				->where($qbDeleteCalendarChanges->expr()->eq('calendarid', $qbDeleteCalendarChanges->createNamedParameter($calendarId)))
945
-				->andWhere($qbDeleteCalendarChanges->expr()->eq('calendartype', $qbDeleteCalendarChanges->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)))
946
-				->executeStatement();
947
-
948
-			$this->calendarSharingBackend->deleteAllShares($calendarId);
949
-
950
-			$qbDeleteCalendar = $this->db->getQueryBuilder();
951
-			$qbDeleteCalendarObjects->delete('calendars')
952
-				->where($qbDeleteCalendar->expr()->eq('id', $qbDeleteCalendar->createNamedParameter($calendarId)))
953
-				->executeStatement();
954
-
955
-			// Only dispatch if we actually deleted anything
956
-			if ($calendarData) {
957
-				$this->dispatcher->dispatchTyped(new CalendarDeletedEvent((int)$calendarId, $calendarData, $shares));
958
-			}
959
-		} else {
960
-			$qbMarkCalendarDeleted = $this->db->getQueryBuilder();
961
-			$qbMarkCalendarDeleted->update('calendars')
962
-				->set('deleted_at', $qbMarkCalendarDeleted->createNamedParameter(time()))
963
-				->where($qbMarkCalendarDeleted->expr()->eq('id', $qbMarkCalendarDeleted->createNamedParameter($calendarId)))
964
-				->executeStatement();
965
-
966
-			$calendarData = $this->getCalendarById($calendarId);
967
-			$shares = $this->getShares($calendarId);
968
-			if ($calendarData) {
969
-				$this->dispatcher->dispatchTyped(new CalendarMovedToTrashEvent(
970
-					(int)$calendarId,
971
-					$calendarData,
972
-					$shares
973
-				));
974
-			}
975
-		}
976
-	}
977
-
978
-	public function restoreCalendar(int $id): void {
979
-		$qb = $this->db->getQueryBuilder();
980
-		$update = $qb->update('calendars')
981
-			->set('deleted_at', $qb->createNamedParameter(null))
982
-			->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT));
983
-		$update->executeStatement();
984
-
985
-		$calendarData = $this->getCalendarById($id);
986
-		$shares = $this->getShares($id);
987
-		if ($calendarData === null) {
988
-			throw new RuntimeException('Calendar data that was just written can\'t be read back. Check your database configuration.');
989
-		}
990
-		$this->dispatcher->dispatchTyped(new CalendarRestoredEvent(
991
-			$id,
992
-			$calendarData,
993
-			$shares
994
-		));
995
-	}
996
-
997
-	/**
998
-	 * Delete all of an user's shares
999
-	 *
1000
-	 * @param string $principaluri
1001
-	 * @return void
1002
-	 */
1003
-	public function deleteAllSharesByUser($principaluri) {
1004
-		$this->calendarSharingBackend->deleteAllSharesByUser($principaluri);
1005
-	}
1006
-
1007
-	/**
1008
-	 * Returns all calendar objects within a calendar.
1009
-	 *
1010
-	 * Every item contains an array with the following keys:
1011
-	 *   * calendardata - The iCalendar-compatible calendar data
1012
-	 *   * uri - a unique key which will be used to construct the uri. This can
1013
-	 *     be any arbitrary string, but making sure it ends with '.ics' is a
1014
-	 *     good idea. This is only the basename, or filename, not the full
1015
-	 *     path.
1016
-	 *   * lastmodified - a timestamp of the last modification time
1017
-	 *   * etag - An arbitrary string, surrounded by double-quotes. (e.g.:
1018
-	 *   '"abcdef"')
1019
-	 *   * size - The size of the calendar objects, in bytes.
1020
-	 *   * component - optional, a string containing the type of object, such
1021
-	 *     as 'vevent' or 'vtodo'. If specified, this will be used to populate
1022
-	 *     the Content-Type header.
1023
-	 *
1024
-	 * Note that the etag is optional, but it's highly encouraged to return for
1025
-	 * speed reasons.
1026
-	 *
1027
-	 * The calendardata is also optional. If it's not returned
1028
-	 * 'getCalendarObject' will be called later, which *is* expected to return
1029
-	 * calendardata.
1030
-	 *
1031
-	 * If neither etag or size are specified, the calendardata will be
1032
-	 * used/fetched to determine these numbers. If both are specified the
1033
-	 * amount of times this is needed is reduced by a great degree.
1034
-	 *
1035
-	 * @param mixed $calendarId
1036
-	 * @param int $calendarType
1037
-	 * @return array
1038
-	 */
1039
-	public function getCalendarObjects($calendarId, $calendarType = self::CALENDAR_TYPE_CALENDAR):array {
1040
-		$query = $this->db->getQueryBuilder();
1041
-		$query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'componenttype', 'classification'])
1042
-			->from('calendarobjects')
1043
-			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
1044
-			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)))
1045
-			->andWhere($query->expr()->isNull('deleted_at'));
1046
-		$stmt = $query->executeQuery();
1047
-
1048
-		$result = [];
1049
-		foreach ($stmt->fetchAll() as $row) {
1050
-			$result[] = [
1051
-				'id' => $row['id'],
1052
-				'uri' => $row['uri'],
1053
-				'lastmodified' => $row['lastmodified'],
1054
-				'etag' => '"' . $row['etag'] . '"',
1055
-				'calendarid' => $row['calendarid'],
1056
-				'size' => (int)$row['size'],
1057
-				'component' => strtolower($row['componenttype']),
1058
-				'classification' => (int)$row['classification']
1059
-			];
1060
-		}
1061
-		$stmt->closeCursor();
1062
-
1063
-		return $result;
1064
-	}
1065
-
1066
-	public function getDeletedCalendarObjects(int $deletedBefore): array {
1067
-		$query = $this->db->getQueryBuilder();
1068
-		$query->select(['co.id', 'co.uri', 'co.lastmodified', 'co.etag', 'co.calendarid', 'co.calendartype', 'co.size', 'co.componenttype', 'co.classification', 'co.deleted_at'])
1069
-			->from('calendarobjects', 'co')
1070
-			->join('co', 'calendars', 'c', $query->expr()->eq('c.id', 'co.calendarid', IQueryBuilder::PARAM_INT))
1071
-			->where($query->expr()->isNotNull('co.deleted_at'))
1072
-			->andWhere($query->expr()->lt('co.deleted_at', $query->createNamedParameter($deletedBefore)));
1073
-		$stmt = $query->executeQuery();
1074
-
1075
-		$result = [];
1076
-		foreach ($stmt->fetchAll() as $row) {
1077
-			$result[] = [
1078
-				'id' => $row['id'],
1079
-				'uri' => $row['uri'],
1080
-				'lastmodified' => $row['lastmodified'],
1081
-				'etag' => '"' . $row['etag'] . '"',
1082
-				'calendarid' => (int) $row['calendarid'],
1083
-				'calendartype' => (int) $row['calendartype'],
1084
-				'size' => (int) $row['size'],
1085
-				'component' => strtolower($row['componenttype']),
1086
-				'classification' => (int) $row['classification'],
1087
-				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => $row['deleted_at'] === null ? $row['deleted_at'] : (int) $row['deleted_at'],
1088
-			];
1089
-		}
1090
-		$stmt->closeCursor();
1091
-
1092
-		return $result;
1093
-	}
1094
-
1095
-	/**
1096
-	 * Return all deleted calendar objects by the given principal that are not
1097
-	 * in deleted calendars.
1098
-	 *
1099
-	 * @param string $principalUri
1100
-	 * @return array
1101
-	 * @throws Exception
1102
-	 */
1103
-	public function getDeletedCalendarObjectsByPrincipal(string $principalUri): array {
1104
-		$query = $this->db->getQueryBuilder();
1105
-		$query->select(['co.id', 'co.uri', 'co.lastmodified', 'co.etag', 'co.calendarid', 'co.size', 'co.componenttype', 'co.classification', 'co.deleted_at'])
1106
-			->selectAlias('c.uri', 'calendaruri')
1107
-			->from('calendarobjects', 'co')
1108
-			->join('co', 'calendars', 'c', $query->expr()->eq('c.id', 'co.calendarid', IQueryBuilder::PARAM_INT))
1109
-			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
1110
-			->andWhere($query->expr()->isNotNull('co.deleted_at'))
1111
-			->andWhere($query->expr()->isNull('c.deleted_at'));
1112
-		$stmt = $query->executeQuery();
1113
-
1114
-		$result = [];
1115
-		while ($row = $stmt->fetch()) {
1116
-			$result[] = [
1117
-				'id' => $row['id'],
1118
-				'uri' => $row['uri'],
1119
-				'lastmodified' => $row['lastmodified'],
1120
-				'etag' => '"' . $row['etag'] . '"',
1121
-				'calendarid' => $row['calendarid'],
1122
-				'calendaruri' => $row['calendaruri'],
1123
-				'size' => (int)$row['size'],
1124
-				'component' => strtolower($row['componenttype']),
1125
-				'classification' => (int)$row['classification'],
1126
-				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => $row['deleted_at'] === null ? $row['deleted_at'] : (int) $row['deleted_at'],
1127
-			];
1128
-		}
1129
-		$stmt->closeCursor();
1130
-
1131
-		return $result;
1132
-	}
1133
-
1134
-	/**
1135
-	 * Returns information from a single calendar object, based on it's object
1136
-	 * uri.
1137
-	 *
1138
-	 * The object uri is only the basename, or filename and not a full path.
1139
-	 *
1140
-	 * The returned array must have the same keys as getCalendarObjects. The
1141
-	 * 'calendardata' object is required here though, while it's not required
1142
-	 * for getCalendarObjects.
1143
-	 *
1144
-	 * This method must return null if the object did not exist.
1145
-	 *
1146
-	 * @param mixed $calendarId
1147
-	 * @param string $objectUri
1148
-	 * @param int $calendarType
1149
-	 * @return array|null
1150
-	 */
1151
-	public function getCalendarObject($calendarId, $objectUri, int $calendarType = self::CALENDAR_TYPE_CALENDAR) {
1152
-		$query = $this->db->getQueryBuilder();
1153
-		$query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification'])
1154
-			->from('calendarobjects')
1155
-			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
1156
-			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
1157
-			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)));
1158
-		$stmt = $query->executeQuery();
1159
-		$row = $stmt->fetch();
1160
-		$stmt->closeCursor();
1161
-
1162
-		if (!$row) {
1163
-			return null;
1164
-		}
1165
-
1166
-		return [
1167
-			'id' => $row['id'],
1168
-			'uri' => $row['uri'],
1169
-			'lastmodified' => $row['lastmodified'],
1170
-			'etag' => '"' . $row['etag'] . '"',
1171
-			'calendarid' => $row['calendarid'],
1172
-			'size' => (int)$row['size'],
1173
-			'calendardata' => $this->readBlob($row['calendardata']),
1174
-			'component' => strtolower($row['componenttype']),
1175
-			'classification' => (int)$row['classification']
1176
-		];
1177
-	}
1178
-
1179
-	/**
1180
-	 * Returns a list of calendar objects.
1181
-	 *
1182
-	 * This method should work identical to getCalendarObject, but instead
1183
-	 * return all the calendar objects in the list as an array.
1184
-	 *
1185
-	 * If the backend supports this, it may allow for some speed-ups.
1186
-	 *
1187
-	 * @param mixed $calendarId
1188
-	 * @param string[] $uris
1189
-	 * @param int $calendarType
1190
-	 * @return array
1191
-	 */
1192
-	public function getMultipleCalendarObjects($calendarId, array $uris, $calendarType = self::CALENDAR_TYPE_CALENDAR):array {
1193
-		if (empty($uris)) {
1194
-			return [];
1195
-		}
1196
-
1197
-		$chunks = array_chunk($uris, 100);
1198
-		$objects = [];
1199
-
1200
-		$query = $this->db->getQueryBuilder();
1201
-		$query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification'])
1202
-			->from('calendarobjects')
1203
-			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
1204
-			->andWhere($query->expr()->in('uri', $query->createParameter('uri')))
1205
-			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)))
1206
-			->andWhere($query->expr()->isNull('deleted_at'));
1207
-
1208
-		foreach ($chunks as $uris) {
1209
-			$query->setParameter('uri', $uris, IQueryBuilder::PARAM_STR_ARRAY);
1210
-			$result = $query->executeQuery();
1211
-
1212
-			while ($row = $result->fetch()) {
1213
-				$objects[] = [
1214
-					'id' => $row['id'],
1215
-					'uri' => $row['uri'],
1216
-					'lastmodified' => $row['lastmodified'],
1217
-					'etag' => '"' . $row['etag'] . '"',
1218
-					'calendarid' => $row['calendarid'],
1219
-					'size' => (int)$row['size'],
1220
-					'calendardata' => $this->readBlob($row['calendardata']),
1221
-					'component' => strtolower($row['componenttype']),
1222
-					'classification' => (int)$row['classification']
1223
-				];
1224
-			}
1225
-			$result->closeCursor();
1226
-		}
1227
-
1228
-		return $objects;
1229
-	}
1230
-
1231
-	/**
1232
-	 * Creates a new calendar object.
1233
-	 *
1234
-	 * The object uri is only the basename, or filename and not a full path.
1235
-	 *
1236
-	 * It is possible return an etag from this function, which will be used in
1237
-	 * the response to this PUT request. Note that the ETag must be surrounded
1238
-	 * by double-quotes.
1239
-	 *
1240
-	 * However, you should only really return this ETag if you don't mangle the
1241
-	 * calendar-data. If the result of a subsequent GET to this object is not
1242
-	 * the exact same as this request body, you should omit the ETag.
1243
-	 *
1244
-	 * @param mixed $calendarId
1245
-	 * @param string $objectUri
1246
-	 * @param string $calendarData
1247
-	 * @param int $calendarType
1248
-	 * @return string
1249
-	 */
1250
-	public function createCalendarObject($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
1251
-		$extraData = $this->getDenormalizedData($calendarData);
1252
-
1253
-		// Try to detect duplicates
1254
-		$qb = $this->db->getQueryBuilder();
1255
-		$qb->select($qb->func()->count('*'))
1256
-			->from('calendarobjects')
1257
-			->where($qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)))
1258
-			->andWhere($qb->expr()->eq('uid', $qb->createNamedParameter($extraData['uid'])))
1259
-			->andWhere($qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType)))
1260
-			->andWhere($qb->expr()->isNull('deleted_at'));
1261
-		$result = $qb->executeQuery();
1262
-		$count = (int) $result->fetchOne();
1263
-		$result->closeCursor();
1264
-
1265
-		if ($count !== 0) {
1266
-			throw new BadRequest('Calendar object with uid already exists in this calendar collection.');
1267
-		}
1268
-		// For a more specific error message we also try to explicitly look up the UID but as a deleted entry
1269
-		$qbDel = $this->db->getQueryBuilder();
1270
-		$qbDel->select($qb->func()->count('*'))
1271
-			->from('calendarobjects')
1272
-			->where($qbDel->expr()->eq('calendarid', $qbDel->createNamedParameter($calendarId)))
1273
-			->andWhere($qbDel->expr()->eq('uid', $qbDel->createNamedParameter($extraData['uid'])))
1274
-			->andWhere($qbDel->expr()->eq('calendartype', $qbDel->createNamedParameter($calendarType)))
1275
-			->andWhere($qbDel->expr()->isNotNull('deleted_at'));
1276
-		$result = $qbDel->executeQuery();
1277
-		$count = (int) $result->fetchOne();
1278
-		$result->closeCursor();
1279
-		if ($count !== 0) {
1280
-			throw new BadRequest('Deleted calendar object with uid already exists in this calendar collection.');
1281
-		}
1282
-
1283
-		$query = $this->db->getQueryBuilder();
1284
-		$query->insert('calendarobjects')
1285
-			->values([
1286
-				'calendarid' => $query->createNamedParameter($calendarId),
1287
-				'uri' => $query->createNamedParameter($objectUri),
1288
-				'calendardata' => $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB),
1289
-				'lastmodified' => $query->createNamedParameter(time()),
1290
-				'etag' => $query->createNamedParameter($extraData['etag']),
1291
-				'size' => $query->createNamedParameter($extraData['size']),
1292
-				'componenttype' => $query->createNamedParameter($extraData['componentType']),
1293
-				'firstoccurence' => $query->createNamedParameter($extraData['firstOccurence']),
1294
-				'lastoccurence' => $query->createNamedParameter($extraData['lastOccurence']),
1295
-				'classification' => $query->createNamedParameter($extraData['classification']),
1296
-				'uid' => $query->createNamedParameter($extraData['uid']),
1297
-				'calendartype' => $query->createNamedParameter($calendarType),
1298
-			])
1299
-			->executeStatement();
1300
-
1301
-		$this->updateProperties($calendarId, $objectUri, $calendarData, $calendarType);
1302
-		$this->addChange($calendarId, $objectUri, 1, $calendarType);
1303
-
1304
-		$objectRow = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
1305
-		if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
1306
-			$calendarRow = $this->getCalendarById($calendarId);
1307
-			$shares = $this->getShares($calendarId);
1308
-
1309
-			$this->dispatcher->dispatchTyped(new CalendarObjectCreatedEvent((int)$calendarId, $calendarRow, $shares, $objectRow));
1310
-		} else {
1311
-			$subscriptionRow = $this->getSubscriptionById($calendarId);
1312
-
1313
-			$this->dispatcher->dispatchTyped(new CachedCalendarObjectCreatedEvent((int)$calendarId, $subscriptionRow, [], $objectRow));
1314
-			$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createCachedCalendarObject', new GenericEvent(
1315
-				'\OCA\DAV\CalDAV\CalDavBackend::createCachedCalendarObject',
1316
-				[
1317
-					'subscriptionId' => $calendarId,
1318
-					'calendarData' => $subscriptionRow,
1319
-					'shares' => [],
1320
-					'objectData' => $objectRow,
1321
-				]
1322
-			));
1323
-		}
1324
-
1325
-		return '"' . $extraData['etag'] . '"';
1326
-	}
1327
-
1328
-	/**
1329
-	 * Updates an existing calendarobject, based on it's uri.
1330
-	 *
1331
-	 * The object uri is only the basename, or filename and not a full path.
1332
-	 *
1333
-	 * It is possible return an etag from this function, which will be used in
1334
-	 * the response to this PUT request. Note that the ETag must be surrounded
1335
-	 * by double-quotes.
1336
-	 *
1337
-	 * However, you should only really return this ETag if you don't mangle the
1338
-	 * calendar-data. If the result of a subsequent GET to this object is not
1339
-	 * the exact same as this request body, you should omit the ETag.
1340
-	 *
1341
-	 * @param mixed $calendarId
1342
-	 * @param string $objectUri
1343
-	 * @param string $calendarData
1344
-	 * @param int $calendarType
1345
-	 * @return string
1346
-	 */
1347
-	public function updateCalendarObject($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
1348
-		$extraData = $this->getDenormalizedData($calendarData);
1349
-		$query = $this->db->getQueryBuilder();
1350
-		$query->update('calendarobjects')
1351
-				->set('calendardata', $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB))
1352
-				->set('lastmodified', $query->createNamedParameter(time()))
1353
-				->set('etag', $query->createNamedParameter($extraData['etag']))
1354
-				->set('size', $query->createNamedParameter($extraData['size']))
1355
-				->set('componenttype', $query->createNamedParameter($extraData['componentType']))
1356
-				->set('firstoccurence', $query->createNamedParameter($extraData['firstOccurence']))
1357
-				->set('lastoccurence', $query->createNamedParameter($extraData['lastOccurence']))
1358
-				->set('classification', $query->createNamedParameter($extraData['classification']))
1359
-				->set('uid', $query->createNamedParameter($extraData['uid']))
1360
-			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
1361
-			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
1362
-			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)))
1363
-			->executeStatement();
1364
-
1365
-		$this->updateProperties($calendarId, $objectUri, $calendarData, $calendarType);
1366
-		$this->addChange($calendarId, $objectUri, 2, $calendarType);
1367
-
1368
-		$objectRow = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
1369
-		if (is_array($objectRow)) {
1370
-			if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
1371
-				$calendarRow = $this->getCalendarById($calendarId);
1372
-				$shares = $this->getShares($calendarId);
1373
-
1374
-				$this->dispatcher->dispatchTyped(new CalendarObjectUpdatedEvent((int)$calendarId, $calendarRow, $shares, $objectRow));
1375
-			} else {
1376
-				$subscriptionRow = $this->getSubscriptionById($calendarId);
1377
-
1378
-				$this->dispatcher->dispatchTyped(new CachedCalendarObjectUpdatedEvent((int)$calendarId, $subscriptionRow, [], $objectRow));
1379
-				$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateCachedCalendarObject', new GenericEvent(
1380
-					'\OCA\DAV\CalDAV\CalDavBackend::updateCachedCalendarObject',
1381
-					[
1382
-						'subscriptionId' => $calendarId,
1383
-						'calendarData' => $subscriptionRow,
1384
-						'shares' => [],
1385
-						'objectData' => $objectRow,
1386
-					]
1387
-				));
1388
-			}
1389
-		}
1390
-
1391
-		return '"' . $extraData['etag'] . '"';
1392
-	}
1393
-
1394
-	/**
1395
-	 * Moves a calendar object from calendar to calendar.
1396
-	 *
1397
-	 * @param int $sourceCalendarId
1398
-	 * @param int $targetCalendarId
1399
-	 * @param int $objectId
1400
-	 * @param string $principalUri
1401
-	 * @param int $calendarType
1402
-	 * @return bool
1403
-	 * @throws Exception
1404
-	 */
1405
-	public function moveCalendarObject(int $sourceCalendarId, int $targetCalendarId, int $objectId, string $principalUri, int $calendarType = self::CALENDAR_TYPE_CALENDAR): bool {
1406
-		$object = $this->getCalendarObjectById($principalUri, $objectId);
1407
-		if (empty($object)) {
1408
-			return false;
1409
-		}
1410
-
1411
-		$query = $this->db->getQueryBuilder();
1412
-		$query->update('calendarobjects')
1413
-			->set('calendarid', $query->createNamedParameter($targetCalendarId, IQueryBuilder::PARAM_INT))
1414
-			->where($query->expr()->eq('id', $query->createNamedParameter($objectId, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT))
1415
-			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT))
1416
-			->executeStatement();
1417
-
1418
-		$this->purgeProperties($sourceCalendarId, $objectId);
1419
-		$this->updateProperties($targetCalendarId, $object['uri'], $object['calendardata'], $calendarType);
1420
-
1421
-		$this->addChange($sourceCalendarId, $object['uri'], 1, $calendarType);
1422
-		$this->addChange($targetCalendarId, $object['uri'], 3, $calendarType);
1423
-
1424
-		$object = $this->getCalendarObjectById($principalUri, $objectId);
1425
-		// Calendar Object wasn't found - possibly because it was deleted in the meantime by a different client
1426
-		if (empty($object)) {
1427
-			return false;
1428
-		}
1429
-
1430
-		$calendarRow = $this->getCalendarById($targetCalendarId);
1431
-		// the calendar this event is being moved to does not exist any longer
1432
-		if (empty($calendarRow)) {
1433
-			return false;
1434
-		}
1435
-
1436
-		if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
1437
-			$shares = $this->getShares($targetCalendarId);
1438
-			$this->dispatcher->dispatchTyped(new CalendarObjectUpdatedEvent($targetCalendarId, $calendarRow, $shares, $object));
1439
-		}
1440
-		return true;
1441
-	}
1442
-
1443
-
1444
-	/**
1445
-	 * @param int $calendarObjectId
1446
-	 * @param int $classification
1447
-	 */
1448
-	public function setClassification($calendarObjectId, $classification) {
1449
-		if (!in_array($classification, [
1450
-			self::CLASSIFICATION_PUBLIC, self::CLASSIFICATION_PRIVATE, self::CLASSIFICATION_CONFIDENTIAL
1451
-		])) {
1452
-			throw new \InvalidArgumentException();
1453
-		}
1454
-		$query = $this->db->getQueryBuilder();
1455
-		$query->update('calendarobjects')
1456
-			->set('classification', $query->createNamedParameter($classification))
1457
-			->where($query->expr()->eq('id', $query->createNamedParameter($calendarObjectId)))
1458
-			->executeStatement();
1459
-	}
1460
-
1461
-	/**
1462
-	 * Deletes an existing calendar object.
1463
-	 *
1464
-	 * The object uri is only the basename, or filename and not a full path.
1465
-	 *
1466
-	 * @param mixed $calendarId
1467
-	 * @param string $objectUri
1468
-	 * @param int $calendarType
1469
-	 * @param bool $forceDeletePermanently
1470
-	 * @return void
1471
-	 */
1472
-	public function deleteCalendarObject($calendarId, $objectUri, $calendarType = self::CALENDAR_TYPE_CALENDAR, bool $forceDeletePermanently = false) {
1473
-		$data = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
1474
-
1475
-		if ($data === null) {
1476
-			// Nothing to delete
1477
-			return;
1478
-		}
1479
-
1480
-		if ($forceDeletePermanently || $this->config->getAppValue(Application::APP_ID, RetentionService::RETENTION_CONFIG_KEY) === '0') {
1481
-			$stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ? AND `uri` = ? AND `calendartype` = ?');
1482
-			$stmt->execute([$calendarId, $objectUri, $calendarType]);
1483
-
1484
-			$this->purgeProperties($calendarId, $data['id']);
1485
-
1486
-			if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
1487
-				$calendarRow = $this->getCalendarById($calendarId);
1488
-				$shares = $this->getShares($calendarId);
1489
-
1490
-				$this->dispatcher->dispatchTyped(new CalendarObjectDeletedEvent((int)$calendarId, $calendarRow, $shares, $data));
1491
-			} else {
1492
-				$subscriptionRow = $this->getSubscriptionById($calendarId);
1493
-
1494
-				$this->dispatcher->dispatchTyped(new CachedCalendarObjectDeletedEvent((int)$calendarId, $subscriptionRow, [], $data));
1495
-				$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteCachedCalendarObject', new GenericEvent(
1496
-					'\OCA\DAV\CalDAV\CalDavBackend::deleteCachedCalendarObject',
1497
-					[
1498
-						'subscriptionId' => $calendarId,
1499
-						'calendarData' => $subscriptionRow,
1500
-						'shares' => [],
1501
-						'objectData' => $data,
1502
-					]
1503
-				));
1504
-			}
1505
-		} else {
1506
-			$pathInfo = pathinfo($data['uri']);
1507
-			if (!empty($pathInfo['extension'])) {
1508
-				// Append a suffix to "free" the old URI for recreation
1509
-				$newUri = sprintf(
1510
-					"%s-deleted.%s",
1511
-					$pathInfo['filename'],
1512
-					$pathInfo['extension']
1513
-				);
1514
-			} else {
1515
-				$newUri = sprintf(
1516
-					"%s-deleted",
1517
-					$pathInfo['filename']
1518
-				);
1519
-			}
1520
-
1521
-			// Try to detect conflicts before the DB does
1522
-			// As unlikely as it seems, this can happen when the user imports, then deletes, imports and deletes again
1523
-			$newObject = $this->getCalendarObject($calendarId, $newUri, $calendarType);
1524
-			if ($newObject !== null) {
1525
-				throw new Forbidden("A calendar object with URI $newUri already exists in calendar $calendarId, therefore this object can't be moved into the trashbin");
1526
-			}
1527
-
1528
-			$qb = $this->db->getQueryBuilder();
1529
-			$markObjectDeletedQuery = $qb->update('calendarobjects')
1530
-				->set('deleted_at', $qb->createNamedParameter(time(), IQueryBuilder::PARAM_INT))
1531
-				->set('uri', $qb->createNamedParameter($newUri))
1532
-				->where(
1533
-					$qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)),
1534
-					$qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT),
1535
-					$qb->expr()->eq('uri', $qb->createNamedParameter($objectUri))
1536
-				);
1537
-			$markObjectDeletedQuery->executeStatement();
1538
-
1539
-			$calendarData = $this->getCalendarById($calendarId);
1540
-			if ($calendarData !== null) {
1541
-				$this->dispatcher->dispatchTyped(
1542
-					new CalendarObjectMovedToTrashEvent(
1543
-						(int)$calendarId,
1544
-						$calendarData,
1545
-						$this->getShares($calendarId),
1546
-						$data
1547
-					)
1548
-				);
1549
-			}
1550
-		}
1551
-
1552
-		$this->addChange($calendarId, $objectUri, 3, $calendarType);
1553
-	}
1554
-
1555
-	/**
1556
-	 * @param mixed $objectData
1557
-	 *
1558
-	 * @throws Forbidden
1559
-	 */
1560
-	public function restoreCalendarObject(array $objectData): void {
1561
-		$id = (int) $objectData['id'];
1562
-		$restoreUri = str_replace("-deleted.ics", ".ics", $objectData['uri']);
1563
-		$targetObject = $this->getCalendarObject(
1564
-			$objectData['calendarid'],
1565
-			$restoreUri
1566
-		);
1567
-		if ($targetObject !== null) {
1568
-			throw new Forbidden("Can not restore calendar $id because a calendar object with the URI $restoreUri already exists");
1569
-		}
1570
-
1571
-		$qb = $this->db->getQueryBuilder();
1572
-		$update = $qb->update('calendarobjects')
1573
-			->set('uri', $qb->createNamedParameter($restoreUri))
1574
-			->set('deleted_at', $qb->createNamedParameter(null))
1575
-			->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT));
1576
-		$update->executeStatement();
1577
-
1578
-		// Make sure this change is tracked in the changes table
1579
-		$qb2 = $this->db->getQueryBuilder();
1580
-		$selectObject = $qb2->select('calendardata', 'uri', 'calendarid', 'calendartype')
1581
-			->selectAlias('componenttype', 'component')
1582
-			->from('calendarobjects')
1583
-			->where($qb2->expr()->eq('id', $qb2->createNamedParameter($id, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT));
1584
-		$result = $selectObject->executeQuery();
1585
-		$row = $result->fetch();
1586
-		$result->closeCursor();
1587
-		if ($row === false) {
1588
-			// Welp, this should possibly not have happened, but let's ignore
1589
-			return;
1590
-		}
1591
-		$this->addChange($row['calendarid'], $row['uri'], 1, (int) $row['calendartype']);
1592
-
1593
-		$calendarRow = $this->getCalendarById((int) $row['calendarid']);
1594
-		if ($calendarRow === null) {
1595
-			throw new RuntimeException('Calendar object data that was just written can\'t be read back. Check your database configuration.');
1596
-		}
1597
-		$this->dispatcher->dispatchTyped(
1598
-			new CalendarObjectRestoredEvent(
1599
-				(int) $objectData['calendarid'],
1600
-				$calendarRow,
1601
-				$this->getShares((int) $row['calendarid']),
1602
-				$row
1603
-			)
1604
-		);
1605
-	}
1606
-
1607
-	/**
1608
-	 * Performs a calendar-query on the contents of this calendar.
1609
-	 *
1610
-	 * The calendar-query is defined in RFC4791 : CalDAV. Using the
1611
-	 * calendar-query it is possible for a client to request a specific set of
1612
-	 * object, based on contents of iCalendar properties, date-ranges and
1613
-	 * iCalendar component types (VTODO, VEVENT).
1614
-	 *
1615
-	 * This method should just return a list of (relative) urls that match this
1616
-	 * query.
1617
-	 *
1618
-	 * The list of filters are specified as an array. The exact array is
1619
-	 * documented by Sabre\CalDAV\CalendarQueryParser.
1620
-	 *
1621
-	 * Note that it is extremely likely that getCalendarObject for every path
1622
-	 * returned from this method will be called almost immediately after. You
1623
-	 * may want to anticipate this to speed up these requests.
1624
-	 *
1625
-	 * This method provides a default implementation, which parses *all* the
1626
-	 * iCalendar objects in the specified calendar.
1627
-	 *
1628
-	 * This default may well be good enough for personal use, and calendars
1629
-	 * that aren't very large. But if you anticipate high usage, big calendars
1630
-	 * or high loads, you are strongly advised to optimize certain paths.
1631
-	 *
1632
-	 * The best way to do so is override this method and to optimize
1633
-	 * specifically for 'common filters'.
1634
-	 *
1635
-	 * Requests that are extremely common are:
1636
-	 *   * requests for just VEVENTS
1637
-	 *   * requests for just VTODO
1638
-	 *   * requests with a time-range-filter on either VEVENT or VTODO.
1639
-	 *
1640
-	 * ..and combinations of these requests. It may not be worth it to try to
1641
-	 * handle every possible situation and just rely on the (relatively
1642
-	 * easy to use) CalendarQueryValidator to handle the rest.
1643
-	 *
1644
-	 * Note that especially time-range-filters may be difficult to parse. A
1645
-	 * time-range filter specified on a VEVENT must for instance also handle
1646
-	 * recurrence rules correctly.
1647
-	 * A good example of how to interprete all these filters can also simply
1648
-	 * be found in Sabre\CalDAV\CalendarQueryFilter. This class is as correct
1649
-	 * as possible, so it gives you a good idea on what type of stuff you need
1650
-	 * to think of.
1651
-	 *
1652
-	 * @param mixed $calendarId
1653
-	 * @param array $filters
1654
-	 * @param int $calendarType
1655
-	 * @return array
1656
-	 */
1657
-	public function calendarQuery($calendarId, array $filters, $calendarType = self::CALENDAR_TYPE_CALENDAR):array {
1658
-		$componentType = null;
1659
-		$requirePostFilter = true;
1660
-		$timeRange = null;
1661
-
1662
-		// if no filters were specified, we don't need to filter after a query
1663
-		if (!$filters['prop-filters'] && !$filters['comp-filters']) {
1664
-			$requirePostFilter = false;
1665
-		}
1666
-
1667
-		// Figuring out if there's a component filter
1668
-		if (count($filters['comp-filters']) > 0 && !$filters['comp-filters'][0]['is-not-defined']) {
1669
-			$componentType = $filters['comp-filters'][0]['name'];
1670
-
1671
-			// Checking if we need post-filters
1672
-			if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['time-range'] && !$filters['comp-filters'][0]['prop-filters']) {
1673
-				$requirePostFilter = false;
1674
-			}
1675
-			// There was a time-range filter
1676
-			if ($componentType === 'VEVENT' && isset($filters['comp-filters'][0]['time-range']) && is_array($filters['comp-filters'][0]['time-range'])) {
1677
-				$timeRange = $filters['comp-filters'][0]['time-range'];
1678
-
1679
-				// If start time OR the end time is not specified, we can do a
1680
-				// 100% accurate mysql query.
1681
-				if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['prop-filters'] && (!$timeRange['start'] || !$timeRange['end'])) {
1682
-					$requirePostFilter = false;
1683
-				}
1684
-			}
1685
-		}
1686
-		$columns = ['uri'];
1687
-		if ($requirePostFilter) {
1688
-			$columns = ['uri', 'calendardata'];
1689
-		}
1690
-		$query = $this->db->getQueryBuilder();
1691
-		$query->select($columns)
1692
-			->from('calendarobjects')
1693
-			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
1694
-			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)))
1695
-			->andWhere($query->expr()->isNull('deleted_at'));
1696
-
1697
-		if ($componentType) {
1698
-			$query->andWhere($query->expr()->eq('componenttype', $query->createNamedParameter($componentType)));
1699
-		}
1700
-
1701
-		if ($timeRange && $timeRange['start']) {
1702
-			$query->andWhere($query->expr()->gt('lastoccurence', $query->createNamedParameter($timeRange['start']->getTimeStamp())));
1703
-		}
1704
-		if ($timeRange && $timeRange['end']) {
1705
-			$query->andWhere($query->expr()->lt('firstoccurence', $query->createNamedParameter($timeRange['end']->getTimeStamp())));
1706
-		}
1707
-
1708
-		$stmt = $query->executeQuery();
1709
-
1710
-		$result = [];
1711
-		while ($row = $stmt->fetch()) {
1712
-			if ($requirePostFilter) {
1713
-				// validateFilterForObject will parse the calendar data
1714
-				// catch parsing errors
1715
-				try {
1716
-					$matches = $this->validateFilterForObject($row, $filters);
1717
-				} catch (ParseException $ex) {
1718
-					$this->logger->logException($ex, [
1719
-						'app' => 'dav',
1720
-						'message' => 'Caught parsing exception for calendar data. This usually indicates invalid calendar data. calendar-id:'.$calendarId.' uri:'.$row['uri']
1721
-					]);
1722
-					continue;
1723
-				} catch (InvalidDataException $ex) {
1724
-					$this->logger->logException($ex, [
1725
-						'app' => 'dav',
1726
-						'message' => 'Caught invalid data exception for calendar data. This usually indicates invalid calendar data. calendar-id:'.$calendarId.' uri:'.$row['uri']
1727
-					]);
1728
-					continue;
1729
-				}
1730
-
1731
-				if (!$matches) {
1732
-					continue;
1733
-				}
1734
-			}
1735
-			$result[] = $row['uri'];
1736
-		}
1737
-
1738
-		return $result;
1739
-	}
1740
-
1741
-	/**
1742
-	 * custom Nextcloud search extension for CalDAV
1743
-	 *
1744
-	 * TODO - this should optionally cover cached calendar objects as well
1745
-	 *
1746
-	 * @param string $principalUri
1747
-	 * @param array $filters
1748
-	 * @param integer|null $limit
1749
-	 * @param integer|null $offset
1750
-	 * @return array
1751
-	 */
1752
-	public function calendarSearch($principalUri, array $filters, $limit = null, $offset = null) {
1753
-		$calendars = $this->getCalendarsForUser($principalUri);
1754
-		$ownCalendars = [];
1755
-		$sharedCalendars = [];
1756
-
1757
-		$uriMapper = [];
1758
-
1759
-		foreach ($calendars as $calendar) {
1760
-			if ($calendar['{http://owncloud.org/ns}owner-principal'] === $principalUri) {
1761
-				$ownCalendars[] = $calendar['id'];
1762
-			} else {
1763
-				$sharedCalendars[] = $calendar['id'];
1764
-			}
1765
-			$uriMapper[$calendar['id']] = $calendar['uri'];
1766
-		}
1767
-		if (count($ownCalendars) === 0 && count($sharedCalendars) === 0) {
1768
-			return [];
1769
-		}
1770
-
1771
-		$query = $this->db->getQueryBuilder();
1772
-		// Calendar id expressions
1773
-		$calendarExpressions = [];
1774
-		foreach ($ownCalendars as $id) {
1775
-			$calendarExpressions[] = $query->expr()->andX(
1776
-				$query->expr()->eq('c.calendarid',
1777
-					$query->createNamedParameter($id)),
1778
-				$query->expr()->eq('c.calendartype',
1779
-						$query->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
1780
-		}
1781
-		foreach ($sharedCalendars as $id) {
1782
-			$calendarExpressions[] = $query->expr()->andX(
1783
-				$query->expr()->eq('c.calendarid',
1784
-					$query->createNamedParameter($id)),
1785
-				$query->expr()->eq('c.classification',
1786
-					$query->createNamedParameter(self::CLASSIFICATION_PUBLIC)),
1787
-				$query->expr()->eq('c.calendartype',
1788
-					$query->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
1789
-		}
1790
-
1791
-		if (count($calendarExpressions) === 1) {
1792
-			$calExpr = $calendarExpressions[0];
1793
-		} else {
1794
-			$calExpr = call_user_func_array([$query->expr(), 'orX'], $calendarExpressions);
1795
-		}
1796
-
1797
-		// Component expressions
1798
-		$compExpressions = [];
1799
-		foreach ($filters['comps'] as $comp) {
1800
-			$compExpressions[] = $query->expr()
1801
-				->eq('c.componenttype', $query->createNamedParameter($comp));
1802
-		}
1803
-
1804
-		if (count($compExpressions) === 1) {
1805
-			$compExpr = $compExpressions[0];
1806
-		} else {
1807
-			$compExpr = call_user_func_array([$query->expr(), 'orX'], $compExpressions);
1808
-		}
1809
-
1810
-		if (!isset($filters['props'])) {
1811
-			$filters['props'] = [];
1812
-		}
1813
-		if (!isset($filters['params'])) {
1814
-			$filters['params'] = [];
1815
-		}
1816
-
1817
-		$propParamExpressions = [];
1818
-		foreach ($filters['props'] as $prop) {
1819
-			$propParamExpressions[] = $query->expr()->andX(
1820
-				$query->expr()->eq('i.name', $query->createNamedParameter($prop)),
1821
-				$query->expr()->isNull('i.parameter')
1822
-			);
1823
-		}
1824
-		foreach ($filters['params'] as $param) {
1825
-			$propParamExpressions[] = $query->expr()->andX(
1826
-				$query->expr()->eq('i.name', $query->createNamedParameter($param['property'])),
1827
-				$query->expr()->eq('i.parameter', $query->createNamedParameter($param['parameter']))
1828
-			);
1829
-		}
1830
-
1831
-		if (count($propParamExpressions) === 1) {
1832
-			$propParamExpr = $propParamExpressions[0];
1833
-		} else {
1834
-			$propParamExpr = call_user_func_array([$query->expr(), 'orX'], $propParamExpressions);
1835
-		}
1836
-
1837
-		$query->select(['c.calendarid', 'c.uri'])
1838
-			->from($this->dbObjectPropertiesTable, 'i')
1839
-			->join('i', 'calendarobjects', 'c', $query->expr()->eq('i.objectid', 'c.id'))
1840
-			->where($calExpr)
1841
-			->andWhere($compExpr)
1842
-			->andWhere($propParamExpr)
1843
-			->andWhere($query->expr()->iLike('i.value',
1844
-				$query->createNamedParameter('%'.$this->db->escapeLikeParameter($filters['search-term']).'%')))
1845
-			->andWhere($query->expr()->isNull('deleted_at'));
1846
-
1847
-		if ($offset) {
1848
-			$query->setFirstResult($offset);
1849
-		}
1850
-		if ($limit) {
1851
-			$query->setMaxResults($limit);
1852
-		}
1853
-
1854
-		$stmt = $query->executeQuery();
1855
-
1856
-		$result = [];
1857
-		while ($row = $stmt->fetch()) {
1858
-			$path = $uriMapper[$row['calendarid']] . '/' . $row['uri'];
1859
-			if (!in_array($path, $result)) {
1860
-				$result[] = $path;
1861
-			}
1862
-		}
1863
-
1864
-		return $result;
1865
-	}
1866
-
1867
-	/**
1868
-	 * used for Nextcloud's calendar API
1869
-	 *
1870
-	 * @param array $calendarInfo
1871
-	 * @param string $pattern
1872
-	 * @param array $searchProperties
1873
-	 * @param array $options
1874
-	 * @param integer|null $limit
1875
-	 * @param integer|null $offset
1876
-	 *
1877
-	 * @return array
1878
-	 */
1879
-	public function search(array $calendarInfo, $pattern, array $searchProperties,
1880
-						   array $options, $limit, $offset) {
1881
-		$outerQuery = $this->db->getQueryBuilder();
1882
-		$innerQuery = $this->db->getQueryBuilder();
1883
-
1884
-		$innerQuery->selectDistinct('op.objectid')
1885
-			->from($this->dbObjectPropertiesTable, 'op')
1886
-			->andWhere($innerQuery->expr()->eq('op.calendarid',
1887
-				$outerQuery->createNamedParameter($calendarInfo['id'])))
1888
-			->andWhere($innerQuery->expr()->eq('op.calendartype',
1889
-				$outerQuery->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
1890
-
1891
-		// only return public items for shared calendars for now
1892
-		if (isset($calendarInfo['{http://owncloud.org/ns}owner-principal']) === false || $calendarInfo['principaluri'] !== $calendarInfo['{http://owncloud.org/ns}owner-principal']) {
1893
-			$innerQuery->andWhere($innerQuery->expr()->eq('c.classification',
1894
-				$outerQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
1895
-		}
1896
-
1897
-		if (!empty($searchProperties)) {
1898
-			$or = $innerQuery->expr()->orX();
1899
-			foreach ($searchProperties as $searchProperty) {
1900
-				$or->add($innerQuery->expr()->eq('op.name',
1901
-					$outerQuery->createNamedParameter($searchProperty)));
1902
-			}
1903
-			$innerQuery->andWhere($or);
1904
-		}
1905
-
1906
-		if ($pattern !== '') {
1907
-			$innerQuery->andWhere($innerQuery->expr()->iLike('op.value',
1908
-				$outerQuery->createNamedParameter('%' .
1909
-					$this->db->escapeLikeParameter($pattern) . '%')));
1910
-		}
1911
-
1912
-		$outerQuery->select('c.id', 'c.calendardata', 'c.componenttype', 'c.uid', 'c.uri')
1913
-			->from('calendarobjects', 'c')
1914
-			->where($outerQuery->expr()->isNull('deleted_at'));
1915
-
1916
-		if (isset($options['timerange'])) {
1917
-			if (isset($options['timerange']['start']) && $options['timerange']['start'] instanceof DateTimeInterface) {
1918
-				$outerQuery->andWhere($outerQuery->expr()->gt('lastoccurence',
1919
-					$outerQuery->createNamedParameter($options['timerange']['start']->getTimeStamp())));
1920
-			}
1921
-			if (isset($options['timerange']['end']) && $options['timerange']['end'] instanceof DateTimeInterface) {
1922
-				$outerQuery->andWhere($outerQuery->expr()->lt('firstoccurence',
1923
-					$outerQuery->createNamedParameter($options['timerange']['end']->getTimeStamp())));
1924
-			}
1925
-		}
1926
-
1927
-		if (!empty($options['types'])) {
1928
-			$or = $outerQuery->expr()->orX();
1929
-			foreach ($options['types'] as $type) {
1930
-				$or->add($outerQuery->expr()->eq('componenttype',
1931
-					$outerQuery->createNamedParameter($type)));
1932
-			}
1933
-			$outerQuery->andWhere($or);
1934
-		}
1935
-
1936
-		$outerQuery->andWhere($outerQuery->expr()->in('c.id', $outerQuery->createFunction($innerQuery->getSQL())));
1937
-
1938
-		if ($offset) {
1939
-			$outerQuery->setFirstResult($offset);
1940
-		}
1941
-		if ($limit) {
1942
-			$outerQuery->setMaxResults($limit);
1943
-		}
1944
-
1945
-		$result = $outerQuery->executeQuery();
1946
-		$calendarObjects = array_filter($result->fetchAll(), function (array $row) use ($options) {
1947
-			$start = $options['timerange']['start'] ?? null;
1948
-			$end = $options['timerange']['end'] ?? null;
1949
-
1950
-			if ($start === null || !($start instanceof DateTimeInterface) || $end === null || !($end instanceof DateTimeInterface)) {
1951
-				// No filter required
1952
-				return true;
1953
-			}
1954
-
1955
-			$isValid = $this->validateFilterForObject($row, [
1956
-				'name' => 'VCALENDAR',
1957
-				'comp-filters' => [
1958
-					[
1959
-						'name' => 'VEVENT',
1960
-						'comp-filters' => [],
1961
-						'prop-filters' => [],
1962
-						'is-not-defined' => false,
1963
-						'time-range' => [
1964
-							'start' => $start,
1965
-							'end' => $end,
1966
-						],
1967
-					],
1968
-				],
1969
-				'prop-filters' => [],
1970
-				'is-not-defined' => false,
1971
-				'time-range' => null,
1972
-			]);
1973
-			if (is_resource($row['calendardata'])) {
1974
-				// Put the stream back to the beginning so it can be read another time
1975
-				rewind($row['calendardata']);
1976
-			}
1977
-			return $isValid;
1978
-		});
1979
-		$result->closeCursor();
1980
-
1981
-		return array_map(function ($o) {
1982
-			$calendarData = Reader::read($o['calendardata']);
1983
-			$comps = $calendarData->getComponents();
1984
-			$objects = [];
1985
-			$timezones = [];
1986
-			foreach ($comps as $comp) {
1987
-				if ($comp instanceof VTimeZone) {
1988
-					$timezones[] = $comp;
1989
-				} else {
1990
-					$objects[] = $comp;
1991
-				}
1992
-			}
1993
-
1994
-			return [
1995
-				'id' => $o['id'],
1996
-				'type' => $o['componenttype'],
1997
-				'uid' => $o['uid'],
1998
-				'uri' => $o['uri'],
1999
-				'objects' => array_map(function ($c) {
2000
-					return $this->transformSearchData($c);
2001
-				}, $objects),
2002
-				'timezones' => array_map(function ($c) {
2003
-					return $this->transformSearchData($c);
2004
-				}, $timezones),
2005
-			];
2006
-		}, $calendarObjects);
2007
-	}
2008
-
2009
-	/**
2010
-	 * @param Component $comp
2011
-	 * @return array
2012
-	 */
2013
-	private function transformSearchData(Component $comp) {
2014
-		$data = [];
2015
-		/** @var Component[] $subComponents */
2016
-		$subComponents = $comp->getComponents();
2017
-		/** @var Property[] $properties */
2018
-		$properties = array_filter($comp->children(), function ($c) {
2019
-			return $c instanceof Property;
2020
-		});
2021
-		$validationRules = $comp->getValidationRules();
2022
-
2023
-		foreach ($subComponents as $subComponent) {
2024
-			$name = $subComponent->name;
2025
-			if (!isset($data[$name])) {
2026
-				$data[$name] = [];
2027
-			}
2028
-			$data[$name][] = $this->transformSearchData($subComponent);
2029
-		}
2030
-
2031
-		foreach ($properties as $property) {
2032
-			$name = $property->name;
2033
-			if (!isset($validationRules[$name])) {
2034
-				$validationRules[$name] = '*';
2035
-			}
2036
-
2037
-			$rule = $validationRules[$property->name];
2038
-			if ($rule === '+' || $rule === '*') { // multiple
2039
-				if (!isset($data[$name])) {
2040
-					$data[$name] = [];
2041
-				}
2042
-
2043
-				$data[$name][] = $this->transformSearchProperty($property);
2044
-			} else { // once
2045
-				$data[$name] = $this->transformSearchProperty($property);
2046
-			}
2047
-		}
2048
-
2049
-		return $data;
2050
-	}
2051
-
2052
-	/**
2053
-	 * @param Property $prop
2054
-	 * @return array
2055
-	 */
2056
-	private function transformSearchProperty(Property $prop) {
2057
-		// No need to check Date, as it extends DateTime
2058
-		if ($prop instanceof Property\ICalendar\DateTime) {
2059
-			$value = $prop->getDateTime();
2060
-		} else {
2061
-			$value = $prop->getValue();
2062
-		}
2063
-
2064
-		return [
2065
-			$value,
2066
-			$prop->parameters()
2067
-		];
2068
-	}
2069
-
2070
-	/**
2071
-	 * @param string $principalUri
2072
-	 * @param string $pattern
2073
-	 * @param array $componentTypes
2074
-	 * @param array $searchProperties
2075
-	 * @param array $searchParameters
2076
-	 * @param array $options
2077
-	 * @return array
2078
-	 */
2079
-	public function searchPrincipalUri(string $principalUri,
2080
-									   string $pattern,
2081
-									   array $componentTypes,
2082
-									   array $searchProperties,
2083
-									   array $searchParameters,
2084
-									   array $options = []): array {
2085
-		$escapePattern = !\array_key_exists('escape_like_param', $options) || $options['escape_like_param'] !== false;
2086
-
2087
-		$calendarObjectIdQuery = $this->db->getQueryBuilder();
2088
-		$calendarOr = $calendarObjectIdQuery->expr()->orX();
2089
-		$searchOr = $calendarObjectIdQuery->expr()->orX();
2090
-
2091
-		// Fetch calendars and subscription
2092
-		$calendars = $this->getCalendarsForUser($principalUri);
2093
-		$subscriptions = $this->getSubscriptionsForUser($principalUri);
2094
-		foreach ($calendars as $calendar) {
2095
-			$calendarAnd = $calendarObjectIdQuery->expr()->andX();
2096
-			$calendarAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int)$calendar['id'])));
2097
-			$calendarAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendartype', $calendarObjectIdQuery->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
2098
-
2099
-			// If it's shared, limit search to public events
2100
-			if (isset($calendar['{http://owncloud.org/ns}owner-principal'])
2101
-				&& $calendar['principaluri'] !== $calendar['{http://owncloud.org/ns}owner-principal']) {
2102
-				$calendarAnd->add($calendarObjectIdQuery->expr()->eq('co.classification', $calendarObjectIdQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
2103
-			}
2104
-
2105
-			$calendarOr->add($calendarAnd);
2106
-		}
2107
-		foreach ($subscriptions as $subscription) {
2108
-			$subscriptionAnd = $calendarObjectIdQuery->expr()->andX();
2109
-			$subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int)$subscription['id'])));
2110
-			$subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendartype', $calendarObjectIdQuery->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)));
2111
-
2112
-			// If it's shared, limit search to public events
2113
-			if (isset($subscription['{http://owncloud.org/ns}owner-principal'])
2114
-				&& $subscription['principaluri'] !== $subscription['{http://owncloud.org/ns}owner-principal']) {
2115
-				$subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('co.classification', $calendarObjectIdQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
2116
-			}
2117
-
2118
-			$calendarOr->add($subscriptionAnd);
2119
-		}
2120
-
2121
-		foreach ($searchProperties as $property) {
2122
-			$propertyAnd = $calendarObjectIdQuery->expr()->andX();
2123
-			$propertyAnd->add($calendarObjectIdQuery->expr()->eq('cob.name', $calendarObjectIdQuery->createNamedParameter($property, IQueryBuilder::PARAM_STR)));
2124
-			$propertyAnd->add($calendarObjectIdQuery->expr()->isNull('cob.parameter'));
2125
-
2126
-			$searchOr->add($propertyAnd);
2127
-		}
2128
-		foreach ($searchParameters as $property => $parameter) {
2129
-			$parameterAnd = $calendarObjectIdQuery->expr()->andX();
2130
-			$parameterAnd->add($calendarObjectIdQuery->expr()->eq('cob.name', $calendarObjectIdQuery->createNamedParameter($property, IQueryBuilder::PARAM_STR)));
2131
-			$parameterAnd->add($calendarObjectIdQuery->expr()->eq('cob.parameter', $calendarObjectIdQuery->createNamedParameter($parameter, IQueryBuilder::PARAM_STR_ARRAY)));
2132
-
2133
-			$searchOr->add($parameterAnd);
2134
-		}
2135
-
2136
-		if ($calendarOr->count() === 0) {
2137
-			return [];
2138
-		}
2139
-		if ($searchOr->count() === 0) {
2140
-			return [];
2141
-		}
2142
-
2143
-		$calendarObjectIdQuery->selectDistinct('cob.objectid')
2144
-			->from($this->dbObjectPropertiesTable, 'cob')
2145
-			->leftJoin('cob', 'calendarobjects', 'co', $calendarObjectIdQuery->expr()->eq('co.id', 'cob.objectid'))
2146
-			->andWhere($calendarObjectIdQuery->expr()->in('co.componenttype', $calendarObjectIdQuery->createNamedParameter($componentTypes, IQueryBuilder::PARAM_STR_ARRAY)))
2147
-			->andWhere($calendarOr)
2148
-			->andWhere($searchOr)
2149
-			->andWhere($calendarObjectIdQuery->expr()->isNull('deleted_at'));
2150
-
2151
-		if ('' !== $pattern) {
2152
-			if (!$escapePattern) {
2153
-				$calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->ilike('cob.value', $calendarObjectIdQuery->createNamedParameter($pattern)));
2154
-			} else {
2155
-				$calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->ilike('cob.value', $calendarObjectIdQuery->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%')));
2156
-			}
2157
-		}
2158
-
2159
-		if (isset($options['limit'])) {
2160
-			$calendarObjectIdQuery->setMaxResults($options['limit']);
2161
-		}
2162
-		if (isset($options['offset'])) {
2163
-			$calendarObjectIdQuery->setFirstResult($options['offset']);
2164
-		}
2165
-
2166
-		$result = $calendarObjectIdQuery->executeQuery();
2167
-		$matches = $result->fetchAll();
2168
-		$result->closeCursor();
2169
-		$matches = array_map(static function (array $match):int {
2170
-			return (int) $match['objectid'];
2171
-		}, $matches);
2172
-
2173
-		$query = $this->db->getQueryBuilder();
2174
-		$query->select('calendardata', 'uri', 'calendarid', 'calendartype')
2175
-			->from('calendarobjects')
2176
-			->where($query->expr()->in('id', $query->createNamedParameter($matches, IQueryBuilder::PARAM_INT_ARRAY)));
2177
-
2178
-		$result = $query->executeQuery();
2179
-		$calendarObjects = $result->fetchAll();
2180
-		$result->closeCursor();
2181
-
2182
-		return array_map(function (array $array): array {
2183
-			$array['calendarid'] = (int)$array['calendarid'];
2184
-			$array['calendartype'] = (int)$array['calendartype'];
2185
-			$array['calendardata'] = $this->readBlob($array['calendardata']);
2186
-
2187
-			return $array;
2188
-		}, $calendarObjects);
2189
-	}
2190
-
2191
-	/**
2192
-	 * Searches through all of a users calendars and calendar objects to find
2193
-	 * an object with a specific UID.
2194
-	 *
2195
-	 * This method should return the path to this object, relative to the
2196
-	 * calendar home, so this path usually only contains two parts:
2197
-	 *
2198
-	 * calendarpath/objectpath.ics
2199
-	 *
2200
-	 * If the uid is not found, return null.
2201
-	 *
2202
-	 * This method should only consider * objects that the principal owns, so
2203
-	 * any calendars owned by other principals that also appear in this
2204
-	 * collection should be ignored.
2205
-	 *
2206
-	 * @param string $principalUri
2207
-	 * @param string $uid
2208
-	 * @return string|null
2209
-	 */
2210
-	public function getCalendarObjectByUID($principalUri, $uid) {
2211
-		$query = $this->db->getQueryBuilder();
2212
-		$query->selectAlias('c.uri', 'calendaruri')->selectAlias('co.uri', 'objecturi')
2213
-			->from('calendarobjects', 'co')
2214
-			->leftJoin('co', 'calendars', 'c', $query->expr()->eq('co.calendarid', 'c.id'))
2215
-			->where($query->expr()->eq('c.principaluri', $query->createNamedParameter($principalUri)))
2216
-			->andWhere($query->expr()->eq('co.uid', $query->createNamedParameter($uid)))
2217
-			->andWhere($query->expr()->isNull('co.deleted_at'));
2218
-		$stmt = $query->executeQuery();
2219
-		$row = $stmt->fetch();
2220
-		$stmt->closeCursor();
2221
-		if ($row) {
2222
-			return $row['calendaruri'] . '/' . $row['objecturi'];
2223
-		}
2224
-
2225
-		return null;
2226
-	}
2227
-
2228
-	public function getCalendarObjectById(string $principalUri, int $id): ?array {
2229
-		$query = $this->db->getQueryBuilder();
2230
-		$query->select(['co.id', 'co.uri', 'co.lastmodified', 'co.etag', 'co.calendarid', 'co.size', 'co.calendardata', 'co.componenttype', 'co.classification', 'co.deleted_at'])
2231
-			->selectAlias('c.uri', 'calendaruri')
2232
-			->from('calendarobjects', 'co')
2233
-			->join('co', 'calendars', 'c', $query->expr()->eq('c.id', 'co.calendarid', IQueryBuilder::PARAM_INT))
2234
-			->where($query->expr()->eq('c.principaluri', $query->createNamedParameter($principalUri)))
2235
-			->andWhere($query->expr()->eq('co.id', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT));
2236
-		$stmt = $query->executeQuery();
2237
-		$row = $stmt->fetch();
2238
-		$stmt->closeCursor();
2239
-
2240
-		if (!$row) {
2241
-			return null;
2242
-		}
2243
-
2244
-		return [
2245
-			'id' => $row['id'],
2246
-			'uri' => $row['uri'],
2247
-			'lastmodified' => $row['lastmodified'],
2248
-			'etag' => '"' . $row['etag'] . '"',
2249
-			'calendarid' => $row['calendarid'],
2250
-			'calendaruri' => $row['calendaruri'],
2251
-			'size' => (int)$row['size'],
2252
-			'calendardata' => $this->readBlob($row['calendardata']),
2253
-			'component' => strtolower($row['componenttype']),
2254
-			'classification' => (int)$row['classification'],
2255
-			'deleted_at' => isset($row['deleted_at']) ? ((int) $row['deleted_at']) : null,
2256
-		];
2257
-	}
2258
-
2259
-	/**
2260
-	 * The getChanges method returns all the changes that have happened, since
2261
-	 * the specified syncToken in the specified calendar.
2262
-	 *
2263
-	 * This function should return an array, such as the following:
2264
-	 *
2265
-	 * [
2266
-	 *   'syncToken' => 'The current synctoken',
2267
-	 *   'added'   => [
2268
-	 *      'new.txt',
2269
-	 *   ],
2270
-	 *   'modified'   => [
2271
-	 *      'modified.txt',
2272
-	 *   ],
2273
-	 *   'deleted' => [
2274
-	 *      'foo.php.bak',
2275
-	 *      'old.txt'
2276
-	 *   ]
2277
-	 * );
2278
-	 *
2279
-	 * The returned syncToken property should reflect the *current* syncToken
2280
-	 * of the calendar, as reported in the {http://sabredav.org/ns}sync-token
2281
-	 * property This is * needed here too, to ensure the operation is atomic.
2282
-	 *
2283
-	 * If the $syncToken argument is specified as null, this is an initial
2284
-	 * sync, and all members should be reported.
2285
-	 *
2286
-	 * The modified property is an array of nodenames that have changed since
2287
-	 * the last token.
2288
-	 *
2289
-	 * The deleted property is an array with nodenames, that have been deleted
2290
-	 * from collection.
2291
-	 *
2292
-	 * The $syncLevel argument is basically the 'depth' of the report. If it's
2293
-	 * 1, you only have to report changes that happened only directly in
2294
-	 * immediate descendants. If it's 2, it should also include changes from
2295
-	 * the nodes below the child collections. (grandchildren)
2296
-	 *
2297
-	 * The $limit argument allows a client to specify how many results should
2298
-	 * be returned at most. If the limit is not specified, it should be treated
2299
-	 * as infinite.
2300
-	 *
2301
-	 * If the limit (infinite or not) is higher than you're willing to return,
2302
-	 * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
2303
-	 *
2304
-	 * If the syncToken is expired (due to data cleanup) or unknown, you must
2305
-	 * return null.
2306
-	 *
2307
-	 * The limit is 'suggestive'. You are free to ignore it.
2308
-	 *
2309
-	 * @param string $calendarId
2310
-	 * @param string $syncToken
2311
-	 * @param int $syncLevel
2312
-	 * @param int|null $limit
2313
-	 * @param int $calendarType
2314
-	 * @return array
2315
-	 */
2316
-	public function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit = null, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
2317
-		// Current synctoken
2318
-		$qb = $this->db->getQueryBuilder();
2319
-		$qb->select('synctoken')
2320
-			->from('calendars')
2321
-			->where(
2322
-				$qb->expr()->eq('id', $qb->createNamedParameter($calendarId))
2323
-			);
2324
-		$stmt = $qb->executeQuery();
2325
-		$currentToken = $stmt->fetchOne();
2326
-
2327
-		if ($currentToken === false) {
2328
-			return null;
2329
-		}
2330
-
2331
-		$result = [
2332
-			'syncToken' => $currentToken,
2333
-			'added' => [],
2334
-			'modified' => [],
2335
-			'deleted' => [],
2336
-		];
2337
-
2338
-		if ($syncToken) {
2339
-			$qb = $this->db->getQueryBuilder();
2340
-
2341
-			$qb->select('uri', 'operation')
2342
-				->from('calendarchanges')
2343
-				->where(
2344
-					$qb->expr()->andX(
2345
-						$qb->expr()->gte('synctoken', $qb->createNamedParameter($syncToken)),
2346
-						$qb->expr()->lt('synctoken', $qb->createNamedParameter($currentToken)),
2347
-						$qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)),
2348
-						$qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType))
2349
-					)
2350
-				)->orderBy('synctoken');
2351
-			if (is_int($limit) && $limit > 0) {
2352
-				$qb->setMaxResults($limit);
2353
-			}
2354
-
2355
-			// Fetching all changes
2356
-			$stmt = $qb->executeQuery();
2357
-			$changes = [];
2358
-
2359
-			// This loop ensures that any duplicates are overwritten, only the
2360
-			// last change on a node is relevant.
2361
-			while ($row = $stmt->fetch()) {
2362
-				$changes[$row['uri']] = $row['operation'];
2363
-			}
2364
-			$stmt->closeCursor();
2365
-
2366
-			foreach ($changes as $uri => $operation) {
2367
-				switch ($operation) {
2368
-					case 1:
2369
-						$result['added'][] = $uri;
2370
-						break;
2371
-					case 2:
2372
-						$result['modified'][] = $uri;
2373
-						break;
2374
-					case 3:
2375
-						$result['deleted'][] = $uri;
2376
-						break;
2377
-				}
2378
-			}
2379
-		} else {
2380
-			// No synctoken supplied, this is the initial sync.
2381
-			$qb = $this->db->getQueryBuilder();
2382
-			$qb->select('uri')
2383
-				->from('calendarobjects')
2384
-				->where(
2385
-					$qb->expr()->andX(
2386
-						$qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)),
2387
-						$qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType))
2388
-					)
2389
-				);
2390
-			$stmt = $qb->executeQuery();
2391
-			$result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
2392
-			$stmt->closeCursor();
2393
-		}
2394
-		return $result;
2395
-	}
2396
-
2397
-	/**
2398
-	 * Returns a list of subscriptions for a principal.
2399
-	 *
2400
-	 * Every subscription is an array with the following keys:
2401
-	 *  * id, a unique id that will be used by other functions to modify the
2402
-	 *    subscription. This can be the same as the uri or a database key.
2403
-	 *  * uri. This is just the 'base uri' or 'filename' of the subscription.
2404
-	 *  * principaluri. The owner of the subscription. Almost always the same as
2405
-	 *    principalUri passed to this method.
2406
-	 *
2407
-	 * Furthermore, all the subscription info must be returned too:
2408
-	 *
2409
-	 * 1. {DAV:}displayname
2410
-	 * 2. {http://apple.com/ns/ical/}refreshrate
2411
-	 * 3. {http://calendarserver.org/ns/}subscribed-strip-todos (omit if todos
2412
-	 *    should not be stripped).
2413
-	 * 4. {http://calendarserver.org/ns/}subscribed-strip-alarms (omit if alarms
2414
-	 *    should not be stripped).
2415
-	 * 5. {http://calendarserver.org/ns/}subscribed-strip-attachments (omit if
2416
-	 *    attachments should not be stripped).
2417
-	 * 6. {http://calendarserver.org/ns/}source (Must be a
2418
-	 *     Sabre\DAV\Property\Href).
2419
-	 * 7. {http://apple.com/ns/ical/}calendar-color
2420
-	 * 8. {http://apple.com/ns/ical/}calendar-order
2421
-	 * 9. {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
2422
-	 *    (should just be an instance of
2423
-	 *    Sabre\CalDAV\Property\SupportedCalendarComponentSet, with a bunch of
2424
-	 *    default components).
2425
-	 *
2426
-	 * @param string $principalUri
2427
-	 * @return array
2428
-	 */
2429
-	public function getSubscriptionsForUser($principalUri) {
2430
-		$fields = array_column($this->subscriptionPropertyMap, 0);
2431
-		$fields[] = 'id';
2432
-		$fields[] = 'uri';
2433
-		$fields[] = 'source';
2434
-		$fields[] = 'principaluri';
2435
-		$fields[] = 'lastmodified';
2436
-		$fields[] = 'synctoken';
2437
-
2438
-		$query = $this->db->getQueryBuilder();
2439
-		$query->select($fields)
2440
-			->from('calendarsubscriptions')
2441
-			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
2442
-			->orderBy('calendarorder', 'asc');
2443
-		$stmt = $query->executeQuery();
2444
-
2445
-		$subscriptions = [];
2446
-		while ($row = $stmt->fetch()) {
2447
-			$subscription = [
2448
-				'id' => $row['id'],
2449
-				'uri' => $row['uri'],
2450
-				'principaluri' => $row['principaluri'],
2451
-				'source' => $row['source'],
2452
-				'lastmodified' => $row['lastmodified'],
2453
-
2454
-				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
2455
-				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
2456
-			];
2457
-
2458
-			$subscriptions[] = $this->rowToSubscription($row, $subscription);
2459
-		}
2460
-
2461
-		return $subscriptions;
2462
-	}
2463
-
2464
-	/**
2465
-	 * Creates a new subscription for a principal.
2466
-	 *
2467
-	 * If the creation was a success, an id must be returned that can be used to reference
2468
-	 * this subscription in other methods, such as updateSubscription.
2469
-	 *
2470
-	 * @param string $principalUri
2471
-	 * @param string $uri
2472
-	 * @param array $properties
2473
-	 * @return mixed
2474
-	 */
2475
-	public function createSubscription($principalUri, $uri, array $properties) {
2476
-		if (!isset($properties['{http://calendarserver.org/ns/}source'])) {
2477
-			throw new Forbidden('The {http://calendarserver.org/ns/}source property is required when creating subscriptions');
2478
-		}
2479
-
2480
-		$values = [
2481
-			'principaluri' => $principalUri,
2482
-			'uri' => $uri,
2483
-			'source' => $properties['{http://calendarserver.org/ns/}source']->getHref(),
2484
-			'lastmodified' => time(),
2485
-		];
2486
-
2487
-		$propertiesBoolean = ['striptodos', 'stripalarms', 'stripattachments'];
2488
-
2489
-		foreach ($this->subscriptionPropertyMap as $xmlName => [$dbName, $type]) {
2490
-			if (array_key_exists($xmlName, $properties)) {
2491
-				$values[$dbName] = $properties[$xmlName];
2492
-				if (in_array($dbName, $propertiesBoolean)) {
2493
-					$values[$dbName] = true;
2494
-				}
2495
-			}
2496
-		}
2497
-
2498
-		$valuesToInsert = [];
2499
-
2500
-		$query = $this->db->getQueryBuilder();
2501
-
2502
-		foreach (array_keys($values) as $name) {
2503
-			$valuesToInsert[$name] = $query->createNamedParameter($values[$name]);
2504
-		}
2505
-
2506
-		$query->insert('calendarsubscriptions')
2507
-			->values($valuesToInsert)
2508
-			->executeStatement();
2509
-
2510
-		$subscriptionId = $query->getLastInsertId();
2511
-
2512
-		$subscriptionRow = $this->getSubscriptionById($subscriptionId);
2513
-		$this->dispatcher->dispatchTyped(new SubscriptionCreatedEvent($subscriptionId, $subscriptionRow));
2514
-		$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createSubscription', new GenericEvent(
2515
-			'\OCA\DAV\CalDAV\CalDavBackend::createSubscription',
2516
-			[
2517
-				'subscriptionId' => $subscriptionId,
2518
-				'subscriptionData' => $subscriptionRow,
2519
-			]));
2520
-
2521
-		return $subscriptionId;
2522
-	}
2523
-
2524
-	/**
2525
-	 * Updates a subscription
2526
-	 *
2527
-	 * The list of mutations is stored in a Sabre\DAV\PropPatch object.
2528
-	 * To do the actual updates, you must tell this object which properties
2529
-	 * you're going to process with the handle() method.
2530
-	 *
2531
-	 * Calling the handle method is like telling the PropPatch object "I
2532
-	 * promise I can handle updating this property".
2533
-	 *
2534
-	 * Read the PropPatch documentation for more info and examples.
2535
-	 *
2536
-	 * @param mixed $subscriptionId
2537
-	 * @param PropPatch $propPatch
2538
-	 * @return void
2539
-	 */
2540
-	public function updateSubscription($subscriptionId, PropPatch $propPatch) {
2541
-		$supportedProperties = array_keys($this->subscriptionPropertyMap);
2542
-		$supportedProperties[] = '{http://calendarserver.org/ns/}source';
2543
-
2544
-		$propPatch->handle($supportedProperties, function ($mutations) use ($subscriptionId) {
2545
-			$newValues = [];
2546
-
2547
-			foreach ($mutations as $propertyName => $propertyValue) {
2548
-				if ($propertyName === '{http://calendarserver.org/ns/}source') {
2549
-					$newValues['source'] = $propertyValue->getHref();
2550
-				} else {
2551
-					$fieldName = $this->subscriptionPropertyMap[$propertyName][0];
2552
-					$newValues[$fieldName] = $propertyValue;
2553
-				}
2554
-			}
2555
-
2556
-			$query = $this->db->getQueryBuilder();
2557
-			$query->update('calendarsubscriptions')
2558
-				->set('lastmodified', $query->createNamedParameter(time()));
2559
-			foreach ($newValues as $fieldName => $value) {
2560
-				$query->set($fieldName, $query->createNamedParameter($value));
2561
-			}
2562
-			$query->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
2563
-				->executeStatement();
2564
-
2565
-			$subscriptionRow = $this->getSubscriptionById($subscriptionId);
2566
-			$this->dispatcher->dispatchTyped(new SubscriptionUpdatedEvent((int)$subscriptionId, $subscriptionRow, [], $mutations));
2567
-			$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateSubscription', new GenericEvent(
2568
-				'\OCA\DAV\CalDAV\CalDavBackend::updateSubscription',
2569
-				[
2570
-					'subscriptionId' => $subscriptionId,
2571
-					'subscriptionData' => $subscriptionRow,
2572
-					'propertyMutations' => $mutations,
2573
-				]));
2574
-
2575
-			return true;
2576
-		});
2577
-	}
2578
-
2579
-	/**
2580
-	 * Deletes a subscription.
2581
-	 *
2582
-	 * @param mixed $subscriptionId
2583
-	 * @return void
2584
-	 */
2585
-	public function deleteSubscription($subscriptionId) {
2586
-		$subscriptionRow = $this->getSubscriptionById($subscriptionId);
2587
-
2588
-		$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteSubscription', new GenericEvent(
2589
-			'\OCA\DAV\CalDAV\CalDavBackend::deleteSubscription',
2590
-			[
2591
-				'subscriptionId' => $subscriptionId,
2592
-				'subscriptionData' => $this->getSubscriptionById($subscriptionId),
2593
-			]));
2594
-
2595
-		$query = $this->db->getQueryBuilder();
2596
-		$query->delete('calendarsubscriptions')
2597
-			->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
2598
-			->executeStatement();
2599
-
2600
-		$query = $this->db->getQueryBuilder();
2601
-		$query->delete('calendarobjects')
2602
-			->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2603
-			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2604
-			->executeStatement();
2605
-
2606
-		$query->delete('calendarchanges')
2607
-			->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2608
-			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2609
-			->executeStatement();
2610
-
2611
-		$query->delete($this->dbObjectPropertiesTable)
2612
-			->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2613
-			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2614
-			->executeStatement();
2615
-
2616
-		if ($subscriptionRow) {
2617
-			$this->dispatcher->dispatchTyped(new SubscriptionDeletedEvent((int)$subscriptionId, $subscriptionRow, []));
2618
-		}
2619
-	}
2620
-
2621
-	/**
2622
-	 * Returns a single scheduling object for the inbox collection.
2623
-	 *
2624
-	 * The returned array should contain the following elements:
2625
-	 *   * uri - A unique basename for the object. This will be used to
2626
-	 *           construct a full uri.
2627
-	 *   * calendardata - The iCalendar object
2628
-	 *   * lastmodified - The last modification date. Can be an int for a unix
2629
-	 *                    timestamp, or a PHP DateTime object.
2630
-	 *   * etag - A unique token that must change if the object changed.
2631
-	 *   * size - The size of the object, in bytes.
2632
-	 *
2633
-	 * @param string $principalUri
2634
-	 * @param string $objectUri
2635
-	 * @return array
2636
-	 */
2637
-	public function getSchedulingObject($principalUri, $objectUri) {
2638
-		$query = $this->db->getQueryBuilder();
2639
-		$stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size'])
2640
-			->from('schedulingobjects')
2641
-			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
2642
-			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
2643
-			->executeQuery();
2644
-
2645
-		$row = $stmt->fetch();
2646
-
2647
-		if (!$row) {
2648
-			return null;
2649
-		}
2650
-
2651
-		return [
2652
-			'uri' => $row['uri'],
2653
-			'calendardata' => $row['calendardata'],
2654
-			'lastmodified' => $row['lastmodified'],
2655
-			'etag' => '"' . $row['etag'] . '"',
2656
-			'size' => (int)$row['size'],
2657
-		];
2658
-	}
2659
-
2660
-	/**
2661
-	 * Returns all scheduling objects for the inbox collection.
2662
-	 *
2663
-	 * These objects should be returned as an array. Every item in the array
2664
-	 * should follow the same structure as returned from getSchedulingObject.
2665
-	 *
2666
-	 * The main difference is that 'calendardata' is optional.
2667
-	 *
2668
-	 * @param string $principalUri
2669
-	 * @return array
2670
-	 */
2671
-	public function getSchedulingObjects($principalUri) {
2672
-		$query = $this->db->getQueryBuilder();
2673
-		$stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size'])
2674
-				->from('schedulingobjects')
2675
-				->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
2676
-				->executeQuery();
2677
-
2678
-		$result = [];
2679
-		foreach ($stmt->fetchAll() as $row) {
2680
-			$result[] = [
2681
-				'calendardata' => $row['calendardata'],
2682
-				'uri' => $row['uri'],
2683
-				'lastmodified' => $row['lastmodified'],
2684
-				'etag' => '"' . $row['etag'] . '"',
2685
-				'size' => (int)$row['size'],
2686
-			];
2687
-		}
2688
-		$stmt->closeCursor();
2689
-
2690
-		return $result;
2691
-	}
2692
-
2693
-	/**
2694
-	 * Deletes a scheduling object from the inbox collection.
2695
-	 *
2696
-	 * @param string $principalUri
2697
-	 * @param string $objectUri
2698
-	 * @return void
2699
-	 */
2700
-	public function deleteSchedulingObject($principalUri, $objectUri) {
2701
-		$query = $this->db->getQueryBuilder();
2702
-		$query->delete('schedulingobjects')
2703
-				->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
2704
-				->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
2705
-				->executeStatement();
2706
-	}
2707
-
2708
-	/**
2709
-	 * Creates a new scheduling object. This should land in a users' inbox.
2710
-	 *
2711
-	 * @param string $principalUri
2712
-	 * @param string $objectUri
2713
-	 * @param string $objectData
2714
-	 * @return void
2715
-	 */
2716
-	public function createSchedulingObject($principalUri, $objectUri, $objectData) {
2717
-		$query = $this->db->getQueryBuilder();
2718
-		$query->insert('schedulingobjects')
2719
-			->values([
2720
-				'principaluri' => $query->createNamedParameter($principalUri),
2721
-				'calendardata' => $query->createNamedParameter($objectData, IQueryBuilder::PARAM_LOB),
2722
-				'uri' => $query->createNamedParameter($objectUri),
2723
-				'lastmodified' => $query->createNamedParameter(time()),
2724
-				'etag' => $query->createNamedParameter(md5($objectData)),
2725
-				'size' => $query->createNamedParameter(strlen($objectData))
2726
-			])
2727
-			->executeStatement();
2728
-	}
2729
-
2730
-	/**
2731
-	 * Adds a change record to the calendarchanges table.
2732
-	 *
2733
-	 * @param mixed $calendarId
2734
-	 * @param string $objectUri
2735
-	 * @param int $operation 1 = add, 2 = modify, 3 = delete.
2736
-	 * @param int $calendarType
2737
-	 * @return void
2738
-	 */
2739
-	protected function addChange($calendarId, $objectUri, $operation, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
2740
-		$table = $calendarType === self::CALENDAR_TYPE_CALENDAR ? 'calendars': 'calendarsubscriptions';
2741
-
2742
-		$query = $this->db->getQueryBuilder();
2743
-		$query->select('synctoken')
2744
-			->from($table)
2745
-			->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)));
2746
-		$result = $query->executeQuery();
2747
-		$syncToken = (int)$result->fetchOne();
2748
-		$result->closeCursor();
2749
-
2750
-		$query = $this->db->getQueryBuilder();
2751
-		$query->insert('calendarchanges')
2752
-			->values([
2753
-				'uri' => $query->createNamedParameter($objectUri),
2754
-				'synctoken' => $query->createNamedParameter($syncToken),
2755
-				'calendarid' => $query->createNamedParameter($calendarId),
2756
-				'operation' => $query->createNamedParameter($operation),
2757
-				'calendartype' => $query->createNamedParameter($calendarType),
2758
-			])
2759
-			->executeStatement();
2760
-
2761
-		$stmt = $this->db->prepare("UPDATE `*PREFIX*$table` SET `synctoken` = `synctoken` + 1 WHERE `id` = ?");
2762
-		$stmt->execute([
2763
-			$calendarId
2764
-		]);
2765
-	}
2766
-
2767
-	/**
2768
-	 * Parses some information from calendar objects, used for optimized
2769
-	 * calendar-queries.
2770
-	 *
2771
-	 * Returns an array with the following keys:
2772
-	 *   * etag - An md5 checksum of the object without the quotes.
2773
-	 *   * size - Size of the object in bytes
2774
-	 *   * componentType - VEVENT, VTODO or VJOURNAL
2775
-	 *   * firstOccurence
2776
-	 *   * lastOccurence
2777
-	 *   * uid - value of the UID property
2778
-	 *
2779
-	 * @param string $calendarData
2780
-	 * @return array
2781
-	 */
2782
-	public function getDenormalizedData($calendarData) {
2783
-		$vObject = Reader::read($calendarData);
2784
-		$vEvents = [];
2785
-		$componentType = null;
2786
-		$component = null;
2787
-		$firstOccurrence = null;
2788
-		$lastOccurrence = null;
2789
-		$uid = null;
2790
-		$classification = self::CLASSIFICATION_PUBLIC;
2791
-		$hasDTSTART = false;
2792
-		foreach ($vObject->getComponents() as $component) {
2793
-			if ($component->name !== 'VTIMEZONE') {
2794
-				// Finding all VEVENTs, and track them
2795
-				if ($component->name === 'VEVENT') {
2796
-					array_push($vEvents, $component);
2797
-					if ($component->DTSTART) {
2798
-						$hasDTSTART = true;
2799
-					}
2800
-				}
2801
-				// Track first component type and uid
2802
-				if ($uid === null) {
2803
-					$componentType = $component->name;
2804
-					$uid = (string)$component->UID;
2805
-				}
2806
-			}
2807
-		}
2808
-		if (!$componentType) {
2809
-			throw new BadRequest('Calendar objects must have a VJOURNAL, VEVENT or VTODO component');
2810
-		}
2811
-
2812
-		if ($hasDTSTART) {
2813
-			$component = $vEvents[0];
2814
-
2815
-			// Finding the last occurrence is a bit harder
2816
-			if (!isset($component->RRULE) && count($vEvents) === 1) {
2817
-				$firstOccurrence = $component->DTSTART->getDateTime()->getTimeStamp();
2818
-				if (isset($component->DTEND)) {
2819
-					$lastOccurrence = $component->DTEND->getDateTime()->getTimeStamp();
2820
-				} elseif (isset($component->DURATION)) {
2821
-					$endDate = clone $component->DTSTART->getDateTime();
2822
-					$endDate->add(DateTimeParser::parse($component->DURATION->getValue()));
2823
-					$lastOccurrence = $endDate->getTimeStamp();
2824
-				} elseif (!$component->DTSTART->hasTime()) {
2825
-					$endDate = clone $component->DTSTART->getDateTime();
2826
-					$endDate->modify('+1 day');
2827
-					$lastOccurrence = $endDate->getTimeStamp();
2828
-				} else {
2829
-					$lastOccurrence = $firstOccurrence;
2830
-				}
2831
-			} else {
2832
-				$it = new EventIterator($vEvents);
2833
-				$maxDate = new DateTime(self::MAX_DATE);
2834
-				$firstOccurrence = $it->getDtStart()->getTimestamp();
2835
-				if ($it->isInfinite()) {
2836
-					$lastOccurrence = $maxDate->getTimestamp();
2837
-				} else {
2838
-					$end = $it->getDtEnd();
2839
-					while ($it->valid() && $end < $maxDate) {
2840
-						$end = $it->getDtEnd();
2841
-						$it->next();
2842
-					}
2843
-					$lastOccurrence = $end->getTimestamp();
2844
-				}
2845
-			}
2846
-		}
2847
-
2848
-		if ($component->CLASS) {
2849
-			$classification = CalDavBackend::CLASSIFICATION_PRIVATE;
2850
-			switch ($component->CLASS->getValue()) {
2851
-				case 'PUBLIC':
2852
-					$classification = CalDavBackend::CLASSIFICATION_PUBLIC;
2853
-					break;
2854
-				case 'CONFIDENTIAL':
2855
-					$classification = CalDavBackend::CLASSIFICATION_CONFIDENTIAL;
2856
-					break;
2857
-			}
2858
-		}
2859
-		return [
2860
-			'etag' => md5($calendarData),
2861
-			'size' => strlen($calendarData),
2862
-			'componentType' => $componentType,
2863
-			'firstOccurence' => is_null($firstOccurrence) ? null : max(0, $firstOccurrence),
2864
-			'lastOccurence' => $lastOccurrence,
2865
-			'uid' => $uid,
2866
-			'classification' => $classification
2867
-		];
2868
-	}
2869
-
2870
-	/**
2871
-	 * @param $cardData
2872
-	 * @return bool|string
2873
-	 */
2874
-	private function readBlob($cardData) {
2875
-		if (is_resource($cardData)) {
2876
-			return stream_get_contents($cardData);
2877
-		}
2878
-
2879
-		return $cardData;
2880
-	}
2881
-
2882
-	/**
2883
-	 * @param IShareable $shareable
2884
-	 * @param array $add
2885
-	 * @param array $remove
2886
-	 */
2887
-	public function updateShares($shareable, $add, $remove) {
2888
-		$calendarId = $shareable->getResourceId();
2889
-		$calendarRow = $this->getCalendarById($calendarId);
2890
-		$oldShares = $this->getShares($calendarId);
2891
-
2892
-		$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateShares', new GenericEvent(
2893
-			'\OCA\DAV\CalDAV\CalDavBackend::updateShares',
2894
-			[
2895
-				'calendarId' => $calendarId,
2896
-				'calendarData' => $calendarRow,
2897
-				'shares' => $oldShares,
2898
-				'add' => $add,
2899
-				'remove' => $remove,
2900
-			]));
2901
-		$this->calendarSharingBackend->updateShares($shareable, $add, $remove);
2902
-
2903
-		$this->dispatcher->dispatchTyped(new CalendarShareUpdatedEvent((int)$calendarId, $calendarRow, $oldShares, $add, $remove));
2904
-	}
2905
-
2906
-	/**
2907
-	 * @param int $resourceId
2908
-	 * @return array
2909
-	 */
2910
-	public function getShares($resourceId) {
2911
-		return $this->calendarSharingBackend->getShares($resourceId);
2912
-	}
2913
-
2914
-	/**
2915
-	 * @param boolean $value
2916
-	 * @param \OCA\DAV\CalDAV\Calendar $calendar
2917
-	 * @return string|null
2918
-	 */
2919
-	public function setPublishStatus($value, $calendar) {
2920
-		$calendarId = $calendar->getResourceId();
2921
-		$calendarData = $this->getCalendarById($calendarId);
2922
-		$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::publishCalendar', new GenericEvent(
2923
-			'\OCA\DAV\CalDAV\CalDavBackend::updateShares',
2924
-			[
2925
-				'calendarId' => $calendarId,
2926
-				'calendarData' => $calendarData,
2927
-				'public' => $value,
2928
-			]));
2929
-
2930
-		$query = $this->db->getQueryBuilder();
2931
-		if ($value) {
2932
-			$publicUri = $this->random->generate(16, ISecureRandom::CHAR_HUMAN_READABLE);
2933
-			$query->insert('dav_shares')
2934
-				->values([
2935
-					'principaluri' => $query->createNamedParameter($calendar->getPrincipalURI()),
2936
-					'type' => $query->createNamedParameter('calendar'),
2937
-					'access' => $query->createNamedParameter(self::ACCESS_PUBLIC),
2938
-					'resourceid' => $query->createNamedParameter($calendar->getResourceId()),
2939
-					'publicuri' => $query->createNamedParameter($publicUri)
2940
-				]);
2941
-			$query->executeStatement();
2942
-
2943
-			$this->dispatcher->dispatchTyped(new CalendarPublishedEvent((int)$calendarId, $calendarData, $publicUri));
2944
-			return $publicUri;
2945
-		}
2946
-		$query->delete('dav_shares')
2947
-			->where($query->expr()->eq('resourceid', $query->createNamedParameter($calendar->getResourceId())))
2948
-			->andWhere($query->expr()->eq('access', $query->createNamedParameter(self::ACCESS_PUBLIC)));
2949
-		$query->executeStatement();
2950
-
2951
-		$this->dispatcher->dispatchTyped(new CalendarUnpublishedEvent((int)$calendarId, $calendarData));
2952
-		return null;
2953
-	}
2954
-
2955
-	/**
2956
-	 * @param \OCA\DAV\CalDAV\Calendar $calendar
2957
-	 * @return mixed
2958
-	 */
2959
-	public function getPublishStatus($calendar) {
2960
-		$query = $this->db->getQueryBuilder();
2961
-		$result = $query->select('publicuri')
2962
-			->from('dav_shares')
2963
-			->where($query->expr()->eq('resourceid', $query->createNamedParameter($calendar->getResourceId())))
2964
-			->andWhere($query->expr()->eq('access', $query->createNamedParameter(self::ACCESS_PUBLIC)))
2965
-			->executeQuery();
2966
-
2967
-		$row = $result->fetch();
2968
-		$result->closeCursor();
2969
-		return $row ? reset($row) : false;
2970
-	}
2971
-
2972
-	/**
2973
-	 * @param int $resourceId
2974
-	 * @param array $acl
2975
-	 * @return array
2976
-	 */
2977
-	public function applyShareAcl($resourceId, $acl) {
2978
-		return $this->calendarSharingBackend->applyShareAcl($resourceId, $acl);
2979
-	}
2980
-
2981
-
2982
-
2983
-	/**
2984
-	 * update properties table
2985
-	 *
2986
-	 * @param int $calendarId
2987
-	 * @param string $objectUri
2988
-	 * @param string $calendarData
2989
-	 * @param int $calendarType
2990
-	 */
2991
-	public function updateProperties($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
2992
-		$objectId = $this->getCalendarObjectId($calendarId, $objectUri, $calendarType);
2993
-
2994
-		try {
2995
-			$vCalendar = $this->readCalendarData($calendarData);
2996
-		} catch (\Exception $ex) {
2997
-			return;
2998
-		}
2999
-
3000
-		$this->purgeProperties($calendarId, $objectId);
3001
-
3002
-		$query = $this->db->getQueryBuilder();
3003
-		$query->insert($this->dbObjectPropertiesTable)
3004
-			->values(
3005
-				[
3006
-					'calendarid' => $query->createNamedParameter($calendarId),
3007
-					'calendartype' => $query->createNamedParameter($calendarType),
3008
-					'objectid' => $query->createNamedParameter($objectId),
3009
-					'name' => $query->createParameter('name'),
3010
-					'parameter' => $query->createParameter('parameter'),
3011
-					'value' => $query->createParameter('value'),
3012
-				]
3013
-			);
3014
-
3015
-		$indexComponents = ['VEVENT', 'VJOURNAL', 'VTODO'];
3016
-		foreach ($vCalendar->getComponents() as $component) {
3017
-			if (!in_array($component->name, $indexComponents)) {
3018
-				continue;
3019
-			}
3020
-
3021
-			foreach ($component->children() as $property) {
3022
-				if (in_array($property->name, self::INDEXED_PROPERTIES, true)) {
3023
-					$value = $property->getValue();
3024
-					// is this a shitty db?
3025
-					if (!$this->db->supports4ByteText()) {
3026
-						$value = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $value);
3027
-					}
3028
-					$value = mb_strcut($value, 0, 254);
3029
-
3030
-					$query->setParameter('name', $property->name);
3031
-					$query->setParameter('parameter', null);
3032
-					$query->setParameter('value', $value);
3033
-					$query->executeStatement();
3034
-				}
3035
-
3036
-				if (array_key_exists($property->name, self::$indexParameters)) {
3037
-					$parameters = $property->parameters();
3038
-					$indexedParametersForProperty = self::$indexParameters[$property->name];
3039
-
3040
-					foreach ($parameters as $key => $value) {
3041
-						if (in_array($key, $indexedParametersForProperty)) {
3042
-							// is this a shitty db?
3043
-							if ($this->db->supports4ByteText()) {
3044
-								$value = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $value);
3045
-							}
3046
-
3047
-							$query->setParameter('name', $property->name);
3048
-							$query->setParameter('parameter', mb_strcut($key, 0, 254));
3049
-							$query->setParameter('value', mb_strcut($value, 0, 254));
3050
-							$query->executeStatement();
3051
-						}
3052
-					}
3053
-				}
3054
-			}
3055
-		}
3056
-	}
3057
-
3058
-	/**
3059
-	 * deletes all birthday calendars
3060
-	 */
3061
-	public function deleteAllBirthdayCalendars() {
3062
-		$query = $this->db->getQueryBuilder();
3063
-		$result = $query->select(['id'])->from('calendars')
3064
-			->where($query->expr()->eq('uri', $query->createNamedParameter(BirthdayService::BIRTHDAY_CALENDAR_URI)))
3065
-			->executeQuery();
3066
-
3067
-		$ids = $result->fetchAll();
3068
-		$result->closeCursor();
3069
-		foreach ($ids as $id) {
3070
-			$this->deleteCalendar(
3071
-				$id['id'],
3072
-				true // No data to keep in the trashbin, if the user re-enables then we regenerate
3073
-			);
3074
-		}
3075
-	}
3076
-
3077
-	/**
3078
-	 * @param $subscriptionId
3079
-	 */
3080
-	public function purgeAllCachedEventsForSubscription($subscriptionId) {
3081
-		$query = $this->db->getQueryBuilder();
3082
-		$query->select('uri')
3083
-			->from('calendarobjects')
3084
-			->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
3085
-			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)));
3086
-		$stmt = $query->executeQuery();
3087
-
3088
-		$uris = [];
3089
-		foreach ($stmt->fetchAll() as $row) {
3090
-			$uris[] = $row['uri'];
3091
-		}
3092
-		$stmt->closeCursor();
3093
-
3094
-		$query = $this->db->getQueryBuilder();
3095
-		$query->delete('calendarobjects')
3096
-			->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
3097
-			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
3098
-			->executeStatement();
3099
-
3100
-		$query->delete('calendarchanges')
3101
-			->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
3102
-			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
3103
-			->executeStatement();
3104
-
3105
-		$query->delete($this->dbObjectPropertiesTable)
3106
-			->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
3107
-			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
3108
-			->executeStatement();
3109
-
3110
-		foreach ($uris as $uri) {
3111
-			$this->addChange($subscriptionId, $uri, 3, self::CALENDAR_TYPE_SUBSCRIPTION);
3112
-		}
3113
-	}
3114
-
3115
-	/**
3116
-	 * Move a calendar from one user to another
3117
-	 *
3118
-	 * @param string $uriName
3119
-	 * @param string $uriOrigin
3120
-	 * @param string $uriDestination
3121
-	 * @param string $newUriName (optional) the new uriName
3122
-	 */
3123
-	public function moveCalendar($uriName, $uriOrigin, $uriDestination, $newUriName = null) {
3124
-		$query = $this->db->getQueryBuilder();
3125
-		$query->update('calendars')
3126
-			->set('principaluri', $query->createNamedParameter($uriDestination))
3127
-			->set('uri', $query->createNamedParameter($newUriName ?: $uriName))
3128
-			->where($query->expr()->eq('principaluri', $query->createNamedParameter($uriOrigin)))
3129
-			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($uriName)))
3130
-			->executeStatement();
3131
-	}
3132
-
3133
-	/**
3134
-	 * read VCalendar data into a VCalendar object
3135
-	 *
3136
-	 * @param string $objectData
3137
-	 * @return VCalendar
3138
-	 */
3139
-	protected function readCalendarData($objectData) {
3140
-		return Reader::read($objectData);
3141
-	}
3142
-
3143
-	/**
3144
-	 * delete all properties from a given calendar object
3145
-	 *
3146
-	 * @param int $calendarId
3147
-	 * @param int $objectId
3148
-	 */
3149
-	protected function purgeProperties($calendarId, $objectId) {
3150
-		$query = $this->db->getQueryBuilder();
3151
-		$query->delete($this->dbObjectPropertiesTable)
3152
-			->where($query->expr()->eq('objectid', $query->createNamedParameter($objectId)))
3153
-			->andWhere($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)));
3154
-		$query->executeStatement();
3155
-	}
3156
-
3157
-	/**
3158
-	 * get ID from a given calendar object
3159
-	 *
3160
-	 * @param int $calendarId
3161
-	 * @param string $uri
3162
-	 * @param int $calendarType
3163
-	 * @return int
3164
-	 */
3165
-	protected function getCalendarObjectId($calendarId, $uri, $calendarType):int {
3166
-		$query = $this->db->getQueryBuilder();
3167
-		$query->select('id')
3168
-			->from('calendarobjects')
3169
-			->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
3170
-			->andWhere($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
3171
-			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)));
3172
-
3173
-		$result = $query->executeQuery();
3174
-		$objectIds = $result->fetch();
3175
-		$result->closeCursor();
3176
-
3177
-		if (!isset($objectIds['id'])) {
3178
-			throw new \InvalidArgumentException('Calendarobject does not exists: ' . $uri);
3179
-		}
3180
-
3181
-		return (int)$objectIds['id'];
3182
-	}
3183
-
3184
-	/**
3185
-	 * return legacy endpoint principal name to new principal name
3186
-	 *
3187
-	 * @param $principalUri
3188
-	 * @param $toV2
3189
-	 * @return string
3190
-	 */
3191
-	private function convertPrincipal($principalUri, $toV2) {
3192
-		if ($this->principalBackend->getPrincipalPrefix() === 'principals') {
3193
-			[, $name] = Uri\split($principalUri);
3194
-			if ($toV2 === true) {
3195
-				return "principals/users/$name";
3196
-			}
3197
-			return "principals/$name";
3198
-		}
3199
-		return $principalUri;
3200
-	}
3201
-
3202
-	/**
3203
-	 * adds information about an owner to the calendar data
3204
-	 *
3205
-	 */
3206
-	private function addOwnerPrincipalToCalendar(array $calendarInfo): array {
3207
-		$ownerPrincipalKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal';
3208
-		$displaynameKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}owner-displayname';
3209
-		if (isset($calendarInfo[$ownerPrincipalKey])) {
3210
-			$uri = $calendarInfo[$ownerPrincipalKey];
3211
-		} else {
3212
-			$uri = $calendarInfo['principaluri'];
3213
-		}
3214
-
3215
-		$principalInformation = $this->principalBackend->getPrincipalByPath($uri);
3216
-		if (isset($principalInformation['{DAV:}displayname'])) {
3217
-			$calendarInfo[$displaynameKey] = $principalInformation['{DAV:}displayname'];
3218
-		}
3219
-		return $calendarInfo;
3220
-	}
3221
-
3222
-	private function addResourceTypeToCalendar(array $row, array $calendar): array {
3223
-		if (isset($row['deleted_at'])) {
3224
-			// Columns is set and not null -> this is a deleted calendar
3225
-			// we send a custom resourcetype to hide the deleted calendar
3226
-			// from ordinary DAV clients, but the Calendar app will know
3227
-			// how to handle this special resource.
3228
-			$calendar['{DAV:}resourcetype'] = new DAV\Xml\Property\ResourceType([
3229
-				'{DAV:}collection',
3230
-				sprintf('{%s}deleted-calendar', \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD),
3231
-			]);
3232
-		}
3233
-		return $calendar;
3234
-	}
3235
-
3236
-	/**
3237
-	 * Amend the calendar info with database row data
3238
-	 *
3239
-	 * @param array $row
3240
-	 * @param array $calendar
3241
-	 *
3242
-	 * @return array
3243
-	 */
3244
-	private function rowToCalendar($row, array $calendar): array {
3245
-		foreach ($this->propertyMap as $xmlName => [$dbName, $type]) {
3246
-			$value = $row[$dbName];
3247
-			if ($value !== null) {
3248
-				settype($value, $type);
3249
-			}
3250
-			$calendar[$xmlName] = $value;
3251
-		}
3252
-		return $calendar;
3253
-	}
3254
-
3255
-	/**
3256
-	 * Amend the subscription info with database row data
3257
-	 *
3258
-	 * @param array $row
3259
-	 * @param array $subscription
3260
-	 *
3261
-	 * @return array
3262
-	 */
3263
-	private function rowToSubscription($row, array $subscription): array {
3264
-		foreach ($this->subscriptionPropertyMap as $xmlName => [$dbName, $type]) {
3265
-			$value = $row[$dbName];
3266
-			if ($value !== null) {
3267
-				settype($value, $type);
3268
-			}
3269
-			$subscription[$xmlName] = $value;
3270
-		}
3271
-		return $subscription;
3272
-	}
122
+    public const CALENDAR_TYPE_CALENDAR = 0;
123
+    public const CALENDAR_TYPE_SUBSCRIPTION = 1;
124
+
125
+    public const PERSONAL_CALENDAR_URI = 'personal';
126
+    public const PERSONAL_CALENDAR_NAME = 'Personal';
127
+
128
+    public const RESOURCE_BOOKING_CALENDAR_URI = 'calendar';
129
+    public const RESOURCE_BOOKING_CALENDAR_NAME = 'Calendar';
130
+
131
+    /**
132
+     * We need to specify a max date, because we need to stop *somewhere*
133
+     *
134
+     * On 32 bit system the maximum for a signed integer is 2147483647, so
135
+     * MAX_DATE cannot be higher than date('Y-m-d', 2147483647) which results
136
+     * in 2038-01-19 to avoid problems when the date is converted
137
+     * to a unix timestamp.
138
+     */
139
+    public const MAX_DATE = '2038-01-01';
140
+
141
+    public const ACCESS_PUBLIC = 4;
142
+    public const CLASSIFICATION_PUBLIC = 0;
143
+    public const CLASSIFICATION_PRIVATE = 1;
144
+    public const CLASSIFICATION_CONFIDENTIAL = 2;
145
+
146
+    /**
147
+     * List of CalDAV properties, and how they map to database field names and their type
148
+     * Add your own properties by simply adding on to this array.
149
+     *
150
+     * @var array
151
+     * @psalm-var array<string, string[]>
152
+     */
153
+    public $propertyMap = [
154
+        '{DAV:}displayname' => ['displayname', 'string'],
155
+        '{urn:ietf:params:xml:ns:caldav}calendar-description' => ['description', 'string'],
156
+        '{urn:ietf:params:xml:ns:caldav}calendar-timezone' => ['timezone', 'string'],
157
+        '{http://apple.com/ns/ical/}calendar-order' => ['calendarorder', 'int'],
158
+        '{http://apple.com/ns/ical/}calendar-color' => ['calendarcolor', 'string'],
159
+        '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => ['deleted_at', 'int'],
160
+    ];
161
+
162
+    /**
163
+     * List of subscription properties, and how they map to database field names.
164
+     *
165
+     * @var array
166
+     */
167
+    public $subscriptionPropertyMap = [
168
+        '{DAV:}displayname' => ['displayname', 'string'],
169
+        '{http://apple.com/ns/ical/}refreshrate' => ['refreshrate', 'string'],
170
+        '{http://apple.com/ns/ical/}calendar-order' => ['calendarorder', 'int'],
171
+        '{http://apple.com/ns/ical/}calendar-color' => ['calendarcolor', 'string'],
172
+        '{http://calendarserver.org/ns/}subscribed-strip-todos' => ['striptodos', 'bool'],
173
+        '{http://calendarserver.org/ns/}subscribed-strip-alarms' => ['stripalarms', 'string'],
174
+        '{http://calendarserver.org/ns/}subscribed-strip-attachments' => ['stripattachments', 'string'],
175
+    ];
176
+
177
+    /**
178
+     * properties to index
179
+     *
180
+     * This list has to be kept in sync with ICalendarQuery::SEARCH_PROPERTY_*
181
+     *
182
+     * @see \OCP\Calendar\ICalendarQuery
183
+     */
184
+    private const INDEXED_PROPERTIES = [
185
+        'CATEGORIES',
186
+        'COMMENT',
187
+        'DESCRIPTION',
188
+        'LOCATION',
189
+        'RESOURCES',
190
+        'STATUS',
191
+        'SUMMARY',
192
+        'ATTENDEE',
193
+        'CONTACT',
194
+        'ORGANIZER'
195
+    ];
196
+
197
+    /** @var array parameters to index */
198
+    public static $indexParameters = [
199
+        'ATTENDEE' => ['CN'],
200
+        'ORGANIZER' => ['CN'],
201
+    ];
202
+
203
+    /**
204
+     * @var string[] Map of uid => display name
205
+     */
206
+    protected $userDisplayNames;
207
+
208
+    /** @var IDBConnection */
209
+    private $db;
210
+
211
+    /** @var Backend */
212
+    private $calendarSharingBackend;
213
+
214
+    /** @var Principal */
215
+    private $principalBackend;
216
+
217
+    /** @var IUserManager */
218
+    private $userManager;
219
+
220
+    /** @var ISecureRandom */
221
+    private $random;
222
+
223
+    /** @var ILogger */
224
+    private $logger;
225
+
226
+    /** @var IEventDispatcher */
227
+    private $dispatcher;
228
+
229
+    /** @var EventDispatcherInterface */
230
+    private $legacyDispatcher;
231
+
232
+    /** @var IConfig */
233
+    private $config;
234
+
235
+    /** @var bool */
236
+    private $legacyEndpoint;
237
+
238
+    /** @var string */
239
+    private $dbObjectPropertiesTable = 'calendarobjects_props';
240
+
241
+    /**
242
+     * CalDavBackend constructor.
243
+     *
244
+     * @param IDBConnection $db
245
+     * @param Principal $principalBackend
246
+     * @param IUserManager $userManager
247
+     * @param IGroupManager $groupManager
248
+     * @param ISecureRandom $random
249
+     * @param ILogger $logger
250
+     * @param IEventDispatcher $dispatcher
251
+     * @param EventDispatcherInterface $legacyDispatcher
252
+     * @param bool $legacyEndpoint
253
+     */
254
+    public function __construct(IDBConnection $db,
255
+                                Principal $principalBackend,
256
+                                IUserManager $userManager,
257
+                                IGroupManager $groupManager,
258
+                                ISecureRandom $random,
259
+                                ILogger $logger,
260
+                                IEventDispatcher $dispatcher,
261
+                                EventDispatcherInterface $legacyDispatcher,
262
+                                IConfig $config,
263
+                                bool $legacyEndpoint = false) {
264
+        $this->db = $db;
265
+        $this->principalBackend = $principalBackend;
266
+        $this->userManager = $userManager;
267
+        $this->calendarSharingBackend = new Backend($this->db, $this->userManager, $groupManager, $principalBackend, 'calendar');
268
+        $this->random = $random;
269
+        $this->logger = $logger;
270
+        $this->dispatcher = $dispatcher;
271
+        $this->legacyDispatcher = $legacyDispatcher;
272
+        $this->config = $config;
273
+        $this->legacyEndpoint = $legacyEndpoint;
274
+    }
275
+
276
+    /**
277
+     * Return the number of calendars for a principal
278
+     *
279
+     * By default this excludes the automatically generated birthday calendar
280
+     *
281
+     * @param $principalUri
282
+     * @param bool $excludeBirthday
283
+     * @return int
284
+     */
285
+    public function getCalendarsForUserCount($principalUri, $excludeBirthday = true) {
286
+        $principalUri = $this->convertPrincipal($principalUri, true);
287
+        $query = $this->db->getQueryBuilder();
288
+        $query->select($query->func()->count('*'))
289
+            ->from('calendars');
290
+
291
+        if ($principalUri === '') {
292
+            $query->where($query->expr()->emptyString('principaluri'));
293
+        } else {
294
+            $query->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
295
+        }
296
+
297
+        if ($excludeBirthday) {
298
+            $query->andWhere($query->expr()->neq('uri', $query->createNamedParameter(BirthdayService::BIRTHDAY_CALENDAR_URI)));
299
+        }
300
+
301
+        $result = $query->executeQuery();
302
+        $column = (int)$result->fetchOne();
303
+        $result->closeCursor();
304
+        return $column;
305
+    }
306
+
307
+    /**
308
+     * @return array{id: int, deleted_at: int}[]
309
+     */
310
+    public function getDeletedCalendars(int $deletedBefore): array {
311
+        $qb = $this->db->getQueryBuilder();
312
+        $qb->select(['id', 'deleted_at'])
313
+            ->from('calendars')
314
+            ->where($qb->expr()->isNotNull('deleted_at'))
315
+            ->andWhere($qb->expr()->lt('deleted_at', $qb->createNamedParameter($deletedBefore)));
316
+        $result = $qb->executeQuery();
317
+        $raw = $result->fetchAll();
318
+        $result->closeCursor();
319
+        return array_map(function ($row) {
320
+            return [
321
+                'id' => (int) $row['id'],
322
+                'deleted_at' => (int) $row['deleted_at'],
323
+            ];
324
+        }, $raw);
325
+    }
326
+
327
+    /**
328
+     * Returns a list of calendars for a principal.
329
+     *
330
+     * Every project is an array with the following keys:
331
+     *  * id, a unique id that will be used by other functions to modify the
332
+     *    calendar. This can be the same as the uri or a database key.
333
+     *  * uri, which the basename of the uri with which the calendar is
334
+     *    accessed.
335
+     *  * principaluri. The owner of the calendar. Almost always the same as
336
+     *    principalUri passed to this method.
337
+     *
338
+     * Furthermore it can contain webdav properties in clark notation. A very
339
+     * common one is '{DAV:}displayname'.
340
+     *
341
+     * Many clients also require:
342
+     * {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
343
+     * For this property, you can just return an instance of
344
+     * Sabre\CalDAV\Property\SupportedCalendarComponentSet.
345
+     *
346
+     * If you return {http://sabredav.org/ns}read-only and set the value to 1,
347
+     * ACL will automatically be put in read-only mode.
348
+     *
349
+     * @param string $principalUri
350
+     * @return array
351
+     */
352
+    public function getCalendarsForUser($principalUri) {
353
+        $principalUriOriginal = $principalUri;
354
+        $principalUri = $this->convertPrincipal($principalUri, true);
355
+        $fields = array_column($this->propertyMap, 0);
356
+        $fields[] = 'id';
357
+        $fields[] = 'uri';
358
+        $fields[] = 'synctoken';
359
+        $fields[] = 'components';
360
+        $fields[] = 'principaluri';
361
+        $fields[] = 'transparent';
362
+
363
+        // Making fields a comma-delimited list
364
+        $query = $this->db->getQueryBuilder();
365
+        $query->select($fields)
366
+            ->from('calendars')
367
+            ->orderBy('calendarorder', 'ASC');
368
+
369
+        if ($principalUri === '') {
370
+            $query->where($query->expr()->emptyString('principaluri'));
371
+        } else {
372
+            $query->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
373
+        }
374
+
375
+        $result = $query->executeQuery();
376
+
377
+        $calendars = [];
378
+        while ($row = $result->fetch()) {
379
+            $row['principaluri'] = (string) $row['principaluri'];
380
+            $components = [];
381
+            if ($row['components']) {
382
+                $components = explode(',',$row['components']);
383
+            }
384
+
385
+            $calendar = [
386
+                'id' => $row['id'],
387
+                'uri' => $row['uri'],
388
+                'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
389
+                '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
390
+                '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
391
+                '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
392
+                '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
393
+                '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($principalUri, !$this->legacyEndpoint),
394
+            ];
395
+
396
+            $calendar = $this->rowToCalendar($row, $calendar);
397
+            $calendar = $this->addOwnerPrincipalToCalendar($calendar);
398
+            $calendar = $this->addResourceTypeToCalendar($row, $calendar);
399
+
400
+            if (!isset($calendars[$calendar['id']])) {
401
+                $calendars[$calendar['id']] = $calendar;
402
+            }
403
+        }
404
+        $result->closeCursor();
405
+
406
+        // query for shared calendars
407
+        $principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true);
408
+        $principals = array_merge($principals, $this->principalBackend->getCircleMembership($principalUriOriginal));
409
+
410
+        $principals[] = $principalUri;
411
+
412
+        $fields = array_column($this->propertyMap, 0);
413
+        $fields[] = 'a.id';
414
+        $fields[] = 'a.uri';
415
+        $fields[] = 'a.synctoken';
416
+        $fields[] = 'a.components';
417
+        $fields[] = 'a.principaluri';
418
+        $fields[] = 'a.transparent';
419
+        $fields[] = 's.access';
420
+        $query = $this->db->getQueryBuilder();
421
+        $query->select($fields)
422
+            ->from('dav_shares', 's')
423
+            ->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
424
+            ->where($query->expr()->in('s.principaluri', $query->createParameter('principaluri')))
425
+            ->andWhere($query->expr()->eq('s.type', $query->createParameter('type')))
426
+            ->setParameter('type', 'calendar')
427
+            ->setParameter('principaluri', $principals, \Doctrine\DBAL\Connection::PARAM_STR_ARRAY);
428
+
429
+        $result = $query->executeQuery();
430
+
431
+        $readOnlyPropertyName = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only';
432
+        while ($row = $result->fetch()) {
433
+            $row['principaluri'] = (string) $row['principaluri'];
434
+            if ($row['principaluri'] === $principalUri) {
435
+                continue;
436
+            }
437
+
438
+            $readOnly = (int) $row['access'] === Backend::ACCESS_READ;
439
+            if (isset($calendars[$row['id']])) {
440
+                if ($readOnly) {
441
+                    // New share can not have more permissions then the old one.
442
+                    continue;
443
+                }
444
+                if (isset($calendars[$row['id']][$readOnlyPropertyName]) &&
445
+                    $calendars[$row['id']][$readOnlyPropertyName] === 0) {
446
+                    // Old share is already read-write, no more permissions can be gained
447
+                    continue;
448
+                }
449
+            }
450
+
451
+            [, $name] = Uri\split($row['principaluri']);
452
+            $uri = $row['uri'] . '_shared_by_' . $name;
453
+            $row['displayname'] = $row['displayname'] . ' (' . $this->getUserDisplayName($name) . ')';
454
+            $components = [];
455
+            if ($row['components']) {
456
+                $components = explode(',',$row['components']);
457
+            }
458
+            $calendar = [
459
+                'id' => $row['id'],
460
+                'uri' => $uri,
461
+                'principaluri' => $this->convertPrincipal($principalUri, !$this->legacyEndpoint),
462
+                '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
463
+                '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
464
+                '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
465
+                '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp('transparent'),
466
+                '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
467
+                $readOnlyPropertyName => $readOnly,
468
+            ];
469
+
470
+            $calendar = $this->rowToCalendar($row, $calendar);
471
+            $calendar = $this->addOwnerPrincipalToCalendar($calendar);
472
+            $calendar = $this->addResourceTypeToCalendar($row, $calendar);
473
+
474
+            $calendars[$calendar['id']] = $calendar;
475
+        }
476
+        $result->closeCursor();
477
+
478
+        return array_values($calendars);
479
+    }
480
+
481
+    /**
482
+     * @param $principalUri
483
+     * @return array
484
+     */
485
+    public function getUsersOwnCalendars($principalUri) {
486
+        $principalUri = $this->convertPrincipal($principalUri, true);
487
+        $fields = array_column($this->propertyMap, 0);
488
+        $fields[] = 'id';
489
+        $fields[] = 'uri';
490
+        $fields[] = 'synctoken';
491
+        $fields[] = 'components';
492
+        $fields[] = 'principaluri';
493
+        $fields[] = 'transparent';
494
+        // Making fields a comma-delimited list
495
+        $query = $this->db->getQueryBuilder();
496
+        $query->select($fields)->from('calendars')
497
+            ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
498
+            ->orderBy('calendarorder', 'ASC');
499
+        $stmt = $query->executeQuery();
500
+        $calendars = [];
501
+        while ($row = $stmt->fetch()) {
502
+            $row['principaluri'] = (string) $row['principaluri'];
503
+            $components = [];
504
+            if ($row['components']) {
505
+                $components = explode(',',$row['components']);
506
+            }
507
+            $calendar = [
508
+                'id' => $row['id'],
509
+                'uri' => $row['uri'],
510
+                'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
511
+                '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
512
+                '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
513
+                '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
514
+                '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
515
+            ];
516
+
517
+            $calendar = $this->rowToCalendar($row, $calendar);
518
+            $calendar = $this->addOwnerPrincipalToCalendar($calendar);
519
+            $calendar = $this->addResourceTypeToCalendar($row, $calendar);
520
+
521
+            if (!isset($calendars[$calendar['id']])) {
522
+                $calendars[$calendar['id']] = $calendar;
523
+            }
524
+        }
525
+        $stmt->closeCursor();
526
+        return array_values($calendars);
527
+    }
528
+
529
+
530
+    /**
531
+     * @param $uid
532
+     * @return string
533
+     */
534
+    private function getUserDisplayName($uid) {
535
+        if (!isset($this->userDisplayNames[$uid])) {
536
+            $user = $this->userManager->get($uid);
537
+
538
+            if ($user instanceof IUser) {
539
+                $this->userDisplayNames[$uid] = $user->getDisplayName();
540
+            } else {
541
+                $this->userDisplayNames[$uid] = $uid;
542
+            }
543
+        }
544
+
545
+        return $this->userDisplayNames[$uid];
546
+    }
547
+
548
+    /**
549
+     * @return array
550
+     */
551
+    public function getPublicCalendars() {
552
+        $fields = array_column($this->propertyMap, 0);
553
+        $fields[] = 'a.id';
554
+        $fields[] = 'a.uri';
555
+        $fields[] = 'a.synctoken';
556
+        $fields[] = 'a.components';
557
+        $fields[] = 'a.principaluri';
558
+        $fields[] = 'a.transparent';
559
+        $fields[] = 's.access';
560
+        $fields[] = 's.publicuri';
561
+        $calendars = [];
562
+        $query = $this->db->getQueryBuilder();
563
+        $result = $query->select($fields)
564
+            ->from('dav_shares', 's')
565
+            ->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
566
+            ->where($query->expr()->in('s.access', $query->createNamedParameter(self::ACCESS_PUBLIC)))
567
+            ->andWhere($query->expr()->eq('s.type', $query->createNamedParameter('calendar')))
568
+            ->executeQuery();
569
+
570
+        while ($row = $result->fetch()) {
571
+            $row['principaluri'] = (string) $row['principaluri'];
572
+            [, $name] = Uri\split($row['principaluri']);
573
+            $row['displayname'] = $row['displayname'] . "($name)";
574
+            $components = [];
575
+            if ($row['components']) {
576
+                $components = explode(',',$row['components']);
577
+            }
578
+            $calendar = [
579
+                'id' => $row['id'],
580
+                'uri' => $row['publicuri'],
581
+                'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
582
+                '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
583
+                '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
584
+                '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
585
+                '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
586
+                '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], $this->legacyEndpoint),
587
+                '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ,
588
+                '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}public' => (int)$row['access'] === self::ACCESS_PUBLIC,
589
+            ];
590
+
591
+            $calendar = $this->rowToCalendar($row, $calendar);
592
+            $calendar = $this->addOwnerPrincipalToCalendar($calendar);
593
+            $calendar = $this->addResourceTypeToCalendar($row, $calendar);
594
+
595
+            if (!isset($calendars[$calendar['id']])) {
596
+                $calendars[$calendar['id']] = $calendar;
597
+            }
598
+        }
599
+        $result->closeCursor();
600
+
601
+        return array_values($calendars);
602
+    }
603
+
604
+    /**
605
+     * @param string $uri
606
+     * @return array
607
+     * @throws NotFound
608
+     */
609
+    public function getPublicCalendar($uri) {
610
+        $fields = array_column($this->propertyMap, 0);
611
+        $fields[] = 'a.id';
612
+        $fields[] = 'a.uri';
613
+        $fields[] = 'a.synctoken';
614
+        $fields[] = 'a.components';
615
+        $fields[] = 'a.principaluri';
616
+        $fields[] = 'a.transparent';
617
+        $fields[] = 's.access';
618
+        $fields[] = 's.publicuri';
619
+        $query = $this->db->getQueryBuilder();
620
+        $result = $query->select($fields)
621
+            ->from('dav_shares', 's')
622
+            ->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
623
+            ->where($query->expr()->in('s.access', $query->createNamedParameter(self::ACCESS_PUBLIC)))
624
+            ->andWhere($query->expr()->eq('s.type', $query->createNamedParameter('calendar')))
625
+            ->andWhere($query->expr()->eq('s.publicuri', $query->createNamedParameter($uri)))
626
+            ->executeQuery();
627
+
628
+        $row = $result->fetch();
629
+
630
+        $result->closeCursor();
631
+
632
+        if ($row === false) {
633
+            throw new NotFound('Node with name \'' . $uri . '\' could not be found');
634
+        }
635
+
636
+        $row['principaluri'] = (string) $row['principaluri'];
637
+        [, $name] = Uri\split($row['principaluri']);
638
+        $row['displayname'] = $row['displayname'] . ' ' . "($name)";
639
+        $components = [];
640
+        if ($row['components']) {
641
+            $components = explode(',',$row['components']);
642
+        }
643
+        $calendar = [
644
+            'id' => $row['id'],
645
+            'uri' => $row['publicuri'],
646
+            'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
647
+            '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
648
+            '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
649
+            '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
650
+            '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
651
+            '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
652
+            '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ,
653
+            '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}public' => (int)$row['access'] === self::ACCESS_PUBLIC,
654
+        ];
655
+
656
+        $calendar = $this->rowToCalendar($row, $calendar);
657
+        $calendar = $this->addOwnerPrincipalToCalendar($calendar);
658
+        $calendar = $this->addResourceTypeToCalendar($row, $calendar);
659
+
660
+        return $calendar;
661
+    }
662
+
663
+    /**
664
+     * @param string $principal
665
+     * @param string $uri
666
+     * @return array|null
667
+     */
668
+    public function getCalendarByUri($principal, $uri) {
669
+        $fields = array_column($this->propertyMap, 0);
670
+        $fields[] = 'id';
671
+        $fields[] = 'uri';
672
+        $fields[] = 'synctoken';
673
+        $fields[] = 'components';
674
+        $fields[] = 'principaluri';
675
+        $fields[] = 'transparent';
676
+
677
+        // Making fields a comma-delimited list
678
+        $query = $this->db->getQueryBuilder();
679
+        $query->select($fields)->from('calendars')
680
+            ->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
681
+            ->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($principal)))
682
+            ->setMaxResults(1);
683
+        $stmt = $query->executeQuery();
684
+
685
+        $row = $stmt->fetch();
686
+        $stmt->closeCursor();
687
+        if ($row === false) {
688
+            return null;
689
+        }
690
+
691
+        $row['principaluri'] = (string) $row['principaluri'];
692
+        $components = [];
693
+        if ($row['components']) {
694
+            $components = explode(',',$row['components']);
695
+        }
696
+
697
+        $calendar = [
698
+            'id' => $row['id'],
699
+            'uri' => $row['uri'],
700
+            'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
701
+            '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
702
+            '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
703
+            '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
704
+            '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
705
+        ];
706
+
707
+        $calendar = $this->rowToCalendar($row, $calendar);
708
+        $calendar = $this->addOwnerPrincipalToCalendar($calendar);
709
+        $calendar = $this->addResourceTypeToCalendar($row, $calendar);
710
+
711
+        return $calendar;
712
+    }
713
+
714
+    /**
715
+     * @param $calendarId
716
+     * @return array|null
717
+     */
718
+    public function getCalendarById($calendarId) {
719
+        $fields = array_column($this->propertyMap, 0);
720
+        $fields[] = 'id';
721
+        $fields[] = 'uri';
722
+        $fields[] = 'synctoken';
723
+        $fields[] = 'components';
724
+        $fields[] = 'principaluri';
725
+        $fields[] = 'transparent';
726
+
727
+        // Making fields a comma-delimited list
728
+        $query = $this->db->getQueryBuilder();
729
+        $query->select($fields)->from('calendars')
730
+            ->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)))
731
+            ->setMaxResults(1);
732
+        $stmt = $query->executeQuery();
733
+
734
+        $row = $stmt->fetch();
735
+        $stmt->closeCursor();
736
+        if ($row === false) {
737
+            return null;
738
+        }
739
+
740
+        $row['principaluri'] = (string) $row['principaluri'];
741
+        $components = [];
742
+        if ($row['components']) {
743
+            $components = explode(',',$row['components']);
744
+        }
745
+
746
+        $calendar = [
747
+            'id' => $row['id'],
748
+            'uri' => $row['uri'],
749
+            'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
750
+            '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
751
+            '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
752
+            '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
753
+            '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
754
+        ];
755
+
756
+        $calendar = $this->rowToCalendar($row, $calendar);
757
+        $calendar = $this->addOwnerPrincipalToCalendar($calendar);
758
+        $calendar = $this->addResourceTypeToCalendar($row, $calendar);
759
+
760
+        return $calendar;
761
+    }
762
+
763
+    /**
764
+     * @param $subscriptionId
765
+     */
766
+    public function getSubscriptionById($subscriptionId) {
767
+        $fields = array_column($this->subscriptionPropertyMap, 0);
768
+        $fields[] = 'id';
769
+        $fields[] = 'uri';
770
+        $fields[] = 'source';
771
+        $fields[] = 'synctoken';
772
+        $fields[] = 'principaluri';
773
+        $fields[] = 'lastmodified';
774
+
775
+        $query = $this->db->getQueryBuilder();
776
+        $query->select($fields)
777
+            ->from('calendarsubscriptions')
778
+            ->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
779
+            ->orderBy('calendarorder', 'asc');
780
+        $stmt = $query->executeQuery();
781
+
782
+        $row = $stmt->fetch();
783
+        $stmt->closeCursor();
784
+        if ($row === false) {
785
+            return null;
786
+        }
787
+
788
+        $row['principaluri'] = (string) $row['principaluri'];
789
+        $subscription = [
790
+            'id' => $row['id'],
791
+            'uri' => $row['uri'],
792
+            'principaluri' => $row['principaluri'],
793
+            'source' => $row['source'],
794
+            'lastmodified' => $row['lastmodified'],
795
+            '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
796
+            '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
797
+        ];
798
+
799
+        return $this->rowToSubscription($row, $subscription);
800
+    }
801
+
802
+    /**
803
+     * Creates a new calendar for a principal.
804
+     *
805
+     * If the creation was a success, an id must be returned that can be used to reference
806
+     * this calendar in other methods, such as updateCalendar.
807
+     *
808
+     * @param string $principalUri
809
+     * @param string $calendarUri
810
+     * @param array $properties
811
+     * @return int
812
+     */
813
+    public function createCalendar($principalUri, $calendarUri, array $properties) {
814
+        $values = [
815
+            'principaluri' => $this->convertPrincipal($principalUri, true),
816
+            'uri' => $calendarUri,
817
+            'synctoken' => 1,
818
+            'transparent' => 0,
819
+            'components' => 'VEVENT,VTODO',
820
+            'displayname' => $calendarUri
821
+        ];
822
+
823
+        // Default value
824
+        $sccs = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set';
825
+        if (isset($properties[$sccs])) {
826
+            if (!($properties[$sccs] instanceof SupportedCalendarComponentSet)) {
827
+                throw new DAV\Exception('The ' . $sccs . ' property must be of type: \Sabre\CalDAV\Property\SupportedCalendarComponentSet');
828
+            }
829
+            $values['components'] = implode(',',$properties[$sccs]->getValue());
830
+        } elseif (isset($properties['components'])) {
831
+            // Allow to provide components internally without having
832
+            // to create a SupportedCalendarComponentSet object
833
+            $values['components'] = $properties['components'];
834
+        }
835
+
836
+        $transp = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp';
837
+        if (isset($properties[$transp])) {
838
+            $values['transparent'] = (int) ($properties[$transp]->getValue() === 'transparent');
839
+        }
840
+
841
+        foreach ($this->propertyMap as $xmlName => [$dbName, $type]) {
842
+            if (isset($properties[$xmlName])) {
843
+                $values[$dbName] = $properties[$xmlName];
844
+            }
845
+        }
846
+
847
+        $query = $this->db->getQueryBuilder();
848
+        $query->insert('calendars');
849
+        foreach ($values as $column => $value) {
850
+            $query->setValue($column, $query->createNamedParameter($value));
851
+        }
852
+        $query->executeStatement();
853
+        $calendarId = $query->getLastInsertId();
854
+
855
+        $calendarData = $this->getCalendarById($calendarId);
856
+        $this->dispatcher->dispatchTyped(new CalendarCreatedEvent((int)$calendarId, $calendarData));
857
+
858
+        return $calendarId;
859
+    }
860
+
861
+    /**
862
+     * Updates properties for a calendar.
863
+     *
864
+     * The list of mutations is stored in a Sabre\DAV\PropPatch object.
865
+     * To do the actual updates, you must tell this object which properties
866
+     * you're going to process with the handle() method.
867
+     *
868
+     * Calling the handle method is like telling the PropPatch object "I
869
+     * promise I can handle updating this property".
870
+     *
871
+     * Read the PropPatch documentation for more info and examples.
872
+     *
873
+     * @param mixed $calendarId
874
+     * @param PropPatch $propPatch
875
+     * @return void
876
+     */
877
+    public function updateCalendar($calendarId, PropPatch $propPatch) {
878
+        $supportedProperties = array_keys($this->propertyMap);
879
+        $supportedProperties[] = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp';
880
+
881
+        $propPatch->handle($supportedProperties, function ($mutations) use ($calendarId) {
882
+            $newValues = [];
883
+            foreach ($mutations as $propertyName => $propertyValue) {
884
+                switch ($propertyName) {
885
+                    case '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp':
886
+                        $fieldName = 'transparent';
887
+                        $newValues[$fieldName] = (int) ($propertyValue->getValue() === 'transparent');
888
+                        break;
889
+                    default:
890
+                        $fieldName = $this->propertyMap[$propertyName][0];
891
+                        $newValues[$fieldName] = $propertyValue;
892
+                        break;
893
+                }
894
+            }
895
+            $query = $this->db->getQueryBuilder();
896
+            $query->update('calendars');
897
+            foreach ($newValues as $fieldName => $value) {
898
+                $query->set($fieldName, $query->createNamedParameter($value));
899
+            }
900
+            $query->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)));
901
+            $query->executeStatement();
902
+
903
+            $this->addChange($calendarId, "", 2);
904
+
905
+            $calendarData = $this->getCalendarById($calendarId);
906
+            $shares = $this->getShares($calendarId);
907
+            $this->dispatcher->dispatchTyped(new CalendarUpdatedEvent((int)$calendarId, $calendarData, $shares, $mutations));
908
+
909
+            return true;
910
+        });
911
+    }
912
+
913
+    /**
914
+     * Delete a calendar and all it's objects
915
+     *
916
+     * @param mixed $calendarId
917
+     * @return void
918
+     */
919
+    public function deleteCalendar($calendarId, bool $forceDeletePermanently = false) {
920
+        // The calendar is deleted right away if this is either enforced by the caller
921
+        // or the special contacts birthday calendar or when the preference of an empty
922
+        // retention (0 seconds) is set, which signals a disabled trashbin.
923
+        $calendarData = $this->getCalendarById($calendarId);
924
+        $isBirthdayCalendar = isset($calendarData['uri']) && $calendarData['uri'] === BirthdayService::BIRTHDAY_CALENDAR_URI;
925
+        $trashbinDisabled = $this->config->getAppValue(Application::APP_ID, RetentionService::RETENTION_CONFIG_KEY) === '0';
926
+        if ($forceDeletePermanently || $isBirthdayCalendar || $trashbinDisabled) {
927
+            $calendarData = $this->getCalendarById($calendarId);
928
+            $shares = $this->getShares($calendarId);
929
+
930
+            $qbDeleteCalendarObjectProps = $this->db->getQueryBuilder();
931
+            $qbDeleteCalendarObjectProps->delete($this->dbObjectPropertiesTable)
932
+                ->where($qbDeleteCalendarObjectProps->expr()->eq('calendarid', $qbDeleteCalendarObjectProps->createNamedParameter($calendarId)))
933
+                ->andWhere($qbDeleteCalendarObjectProps->expr()->eq('calendartype', $qbDeleteCalendarObjectProps->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)))
934
+                ->executeStatement();
935
+
936
+            $qbDeleteCalendarObjects = $this->db->getQueryBuilder();
937
+            $qbDeleteCalendarObjects->delete('calendarobjects')
938
+                ->where($qbDeleteCalendarObjects->expr()->eq('calendarid', $qbDeleteCalendarObjects->createNamedParameter($calendarId)))
939
+                ->andWhere($qbDeleteCalendarObjects->expr()->eq('calendartype', $qbDeleteCalendarObjects->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)))
940
+                ->executeStatement();
941
+
942
+            $qbDeleteCalendarChanges = $this->db->getQueryBuilder();
943
+            $qbDeleteCalendarObjects->delete('calendarchanges')
944
+                ->where($qbDeleteCalendarChanges->expr()->eq('calendarid', $qbDeleteCalendarChanges->createNamedParameter($calendarId)))
945
+                ->andWhere($qbDeleteCalendarChanges->expr()->eq('calendartype', $qbDeleteCalendarChanges->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)))
946
+                ->executeStatement();
947
+
948
+            $this->calendarSharingBackend->deleteAllShares($calendarId);
949
+
950
+            $qbDeleteCalendar = $this->db->getQueryBuilder();
951
+            $qbDeleteCalendarObjects->delete('calendars')
952
+                ->where($qbDeleteCalendar->expr()->eq('id', $qbDeleteCalendar->createNamedParameter($calendarId)))
953
+                ->executeStatement();
954
+
955
+            // Only dispatch if we actually deleted anything
956
+            if ($calendarData) {
957
+                $this->dispatcher->dispatchTyped(new CalendarDeletedEvent((int)$calendarId, $calendarData, $shares));
958
+            }
959
+        } else {
960
+            $qbMarkCalendarDeleted = $this->db->getQueryBuilder();
961
+            $qbMarkCalendarDeleted->update('calendars')
962
+                ->set('deleted_at', $qbMarkCalendarDeleted->createNamedParameter(time()))
963
+                ->where($qbMarkCalendarDeleted->expr()->eq('id', $qbMarkCalendarDeleted->createNamedParameter($calendarId)))
964
+                ->executeStatement();
965
+
966
+            $calendarData = $this->getCalendarById($calendarId);
967
+            $shares = $this->getShares($calendarId);
968
+            if ($calendarData) {
969
+                $this->dispatcher->dispatchTyped(new CalendarMovedToTrashEvent(
970
+                    (int)$calendarId,
971
+                    $calendarData,
972
+                    $shares
973
+                ));
974
+            }
975
+        }
976
+    }
977
+
978
+    public function restoreCalendar(int $id): void {
979
+        $qb = $this->db->getQueryBuilder();
980
+        $update = $qb->update('calendars')
981
+            ->set('deleted_at', $qb->createNamedParameter(null))
982
+            ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT));
983
+        $update->executeStatement();
984
+
985
+        $calendarData = $this->getCalendarById($id);
986
+        $shares = $this->getShares($id);
987
+        if ($calendarData === null) {
988
+            throw new RuntimeException('Calendar data that was just written can\'t be read back. Check your database configuration.');
989
+        }
990
+        $this->dispatcher->dispatchTyped(new CalendarRestoredEvent(
991
+            $id,
992
+            $calendarData,
993
+            $shares
994
+        ));
995
+    }
996
+
997
+    /**
998
+     * Delete all of an user's shares
999
+     *
1000
+     * @param string $principaluri
1001
+     * @return void
1002
+     */
1003
+    public function deleteAllSharesByUser($principaluri) {
1004
+        $this->calendarSharingBackend->deleteAllSharesByUser($principaluri);
1005
+    }
1006
+
1007
+    /**
1008
+     * Returns all calendar objects within a calendar.
1009
+     *
1010
+     * Every item contains an array with the following keys:
1011
+     *   * calendardata - The iCalendar-compatible calendar data
1012
+     *   * uri - a unique key which will be used to construct the uri. This can
1013
+     *     be any arbitrary string, but making sure it ends with '.ics' is a
1014
+     *     good idea. This is only the basename, or filename, not the full
1015
+     *     path.
1016
+     *   * lastmodified - a timestamp of the last modification time
1017
+     *   * etag - An arbitrary string, surrounded by double-quotes. (e.g.:
1018
+     *   '"abcdef"')
1019
+     *   * size - The size of the calendar objects, in bytes.
1020
+     *   * component - optional, a string containing the type of object, such
1021
+     *     as 'vevent' or 'vtodo'. If specified, this will be used to populate
1022
+     *     the Content-Type header.
1023
+     *
1024
+     * Note that the etag is optional, but it's highly encouraged to return for
1025
+     * speed reasons.
1026
+     *
1027
+     * The calendardata is also optional. If it's not returned
1028
+     * 'getCalendarObject' will be called later, which *is* expected to return
1029
+     * calendardata.
1030
+     *
1031
+     * If neither etag or size are specified, the calendardata will be
1032
+     * used/fetched to determine these numbers. If both are specified the
1033
+     * amount of times this is needed is reduced by a great degree.
1034
+     *
1035
+     * @param mixed $calendarId
1036
+     * @param int $calendarType
1037
+     * @return array
1038
+     */
1039
+    public function getCalendarObjects($calendarId, $calendarType = self::CALENDAR_TYPE_CALENDAR):array {
1040
+        $query = $this->db->getQueryBuilder();
1041
+        $query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'componenttype', 'classification'])
1042
+            ->from('calendarobjects')
1043
+            ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
1044
+            ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)))
1045
+            ->andWhere($query->expr()->isNull('deleted_at'));
1046
+        $stmt = $query->executeQuery();
1047
+
1048
+        $result = [];
1049
+        foreach ($stmt->fetchAll() as $row) {
1050
+            $result[] = [
1051
+                'id' => $row['id'],
1052
+                'uri' => $row['uri'],
1053
+                'lastmodified' => $row['lastmodified'],
1054
+                'etag' => '"' . $row['etag'] . '"',
1055
+                'calendarid' => $row['calendarid'],
1056
+                'size' => (int)$row['size'],
1057
+                'component' => strtolower($row['componenttype']),
1058
+                'classification' => (int)$row['classification']
1059
+            ];
1060
+        }
1061
+        $stmt->closeCursor();
1062
+
1063
+        return $result;
1064
+    }
1065
+
1066
+    public function getDeletedCalendarObjects(int $deletedBefore): array {
1067
+        $query = $this->db->getQueryBuilder();
1068
+        $query->select(['co.id', 'co.uri', 'co.lastmodified', 'co.etag', 'co.calendarid', 'co.calendartype', 'co.size', 'co.componenttype', 'co.classification', 'co.deleted_at'])
1069
+            ->from('calendarobjects', 'co')
1070
+            ->join('co', 'calendars', 'c', $query->expr()->eq('c.id', 'co.calendarid', IQueryBuilder::PARAM_INT))
1071
+            ->where($query->expr()->isNotNull('co.deleted_at'))
1072
+            ->andWhere($query->expr()->lt('co.deleted_at', $query->createNamedParameter($deletedBefore)));
1073
+        $stmt = $query->executeQuery();
1074
+
1075
+        $result = [];
1076
+        foreach ($stmt->fetchAll() as $row) {
1077
+            $result[] = [
1078
+                'id' => $row['id'],
1079
+                'uri' => $row['uri'],
1080
+                'lastmodified' => $row['lastmodified'],
1081
+                'etag' => '"' . $row['etag'] . '"',
1082
+                'calendarid' => (int) $row['calendarid'],
1083
+                'calendartype' => (int) $row['calendartype'],
1084
+                'size' => (int) $row['size'],
1085
+                'component' => strtolower($row['componenttype']),
1086
+                'classification' => (int) $row['classification'],
1087
+                '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => $row['deleted_at'] === null ? $row['deleted_at'] : (int) $row['deleted_at'],
1088
+            ];
1089
+        }
1090
+        $stmt->closeCursor();
1091
+
1092
+        return $result;
1093
+    }
1094
+
1095
+    /**
1096
+     * Return all deleted calendar objects by the given principal that are not
1097
+     * in deleted calendars.
1098
+     *
1099
+     * @param string $principalUri
1100
+     * @return array
1101
+     * @throws Exception
1102
+     */
1103
+    public function getDeletedCalendarObjectsByPrincipal(string $principalUri): array {
1104
+        $query = $this->db->getQueryBuilder();
1105
+        $query->select(['co.id', 'co.uri', 'co.lastmodified', 'co.etag', 'co.calendarid', 'co.size', 'co.componenttype', 'co.classification', 'co.deleted_at'])
1106
+            ->selectAlias('c.uri', 'calendaruri')
1107
+            ->from('calendarobjects', 'co')
1108
+            ->join('co', 'calendars', 'c', $query->expr()->eq('c.id', 'co.calendarid', IQueryBuilder::PARAM_INT))
1109
+            ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
1110
+            ->andWhere($query->expr()->isNotNull('co.deleted_at'))
1111
+            ->andWhere($query->expr()->isNull('c.deleted_at'));
1112
+        $stmt = $query->executeQuery();
1113
+
1114
+        $result = [];
1115
+        while ($row = $stmt->fetch()) {
1116
+            $result[] = [
1117
+                'id' => $row['id'],
1118
+                'uri' => $row['uri'],
1119
+                'lastmodified' => $row['lastmodified'],
1120
+                'etag' => '"' . $row['etag'] . '"',
1121
+                'calendarid' => $row['calendarid'],
1122
+                'calendaruri' => $row['calendaruri'],
1123
+                'size' => (int)$row['size'],
1124
+                'component' => strtolower($row['componenttype']),
1125
+                'classification' => (int)$row['classification'],
1126
+                '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => $row['deleted_at'] === null ? $row['deleted_at'] : (int) $row['deleted_at'],
1127
+            ];
1128
+        }
1129
+        $stmt->closeCursor();
1130
+
1131
+        return $result;
1132
+    }
1133
+
1134
+    /**
1135
+     * Returns information from a single calendar object, based on it's object
1136
+     * uri.
1137
+     *
1138
+     * The object uri is only the basename, or filename and not a full path.
1139
+     *
1140
+     * The returned array must have the same keys as getCalendarObjects. The
1141
+     * 'calendardata' object is required here though, while it's not required
1142
+     * for getCalendarObjects.
1143
+     *
1144
+     * This method must return null if the object did not exist.
1145
+     *
1146
+     * @param mixed $calendarId
1147
+     * @param string $objectUri
1148
+     * @param int $calendarType
1149
+     * @return array|null
1150
+     */
1151
+    public function getCalendarObject($calendarId, $objectUri, int $calendarType = self::CALENDAR_TYPE_CALENDAR) {
1152
+        $query = $this->db->getQueryBuilder();
1153
+        $query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification'])
1154
+            ->from('calendarobjects')
1155
+            ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
1156
+            ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
1157
+            ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)));
1158
+        $stmt = $query->executeQuery();
1159
+        $row = $stmt->fetch();
1160
+        $stmt->closeCursor();
1161
+
1162
+        if (!$row) {
1163
+            return null;
1164
+        }
1165
+
1166
+        return [
1167
+            'id' => $row['id'],
1168
+            'uri' => $row['uri'],
1169
+            'lastmodified' => $row['lastmodified'],
1170
+            'etag' => '"' . $row['etag'] . '"',
1171
+            'calendarid' => $row['calendarid'],
1172
+            'size' => (int)$row['size'],
1173
+            'calendardata' => $this->readBlob($row['calendardata']),
1174
+            'component' => strtolower($row['componenttype']),
1175
+            'classification' => (int)$row['classification']
1176
+        ];
1177
+    }
1178
+
1179
+    /**
1180
+     * Returns a list of calendar objects.
1181
+     *
1182
+     * This method should work identical to getCalendarObject, but instead
1183
+     * return all the calendar objects in the list as an array.
1184
+     *
1185
+     * If the backend supports this, it may allow for some speed-ups.
1186
+     *
1187
+     * @param mixed $calendarId
1188
+     * @param string[] $uris
1189
+     * @param int $calendarType
1190
+     * @return array
1191
+     */
1192
+    public function getMultipleCalendarObjects($calendarId, array $uris, $calendarType = self::CALENDAR_TYPE_CALENDAR):array {
1193
+        if (empty($uris)) {
1194
+            return [];
1195
+        }
1196
+
1197
+        $chunks = array_chunk($uris, 100);
1198
+        $objects = [];
1199
+
1200
+        $query = $this->db->getQueryBuilder();
1201
+        $query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification'])
1202
+            ->from('calendarobjects')
1203
+            ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
1204
+            ->andWhere($query->expr()->in('uri', $query->createParameter('uri')))
1205
+            ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)))
1206
+            ->andWhere($query->expr()->isNull('deleted_at'));
1207
+
1208
+        foreach ($chunks as $uris) {
1209
+            $query->setParameter('uri', $uris, IQueryBuilder::PARAM_STR_ARRAY);
1210
+            $result = $query->executeQuery();
1211
+
1212
+            while ($row = $result->fetch()) {
1213
+                $objects[] = [
1214
+                    'id' => $row['id'],
1215
+                    'uri' => $row['uri'],
1216
+                    'lastmodified' => $row['lastmodified'],
1217
+                    'etag' => '"' . $row['etag'] . '"',
1218
+                    'calendarid' => $row['calendarid'],
1219
+                    'size' => (int)$row['size'],
1220
+                    'calendardata' => $this->readBlob($row['calendardata']),
1221
+                    'component' => strtolower($row['componenttype']),
1222
+                    'classification' => (int)$row['classification']
1223
+                ];
1224
+            }
1225
+            $result->closeCursor();
1226
+        }
1227
+
1228
+        return $objects;
1229
+    }
1230
+
1231
+    /**
1232
+     * Creates a new calendar object.
1233
+     *
1234
+     * The object uri is only the basename, or filename and not a full path.
1235
+     *
1236
+     * It is possible return an etag from this function, which will be used in
1237
+     * the response to this PUT request. Note that the ETag must be surrounded
1238
+     * by double-quotes.
1239
+     *
1240
+     * However, you should only really return this ETag if you don't mangle the
1241
+     * calendar-data. If the result of a subsequent GET to this object is not
1242
+     * the exact same as this request body, you should omit the ETag.
1243
+     *
1244
+     * @param mixed $calendarId
1245
+     * @param string $objectUri
1246
+     * @param string $calendarData
1247
+     * @param int $calendarType
1248
+     * @return string
1249
+     */
1250
+    public function createCalendarObject($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
1251
+        $extraData = $this->getDenormalizedData($calendarData);
1252
+
1253
+        // Try to detect duplicates
1254
+        $qb = $this->db->getQueryBuilder();
1255
+        $qb->select($qb->func()->count('*'))
1256
+            ->from('calendarobjects')
1257
+            ->where($qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)))
1258
+            ->andWhere($qb->expr()->eq('uid', $qb->createNamedParameter($extraData['uid'])))
1259
+            ->andWhere($qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType)))
1260
+            ->andWhere($qb->expr()->isNull('deleted_at'));
1261
+        $result = $qb->executeQuery();
1262
+        $count = (int) $result->fetchOne();
1263
+        $result->closeCursor();
1264
+
1265
+        if ($count !== 0) {
1266
+            throw new BadRequest('Calendar object with uid already exists in this calendar collection.');
1267
+        }
1268
+        // For a more specific error message we also try to explicitly look up the UID but as a deleted entry
1269
+        $qbDel = $this->db->getQueryBuilder();
1270
+        $qbDel->select($qb->func()->count('*'))
1271
+            ->from('calendarobjects')
1272
+            ->where($qbDel->expr()->eq('calendarid', $qbDel->createNamedParameter($calendarId)))
1273
+            ->andWhere($qbDel->expr()->eq('uid', $qbDel->createNamedParameter($extraData['uid'])))
1274
+            ->andWhere($qbDel->expr()->eq('calendartype', $qbDel->createNamedParameter($calendarType)))
1275
+            ->andWhere($qbDel->expr()->isNotNull('deleted_at'));
1276
+        $result = $qbDel->executeQuery();
1277
+        $count = (int) $result->fetchOne();
1278
+        $result->closeCursor();
1279
+        if ($count !== 0) {
1280
+            throw new BadRequest('Deleted calendar object with uid already exists in this calendar collection.');
1281
+        }
1282
+
1283
+        $query = $this->db->getQueryBuilder();
1284
+        $query->insert('calendarobjects')
1285
+            ->values([
1286
+                'calendarid' => $query->createNamedParameter($calendarId),
1287
+                'uri' => $query->createNamedParameter($objectUri),
1288
+                'calendardata' => $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB),
1289
+                'lastmodified' => $query->createNamedParameter(time()),
1290
+                'etag' => $query->createNamedParameter($extraData['etag']),
1291
+                'size' => $query->createNamedParameter($extraData['size']),
1292
+                'componenttype' => $query->createNamedParameter($extraData['componentType']),
1293
+                'firstoccurence' => $query->createNamedParameter($extraData['firstOccurence']),
1294
+                'lastoccurence' => $query->createNamedParameter($extraData['lastOccurence']),
1295
+                'classification' => $query->createNamedParameter($extraData['classification']),
1296
+                'uid' => $query->createNamedParameter($extraData['uid']),
1297
+                'calendartype' => $query->createNamedParameter($calendarType),
1298
+            ])
1299
+            ->executeStatement();
1300
+
1301
+        $this->updateProperties($calendarId, $objectUri, $calendarData, $calendarType);
1302
+        $this->addChange($calendarId, $objectUri, 1, $calendarType);
1303
+
1304
+        $objectRow = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
1305
+        if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
1306
+            $calendarRow = $this->getCalendarById($calendarId);
1307
+            $shares = $this->getShares($calendarId);
1308
+
1309
+            $this->dispatcher->dispatchTyped(new CalendarObjectCreatedEvent((int)$calendarId, $calendarRow, $shares, $objectRow));
1310
+        } else {
1311
+            $subscriptionRow = $this->getSubscriptionById($calendarId);
1312
+
1313
+            $this->dispatcher->dispatchTyped(new CachedCalendarObjectCreatedEvent((int)$calendarId, $subscriptionRow, [], $objectRow));
1314
+            $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createCachedCalendarObject', new GenericEvent(
1315
+                '\OCA\DAV\CalDAV\CalDavBackend::createCachedCalendarObject',
1316
+                [
1317
+                    'subscriptionId' => $calendarId,
1318
+                    'calendarData' => $subscriptionRow,
1319
+                    'shares' => [],
1320
+                    'objectData' => $objectRow,
1321
+                ]
1322
+            ));
1323
+        }
1324
+
1325
+        return '"' . $extraData['etag'] . '"';
1326
+    }
1327
+
1328
+    /**
1329
+     * Updates an existing calendarobject, based on it's uri.
1330
+     *
1331
+     * The object uri is only the basename, or filename and not a full path.
1332
+     *
1333
+     * It is possible return an etag from this function, which will be used in
1334
+     * the response to this PUT request. Note that the ETag must be surrounded
1335
+     * by double-quotes.
1336
+     *
1337
+     * However, you should only really return this ETag if you don't mangle the
1338
+     * calendar-data. If the result of a subsequent GET to this object is not
1339
+     * the exact same as this request body, you should omit the ETag.
1340
+     *
1341
+     * @param mixed $calendarId
1342
+     * @param string $objectUri
1343
+     * @param string $calendarData
1344
+     * @param int $calendarType
1345
+     * @return string
1346
+     */
1347
+    public function updateCalendarObject($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
1348
+        $extraData = $this->getDenormalizedData($calendarData);
1349
+        $query = $this->db->getQueryBuilder();
1350
+        $query->update('calendarobjects')
1351
+                ->set('calendardata', $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB))
1352
+                ->set('lastmodified', $query->createNamedParameter(time()))
1353
+                ->set('etag', $query->createNamedParameter($extraData['etag']))
1354
+                ->set('size', $query->createNamedParameter($extraData['size']))
1355
+                ->set('componenttype', $query->createNamedParameter($extraData['componentType']))
1356
+                ->set('firstoccurence', $query->createNamedParameter($extraData['firstOccurence']))
1357
+                ->set('lastoccurence', $query->createNamedParameter($extraData['lastOccurence']))
1358
+                ->set('classification', $query->createNamedParameter($extraData['classification']))
1359
+                ->set('uid', $query->createNamedParameter($extraData['uid']))
1360
+            ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
1361
+            ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
1362
+            ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)))
1363
+            ->executeStatement();
1364
+
1365
+        $this->updateProperties($calendarId, $objectUri, $calendarData, $calendarType);
1366
+        $this->addChange($calendarId, $objectUri, 2, $calendarType);
1367
+
1368
+        $objectRow = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
1369
+        if (is_array($objectRow)) {
1370
+            if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
1371
+                $calendarRow = $this->getCalendarById($calendarId);
1372
+                $shares = $this->getShares($calendarId);
1373
+
1374
+                $this->dispatcher->dispatchTyped(new CalendarObjectUpdatedEvent((int)$calendarId, $calendarRow, $shares, $objectRow));
1375
+            } else {
1376
+                $subscriptionRow = $this->getSubscriptionById($calendarId);
1377
+
1378
+                $this->dispatcher->dispatchTyped(new CachedCalendarObjectUpdatedEvent((int)$calendarId, $subscriptionRow, [], $objectRow));
1379
+                $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateCachedCalendarObject', new GenericEvent(
1380
+                    '\OCA\DAV\CalDAV\CalDavBackend::updateCachedCalendarObject',
1381
+                    [
1382
+                        'subscriptionId' => $calendarId,
1383
+                        'calendarData' => $subscriptionRow,
1384
+                        'shares' => [],
1385
+                        'objectData' => $objectRow,
1386
+                    ]
1387
+                ));
1388
+            }
1389
+        }
1390
+
1391
+        return '"' . $extraData['etag'] . '"';
1392
+    }
1393
+
1394
+    /**
1395
+     * Moves a calendar object from calendar to calendar.
1396
+     *
1397
+     * @param int $sourceCalendarId
1398
+     * @param int $targetCalendarId
1399
+     * @param int $objectId
1400
+     * @param string $principalUri
1401
+     * @param int $calendarType
1402
+     * @return bool
1403
+     * @throws Exception
1404
+     */
1405
+    public function moveCalendarObject(int $sourceCalendarId, int $targetCalendarId, int $objectId, string $principalUri, int $calendarType = self::CALENDAR_TYPE_CALENDAR): bool {
1406
+        $object = $this->getCalendarObjectById($principalUri, $objectId);
1407
+        if (empty($object)) {
1408
+            return false;
1409
+        }
1410
+
1411
+        $query = $this->db->getQueryBuilder();
1412
+        $query->update('calendarobjects')
1413
+            ->set('calendarid', $query->createNamedParameter($targetCalendarId, IQueryBuilder::PARAM_INT))
1414
+            ->where($query->expr()->eq('id', $query->createNamedParameter($objectId, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT))
1415
+            ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT))
1416
+            ->executeStatement();
1417
+
1418
+        $this->purgeProperties($sourceCalendarId, $objectId);
1419
+        $this->updateProperties($targetCalendarId, $object['uri'], $object['calendardata'], $calendarType);
1420
+
1421
+        $this->addChange($sourceCalendarId, $object['uri'], 1, $calendarType);
1422
+        $this->addChange($targetCalendarId, $object['uri'], 3, $calendarType);
1423
+
1424
+        $object = $this->getCalendarObjectById($principalUri, $objectId);
1425
+        // Calendar Object wasn't found - possibly because it was deleted in the meantime by a different client
1426
+        if (empty($object)) {
1427
+            return false;
1428
+        }
1429
+
1430
+        $calendarRow = $this->getCalendarById($targetCalendarId);
1431
+        // the calendar this event is being moved to does not exist any longer
1432
+        if (empty($calendarRow)) {
1433
+            return false;
1434
+        }
1435
+
1436
+        if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
1437
+            $shares = $this->getShares($targetCalendarId);
1438
+            $this->dispatcher->dispatchTyped(new CalendarObjectUpdatedEvent($targetCalendarId, $calendarRow, $shares, $object));
1439
+        }
1440
+        return true;
1441
+    }
1442
+
1443
+
1444
+    /**
1445
+     * @param int $calendarObjectId
1446
+     * @param int $classification
1447
+     */
1448
+    public function setClassification($calendarObjectId, $classification) {
1449
+        if (!in_array($classification, [
1450
+            self::CLASSIFICATION_PUBLIC, self::CLASSIFICATION_PRIVATE, self::CLASSIFICATION_CONFIDENTIAL
1451
+        ])) {
1452
+            throw new \InvalidArgumentException();
1453
+        }
1454
+        $query = $this->db->getQueryBuilder();
1455
+        $query->update('calendarobjects')
1456
+            ->set('classification', $query->createNamedParameter($classification))
1457
+            ->where($query->expr()->eq('id', $query->createNamedParameter($calendarObjectId)))
1458
+            ->executeStatement();
1459
+    }
1460
+
1461
+    /**
1462
+     * Deletes an existing calendar object.
1463
+     *
1464
+     * The object uri is only the basename, or filename and not a full path.
1465
+     *
1466
+     * @param mixed $calendarId
1467
+     * @param string $objectUri
1468
+     * @param int $calendarType
1469
+     * @param bool $forceDeletePermanently
1470
+     * @return void
1471
+     */
1472
+    public function deleteCalendarObject($calendarId, $objectUri, $calendarType = self::CALENDAR_TYPE_CALENDAR, bool $forceDeletePermanently = false) {
1473
+        $data = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
1474
+
1475
+        if ($data === null) {
1476
+            // Nothing to delete
1477
+            return;
1478
+        }
1479
+
1480
+        if ($forceDeletePermanently || $this->config->getAppValue(Application::APP_ID, RetentionService::RETENTION_CONFIG_KEY) === '0') {
1481
+            $stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ? AND `uri` = ? AND `calendartype` = ?');
1482
+            $stmt->execute([$calendarId, $objectUri, $calendarType]);
1483
+
1484
+            $this->purgeProperties($calendarId, $data['id']);
1485
+
1486
+            if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
1487
+                $calendarRow = $this->getCalendarById($calendarId);
1488
+                $shares = $this->getShares($calendarId);
1489
+
1490
+                $this->dispatcher->dispatchTyped(new CalendarObjectDeletedEvent((int)$calendarId, $calendarRow, $shares, $data));
1491
+            } else {
1492
+                $subscriptionRow = $this->getSubscriptionById($calendarId);
1493
+
1494
+                $this->dispatcher->dispatchTyped(new CachedCalendarObjectDeletedEvent((int)$calendarId, $subscriptionRow, [], $data));
1495
+                $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteCachedCalendarObject', new GenericEvent(
1496
+                    '\OCA\DAV\CalDAV\CalDavBackend::deleteCachedCalendarObject',
1497
+                    [
1498
+                        'subscriptionId' => $calendarId,
1499
+                        'calendarData' => $subscriptionRow,
1500
+                        'shares' => [],
1501
+                        'objectData' => $data,
1502
+                    ]
1503
+                ));
1504
+            }
1505
+        } else {
1506
+            $pathInfo = pathinfo($data['uri']);
1507
+            if (!empty($pathInfo['extension'])) {
1508
+                // Append a suffix to "free" the old URI for recreation
1509
+                $newUri = sprintf(
1510
+                    "%s-deleted.%s",
1511
+                    $pathInfo['filename'],
1512
+                    $pathInfo['extension']
1513
+                );
1514
+            } else {
1515
+                $newUri = sprintf(
1516
+                    "%s-deleted",
1517
+                    $pathInfo['filename']
1518
+                );
1519
+            }
1520
+
1521
+            // Try to detect conflicts before the DB does
1522
+            // As unlikely as it seems, this can happen when the user imports, then deletes, imports and deletes again
1523
+            $newObject = $this->getCalendarObject($calendarId, $newUri, $calendarType);
1524
+            if ($newObject !== null) {
1525
+                throw new Forbidden("A calendar object with URI $newUri already exists in calendar $calendarId, therefore this object can't be moved into the trashbin");
1526
+            }
1527
+
1528
+            $qb = $this->db->getQueryBuilder();
1529
+            $markObjectDeletedQuery = $qb->update('calendarobjects')
1530
+                ->set('deleted_at', $qb->createNamedParameter(time(), IQueryBuilder::PARAM_INT))
1531
+                ->set('uri', $qb->createNamedParameter($newUri))
1532
+                ->where(
1533
+                    $qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)),
1534
+                    $qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT),
1535
+                    $qb->expr()->eq('uri', $qb->createNamedParameter($objectUri))
1536
+                );
1537
+            $markObjectDeletedQuery->executeStatement();
1538
+
1539
+            $calendarData = $this->getCalendarById($calendarId);
1540
+            if ($calendarData !== null) {
1541
+                $this->dispatcher->dispatchTyped(
1542
+                    new CalendarObjectMovedToTrashEvent(
1543
+                        (int)$calendarId,
1544
+                        $calendarData,
1545
+                        $this->getShares($calendarId),
1546
+                        $data
1547
+                    )
1548
+                );
1549
+            }
1550
+        }
1551
+
1552
+        $this->addChange($calendarId, $objectUri, 3, $calendarType);
1553
+    }
1554
+
1555
+    /**
1556
+     * @param mixed $objectData
1557
+     *
1558
+     * @throws Forbidden
1559
+     */
1560
+    public function restoreCalendarObject(array $objectData): void {
1561
+        $id = (int) $objectData['id'];
1562
+        $restoreUri = str_replace("-deleted.ics", ".ics", $objectData['uri']);
1563
+        $targetObject = $this->getCalendarObject(
1564
+            $objectData['calendarid'],
1565
+            $restoreUri
1566
+        );
1567
+        if ($targetObject !== null) {
1568
+            throw new Forbidden("Can not restore calendar $id because a calendar object with the URI $restoreUri already exists");
1569
+        }
1570
+
1571
+        $qb = $this->db->getQueryBuilder();
1572
+        $update = $qb->update('calendarobjects')
1573
+            ->set('uri', $qb->createNamedParameter($restoreUri))
1574
+            ->set('deleted_at', $qb->createNamedParameter(null))
1575
+            ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT));
1576
+        $update->executeStatement();
1577
+
1578
+        // Make sure this change is tracked in the changes table
1579
+        $qb2 = $this->db->getQueryBuilder();
1580
+        $selectObject = $qb2->select('calendardata', 'uri', 'calendarid', 'calendartype')
1581
+            ->selectAlias('componenttype', 'component')
1582
+            ->from('calendarobjects')
1583
+            ->where($qb2->expr()->eq('id', $qb2->createNamedParameter($id, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT));
1584
+        $result = $selectObject->executeQuery();
1585
+        $row = $result->fetch();
1586
+        $result->closeCursor();
1587
+        if ($row === false) {
1588
+            // Welp, this should possibly not have happened, but let's ignore
1589
+            return;
1590
+        }
1591
+        $this->addChange($row['calendarid'], $row['uri'], 1, (int) $row['calendartype']);
1592
+
1593
+        $calendarRow = $this->getCalendarById((int) $row['calendarid']);
1594
+        if ($calendarRow === null) {
1595
+            throw new RuntimeException('Calendar object data that was just written can\'t be read back. Check your database configuration.');
1596
+        }
1597
+        $this->dispatcher->dispatchTyped(
1598
+            new CalendarObjectRestoredEvent(
1599
+                (int) $objectData['calendarid'],
1600
+                $calendarRow,
1601
+                $this->getShares((int) $row['calendarid']),
1602
+                $row
1603
+            )
1604
+        );
1605
+    }
1606
+
1607
+    /**
1608
+     * Performs a calendar-query on the contents of this calendar.
1609
+     *
1610
+     * The calendar-query is defined in RFC4791 : CalDAV. Using the
1611
+     * calendar-query it is possible for a client to request a specific set of
1612
+     * object, based on contents of iCalendar properties, date-ranges and
1613
+     * iCalendar component types (VTODO, VEVENT).
1614
+     *
1615
+     * This method should just return a list of (relative) urls that match this
1616
+     * query.
1617
+     *
1618
+     * The list of filters are specified as an array. The exact array is
1619
+     * documented by Sabre\CalDAV\CalendarQueryParser.
1620
+     *
1621
+     * Note that it is extremely likely that getCalendarObject for every path
1622
+     * returned from this method will be called almost immediately after. You
1623
+     * may want to anticipate this to speed up these requests.
1624
+     *
1625
+     * This method provides a default implementation, which parses *all* the
1626
+     * iCalendar objects in the specified calendar.
1627
+     *
1628
+     * This default may well be good enough for personal use, and calendars
1629
+     * that aren't very large. But if you anticipate high usage, big calendars
1630
+     * or high loads, you are strongly advised to optimize certain paths.
1631
+     *
1632
+     * The best way to do so is override this method and to optimize
1633
+     * specifically for 'common filters'.
1634
+     *
1635
+     * Requests that are extremely common are:
1636
+     *   * requests for just VEVENTS
1637
+     *   * requests for just VTODO
1638
+     *   * requests with a time-range-filter on either VEVENT or VTODO.
1639
+     *
1640
+     * ..and combinations of these requests. It may not be worth it to try to
1641
+     * handle every possible situation and just rely on the (relatively
1642
+     * easy to use) CalendarQueryValidator to handle the rest.
1643
+     *
1644
+     * Note that especially time-range-filters may be difficult to parse. A
1645
+     * time-range filter specified on a VEVENT must for instance also handle
1646
+     * recurrence rules correctly.
1647
+     * A good example of how to interprete all these filters can also simply
1648
+     * be found in Sabre\CalDAV\CalendarQueryFilter. This class is as correct
1649
+     * as possible, so it gives you a good idea on what type of stuff you need
1650
+     * to think of.
1651
+     *
1652
+     * @param mixed $calendarId
1653
+     * @param array $filters
1654
+     * @param int $calendarType
1655
+     * @return array
1656
+     */
1657
+    public function calendarQuery($calendarId, array $filters, $calendarType = self::CALENDAR_TYPE_CALENDAR):array {
1658
+        $componentType = null;
1659
+        $requirePostFilter = true;
1660
+        $timeRange = null;
1661
+
1662
+        // if no filters were specified, we don't need to filter after a query
1663
+        if (!$filters['prop-filters'] && !$filters['comp-filters']) {
1664
+            $requirePostFilter = false;
1665
+        }
1666
+
1667
+        // Figuring out if there's a component filter
1668
+        if (count($filters['comp-filters']) > 0 && !$filters['comp-filters'][0]['is-not-defined']) {
1669
+            $componentType = $filters['comp-filters'][0]['name'];
1670
+
1671
+            // Checking if we need post-filters
1672
+            if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['time-range'] && !$filters['comp-filters'][0]['prop-filters']) {
1673
+                $requirePostFilter = false;
1674
+            }
1675
+            // There was a time-range filter
1676
+            if ($componentType === 'VEVENT' && isset($filters['comp-filters'][0]['time-range']) && is_array($filters['comp-filters'][0]['time-range'])) {
1677
+                $timeRange = $filters['comp-filters'][0]['time-range'];
1678
+
1679
+                // If start time OR the end time is not specified, we can do a
1680
+                // 100% accurate mysql query.
1681
+                if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['prop-filters'] && (!$timeRange['start'] || !$timeRange['end'])) {
1682
+                    $requirePostFilter = false;
1683
+                }
1684
+            }
1685
+        }
1686
+        $columns = ['uri'];
1687
+        if ($requirePostFilter) {
1688
+            $columns = ['uri', 'calendardata'];
1689
+        }
1690
+        $query = $this->db->getQueryBuilder();
1691
+        $query->select($columns)
1692
+            ->from('calendarobjects')
1693
+            ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
1694
+            ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)))
1695
+            ->andWhere($query->expr()->isNull('deleted_at'));
1696
+
1697
+        if ($componentType) {
1698
+            $query->andWhere($query->expr()->eq('componenttype', $query->createNamedParameter($componentType)));
1699
+        }
1700
+
1701
+        if ($timeRange && $timeRange['start']) {
1702
+            $query->andWhere($query->expr()->gt('lastoccurence', $query->createNamedParameter($timeRange['start']->getTimeStamp())));
1703
+        }
1704
+        if ($timeRange && $timeRange['end']) {
1705
+            $query->andWhere($query->expr()->lt('firstoccurence', $query->createNamedParameter($timeRange['end']->getTimeStamp())));
1706
+        }
1707
+
1708
+        $stmt = $query->executeQuery();
1709
+
1710
+        $result = [];
1711
+        while ($row = $stmt->fetch()) {
1712
+            if ($requirePostFilter) {
1713
+                // validateFilterForObject will parse the calendar data
1714
+                // catch parsing errors
1715
+                try {
1716
+                    $matches = $this->validateFilterForObject($row, $filters);
1717
+                } catch (ParseException $ex) {
1718
+                    $this->logger->logException($ex, [
1719
+                        'app' => 'dav',
1720
+                        'message' => 'Caught parsing exception for calendar data. This usually indicates invalid calendar data. calendar-id:'.$calendarId.' uri:'.$row['uri']
1721
+                    ]);
1722
+                    continue;
1723
+                } catch (InvalidDataException $ex) {
1724
+                    $this->logger->logException($ex, [
1725
+                        'app' => 'dav',
1726
+                        'message' => 'Caught invalid data exception for calendar data. This usually indicates invalid calendar data. calendar-id:'.$calendarId.' uri:'.$row['uri']
1727
+                    ]);
1728
+                    continue;
1729
+                }
1730
+
1731
+                if (!$matches) {
1732
+                    continue;
1733
+                }
1734
+            }
1735
+            $result[] = $row['uri'];
1736
+        }
1737
+
1738
+        return $result;
1739
+    }
1740
+
1741
+    /**
1742
+     * custom Nextcloud search extension for CalDAV
1743
+     *
1744
+     * TODO - this should optionally cover cached calendar objects as well
1745
+     *
1746
+     * @param string $principalUri
1747
+     * @param array $filters
1748
+     * @param integer|null $limit
1749
+     * @param integer|null $offset
1750
+     * @return array
1751
+     */
1752
+    public function calendarSearch($principalUri, array $filters, $limit = null, $offset = null) {
1753
+        $calendars = $this->getCalendarsForUser($principalUri);
1754
+        $ownCalendars = [];
1755
+        $sharedCalendars = [];
1756
+
1757
+        $uriMapper = [];
1758
+
1759
+        foreach ($calendars as $calendar) {
1760
+            if ($calendar['{http://owncloud.org/ns}owner-principal'] === $principalUri) {
1761
+                $ownCalendars[] = $calendar['id'];
1762
+            } else {
1763
+                $sharedCalendars[] = $calendar['id'];
1764
+            }
1765
+            $uriMapper[$calendar['id']] = $calendar['uri'];
1766
+        }
1767
+        if (count($ownCalendars) === 0 && count($sharedCalendars) === 0) {
1768
+            return [];
1769
+        }
1770
+
1771
+        $query = $this->db->getQueryBuilder();
1772
+        // Calendar id expressions
1773
+        $calendarExpressions = [];
1774
+        foreach ($ownCalendars as $id) {
1775
+            $calendarExpressions[] = $query->expr()->andX(
1776
+                $query->expr()->eq('c.calendarid',
1777
+                    $query->createNamedParameter($id)),
1778
+                $query->expr()->eq('c.calendartype',
1779
+                        $query->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
1780
+        }
1781
+        foreach ($sharedCalendars as $id) {
1782
+            $calendarExpressions[] = $query->expr()->andX(
1783
+                $query->expr()->eq('c.calendarid',
1784
+                    $query->createNamedParameter($id)),
1785
+                $query->expr()->eq('c.classification',
1786
+                    $query->createNamedParameter(self::CLASSIFICATION_PUBLIC)),
1787
+                $query->expr()->eq('c.calendartype',
1788
+                    $query->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
1789
+        }
1790
+
1791
+        if (count($calendarExpressions) === 1) {
1792
+            $calExpr = $calendarExpressions[0];
1793
+        } else {
1794
+            $calExpr = call_user_func_array([$query->expr(), 'orX'], $calendarExpressions);
1795
+        }
1796
+
1797
+        // Component expressions
1798
+        $compExpressions = [];
1799
+        foreach ($filters['comps'] as $comp) {
1800
+            $compExpressions[] = $query->expr()
1801
+                ->eq('c.componenttype', $query->createNamedParameter($comp));
1802
+        }
1803
+
1804
+        if (count($compExpressions) === 1) {
1805
+            $compExpr = $compExpressions[0];
1806
+        } else {
1807
+            $compExpr = call_user_func_array([$query->expr(), 'orX'], $compExpressions);
1808
+        }
1809
+
1810
+        if (!isset($filters['props'])) {
1811
+            $filters['props'] = [];
1812
+        }
1813
+        if (!isset($filters['params'])) {
1814
+            $filters['params'] = [];
1815
+        }
1816
+
1817
+        $propParamExpressions = [];
1818
+        foreach ($filters['props'] as $prop) {
1819
+            $propParamExpressions[] = $query->expr()->andX(
1820
+                $query->expr()->eq('i.name', $query->createNamedParameter($prop)),
1821
+                $query->expr()->isNull('i.parameter')
1822
+            );
1823
+        }
1824
+        foreach ($filters['params'] as $param) {
1825
+            $propParamExpressions[] = $query->expr()->andX(
1826
+                $query->expr()->eq('i.name', $query->createNamedParameter($param['property'])),
1827
+                $query->expr()->eq('i.parameter', $query->createNamedParameter($param['parameter']))
1828
+            );
1829
+        }
1830
+
1831
+        if (count($propParamExpressions) === 1) {
1832
+            $propParamExpr = $propParamExpressions[0];
1833
+        } else {
1834
+            $propParamExpr = call_user_func_array([$query->expr(), 'orX'], $propParamExpressions);
1835
+        }
1836
+
1837
+        $query->select(['c.calendarid', 'c.uri'])
1838
+            ->from($this->dbObjectPropertiesTable, 'i')
1839
+            ->join('i', 'calendarobjects', 'c', $query->expr()->eq('i.objectid', 'c.id'))
1840
+            ->where($calExpr)
1841
+            ->andWhere($compExpr)
1842
+            ->andWhere($propParamExpr)
1843
+            ->andWhere($query->expr()->iLike('i.value',
1844
+                $query->createNamedParameter('%'.$this->db->escapeLikeParameter($filters['search-term']).'%')))
1845
+            ->andWhere($query->expr()->isNull('deleted_at'));
1846
+
1847
+        if ($offset) {
1848
+            $query->setFirstResult($offset);
1849
+        }
1850
+        if ($limit) {
1851
+            $query->setMaxResults($limit);
1852
+        }
1853
+
1854
+        $stmt = $query->executeQuery();
1855
+
1856
+        $result = [];
1857
+        while ($row = $stmt->fetch()) {
1858
+            $path = $uriMapper[$row['calendarid']] . '/' . $row['uri'];
1859
+            if (!in_array($path, $result)) {
1860
+                $result[] = $path;
1861
+            }
1862
+        }
1863
+
1864
+        return $result;
1865
+    }
1866
+
1867
+    /**
1868
+     * used for Nextcloud's calendar API
1869
+     *
1870
+     * @param array $calendarInfo
1871
+     * @param string $pattern
1872
+     * @param array $searchProperties
1873
+     * @param array $options
1874
+     * @param integer|null $limit
1875
+     * @param integer|null $offset
1876
+     *
1877
+     * @return array
1878
+     */
1879
+    public function search(array $calendarInfo, $pattern, array $searchProperties,
1880
+                            array $options, $limit, $offset) {
1881
+        $outerQuery = $this->db->getQueryBuilder();
1882
+        $innerQuery = $this->db->getQueryBuilder();
1883
+
1884
+        $innerQuery->selectDistinct('op.objectid')
1885
+            ->from($this->dbObjectPropertiesTable, 'op')
1886
+            ->andWhere($innerQuery->expr()->eq('op.calendarid',
1887
+                $outerQuery->createNamedParameter($calendarInfo['id'])))
1888
+            ->andWhere($innerQuery->expr()->eq('op.calendartype',
1889
+                $outerQuery->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
1890
+
1891
+        // only return public items for shared calendars for now
1892
+        if (isset($calendarInfo['{http://owncloud.org/ns}owner-principal']) === false || $calendarInfo['principaluri'] !== $calendarInfo['{http://owncloud.org/ns}owner-principal']) {
1893
+            $innerQuery->andWhere($innerQuery->expr()->eq('c.classification',
1894
+                $outerQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
1895
+        }
1896
+
1897
+        if (!empty($searchProperties)) {
1898
+            $or = $innerQuery->expr()->orX();
1899
+            foreach ($searchProperties as $searchProperty) {
1900
+                $or->add($innerQuery->expr()->eq('op.name',
1901
+                    $outerQuery->createNamedParameter($searchProperty)));
1902
+            }
1903
+            $innerQuery->andWhere($or);
1904
+        }
1905
+
1906
+        if ($pattern !== '') {
1907
+            $innerQuery->andWhere($innerQuery->expr()->iLike('op.value',
1908
+                $outerQuery->createNamedParameter('%' .
1909
+                    $this->db->escapeLikeParameter($pattern) . '%')));
1910
+        }
1911
+
1912
+        $outerQuery->select('c.id', 'c.calendardata', 'c.componenttype', 'c.uid', 'c.uri')
1913
+            ->from('calendarobjects', 'c')
1914
+            ->where($outerQuery->expr()->isNull('deleted_at'));
1915
+
1916
+        if (isset($options['timerange'])) {
1917
+            if (isset($options['timerange']['start']) && $options['timerange']['start'] instanceof DateTimeInterface) {
1918
+                $outerQuery->andWhere($outerQuery->expr()->gt('lastoccurence',
1919
+                    $outerQuery->createNamedParameter($options['timerange']['start']->getTimeStamp())));
1920
+            }
1921
+            if (isset($options['timerange']['end']) && $options['timerange']['end'] instanceof DateTimeInterface) {
1922
+                $outerQuery->andWhere($outerQuery->expr()->lt('firstoccurence',
1923
+                    $outerQuery->createNamedParameter($options['timerange']['end']->getTimeStamp())));
1924
+            }
1925
+        }
1926
+
1927
+        if (!empty($options['types'])) {
1928
+            $or = $outerQuery->expr()->orX();
1929
+            foreach ($options['types'] as $type) {
1930
+                $or->add($outerQuery->expr()->eq('componenttype',
1931
+                    $outerQuery->createNamedParameter($type)));
1932
+            }
1933
+            $outerQuery->andWhere($or);
1934
+        }
1935
+
1936
+        $outerQuery->andWhere($outerQuery->expr()->in('c.id', $outerQuery->createFunction($innerQuery->getSQL())));
1937
+
1938
+        if ($offset) {
1939
+            $outerQuery->setFirstResult($offset);
1940
+        }
1941
+        if ($limit) {
1942
+            $outerQuery->setMaxResults($limit);
1943
+        }
1944
+
1945
+        $result = $outerQuery->executeQuery();
1946
+        $calendarObjects = array_filter($result->fetchAll(), function (array $row) use ($options) {
1947
+            $start = $options['timerange']['start'] ?? null;
1948
+            $end = $options['timerange']['end'] ?? null;
1949
+
1950
+            if ($start === null || !($start instanceof DateTimeInterface) || $end === null || !($end instanceof DateTimeInterface)) {
1951
+                // No filter required
1952
+                return true;
1953
+            }
1954
+
1955
+            $isValid = $this->validateFilterForObject($row, [
1956
+                'name' => 'VCALENDAR',
1957
+                'comp-filters' => [
1958
+                    [
1959
+                        'name' => 'VEVENT',
1960
+                        'comp-filters' => [],
1961
+                        'prop-filters' => [],
1962
+                        'is-not-defined' => false,
1963
+                        'time-range' => [
1964
+                            'start' => $start,
1965
+                            'end' => $end,
1966
+                        ],
1967
+                    ],
1968
+                ],
1969
+                'prop-filters' => [],
1970
+                'is-not-defined' => false,
1971
+                'time-range' => null,
1972
+            ]);
1973
+            if (is_resource($row['calendardata'])) {
1974
+                // Put the stream back to the beginning so it can be read another time
1975
+                rewind($row['calendardata']);
1976
+            }
1977
+            return $isValid;
1978
+        });
1979
+        $result->closeCursor();
1980
+
1981
+        return array_map(function ($o) {
1982
+            $calendarData = Reader::read($o['calendardata']);
1983
+            $comps = $calendarData->getComponents();
1984
+            $objects = [];
1985
+            $timezones = [];
1986
+            foreach ($comps as $comp) {
1987
+                if ($comp instanceof VTimeZone) {
1988
+                    $timezones[] = $comp;
1989
+                } else {
1990
+                    $objects[] = $comp;
1991
+                }
1992
+            }
1993
+
1994
+            return [
1995
+                'id' => $o['id'],
1996
+                'type' => $o['componenttype'],
1997
+                'uid' => $o['uid'],
1998
+                'uri' => $o['uri'],
1999
+                'objects' => array_map(function ($c) {
2000
+                    return $this->transformSearchData($c);
2001
+                }, $objects),
2002
+                'timezones' => array_map(function ($c) {
2003
+                    return $this->transformSearchData($c);
2004
+                }, $timezones),
2005
+            ];
2006
+        }, $calendarObjects);
2007
+    }
2008
+
2009
+    /**
2010
+     * @param Component $comp
2011
+     * @return array
2012
+     */
2013
+    private function transformSearchData(Component $comp) {
2014
+        $data = [];
2015
+        /** @var Component[] $subComponents */
2016
+        $subComponents = $comp->getComponents();
2017
+        /** @var Property[] $properties */
2018
+        $properties = array_filter($comp->children(), function ($c) {
2019
+            return $c instanceof Property;
2020
+        });
2021
+        $validationRules = $comp->getValidationRules();
2022
+
2023
+        foreach ($subComponents as $subComponent) {
2024
+            $name = $subComponent->name;
2025
+            if (!isset($data[$name])) {
2026
+                $data[$name] = [];
2027
+            }
2028
+            $data[$name][] = $this->transformSearchData($subComponent);
2029
+        }
2030
+
2031
+        foreach ($properties as $property) {
2032
+            $name = $property->name;
2033
+            if (!isset($validationRules[$name])) {
2034
+                $validationRules[$name] = '*';
2035
+            }
2036
+
2037
+            $rule = $validationRules[$property->name];
2038
+            if ($rule === '+' || $rule === '*') { // multiple
2039
+                if (!isset($data[$name])) {
2040
+                    $data[$name] = [];
2041
+                }
2042
+
2043
+                $data[$name][] = $this->transformSearchProperty($property);
2044
+            } else { // once
2045
+                $data[$name] = $this->transformSearchProperty($property);
2046
+            }
2047
+        }
2048
+
2049
+        return $data;
2050
+    }
2051
+
2052
+    /**
2053
+     * @param Property $prop
2054
+     * @return array
2055
+     */
2056
+    private function transformSearchProperty(Property $prop) {
2057
+        // No need to check Date, as it extends DateTime
2058
+        if ($prop instanceof Property\ICalendar\DateTime) {
2059
+            $value = $prop->getDateTime();
2060
+        } else {
2061
+            $value = $prop->getValue();
2062
+        }
2063
+
2064
+        return [
2065
+            $value,
2066
+            $prop->parameters()
2067
+        ];
2068
+    }
2069
+
2070
+    /**
2071
+     * @param string $principalUri
2072
+     * @param string $pattern
2073
+     * @param array $componentTypes
2074
+     * @param array $searchProperties
2075
+     * @param array $searchParameters
2076
+     * @param array $options
2077
+     * @return array
2078
+     */
2079
+    public function searchPrincipalUri(string $principalUri,
2080
+                                        string $pattern,
2081
+                                        array $componentTypes,
2082
+                                        array $searchProperties,
2083
+                                        array $searchParameters,
2084
+                                        array $options = []): array {
2085
+        $escapePattern = !\array_key_exists('escape_like_param', $options) || $options['escape_like_param'] !== false;
2086
+
2087
+        $calendarObjectIdQuery = $this->db->getQueryBuilder();
2088
+        $calendarOr = $calendarObjectIdQuery->expr()->orX();
2089
+        $searchOr = $calendarObjectIdQuery->expr()->orX();
2090
+
2091
+        // Fetch calendars and subscription
2092
+        $calendars = $this->getCalendarsForUser($principalUri);
2093
+        $subscriptions = $this->getSubscriptionsForUser($principalUri);
2094
+        foreach ($calendars as $calendar) {
2095
+            $calendarAnd = $calendarObjectIdQuery->expr()->andX();
2096
+            $calendarAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int)$calendar['id'])));
2097
+            $calendarAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendartype', $calendarObjectIdQuery->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
2098
+
2099
+            // If it's shared, limit search to public events
2100
+            if (isset($calendar['{http://owncloud.org/ns}owner-principal'])
2101
+                && $calendar['principaluri'] !== $calendar['{http://owncloud.org/ns}owner-principal']) {
2102
+                $calendarAnd->add($calendarObjectIdQuery->expr()->eq('co.classification', $calendarObjectIdQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
2103
+            }
2104
+
2105
+            $calendarOr->add($calendarAnd);
2106
+        }
2107
+        foreach ($subscriptions as $subscription) {
2108
+            $subscriptionAnd = $calendarObjectIdQuery->expr()->andX();
2109
+            $subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int)$subscription['id'])));
2110
+            $subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendartype', $calendarObjectIdQuery->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)));
2111
+
2112
+            // If it's shared, limit search to public events
2113
+            if (isset($subscription['{http://owncloud.org/ns}owner-principal'])
2114
+                && $subscription['principaluri'] !== $subscription['{http://owncloud.org/ns}owner-principal']) {
2115
+                $subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('co.classification', $calendarObjectIdQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
2116
+            }
2117
+
2118
+            $calendarOr->add($subscriptionAnd);
2119
+        }
2120
+
2121
+        foreach ($searchProperties as $property) {
2122
+            $propertyAnd = $calendarObjectIdQuery->expr()->andX();
2123
+            $propertyAnd->add($calendarObjectIdQuery->expr()->eq('cob.name', $calendarObjectIdQuery->createNamedParameter($property, IQueryBuilder::PARAM_STR)));
2124
+            $propertyAnd->add($calendarObjectIdQuery->expr()->isNull('cob.parameter'));
2125
+
2126
+            $searchOr->add($propertyAnd);
2127
+        }
2128
+        foreach ($searchParameters as $property => $parameter) {
2129
+            $parameterAnd = $calendarObjectIdQuery->expr()->andX();
2130
+            $parameterAnd->add($calendarObjectIdQuery->expr()->eq('cob.name', $calendarObjectIdQuery->createNamedParameter($property, IQueryBuilder::PARAM_STR)));
2131
+            $parameterAnd->add($calendarObjectIdQuery->expr()->eq('cob.parameter', $calendarObjectIdQuery->createNamedParameter($parameter, IQueryBuilder::PARAM_STR_ARRAY)));
2132
+
2133
+            $searchOr->add($parameterAnd);
2134
+        }
2135
+
2136
+        if ($calendarOr->count() === 0) {
2137
+            return [];
2138
+        }
2139
+        if ($searchOr->count() === 0) {
2140
+            return [];
2141
+        }
2142
+
2143
+        $calendarObjectIdQuery->selectDistinct('cob.objectid')
2144
+            ->from($this->dbObjectPropertiesTable, 'cob')
2145
+            ->leftJoin('cob', 'calendarobjects', 'co', $calendarObjectIdQuery->expr()->eq('co.id', 'cob.objectid'))
2146
+            ->andWhere($calendarObjectIdQuery->expr()->in('co.componenttype', $calendarObjectIdQuery->createNamedParameter($componentTypes, IQueryBuilder::PARAM_STR_ARRAY)))
2147
+            ->andWhere($calendarOr)
2148
+            ->andWhere($searchOr)
2149
+            ->andWhere($calendarObjectIdQuery->expr()->isNull('deleted_at'));
2150
+
2151
+        if ('' !== $pattern) {
2152
+            if (!$escapePattern) {
2153
+                $calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->ilike('cob.value', $calendarObjectIdQuery->createNamedParameter($pattern)));
2154
+            } else {
2155
+                $calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->ilike('cob.value', $calendarObjectIdQuery->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%')));
2156
+            }
2157
+        }
2158
+
2159
+        if (isset($options['limit'])) {
2160
+            $calendarObjectIdQuery->setMaxResults($options['limit']);
2161
+        }
2162
+        if (isset($options['offset'])) {
2163
+            $calendarObjectIdQuery->setFirstResult($options['offset']);
2164
+        }
2165
+
2166
+        $result = $calendarObjectIdQuery->executeQuery();
2167
+        $matches = $result->fetchAll();
2168
+        $result->closeCursor();
2169
+        $matches = array_map(static function (array $match):int {
2170
+            return (int) $match['objectid'];
2171
+        }, $matches);
2172
+
2173
+        $query = $this->db->getQueryBuilder();
2174
+        $query->select('calendardata', 'uri', 'calendarid', 'calendartype')
2175
+            ->from('calendarobjects')
2176
+            ->where($query->expr()->in('id', $query->createNamedParameter($matches, IQueryBuilder::PARAM_INT_ARRAY)));
2177
+
2178
+        $result = $query->executeQuery();
2179
+        $calendarObjects = $result->fetchAll();
2180
+        $result->closeCursor();
2181
+
2182
+        return array_map(function (array $array): array {
2183
+            $array['calendarid'] = (int)$array['calendarid'];
2184
+            $array['calendartype'] = (int)$array['calendartype'];
2185
+            $array['calendardata'] = $this->readBlob($array['calendardata']);
2186
+
2187
+            return $array;
2188
+        }, $calendarObjects);
2189
+    }
2190
+
2191
+    /**
2192
+     * Searches through all of a users calendars and calendar objects to find
2193
+     * an object with a specific UID.
2194
+     *
2195
+     * This method should return the path to this object, relative to the
2196
+     * calendar home, so this path usually only contains two parts:
2197
+     *
2198
+     * calendarpath/objectpath.ics
2199
+     *
2200
+     * If the uid is not found, return null.
2201
+     *
2202
+     * This method should only consider * objects that the principal owns, so
2203
+     * any calendars owned by other principals that also appear in this
2204
+     * collection should be ignored.
2205
+     *
2206
+     * @param string $principalUri
2207
+     * @param string $uid
2208
+     * @return string|null
2209
+     */
2210
+    public function getCalendarObjectByUID($principalUri, $uid) {
2211
+        $query = $this->db->getQueryBuilder();
2212
+        $query->selectAlias('c.uri', 'calendaruri')->selectAlias('co.uri', 'objecturi')
2213
+            ->from('calendarobjects', 'co')
2214
+            ->leftJoin('co', 'calendars', 'c', $query->expr()->eq('co.calendarid', 'c.id'))
2215
+            ->where($query->expr()->eq('c.principaluri', $query->createNamedParameter($principalUri)))
2216
+            ->andWhere($query->expr()->eq('co.uid', $query->createNamedParameter($uid)))
2217
+            ->andWhere($query->expr()->isNull('co.deleted_at'));
2218
+        $stmt = $query->executeQuery();
2219
+        $row = $stmt->fetch();
2220
+        $stmt->closeCursor();
2221
+        if ($row) {
2222
+            return $row['calendaruri'] . '/' . $row['objecturi'];
2223
+        }
2224
+
2225
+        return null;
2226
+    }
2227
+
2228
+    public function getCalendarObjectById(string $principalUri, int $id): ?array {
2229
+        $query = $this->db->getQueryBuilder();
2230
+        $query->select(['co.id', 'co.uri', 'co.lastmodified', 'co.etag', 'co.calendarid', 'co.size', 'co.calendardata', 'co.componenttype', 'co.classification', 'co.deleted_at'])
2231
+            ->selectAlias('c.uri', 'calendaruri')
2232
+            ->from('calendarobjects', 'co')
2233
+            ->join('co', 'calendars', 'c', $query->expr()->eq('c.id', 'co.calendarid', IQueryBuilder::PARAM_INT))
2234
+            ->where($query->expr()->eq('c.principaluri', $query->createNamedParameter($principalUri)))
2235
+            ->andWhere($query->expr()->eq('co.id', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT));
2236
+        $stmt = $query->executeQuery();
2237
+        $row = $stmt->fetch();
2238
+        $stmt->closeCursor();
2239
+
2240
+        if (!$row) {
2241
+            return null;
2242
+        }
2243
+
2244
+        return [
2245
+            'id' => $row['id'],
2246
+            'uri' => $row['uri'],
2247
+            'lastmodified' => $row['lastmodified'],
2248
+            'etag' => '"' . $row['etag'] . '"',
2249
+            'calendarid' => $row['calendarid'],
2250
+            'calendaruri' => $row['calendaruri'],
2251
+            'size' => (int)$row['size'],
2252
+            'calendardata' => $this->readBlob($row['calendardata']),
2253
+            'component' => strtolower($row['componenttype']),
2254
+            'classification' => (int)$row['classification'],
2255
+            'deleted_at' => isset($row['deleted_at']) ? ((int) $row['deleted_at']) : null,
2256
+        ];
2257
+    }
2258
+
2259
+    /**
2260
+     * The getChanges method returns all the changes that have happened, since
2261
+     * the specified syncToken in the specified calendar.
2262
+     *
2263
+     * This function should return an array, such as the following:
2264
+     *
2265
+     * [
2266
+     *   'syncToken' => 'The current synctoken',
2267
+     *   'added'   => [
2268
+     *      'new.txt',
2269
+     *   ],
2270
+     *   'modified'   => [
2271
+     *      'modified.txt',
2272
+     *   ],
2273
+     *   'deleted' => [
2274
+     *      'foo.php.bak',
2275
+     *      'old.txt'
2276
+     *   ]
2277
+     * );
2278
+     *
2279
+     * The returned syncToken property should reflect the *current* syncToken
2280
+     * of the calendar, as reported in the {http://sabredav.org/ns}sync-token
2281
+     * property This is * needed here too, to ensure the operation is atomic.
2282
+     *
2283
+     * If the $syncToken argument is specified as null, this is an initial
2284
+     * sync, and all members should be reported.
2285
+     *
2286
+     * The modified property is an array of nodenames that have changed since
2287
+     * the last token.
2288
+     *
2289
+     * The deleted property is an array with nodenames, that have been deleted
2290
+     * from collection.
2291
+     *
2292
+     * The $syncLevel argument is basically the 'depth' of the report. If it's
2293
+     * 1, you only have to report changes that happened only directly in
2294
+     * immediate descendants. If it's 2, it should also include changes from
2295
+     * the nodes below the child collections. (grandchildren)
2296
+     *
2297
+     * The $limit argument allows a client to specify how many results should
2298
+     * be returned at most. If the limit is not specified, it should be treated
2299
+     * as infinite.
2300
+     *
2301
+     * If the limit (infinite or not) is higher than you're willing to return,
2302
+     * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
2303
+     *
2304
+     * If the syncToken is expired (due to data cleanup) or unknown, you must
2305
+     * return null.
2306
+     *
2307
+     * The limit is 'suggestive'. You are free to ignore it.
2308
+     *
2309
+     * @param string $calendarId
2310
+     * @param string $syncToken
2311
+     * @param int $syncLevel
2312
+     * @param int|null $limit
2313
+     * @param int $calendarType
2314
+     * @return array
2315
+     */
2316
+    public function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit = null, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
2317
+        // Current synctoken
2318
+        $qb = $this->db->getQueryBuilder();
2319
+        $qb->select('synctoken')
2320
+            ->from('calendars')
2321
+            ->where(
2322
+                $qb->expr()->eq('id', $qb->createNamedParameter($calendarId))
2323
+            );
2324
+        $stmt = $qb->executeQuery();
2325
+        $currentToken = $stmt->fetchOne();
2326
+
2327
+        if ($currentToken === false) {
2328
+            return null;
2329
+        }
2330
+
2331
+        $result = [
2332
+            'syncToken' => $currentToken,
2333
+            'added' => [],
2334
+            'modified' => [],
2335
+            'deleted' => [],
2336
+        ];
2337
+
2338
+        if ($syncToken) {
2339
+            $qb = $this->db->getQueryBuilder();
2340
+
2341
+            $qb->select('uri', 'operation')
2342
+                ->from('calendarchanges')
2343
+                ->where(
2344
+                    $qb->expr()->andX(
2345
+                        $qb->expr()->gte('synctoken', $qb->createNamedParameter($syncToken)),
2346
+                        $qb->expr()->lt('synctoken', $qb->createNamedParameter($currentToken)),
2347
+                        $qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)),
2348
+                        $qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType))
2349
+                    )
2350
+                )->orderBy('synctoken');
2351
+            if (is_int($limit) && $limit > 0) {
2352
+                $qb->setMaxResults($limit);
2353
+            }
2354
+
2355
+            // Fetching all changes
2356
+            $stmt = $qb->executeQuery();
2357
+            $changes = [];
2358
+
2359
+            // This loop ensures that any duplicates are overwritten, only the
2360
+            // last change on a node is relevant.
2361
+            while ($row = $stmt->fetch()) {
2362
+                $changes[$row['uri']] = $row['operation'];
2363
+            }
2364
+            $stmt->closeCursor();
2365
+
2366
+            foreach ($changes as $uri => $operation) {
2367
+                switch ($operation) {
2368
+                    case 1:
2369
+                        $result['added'][] = $uri;
2370
+                        break;
2371
+                    case 2:
2372
+                        $result['modified'][] = $uri;
2373
+                        break;
2374
+                    case 3:
2375
+                        $result['deleted'][] = $uri;
2376
+                        break;
2377
+                }
2378
+            }
2379
+        } else {
2380
+            // No synctoken supplied, this is the initial sync.
2381
+            $qb = $this->db->getQueryBuilder();
2382
+            $qb->select('uri')
2383
+                ->from('calendarobjects')
2384
+                ->where(
2385
+                    $qb->expr()->andX(
2386
+                        $qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)),
2387
+                        $qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType))
2388
+                    )
2389
+                );
2390
+            $stmt = $qb->executeQuery();
2391
+            $result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
2392
+            $stmt->closeCursor();
2393
+        }
2394
+        return $result;
2395
+    }
2396
+
2397
+    /**
2398
+     * Returns a list of subscriptions for a principal.
2399
+     *
2400
+     * Every subscription is an array with the following keys:
2401
+     *  * id, a unique id that will be used by other functions to modify the
2402
+     *    subscription. This can be the same as the uri or a database key.
2403
+     *  * uri. This is just the 'base uri' or 'filename' of the subscription.
2404
+     *  * principaluri. The owner of the subscription. Almost always the same as
2405
+     *    principalUri passed to this method.
2406
+     *
2407
+     * Furthermore, all the subscription info must be returned too:
2408
+     *
2409
+     * 1. {DAV:}displayname
2410
+     * 2. {http://apple.com/ns/ical/}refreshrate
2411
+     * 3. {http://calendarserver.org/ns/}subscribed-strip-todos (omit if todos
2412
+     *    should not be stripped).
2413
+     * 4. {http://calendarserver.org/ns/}subscribed-strip-alarms (omit if alarms
2414
+     *    should not be stripped).
2415
+     * 5. {http://calendarserver.org/ns/}subscribed-strip-attachments (omit if
2416
+     *    attachments should not be stripped).
2417
+     * 6. {http://calendarserver.org/ns/}source (Must be a
2418
+     *     Sabre\DAV\Property\Href).
2419
+     * 7. {http://apple.com/ns/ical/}calendar-color
2420
+     * 8. {http://apple.com/ns/ical/}calendar-order
2421
+     * 9. {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
2422
+     *    (should just be an instance of
2423
+     *    Sabre\CalDAV\Property\SupportedCalendarComponentSet, with a bunch of
2424
+     *    default components).
2425
+     *
2426
+     * @param string $principalUri
2427
+     * @return array
2428
+     */
2429
+    public function getSubscriptionsForUser($principalUri) {
2430
+        $fields = array_column($this->subscriptionPropertyMap, 0);
2431
+        $fields[] = 'id';
2432
+        $fields[] = 'uri';
2433
+        $fields[] = 'source';
2434
+        $fields[] = 'principaluri';
2435
+        $fields[] = 'lastmodified';
2436
+        $fields[] = 'synctoken';
2437
+
2438
+        $query = $this->db->getQueryBuilder();
2439
+        $query->select($fields)
2440
+            ->from('calendarsubscriptions')
2441
+            ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
2442
+            ->orderBy('calendarorder', 'asc');
2443
+        $stmt = $query->executeQuery();
2444
+
2445
+        $subscriptions = [];
2446
+        while ($row = $stmt->fetch()) {
2447
+            $subscription = [
2448
+                'id' => $row['id'],
2449
+                'uri' => $row['uri'],
2450
+                'principaluri' => $row['principaluri'],
2451
+                'source' => $row['source'],
2452
+                'lastmodified' => $row['lastmodified'],
2453
+
2454
+                '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
2455
+                '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
2456
+            ];
2457
+
2458
+            $subscriptions[] = $this->rowToSubscription($row, $subscription);
2459
+        }
2460
+
2461
+        return $subscriptions;
2462
+    }
2463
+
2464
+    /**
2465
+     * Creates a new subscription for a principal.
2466
+     *
2467
+     * If the creation was a success, an id must be returned that can be used to reference
2468
+     * this subscription in other methods, such as updateSubscription.
2469
+     *
2470
+     * @param string $principalUri
2471
+     * @param string $uri
2472
+     * @param array $properties
2473
+     * @return mixed
2474
+     */
2475
+    public function createSubscription($principalUri, $uri, array $properties) {
2476
+        if (!isset($properties['{http://calendarserver.org/ns/}source'])) {
2477
+            throw new Forbidden('The {http://calendarserver.org/ns/}source property is required when creating subscriptions');
2478
+        }
2479
+
2480
+        $values = [
2481
+            'principaluri' => $principalUri,
2482
+            'uri' => $uri,
2483
+            'source' => $properties['{http://calendarserver.org/ns/}source']->getHref(),
2484
+            'lastmodified' => time(),
2485
+        ];
2486
+
2487
+        $propertiesBoolean = ['striptodos', 'stripalarms', 'stripattachments'];
2488
+
2489
+        foreach ($this->subscriptionPropertyMap as $xmlName => [$dbName, $type]) {
2490
+            if (array_key_exists($xmlName, $properties)) {
2491
+                $values[$dbName] = $properties[$xmlName];
2492
+                if (in_array($dbName, $propertiesBoolean)) {
2493
+                    $values[$dbName] = true;
2494
+                }
2495
+            }
2496
+        }
2497
+
2498
+        $valuesToInsert = [];
2499
+
2500
+        $query = $this->db->getQueryBuilder();
2501
+
2502
+        foreach (array_keys($values) as $name) {
2503
+            $valuesToInsert[$name] = $query->createNamedParameter($values[$name]);
2504
+        }
2505
+
2506
+        $query->insert('calendarsubscriptions')
2507
+            ->values($valuesToInsert)
2508
+            ->executeStatement();
2509
+
2510
+        $subscriptionId = $query->getLastInsertId();
2511
+
2512
+        $subscriptionRow = $this->getSubscriptionById($subscriptionId);
2513
+        $this->dispatcher->dispatchTyped(new SubscriptionCreatedEvent($subscriptionId, $subscriptionRow));
2514
+        $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createSubscription', new GenericEvent(
2515
+            '\OCA\DAV\CalDAV\CalDavBackend::createSubscription',
2516
+            [
2517
+                'subscriptionId' => $subscriptionId,
2518
+                'subscriptionData' => $subscriptionRow,
2519
+            ]));
2520
+
2521
+        return $subscriptionId;
2522
+    }
2523
+
2524
+    /**
2525
+     * Updates a subscription
2526
+     *
2527
+     * The list of mutations is stored in a Sabre\DAV\PropPatch object.
2528
+     * To do the actual updates, you must tell this object which properties
2529
+     * you're going to process with the handle() method.
2530
+     *
2531
+     * Calling the handle method is like telling the PropPatch object "I
2532
+     * promise I can handle updating this property".
2533
+     *
2534
+     * Read the PropPatch documentation for more info and examples.
2535
+     *
2536
+     * @param mixed $subscriptionId
2537
+     * @param PropPatch $propPatch
2538
+     * @return void
2539
+     */
2540
+    public function updateSubscription($subscriptionId, PropPatch $propPatch) {
2541
+        $supportedProperties = array_keys($this->subscriptionPropertyMap);
2542
+        $supportedProperties[] = '{http://calendarserver.org/ns/}source';
2543
+
2544
+        $propPatch->handle($supportedProperties, function ($mutations) use ($subscriptionId) {
2545
+            $newValues = [];
2546
+
2547
+            foreach ($mutations as $propertyName => $propertyValue) {
2548
+                if ($propertyName === '{http://calendarserver.org/ns/}source') {
2549
+                    $newValues['source'] = $propertyValue->getHref();
2550
+                } else {
2551
+                    $fieldName = $this->subscriptionPropertyMap[$propertyName][0];
2552
+                    $newValues[$fieldName] = $propertyValue;
2553
+                }
2554
+            }
2555
+
2556
+            $query = $this->db->getQueryBuilder();
2557
+            $query->update('calendarsubscriptions')
2558
+                ->set('lastmodified', $query->createNamedParameter(time()));
2559
+            foreach ($newValues as $fieldName => $value) {
2560
+                $query->set($fieldName, $query->createNamedParameter($value));
2561
+            }
2562
+            $query->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
2563
+                ->executeStatement();
2564
+
2565
+            $subscriptionRow = $this->getSubscriptionById($subscriptionId);
2566
+            $this->dispatcher->dispatchTyped(new SubscriptionUpdatedEvent((int)$subscriptionId, $subscriptionRow, [], $mutations));
2567
+            $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateSubscription', new GenericEvent(
2568
+                '\OCA\DAV\CalDAV\CalDavBackend::updateSubscription',
2569
+                [
2570
+                    'subscriptionId' => $subscriptionId,
2571
+                    'subscriptionData' => $subscriptionRow,
2572
+                    'propertyMutations' => $mutations,
2573
+                ]));
2574
+
2575
+            return true;
2576
+        });
2577
+    }
2578
+
2579
+    /**
2580
+     * Deletes a subscription.
2581
+     *
2582
+     * @param mixed $subscriptionId
2583
+     * @return void
2584
+     */
2585
+    public function deleteSubscription($subscriptionId) {
2586
+        $subscriptionRow = $this->getSubscriptionById($subscriptionId);
2587
+
2588
+        $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteSubscription', new GenericEvent(
2589
+            '\OCA\DAV\CalDAV\CalDavBackend::deleteSubscription',
2590
+            [
2591
+                'subscriptionId' => $subscriptionId,
2592
+                'subscriptionData' => $this->getSubscriptionById($subscriptionId),
2593
+            ]));
2594
+
2595
+        $query = $this->db->getQueryBuilder();
2596
+        $query->delete('calendarsubscriptions')
2597
+            ->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
2598
+            ->executeStatement();
2599
+
2600
+        $query = $this->db->getQueryBuilder();
2601
+        $query->delete('calendarobjects')
2602
+            ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2603
+            ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2604
+            ->executeStatement();
2605
+
2606
+        $query->delete('calendarchanges')
2607
+            ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2608
+            ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2609
+            ->executeStatement();
2610
+
2611
+        $query->delete($this->dbObjectPropertiesTable)
2612
+            ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2613
+            ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2614
+            ->executeStatement();
2615
+
2616
+        if ($subscriptionRow) {
2617
+            $this->dispatcher->dispatchTyped(new SubscriptionDeletedEvent((int)$subscriptionId, $subscriptionRow, []));
2618
+        }
2619
+    }
2620
+
2621
+    /**
2622
+     * Returns a single scheduling object for the inbox collection.
2623
+     *
2624
+     * The returned array should contain the following elements:
2625
+     *   * uri - A unique basename for the object. This will be used to
2626
+     *           construct a full uri.
2627
+     *   * calendardata - The iCalendar object
2628
+     *   * lastmodified - The last modification date. Can be an int for a unix
2629
+     *                    timestamp, or a PHP DateTime object.
2630
+     *   * etag - A unique token that must change if the object changed.
2631
+     *   * size - The size of the object, in bytes.
2632
+     *
2633
+     * @param string $principalUri
2634
+     * @param string $objectUri
2635
+     * @return array
2636
+     */
2637
+    public function getSchedulingObject($principalUri, $objectUri) {
2638
+        $query = $this->db->getQueryBuilder();
2639
+        $stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size'])
2640
+            ->from('schedulingobjects')
2641
+            ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
2642
+            ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
2643
+            ->executeQuery();
2644
+
2645
+        $row = $stmt->fetch();
2646
+
2647
+        if (!$row) {
2648
+            return null;
2649
+        }
2650
+
2651
+        return [
2652
+            'uri' => $row['uri'],
2653
+            'calendardata' => $row['calendardata'],
2654
+            'lastmodified' => $row['lastmodified'],
2655
+            'etag' => '"' . $row['etag'] . '"',
2656
+            'size' => (int)$row['size'],
2657
+        ];
2658
+    }
2659
+
2660
+    /**
2661
+     * Returns all scheduling objects for the inbox collection.
2662
+     *
2663
+     * These objects should be returned as an array. Every item in the array
2664
+     * should follow the same structure as returned from getSchedulingObject.
2665
+     *
2666
+     * The main difference is that 'calendardata' is optional.
2667
+     *
2668
+     * @param string $principalUri
2669
+     * @return array
2670
+     */
2671
+    public function getSchedulingObjects($principalUri) {
2672
+        $query = $this->db->getQueryBuilder();
2673
+        $stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size'])
2674
+                ->from('schedulingobjects')
2675
+                ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
2676
+                ->executeQuery();
2677
+
2678
+        $result = [];
2679
+        foreach ($stmt->fetchAll() as $row) {
2680
+            $result[] = [
2681
+                'calendardata' => $row['calendardata'],
2682
+                'uri' => $row['uri'],
2683
+                'lastmodified' => $row['lastmodified'],
2684
+                'etag' => '"' . $row['etag'] . '"',
2685
+                'size' => (int)$row['size'],
2686
+            ];
2687
+        }
2688
+        $stmt->closeCursor();
2689
+
2690
+        return $result;
2691
+    }
2692
+
2693
+    /**
2694
+     * Deletes a scheduling object from the inbox collection.
2695
+     *
2696
+     * @param string $principalUri
2697
+     * @param string $objectUri
2698
+     * @return void
2699
+     */
2700
+    public function deleteSchedulingObject($principalUri, $objectUri) {
2701
+        $query = $this->db->getQueryBuilder();
2702
+        $query->delete('schedulingobjects')
2703
+                ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
2704
+                ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
2705
+                ->executeStatement();
2706
+    }
2707
+
2708
+    /**
2709
+     * Creates a new scheduling object. This should land in a users' inbox.
2710
+     *
2711
+     * @param string $principalUri
2712
+     * @param string $objectUri
2713
+     * @param string $objectData
2714
+     * @return void
2715
+     */
2716
+    public function createSchedulingObject($principalUri, $objectUri, $objectData) {
2717
+        $query = $this->db->getQueryBuilder();
2718
+        $query->insert('schedulingobjects')
2719
+            ->values([
2720
+                'principaluri' => $query->createNamedParameter($principalUri),
2721
+                'calendardata' => $query->createNamedParameter($objectData, IQueryBuilder::PARAM_LOB),
2722
+                'uri' => $query->createNamedParameter($objectUri),
2723
+                'lastmodified' => $query->createNamedParameter(time()),
2724
+                'etag' => $query->createNamedParameter(md5($objectData)),
2725
+                'size' => $query->createNamedParameter(strlen($objectData))
2726
+            ])
2727
+            ->executeStatement();
2728
+    }
2729
+
2730
+    /**
2731
+     * Adds a change record to the calendarchanges table.
2732
+     *
2733
+     * @param mixed $calendarId
2734
+     * @param string $objectUri
2735
+     * @param int $operation 1 = add, 2 = modify, 3 = delete.
2736
+     * @param int $calendarType
2737
+     * @return void
2738
+     */
2739
+    protected function addChange($calendarId, $objectUri, $operation, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
2740
+        $table = $calendarType === self::CALENDAR_TYPE_CALENDAR ? 'calendars': 'calendarsubscriptions';
2741
+
2742
+        $query = $this->db->getQueryBuilder();
2743
+        $query->select('synctoken')
2744
+            ->from($table)
2745
+            ->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)));
2746
+        $result = $query->executeQuery();
2747
+        $syncToken = (int)$result->fetchOne();
2748
+        $result->closeCursor();
2749
+
2750
+        $query = $this->db->getQueryBuilder();
2751
+        $query->insert('calendarchanges')
2752
+            ->values([
2753
+                'uri' => $query->createNamedParameter($objectUri),
2754
+                'synctoken' => $query->createNamedParameter($syncToken),
2755
+                'calendarid' => $query->createNamedParameter($calendarId),
2756
+                'operation' => $query->createNamedParameter($operation),
2757
+                'calendartype' => $query->createNamedParameter($calendarType),
2758
+            ])
2759
+            ->executeStatement();
2760
+
2761
+        $stmt = $this->db->prepare("UPDATE `*PREFIX*$table` SET `synctoken` = `synctoken` + 1 WHERE `id` = ?");
2762
+        $stmt->execute([
2763
+            $calendarId
2764
+        ]);
2765
+    }
2766
+
2767
+    /**
2768
+     * Parses some information from calendar objects, used for optimized
2769
+     * calendar-queries.
2770
+     *
2771
+     * Returns an array with the following keys:
2772
+     *   * etag - An md5 checksum of the object without the quotes.
2773
+     *   * size - Size of the object in bytes
2774
+     *   * componentType - VEVENT, VTODO or VJOURNAL
2775
+     *   * firstOccurence
2776
+     *   * lastOccurence
2777
+     *   * uid - value of the UID property
2778
+     *
2779
+     * @param string $calendarData
2780
+     * @return array
2781
+     */
2782
+    public function getDenormalizedData($calendarData) {
2783
+        $vObject = Reader::read($calendarData);
2784
+        $vEvents = [];
2785
+        $componentType = null;
2786
+        $component = null;
2787
+        $firstOccurrence = null;
2788
+        $lastOccurrence = null;
2789
+        $uid = null;
2790
+        $classification = self::CLASSIFICATION_PUBLIC;
2791
+        $hasDTSTART = false;
2792
+        foreach ($vObject->getComponents() as $component) {
2793
+            if ($component->name !== 'VTIMEZONE') {
2794
+                // Finding all VEVENTs, and track them
2795
+                if ($component->name === 'VEVENT') {
2796
+                    array_push($vEvents, $component);
2797
+                    if ($component->DTSTART) {
2798
+                        $hasDTSTART = true;
2799
+                    }
2800
+                }
2801
+                // Track first component type and uid
2802
+                if ($uid === null) {
2803
+                    $componentType = $component->name;
2804
+                    $uid = (string)$component->UID;
2805
+                }
2806
+            }
2807
+        }
2808
+        if (!$componentType) {
2809
+            throw new BadRequest('Calendar objects must have a VJOURNAL, VEVENT or VTODO component');
2810
+        }
2811
+
2812
+        if ($hasDTSTART) {
2813
+            $component = $vEvents[0];
2814
+
2815
+            // Finding the last occurrence is a bit harder
2816
+            if (!isset($component->RRULE) && count($vEvents) === 1) {
2817
+                $firstOccurrence = $component->DTSTART->getDateTime()->getTimeStamp();
2818
+                if (isset($component->DTEND)) {
2819
+                    $lastOccurrence = $component->DTEND->getDateTime()->getTimeStamp();
2820
+                } elseif (isset($component->DURATION)) {
2821
+                    $endDate = clone $component->DTSTART->getDateTime();
2822
+                    $endDate->add(DateTimeParser::parse($component->DURATION->getValue()));
2823
+                    $lastOccurrence = $endDate->getTimeStamp();
2824
+                } elseif (!$component->DTSTART->hasTime()) {
2825
+                    $endDate = clone $component->DTSTART->getDateTime();
2826
+                    $endDate->modify('+1 day');
2827
+                    $lastOccurrence = $endDate->getTimeStamp();
2828
+                } else {
2829
+                    $lastOccurrence = $firstOccurrence;
2830
+                }
2831
+            } else {
2832
+                $it = new EventIterator($vEvents);
2833
+                $maxDate = new DateTime(self::MAX_DATE);
2834
+                $firstOccurrence = $it->getDtStart()->getTimestamp();
2835
+                if ($it->isInfinite()) {
2836
+                    $lastOccurrence = $maxDate->getTimestamp();
2837
+                } else {
2838
+                    $end = $it->getDtEnd();
2839
+                    while ($it->valid() && $end < $maxDate) {
2840
+                        $end = $it->getDtEnd();
2841
+                        $it->next();
2842
+                    }
2843
+                    $lastOccurrence = $end->getTimestamp();
2844
+                }
2845
+            }
2846
+        }
2847
+
2848
+        if ($component->CLASS) {
2849
+            $classification = CalDavBackend::CLASSIFICATION_PRIVATE;
2850
+            switch ($component->CLASS->getValue()) {
2851
+                case 'PUBLIC':
2852
+                    $classification = CalDavBackend::CLASSIFICATION_PUBLIC;
2853
+                    break;
2854
+                case 'CONFIDENTIAL':
2855
+                    $classification = CalDavBackend::CLASSIFICATION_CONFIDENTIAL;
2856
+                    break;
2857
+            }
2858
+        }
2859
+        return [
2860
+            'etag' => md5($calendarData),
2861
+            'size' => strlen($calendarData),
2862
+            'componentType' => $componentType,
2863
+            'firstOccurence' => is_null($firstOccurrence) ? null : max(0, $firstOccurrence),
2864
+            'lastOccurence' => $lastOccurrence,
2865
+            'uid' => $uid,
2866
+            'classification' => $classification
2867
+        ];
2868
+    }
2869
+
2870
+    /**
2871
+     * @param $cardData
2872
+     * @return bool|string
2873
+     */
2874
+    private function readBlob($cardData) {
2875
+        if (is_resource($cardData)) {
2876
+            return stream_get_contents($cardData);
2877
+        }
2878
+
2879
+        return $cardData;
2880
+    }
2881
+
2882
+    /**
2883
+     * @param IShareable $shareable
2884
+     * @param array $add
2885
+     * @param array $remove
2886
+     */
2887
+    public function updateShares($shareable, $add, $remove) {
2888
+        $calendarId = $shareable->getResourceId();
2889
+        $calendarRow = $this->getCalendarById($calendarId);
2890
+        $oldShares = $this->getShares($calendarId);
2891
+
2892
+        $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateShares', new GenericEvent(
2893
+            '\OCA\DAV\CalDAV\CalDavBackend::updateShares',
2894
+            [
2895
+                'calendarId' => $calendarId,
2896
+                'calendarData' => $calendarRow,
2897
+                'shares' => $oldShares,
2898
+                'add' => $add,
2899
+                'remove' => $remove,
2900
+            ]));
2901
+        $this->calendarSharingBackend->updateShares($shareable, $add, $remove);
2902
+
2903
+        $this->dispatcher->dispatchTyped(new CalendarShareUpdatedEvent((int)$calendarId, $calendarRow, $oldShares, $add, $remove));
2904
+    }
2905
+
2906
+    /**
2907
+     * @param int $resourceId
2908
+     * @return array
2909
+     */
2910
+    public function getShares($resourceId) {
2911
+        return $this->calendarSharingBackend->getShares($resourceId);
2912
+    }
2913
+
2914
+    /**
2915
+     * @param boolean $value
2916
+     * @param \OCA\DAV\CalDAV\Calendar $calendar
2917
+     * @return string|null
2918
+     */
2919
+    public function setPublishStatus($value, $calendar) {
2920
+        $calendarId = $calendar->getResourceId();
2921
+        $calendarData = $this->getCalendarById($calendarId);
2922
+        $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::publishCalendar', new GenericEvent(
2923
+            '\OCA\DAV\CalDAV\CalDavBackend::updateShares',
2924
+            [
2925
+                'calendarId' => $calendarId,
2926
+                'calendarData' => $calendarData,
2927
+                'public' => $value,
2928
+            ]));
2929
+
2930
+        $query = $this->db->getQueryBuilder();
2931
+        if ($value) {
2932
+            $publicUri = $this->random->generate(16, ISecureRandom::CHAR_HUMAN_READABLE);
2933
+            $query->insert('dav_shares')
2934
+                ->values([
2935
+                    'principaluri' => $query->createNamedParameter($calendar->getPrincipalURI()),
2936
+                    'type' => $query->createNamedParameter('calendar'),
2937
+                    'access' => $query->createNamedParameter(self::ACCESS_PUBLIC),
2938
+                    'resourceid' => $query->createNamedParameter($calendar->getResourceId()),
2939
+                    'publicuri' => $query->createNamedParameter($publicUri)
2940
+                ]);
2941
+            $query->executeStatement();
2942
+
2943
+            $this->dispatcher->dispatchTyped(new CalendarPublishedEvent((int)$calendarId, $calendarData, $publicUri));
2944
+            return $publicUri;
2945
+        }
2946
+        $query->delete('dav_shares')
2947
+            ->where($query->expr()->eq('resourceid', $query->createNamedParameter($calendar->getResourceId())))
2948
+            ->andWhere($query->expr()->eq('access', $query->createNamedParameter(self::ACCESS_PUBLIC)));
2949
+        $query->executeStatement();
2950
+
2951
+        $this->dispatcher->dispatchTyped(new CalendarUnpublishedEvent((int)$calendarId, $calendarData));
2952
+        return null;
2953
+    }
2954
+
2955
+    /**
2956
+     * @param \OCA\DAV\CalDAV\Calendar $calendar
2957
+     * @return mixed
2958
+     */
2959
+    public function getPublishStatus($calendar) {
2960
+        $query = $this->db->getQueryBuilder();
2961
+        $result = $query->select('publicuri')
2962
+            ->from('dav_shares')
2963
+            ->where($query->expr()->eq('resourceid', $query->createNamedParameter($calendar->getResourceId())))
2964
+            ->andWhere($query->expr()->eq('access', $query->createNamedParameter(self::ACCESS_PUBLIC)))
2965
+            ->executeQuery();
2966
+
2967
+        $row = $result->fetch();
2968
+        $result->closeCursor();
2969
+        return $row ? reset($row) : false;
2970
+    }
2971
+
2972
+    /**
2973
+     * @param int $resourceId
2974
+     * @param array $acl
2975
+     * @return array
2976
+     */
2977
+    public function applyShareAcl($resourceId, $acl) {
2978
+        return $this->calendarSharingBackend->applyShareAcl($resourceId, $acl);
2979
+    }
2980
+
2981
+
2982
+
2983
+    /**
2984
+     * update properties table
2985
+     *
2986
+     * @param int $calendarId
2987
+     * @param string $objectUri
2988
+     * @param string $calendarData
2989
+     * @param int $calendarType
2990
+     */
2991
+    public function updateProperties($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
2992
+        $objectId = $this->getCalendarObjectId($calendarId, $objectUri, $calendarType);
2993
+
2994
+        try {
2995
+            $vCalendar = $this->readCalendarData($calendarData);
2996
+        } catch (\Exception $ex) {
2997
+            return;
2998
+        }
2999
+
3000
+        $this->purgeProperties($calendarId, $objectId);
3001
+
3002
+        $query = $this->db->getQueryBuilder();
3003
+        $query->insert($this->dbObjectPropertiesTable)
3004
+            ->values(
3005
+                [
3006
+                    'calendarid' => $query->createNamedParameter($calendarId),
3007
+                    'calendartype' => $query->createNamedParameter($calendarType),
3008
+                    'objectid' => $query->createNamedParameter($objectId),
3009
+                    'name' => $query->createParameter('name'),
3010
+                    'parameter' => $query->createParameter('parameter'),
3011
+                    'value' => $query->createParameter('value'),
3012
+                ]
3013
+            );
3014
+
3015
+        $indexComponents = ['VEVENT', 'VJOURNAL', 'VTODO'];
3016
+        foreach ($vCalendar->getComponents() as $component) {
3017
+            if (!in_array($component->name, $indexComponents)) {
3018
+                continue;
3019
+            }
3020
+
3021
+            foreach ($component->children() as $property) {
3022
+                if (in_array($property->name, self::INDEXED_PROPERTIES, true)) {
3023
+                    $value = $property->getValue();
3024
+                    // is this a shitty db?
3025
+                    if (!$this->db->supports4ByteText()) {
3026
+                        $value = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $value);
3027
+                    }
3028
+                    $value = mb_strcut($value, 0, 254);
3029
+
3030
+                    $query->setParameter('name', $property->name);
3031
+                    $query->setParameter('parameter', null);
3032
+                    $query->setParameter('value', $value);
3033
+                    $query->executeStatement();
3034
+                }
3035
+
3036
+                if (array_key_exists($property->name, self::$indexParameters)) {
3037
+                    $parameters = $property->parameters();
3038
+                    $indexedParametersForProperty = self::$indexParameters[$property->name];
3039
+
3040
+                    foreach ($parameters as $key => $value) {
3041
+                        if (in_array($key, $indexedParametersForProperty)) {
3042
+                            // is this a shitty db?
3043
+                            if ($this->db->supports4ByteText()) {
3044
+                                $value = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $value);
3045
+                            }
3046
+
3047
+                            $query->setParameter('name', $property->name);
3048
+                            $query->setParameter('parameter', mb_strcut($key, 0, 254));
3049
+                            $query->setParameter('value', mb_strcut($value, 0, 254));
3050
+                            $query->executeStatement();
3051
+                        }
3052
+                    }
3053
+                }
3054
+            }
3055
+        }
3056
+    }
3057
+
3058
+    /**
3059
+     * deletes all birthday calendars
3060
+     */
3061
+    public function deleteAllBirthdayCalendars() {
3062
+        $query = $this->db->getQueryBuilder();
3063
+        $result = $query->select(['id'])->from('calendars')
3064
+            ->where($query->expr()->eq('uri', $query->createNamedParameter(BirthdayService::BIRTHDAY_CALENDAR_URI)))
3065
+            ->executeQuery();
3066
+
3067
+        $ids = $result->fetchAll();
3068
+        $result->closeCursor();
3069
+        foreach ($ids as $id) {
3070
+            $this->deleteCalendar(
3071
+                $id['id'],
3072
+                true // No data to keep in the trashbin, if the user re-enables then we regenerate
3073
+            );
3074
+        }
3075
+    }
3076
+
3077
+    /**
3078
+     * @param $subscriptionId
3079
+     */
3080
+    public function purgeAllCachedEventsForSubscription($subscriptionId) {
3081
+        $query = $this->db->getQueryBuilder();
3082
+        $query->select('uri')
3083
+            ->from('calendarobjects')
3084
+            ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
3085
+            ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)));
3086
+        $stmt = $query->executeQuery();
3087
+
3088
+        $uris = [];
3089
+        foreach ($stmt->fetchAll() as $row) {
3090
+            $uris[] = $row['uri'];
3091
+        }
3092
+        $stmt->closeCursor();
3093
+
3094
+        $query = $this->db->getQueryBuilder();
3095
+        $query->delete('calendarobjects')
3096
+            ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
3097
+            ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
3098
+            ->executeStatement();
3099
+
3100
+        $query->delete('calendarchanges')
3101
+            ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
3102
+            ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
3103
+            ->executeStatement();
3104
+
3105
+        $query->delete($this->dbObjectPropertiesTable)
3106
+            ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
3107
+            ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
3108
+            ->executeStatement();
3109
+
3110
+        foreach ($uris as $uri) {
3111
+            $this->addChange($subscriptionId, $uri, 3, self::CALENDAR_TYPE_SUBSCRIPTION);
3112
+        }
3113
+    }
3114
+
3115
+    /**
3116
+     * Move a calendar from one user to another
3117
+     *
3118
+     * @param string $uriName
3119
+     * @param string $uriOrigin
3120
+     * @param string $uriDestination
3121
+     * @param string $newUriName (optional) the new uriName
3122
+     */
3123
+    public function moveCalendar($uriName, $uriOrigin, $uriDestination, $newUriName = null) {
3124
+        $query = $this->db->getQueryBuilder();
3125
+        $query->update('calendars')
3126
+            ->set('principaluri', $query->createNamedParameter($uriDestination))
3127
+            ->set('uri', $query->createNamedParameter($newUriName ?: $uriName))
3128
+            ->where($query->expr()->eq('principaluri', $query->createNamedParameter($uriOrigin)))
3129
+            ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($uriName)))
3130
+            ->executeStatement();
3131
+    }
3132
+
3133
+    /**
3134
+     * read VCalendar data into a VCalendar object
3135
+     *
3136
+     * @param string $objectData
3137
+     * @return VCalendar
3138
+     */
3139
+    protected function readCalendarData($objectData) {
3140
+        return Reader::read($objectData);
3141
+    }
3142
+
3143
+    /**
3144
+     * delete all properties from a given calendar object
3145
+     *
3146
+     * @param int $calendarId
3147
+     * @param int $objectId
3148
+     */
3149
+    protected function purgeProperties($calendarId, $objectId) {
3150
+        $query = $this->db->getQueryBuilder();
3151
+        $query->delete($this->dbObjectPropertiesTable)
3152
+            ->where($query->expr()->eq('objectid', $query->createNamedParameter($objectId)))
3153
+            ->andWhere($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)));
3154
+        $query->executeStatement();
3155
+    }
3156
+
3157
+    /**
3158
+     * get ID from a given calendar object
3159
+     *
3160
+     * @param int $calendarId
3161
+     * @param string $uri
3162
+     * @param int $calendarType
3163
+     * @return int
3164
+     */
3165
+    protected function getCalendarObjectId($calendarId, $uri, $calendarType):int {
3166
+        $query = $this->db->getQueryBuilder();
3167
+        $query->select('id')
3168
+            ->from('calendarobjects')
3169
+            ->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
3170
+            ->andWhere($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
3171
+            ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)));
3172
+
3173
+        $result = $query->executeQuery();
3174
+        $objectIds = $result->fetch();
3175
+        $result->closeCursor();
3176
+
3177
+        if (!isset($objectIds['id'])) {
3178
+            throw new \InvalidArgumentException('Calendarobject does not exists: ' . $uri);
3179
+        }
3180
+
3181
+        return (int)$objectIds['id'];
3182
+    }
3183
+
3184
+    /**
3185
+     * return legacy endpoint principal name to new principal name
3186
+     *
3187
+     * @param $principalUri
3188
+     * @param $toV2
3189
+     * @return string
3190
+     */
3191
+    private function convertPrincipal($principalUri, $toV2) {
3192
+        if ($this->principalBackend->getPrincipalPrefix() === 'principals') {
3193
+            [, $name] = Uri\split($principalUri);
3194
+            if ($toV2 === true) {
3195
+                return "principals/users/$name";
3196
+            }
3197
+            return "principals/$name";
3198
+        }
3199
+        return $principalUri;
3200
+    }
3201
+
3202
+    /**
3203
+     * adds information about an owner to the calendar data
3204
+     *
3205
+     */
3206
+    private function addOwnerPrincipalToCalendar(array $calendarInfo): array {
3207
+        $ownerPrincipalKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal';
3208
+        $displaynameKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}owner-displayname';
3209
+        if (isset($calendarInfo[$ownerPrincipalKey])) {
3210
+            $uri = $calendarInfo[$ownerPrincipalKey];
3211
+        } else {
3212
+            $uri = $calendarInfo['principaluri'];
3213
+        }
3214
+
3215
+        $principalInformation = $this->principalBackend->getPrincipalByPath($uri);
3216
+        if (isset($principalInformation['{DAV:}displayname'])) {
3217
+            $calendarInfo[$displaynameKey] = $principalInformation['{DAV:}displayname'];
3218
+        }
3219
+        return $calendarInfo;
3220
+    }
3221
+
3222
+    private function addResourceTypeToCalendar(array $row, array $calendar): array {
3223
+        if (isset($row['deleted_at'])) {
3224
+            // Columns is set and not null -> this is a deleted calendar
3225
+            // we send a custom resourcetype to hide the deleted calendar
3226
+            // from ordinary DAV clients, but the Calendar app will know
3227
+            // how to handle this special resource.
3228
+            $calendar['{DAV:}resourcetype'] = new DAV\Xml\Property\ResourceType([
3229
+                '{DAV:}collection',
3230
+                sprintf('{%s}deleted-calendar', \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD),
3231
+            ]);
3232
+        }
3233
+        return $calendar;
3234
+    }
3235
+
3236
+    /**
3237
+     * Amend the calendar info with database row data
3238
+     *
3239
+     * @param array $row
3240
+     * @param array $calendar
3241
+     *
3242
+     * @return array
3243
+     */
3244
+    private function rowToCalendar($row, array $calendar): array {
3245
+        foreach ($this->propertyMap as $xmlName => [$dbName, $type]) {
3246
+            $value = $row[$dbName];
3247
+            if ($value !== null) {
3248
+                settype($value, $type);
3249
+            }
3250
+            $calendar[$xmlName] = $value;
3251
+        }
3252
+        return $calendar;
3253
+    }
3254
+
3255
+    /**
3256
+     * Amend the subscription info with database row data
3257
+     *
3258
+     * @param array $row
3259
+     * @param array $subscription
3260
+     *
3261
+     * @return array
3262
+     */
3263
+    private function rowToSubscription($row, array $subscription): array {
3264
+        foreach ($this->subscriptionPropertyMap as $xmlName => [$dbName, $type]) {
3265
+            $value = $row[$dbName];
3266
+            if ($value !== null) {
3267
+                settype($value, $type);
3268
+            }
3269
+            $subscription[$xmlName] = $value;
3270
+        }
3271
+        return $subscription;
3272
+    }
3273 3273
 }
Please login to merge, or discard this patch.
Spacing   +126 added lines, -126 removed lines patch added patch discarded remove patch
@@ -156,7 +156,7 @@  discard block
 block discarded – undo
156 156
 		'{urn:ietf:params:xml:ns:caldav}calendar-timezone' => ['timezone', 'string'],
157 157
 		'{http://apple.com/ns/ical/}calendar-order' => ['calendarorder', 'int'],
158 158
 		'{http://apple.com/ns/ical/}calendar-color' => ['calendarcolor', 'string'],
159
-		'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => ['deleted_at', 'int'],
159
+		'{'.\OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD.'}deleted-at' => ['deleted_at', 'int'],
160 160
 	];
161 161
 
162 162
 	/**
@@ -299,7 +299,7 @@  discard block
 block discarded – undo
299 299
 		}
300 300
 
301 301
 		$result = $query->executeQuery();
302
-		$column = (int)$result->fetchOne();
302
+		$column = (int) $result->fetchOne();
303 303
 		$result->closeCursor();
304 304
 		return $column;
305 305
 	}
@@ -316,7 +316,7 @@  discard block
 block discarded – undo
316 316
 		$result = $qb->executeQuery();
317 317
 		$raw = $result->fetchAll();
318 318
 		$result->closeCursor();
319
-		return array_map(function ($row) {
319
+		return array_map(function($row) {
320 320
 			return [
321 321
 				'id' => (int) $row['id'],
322 322
 				'deleted_at' => (int) $row['deleted_at'],
@@ -379,18 +379,18 @@  discard block
 block discarded – undo
379 379
 			$row['principaluri'] = (string) $row['principaluri'];
380 380
 			$components = [];
381 381
 			if ($row['components']) {
382
-				$components = explode(',',$row['components']);
382
+				$components = explode(',', $row['components']);
383 383
 			}
384 384
 
385 385
 			$calendar = [
386 386
 				'id' => $row['id'],
387 387
 				'uri' => $row['uri'],
388 388
 				'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
389
-				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
390
-				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
391
-				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
392
-				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
393
-				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($principalUri, !$this->legacyEndpoint),
389
+				'{'.Plugin::NS_CALENDARSERVER.'}getctag' => 'http://sabre.io/ns/sync/'.($row['synctoken'] ? $row['synctoken'] : '0'),
390
+				'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ? $row['synctoken'] : '0',
391
+				'{'.Plugin::NS_CALDAV.'}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
392
+				'{'.Plugin::NS_CALDAV.'}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent'] ? 'transparent' : 'opaque'),
393
+				'{'.\OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD.'}owner-principal' => $this->convertPrincipal($principalUri, !$this->legacyEndpoint),
394 394
 			];
395 395
 
396 396
 			$calendar = $this->rowToCalendar($row, $calendar);
@@ -428,7 +428,7 @@  discard block
 block discarded – undo
428 428
 
429 429
 		$result = $query->executeQuery();
430 430
 
431
-		$readOnlyPropertyName = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only';
431
+		$readOnlyPropertyName = '{'.\OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD.'}read-only';
432 432
 		while ($row = $result->fetch()) {
433 433
 			$row['principaluri'] = (string) $row['principaluri'];
434 434
 			if ($row['principaluri'] === $principalUri) {
@@ -449,21 +449,21 @@  discard block
 block discarded – undo
449 449
 			}
450 450
 
451 451
 			[, $name] = Uri\split($row['principaluri']);
452
-			$uri = $row['uri'] . '_shared_by_' . $name;
453
-			$row['displayname'] = $row['displayname'] . ' (' . $this->getUserDisplayName($name) . ')';
452
+			$uri = $row['uri'].'_shared_by_'.$name;
453
+			$row['displayname'] = $row['displayname'].' ('.$this->getUserDisplayName($name).')';
454 454
 			$components = [];
455 455
 			if ($row['components']) {
456
-				$components = explode(',',$row['components']);
456
+				$components = explode(',', $row['components']);
457 457
 			}
458 458
 			$calendar = [
459 459
 				'id' => $row['id'],
460 460
 				'uri' => $uri,
461 461
 				'principaluri' => $this->convertPrincipal($principalUri, !$this->legacyEndpoint),
462
-				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
463
-				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
464
-				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
465
-				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp('transparent'),
466
-				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
462
+				'{'.Plugin::NS_CALENDARSERVER.'}getctag' => 'http://sabre.io/ns/sync/'.($row['synctoken'] ? $row['synctoken'] : '0'),
463
+				'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ? $row['synctoken'] : '0',
464
+				'{'.Plugin::NS_CALDAV.'}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
465
+				'{'.Plugin::NS_CALDAV.'}schedule-calendar-transp' => new ScheduleCalendarTransp('transparent'),
466
+				'{'.\OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD.'}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
467 467
 				$readOnlyPropertyName => $readOnly,
468 468
 			];
469 469
 
@@ -502,16 +502,16 @@  discard block
 block discarded – undo
502 502
 			$row['principaluri'] = (string) $row['principaluri'];
503 503
 			$components = [];
504 504
 			if ($row['components']) {
505
-				$components = explode(',',$row['components']);
505
+				$components = explode(',', $row['components']);
506 506
 			}
507 507
 			$calendar = [
508 508
 				'id' => $row['id'],
509 509
 				'uri' => $row['uri'],
510 510
 				'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
511
-				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
512
-				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
513
-				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
514
-				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
511
+				'{'.Plugin::NS_CALENDARSERVER.'}getctag' => 'http://sabre.io/ns/sync/'.($row['synctoken'] ? $row['synctoken'] : '0'),
512
+				'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ? $row['synctoken'] : '0',
513
+				'{'.Plugin::NS_CALDAV.'}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
514
+				'{'.Plugin::NS_CALDAV.'}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent'] ? 'transparent' : 'opaque'),
515 515
 			];
516 516
 
517 517
 			$calendar = $this->rowToCalendar($row, $calendar);
@@ -570,22 +570,22 @@  discard block
 block discarded – undo
570 570
 		while ($row = $result->fetch()) {
571 571
 			$row['principaluri'] = (string) $row['principaluri'];
572 572
 			[, $name] = Uri\split($row['principaluri']);
573
-			$row['displayname'] = $row['displayname'] . "($name)";
573
+			$row['displayname'] = $row['displayname']."($name)";
574 574
 			$components = [];
575 575
 			if ($row['components']) {
576
-				$components = explode(',',$row['components']);
576
+				$components = explode(',', $row['components']);
577 577
 			}
578 578
 			$calendar = [
579 579
 				'id' => $row['id'],
580 580
 				'uri' => $row['publicuri'],
581 581
 				'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
582
-				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
583
-				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
584
-				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
585
-				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
586
-				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], $this->legacyEndpoint),
587
-				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ,
588
-				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}public' => (int)$row['access'] === self::ACCESS_PUBLIC,
582
+				'{'.Plugin::NS_CALENDARSERVER.'}getctag' => 'http://sabre.io/ns/sync/'.($row['synctoken'] ? $row['synctoken'] : '0'),
583
+				'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ? $row['synctoken'] : '0',
584
+				'{'.Plugin::NS_CALDAV.'}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
585
+				'{'.Plugin::NS_CALDAV.'}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent'] ? 'transparent' : 'opaque'),
586
+				'{'.\OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD.'}owner-principal' => $this->convertPrincipal($row['principaluri'], $this->legacyEndpoint),
587
+				'{'.\OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD.'}read-only' => (int) $row['access'] === Backend::ACCESS_READ,
588
+				'{'.\OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD.'}public' => (int) $row['access'] === self::ACCESS_PUBLIC,
589 589
 			];
590 590
 
591 591
 			$calendar = $this->rowToCalendar($row, $calendar);
@@ -630,27 +630,27 @@  discard block
 block discarded – undo
630 630
 		$result->closeCursor();
631 631
 
632 632
 		if ($row === false) {
633
-			throw new NotFound('Node with name \'' . $uri . '\' could not be found');
633
+			throw new NotFound('Node with name \''.$uri.'\' could not be found');
634 634
 		}
635 635
 
636 636
 		$row['principaluri'] = (string) $row['principaluri'];
637 637
 		[, $name] = Uri\split($row['principaluri']);
638
-		$row['displayname'] = $row['displayname'] . ' ' . "($name)";
638
+		$row['displayname'] = $row['displayname'].' '."($name)";
639 639
 		$components = [];
640 640
 		if ($row['components']) {
641
-			$components = explode(',',$row['components']);
641
+			$components = explode(',', $row['components']);
642 642
 		}
643 643
 		$calendar = [
644 644
 			'id' => $row['id'],
645 645
 			'uri' => $row['publicuri'],
646 646
 			'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
647
-			'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
648
-			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
649
-			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
650
-			'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
651
-			'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
652
-			'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ,
653
-			'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}public' => (int)$row['access'] === self::ACCESS_PUBLIC,
647
+			'{'.Plugin::NS_CALENDARSERVER.'}getctag' => 'http://sabre.io/ns/sync/'.($row['synctoken'] ? $row['synctoken'] : '0'),
648
+			'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ? $row['synctoken'] : '0',
649
+			'{'.Plugin::NS_CALDAV.'}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
650
+			'{'.Plugin::NS_CALDAV.'}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent'] ? 'transparent' : 'opaque'),
651
+			'{'.\OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD.'}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
652
+			'{'.\OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD.'}read-only' => (int) $row['access'] === Backend::ACCESS_READ,
653
+			'{'.\OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD.'}public' => (int) $row['access'] === self::ACCESS_PUBLIC,
654 654
 		];
655 655
 
656 656
 		$calendar = $this->rowToCalendar($row, $calendar);
@@ -691,17 +691,17 @@  discard block
 block discarded – undo
691 691
 		$row['principaluri'] = (string) $row['principaluri'];
692 692
 		$components = [];
693 693
 		if ($row['components']) {
694
-			$components = explode(',',$row['components']);
694
+			$components = explode(',', $row['components']);
695 695
 		}
696 696
 
697 697
 		$calendar = [
698 698
 			'id' => $row['id'],
699 699
 			'uri' => $row['uri'],
700 700
 			'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
701
-			'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
702
-			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
703
-			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
704
-			'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
701
+			'{'.Plugin::NS_CALENDARSERVER.'}getctag' => 'http://sabre.io/ns/sync/'.($row['synctoken'] ? $row['synctoken'] : '0'),
702
+			'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ? $row['synctoken'] : '0',
703
+			'{'.Plugin::NS_CALDAV.'}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
704
+			'{'.Plugin::NS_CALDAV.'}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent'] ? 'transparent' : 'opaque'),
705 705
 		];
706 706
 
707 707
 		$calendar = $this->rowToCalendar($row, $calendar);
@@ -740,17 +740,17 @@  discard block
 block discarded – undo
740 740
 		$row['principaluri'] = (string) $row['principaluri'];
741 741
 		$components = [];
742 742
 		if ($row['components']) {
743
-			$components = explode(',',$row['components']);
743
+			$components = explode(',', $row['components']);
744 744
 		}
745 745
 
746 746
 		$calendar = [
747 747
 			'id' => $row['id'],
748 748
 			'uri' => $row['uri'],
749 749
 			'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
750
-			'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
751
-			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
752
-			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
753
-			'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
750
+			'{'.Plugin::NS_CALENDARSERVER.'}getctag' => 'http://sabre.io/ns/sync/'.($row['synctoken'] ? $row['synctoken'] : '0'),
751
+			'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ? $row['synctoken'] : '0',
752
+			'{'.Plugin::NS_CALDAV.'}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
753
+			'{'.Plugin::NS_CALDAV.'}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent'] ? 'transparent' : 'opaque'),
754 754
 		];
755 755
 
756 756
 		$calendar = $this->rowToCalendar($row, $calendar);
@@ -792,8 +792,8 @@  discard block
 block discarded – undo
792 792
 			'principaluri' => $row['principaluri'],
793 793
 			'source' => $row['source'],
794 794
 			'lastmodified' => $row['lastmodified'],
795
-			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
796
-			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
795
+			'{'.Plugin::NS_CALDAV.'}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
796
+			'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ? $row['synctoken'] : '0',
797 797
 		];
798 798
 
799 799
 		return $this->rowToSubscription($row, $subscription);
@@ -824,16 +824,16 @@  discard block
 block discarded – undo
824 824
 		$sccs = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set';
825 825
 		if (isset($properties[$sccs])) {
826 826
 			if (!($properties[$sccs] instanceof SupportedCalendarComponentSet)) {
827
-				throw new DAV\Exception('The ' . $sccs . ' property must be of type: \Sabre\CalDAV\Property\SupportedCalendarComponentSet');
827
+				throw new DAV\Exception('The '.$sccs.' property must be of type: \Sabre\CalDAV\Property\SupportedCalendarComponentSet');
828 828
 			}
829
-			$values['components'] = implode(',',$properties[$sccs]->getValue());
829
+			$values['components'] = implode(',', $properties[$sccs]->getValue());
830 830
 		} elseif (isset($properties['components'])) {
831 831
 			// Allow to provide components internally without having
832 832
 			// to create a SupportedCalendarComponentSet object
833 833
 			$values['components'] = $properties['components'];
834 834
 		}
835 835
 
836
-		$transp = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp';
836
+		$transp = '{'.Plugin::NS_CALDAV.'}schedule-calendar-transp';
837 837
 		if (isset($properties[$transp])) {
838 838
 			$values['transparent'] = (int) ($properties[$transp]->getValue() === 'transparent');
839 839
 		}
@@ -853,7 +853,7 @@  discard block
 block discarded – undo
853 853
 		$calendarId = $query->getLastInsertId();
854 854
 
855 855
 		$calendarData = $this->getCalendarById($calendarId);
856
-		$this->dispatcher->dispatchTyped(new CalendarCreatedEvent((int)$calendarId, $calendarData));
856
+		$this->dispatcher->dispatchTyped(new CalendarCreatedEvent((int) $calendarId, $calendarData));
857 857
 
858 858
 		return $calendarId;
859 859
 	}
@@ -876,13 +876,13 @@  discard block
 block discarded – undo
876 876
 	 */
877 877
 	public function updateCalendar($calendarId, PropPatch $propPatch) {
878 878
 		$supportedProperties = array_keys($this->propertyMap);
879
-		$supportedProperties[] = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp';
879
+		$supportedProperties[] = '{'.Plugin::NS_CALDAV.'}schedule-calendar-transp';
880 880
 
881
-		$propPatch->handle($supportedProperties, function ($mutations) use ($calendarId) {
881
+		$propPatch->handle($supportedProperties, function($mutations) use ($calendarId) {
882 882
 			$newValues = [];
883 883
 			foreach ($mutations as $propertyName => $propertyValue) {
884 884
 				switch ($propertyName) {
885
-					case '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp':
885
+					case '{'.Plugin::NS_CALDAV.'}schedule-calendar-transp':
886 886
 						$fieldName = 'transparent';
887 887
 						$newValues[$fieldName] = (int) ($propertyValue->getValue() === 'transparent');
888 888
 						break;
@@ -904,7 +904,7 @@  discard block
 block discarded – undo
904 904
 
905 905
 			$calendarData = $this->getCalendarById($calendarId);
906 906
 			$shares = $this->getShares($calendarId);
907
-			$this->dispatcher->dispatchTyped(new CalendarUpdatedEvent((int)$calendarId, $calendarData, $shares, $mutations));
907
+			$this->dispatcher->dispatchTyped(new CalendarUpdatedEvent((int) $calendarId, $calendarData, $shares, $mutations));
908 908
 
909 909
 			return true;
910 910
 		});
@@ -954,7 +954,7 @@  discard block
 block discarded – undo
954 954
 
955 955
 			// Only dispatch if we actually deleted anything
956 956
 			if ($calendarData) {
957
-				$this->dispatcher->dispatchTyped(new CalendarDeletedEvent((int)$calendarId, $calendarData, $shares));
957
+				$this->dispatcher->dispatchTyped(new CalendarDeletedEvent((int) $calendarId, $calendarData, $shares));
958 958
 			}
959 959
 		} else {
960 960
 			$qbMarkCalendarDeleted = $this->db->getQueryBuilder();
@@ -967,7 +967,7 @@  discard block
 block discarded – undo
967 967
 			$shares = $this->getShares($calendarId);
968 968
 			if ($calendarData) {
969 969
 				$this->dispatcher->dispatchTyped(new CalendarMovedToTrashEvent(
970
-					(int)$calendarId,
970
+					(int) $calendarId,
971 971
 					$calendarData,
972 972
 					$shares
973 973
 				));
@@ -1051,11 +1051,11 @@  discard block
 block discarded – undo
1051 1051
 				'id' => $row['id'],
1052 1052
 				'uri' => $row['uri'],
1053 1053
 				'lastmodified' => $row['lastmodified'],
1054
-				'etag' => '"' . $row['etag'] . '"',
1054
+				'etag' => '"'.$row['etag'].'"',
1055 1055
 				'calendarid' => $row['calendarid'],
1056
-				'size' => (int)$row['size'],
1056
+				'size' => (int) $row['size'],
1057 1057
 				'component' => strtolower($row['componenttype']),
1058
-				'classification' => (int)$row['classification']
1058
+				'classification' => (int) $row['classification']
1059 1059
 			];
1060 1060
 		}
1061 1061
 		$stmt->closeCursor();
@@ -1078,13 +1078,13 @@  discard block
 block discarded – undo
1078 1078
 				'id' => $row['id'],
1079 1079
 				'uri' => $row['uri'],
1080 1080
 				'lastmodified' => $row['lastmodified'],
1081
-				'etag' => '"' . $row['etag'] . '"',
1081
+				'etag' => '"'.$row['etag'].'"',
1082 1082
 				'calendarid' => (int) $row['calendarid'],
1083 1083
 				'calendartype' => (int) $row['calendartype'],
1084 1084
 				'size' => (int) $row['size'],
1085 1085
 				'component' => strtolower($row['componenttype']),
1086 1086
 				'classification' => (int) $row['classification'],
1087
-				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => $row['deleted_at'] === null ? $row['deleted_at'] : (int) $row['deleted_at'],
1087
+				'{'.\OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD.'}deleted-at' => $row['deleted_at'] === null ? $row['deleted_at'] : (int) $row['deleted_at'],
1088 1088
 			];
1089 1089
 		}
1090 1090
 		$stmt->closeCursor();
@@ -1117,13 +1117,13 @@  discard block
 block discarded – undo
1117 1117
 				'id' => $row['id'],
1118 1118
 				'uri' => $row['uri'],
1119 1119
 				'lastmodified' => $row['lastmodified'],
1120
-				'etag' => '"' . $row['etag'] . '"',
1120
+				'etag' => '"'.$row['etag'].'"',
1121 1121
 				'calendarid' => $row['calendarid'],
1122 1122
 				'calendaruri' => $row['calendaruri'],
1123
-				'size' => (int)$row['size'],
1123
+				'size' => (int) $row['size'],
1124 1124
 				'component' => strtolower($row['componenttype']),
1125
-				'classification' => (int)$row['classification'],
1126
-				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => $row['deleted_at'] === null ? $row['deleted_at'] : (int) $row['deleted_at'],
1125
+				'classification' => (int) $row['classification'],
1126
+				'{'.\OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD.'}deleted-at' => $row['deleted_at'] === null ? $row['deleted_at'] : (int) $row['deleted_at'],
1127 1127
 			];
1128 1128
 		}
1129 1129
 		$stmt->closeCursor();
@@ -1167,12 +1167,12 @@  discard block
 block discarded – undo
1167 1167
 			'id' => $row['id'],
1168 1168
 			'uri' => $row['uri'],
1169 1169
 			'lastmodified' => $row['lastmodified'],
1170
-			'etag' => '"' . $row['etag'] . '"',
1170
+			'etag' => '"'.$row['etag'].'"',
1171 1171
 			'calendarid' => $row['calendarid'],
1172
-			'size' => (int)$row['size'],
1172
+			'size' => (int) $row['size'],
1173 1173
 			'calendardata' => $this->readBlob($row['calendardata']),
1174 1174
 			'component' => strtolower($row['componenttype']),
1175
-			'classification' => (int)$row['classification']
1175
+			'classification' => (int) $row['classification']
1176 1176
 		];
1177 1177
 	}
1178 1178
 
@@ -1214,12 +1214,12 @@  discard block
 block discarded – undo
1214 1214
 					'id' => $row['id'],
1215 1215
 					'uri' => $row['uri'],
1216 1216
 					'lastmodified' => $row['lastmodified'],
1217
-					'etag' => '"' . $row['etag'] . '"',
1217
+					'etag' => '"'.$row['etag'].'"',
1218 1218
 					'calendarid' => $row['calendarid'],
1219
-					'size' => (int)$row['size'],
1219
+					'size' => (int) $row['size'],
1220 1220
 					'calendardata' => $this->readBlob($row['calendardata']),
1221 1221
 					'component' => strtolower($row['componenttype']),
1222
-					'classification' => (int)$row['classification']
1222
+					'classification' => (int) $row['classification']
1223 1223
 				];
1224 1224
 			}
1225 1225
 			$result->closeCursor();
@@ -1306,11 +1306,11 @@  discard block
 block discarded – undo
1306 1306
 			$calendarRow = $this->getCalendarById($calendarId);
1307 1307
 			$shares = $this->getShares($calendarId);
1308 1308
 
1309
-			$this->dispatcher->dispatchTyped(new CalendarObjectCreatedEvent((int)$calendarId, $calendarRow, $shares, $objectRow));
1309
+			$this->dispatcher->dispatchTyped(new CalendarObjectCreatedEvent((int) $calendarId, $calendarRow, $shares, $objectRow));
1310 1310
 		} else {
1311 1311
 			$subscriptionRow = $this->getSubscriptionById($calendarId);
1312 1312
 
1313
-			$this->dispatcher->dispatchTyped(new CachedCalendarObjectCreatedEvent((int)$calendarId, $subscriptionRow, [], $objectRow));
1313
+			$this->dispatcher->dispatchTyped(new CachedCalendarObjectCreatedEvent((int) $calendarId, $subscriptionRow, [], $objectRow));
1314 1314
 			$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createCachedCalendarObject', new GenericEvent(
1315 1315
 				'\OCA\DAV\CalDAV\CalDavBackend::createCachedCalendarObject',
1316 1316
 				[
@@ -1322,7 +1322,7 @@  discard block
 block discarded – undo
1322 1322
 			));
1323 1323
 		}
1324 1324
 
1325
-		return '"' . $extraData['etag'] . '"';
1325
+		return '"'.$extraData['etag'].'"';
1326 1326
 	}
1327 1327
 
1328 1328
 	/**
@@ -1371,11 +1371,11 @@  discard block
 block discarded – undo
1371 1371
 				$calendarRow = $this->getCalendarById($calendarId);
1372 1372
 				$shares = $this->getShares($calendarId);
1373 1373
 
1374
-				$this->dispatcher->dispatchTyped(new CalendarObjectUpdatedEvent((int)$calendarId, $calendarRow, $shares, $objectRow));
1374
+				$this->dispatcher->dispatchTyped(new CalendarObjectUpdatedEvent((int) $calendarId, $calendarRow, $shares, $objectRow));
1375 1375
 			} else {
1376 1376
 				$subscriptionRow = $this->getSubscriptionById($calendarId);
1377 1377
 
1378
-				$this->dispatcher->dispatchTyped(new CachedCalendarObjectUpdatedEvent((int)$calendarId, $subscriptionRow, [], $objectRow));
1378
+				$this->dispatcher->dispatchTyped(new CachedCalendarObjectUpdatedEvent((int) $calendarId, $subscriptionRow, [], $objectRow));
1379 1379
 				$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateCachedCalendarObject', new GenericEvent(
1380 1380
 					'\OCA\DAV\CalDAV\CalDavBackend::updateCachedCalendarObject',
1381 1381
 					[
@@ -1388,7 +1388,7 @@  discard block
 block discarded – undo
1388 1388
 			}
1389 1389
 		}
1390 1390
 
1391
-		return '"' . $extraData['etag'] . '"';
1391
+		return '"'.$extraData['etag'].'"';
1392 1392
 	}
1393 1393
 
1394 1394
 	/**
@@ -1487,11 +1487,11 @@  discard block
 block discarded – undo
1487 1487
 				$calendarRow = $this->getCalendarById($calendarId);
1488 1488
 				$shares = $this->getShares($calendarId);
1489 1489
 
1490
-				$this->dispatcher->dispatchTyped(new CalendarObjectDeletedEvent((int)$calendarId, $calendarRow, $shares, $data));
1490
+				$this->dispatcher->dispatchTyped(new CalendarObjectDeletedEvent((int) $calendarId, $calendarRow, $shares, $data));
1491 1491
 			} else {
1492 1492
 				$subscriptionRow = $this->getSubscriptionById($calendarId);
1493 1493
 
1494
-				$this->dispatcher->dispatchTyped(new CachedCalendarObjectDeletedEvent((int)$calendarId, $subscriptionRow, [], $data));
1494
+				$this->dispatcher->dispatchTyped(new CachedCalendarObjectDeletedEvent((int) $calendarId, $subscriptionRow, [], $data));
1495 1495
 				$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteCachedCalendarObject', new GenericEvent(
1496 1496
 					'\OCA\DAV\CalDAV\CalDavBackend::deleteCachedCalendarObject',
1497 1497
 					[
@@ -1540,7 +1540,7 @@  discard block
 block discarded – undo
1540 1540
 			if ($calendarData !== null) {
1541 1541
 				$this->dispatcher->dispatchTyped(
1542 1542
 					new CalendarObjectMovedToTrashEvent(
1543
-						(int)$calendarId,
1543
+						(int) $calendarId,
1544 1544
 						$calendarData,
1545 1545
 						$this->getShares($calendarId),
1546 1546
 						$data
@@ -1855,7 +1855,7 @@  discard block
 block discarded – undo
1855 1855
 
1856 1856
 		$result = [];
1857 1857
 		while ($row = $stmt->fetch()) {
1858
-			$path = $uriMapper[$row['calendarid']] . '/' . $row['uri'];
1858
+			$path = $uriMapper[$row['calendarid']].'/'.$row['uri'];
1859 1859
 			if (!in_array($path, $result)) {
1860 1860
 				$result[] = $path;
1861 1861
 			}
@@ -1905,8 +1905,8 @@  discard block
 block discarded – undo
1905 1905
 
1906 1906
 		if ($pattern !== '') {
1907 1907
 			$innerQuery->andWhere($innerQuery->expr()->iLike('op.value',
1908
-				$outerQuery->createNamedParameter('%' .
1909
-					$this->db->escapeLikeParameter($pattern) . '%')));
1908
+				$outerQuery->createNamedParameter('%'.
1909
+					$this->db->escapeLikeParameter($pattern).'%')));
1910 1910
 		}
1911 1911
 
1912 1912
 		$outerQuery->select('c.id', 'c.calendardata', 'c.componenttype', 'c.uid', 'c.uri')
@@ -1943,7 +1943,7 @@  discard block
 block discarded – undo
1943 1943
 		}
1944 1944
 
1945 1945
 		$result = $outerQuery->executeQuery();
1946
-		$calendarObjects = array_filter($result->fetchAll(), function (array $row) use ($options) {
1946
+		$calendarObjects = array_filter($result->fetchAll(), function(array $row) use ($options) {
1947 1947
 			$start = $options['timerange']['start'] ?? null;
1948 1948
 			$end = $options['timerange']['end'] ?? null;
1949 1949
 
@@ -1978,7 +1978,7 @@  discard block
 block discarded – undo
1978 1978
 		});
1979 1979
 		$result->closeCursor();
1980 1980
 
1981
-		return array_map(function ($o) {
1981
+		return array_map(function($o) {
1982 1982
 			$calendarData = Reader::read($o['calendardata']);
1983 1983
 			$comps = $calendarData->getComponents();
1984 1984
 			$objects = [];
@@ -1996,10 +1996,10 @@  discard block
 block discarded – undo
1996 1996
 				'type' => $o['componenttype'],
1997 1997
 				'uid' => $o['uid'],
1998 1998
 				'uri' => $o['uri'],
1999
-				'objects' => array_map(function ($c) {
1999
+				'objects' => array_map(function($c) {
2000 2000
 					return $this->transformSearchData($c);
2001 2001
 				}, $objects),
2002
-				'timezones' => array_map(function ($c) {
2002
+				'timezones' => array_map(function($c) {
2003 2003
 					return $this->transformSearchData($c);
2004 2004
 				}, $timezones),
2005 2005
 			];
@@ -2015,7 +2015,7 @@  discard block
 block discarded – undo
2015 2015
 		/** @var Component[] $subComponents */
2016 2016
 		$subComponents = $comp->getComponents();
2017 2017
 		/** @var Property[] $properties */
2018
-		$properties = array_filter($comp->children(), function ($c) {
2018
+		$properties = array_filter($comp->children(), function($c) {
2019 2019
 			return $c instanceof Property;
2020 2020
 		});
2021 2021
 		$validationRules = $comp->getValidationRules();
@@ -2093,7 +2093,7 @@  discard block
 block discarded – undo
2093 2093
 		$subscriptions = $this->getSubscriptionsForUser($principalUri);
2094 2094
 		foreach ($calendars as $calendar) {
2095 2095
 			$calendarAnd = $calendarObjectIdQuery->expr()->andX();
2096
-			$calendarAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int)$calendar['id'])));
2096
+			$calendarAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int) $calendar['id'])));
2097 2097
 			$calendarAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendartype', $calendarObjectIdQuery->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
2098 2098
 
2099 2099
 			// If it's shared, limit search to public events
@@ -2106,7 +2106,7 @@  discard block
 block discarded – undo
2106 2106
 		}
2107 2107
 		foreach ($subscriptions as $subscription) {
2108 2108
 			$subscriptionAnd = $calendarObjectIdQuery->expr()->andX();
2109
-			$subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int)$subscription['id'])));
2109
+			$subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int) $subscription['id'])));
2110 2110
 			$subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendartype', $calendarObjectIdQuery->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)));
2111 2111
 
2112 2112
 			// If it's shared, limit search to public events
@@ -2152,7 +2152,7 @@  discard block
 block discarded – undo
2152 2152
 			if (!$escapePattern) {
2153 2153
 				$calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->ilike('cob.value', $calendarObjectIdQuery->createNamedParameter($pattern)));
2154 2154
 			} else {
2155
-				$calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->ilike('cob.value', $calendarObjectIdQuery->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%')));
2155
+				$calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->ilike('cob.value', $calendarObjectIdQuery->createNamedParameter('%'.$this->db->escapeLikeParameter($pattern).'%')));
2156 2156
 			}
2157 2157
 		}
2158 2158
 
@@ -2166,7 +2166,7 @@  discard block
 block discarded – undo
2166 2166
 		$result = $calendarObjectIdQuery->executeQuery();
2167 2167
 		$matches = $result->fetchAll();
2168 2168
 		$result->closeCursor();
2169
-		$matches = array_map(static function (array $match):int {
2169
+		$matches = array_map(static function(array $match):int {
2170 2170
 			return (int) $match['objectid'];
2171 2171
 		}, $matches);
2172 2172
 
@@ -2179,9 +2179,9 @@  discard block
 block discarded – undo
2179 2179
 		$calendarObjects = $result->fetchAll();
2180 2180
 		$result->closeCursor();
2181 2181
 
2182
-		return array_map(function (array $array): array {
2183
-			$array['calendarid'] = (int)$array['calendarid'];
2184
-			$array['calendartype'] = (int)$array['calendartype'];
2182
+		return array_map(function(array $array): array {
2183
+			$array['calendarid'] = (int) $array['calendarid'];
2184
+			$array['calendartype'] = (int) $array['calendartype'];
2185 2185
 			$array['calendardata'] = $this->readBlob($array['calendardata']);
2186 2186
 
2187 2187
 			return $array;
@@ -2219,7 +2219,7 @@  discard block
 block discarded – undo
2219 2219
 		$row = $stmt->fetch();
2220 2220
 		$stmt->closeCursor();
2221 2221
 		if ($row) {
2222
-			return $row['calendaruri'] . '/' . $row['objecturi'];
2222
+			return $row['calendaruri'].'/'.$row['objecturi'];
2223 2223
 		}
2224 2224
 
2225 2225
 		return null;
@@ -2245,13 +2245,13 @@  discard block
 block discarded – undo
2245 2245
 			'id' => $row['id'],
2246 2246
 			'uri' => $row['uri'],
2247 2247
 			'lastmodified' => $row['lastmodified'],
2248
-			'etag' => '"' . $row['etag'] . '"',
2248
+			'etag' => '"'.$row['etag'].'"',
2249 2249
 			'calendarid' => $row['calendarid'],
2250 2250
 			'calendaruri' => $row['calendaruri'],
2251
-			'size' => (int)$row['size'],
2251
+			'size' => (int) $row['size'],
2252 2252
 			'calendardata' => $this->readBlob($row['calendardata']),
2253 2253
 			'component' => strtolower($row['componenttype']),
2254
-			'classification' => (int)$row['classification'],
2254
+			'classification' => (int) $row['classification'],
2255 2255
 			'deleted_at' => isset($row['deleted_at']) ? ((int) $row['deleted_at']) : null,
2256 2256
 		];
2257 2257
 	}
@@ -2451,8 +2451,8 @@  discard block
 block discarded – undo
2451 2451
 				'source' => $row['source'],
2452 2452
 				'lastmodified' => $row['lastmodified'],
2453 2453
 
2454
-				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
2455
-				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
2454
+				'{'.Plugin::NS_CALDAV.'}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
2455
+				'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ? $row['synctoken'] : '0',
2456 2456
 			];
2457 2457
 
2458 2458
 			$subscriptions[] = $this->rowToSubscription($row, $subscription);
@@ -2541,7 +2541,7 @@  discard block
 block discarded – undo
2541 2541
 		$supportedProperties = array_keys($this->subscriptionPropertyMap);
2542 2542
 		$supportedProperties[] = '{http://calendarserver.org/ns/}source';
2543 2543
 
2544
-		$propPatch->handle($supportedProperties, function ($mutations) use ($subscriptionId) {
2544
+		$propPatch->handle($supportedProperties, function($mutations) use ($subscriptionId) {
2545 2545
 			$newValues = [];
2546 2546
 
2547 2547
 			foreach ($mutations as $propertyName => $propertyValue) {
@@ -2563,7 +2563,7 @@  discard block
 block discarded – undo
2563 2563
 				->executeStatement();
2564 2564
 
2565 2565
 			$subscriptionRow = $this->getSubscriptionById($subscriptionId);
2566
-			$this->dispatcher->dispatchTyped(new SubscriptionUpdatedEvent((int)$subscriptionId, $subscriptionRow, [], $mutations));
2566
+			$this->dispatcher->dispatchTyped(new SubscriptionUpdatedEvent((int) $subscriptionId, $subscriptionRow, [], $mutations));
2567 2567
 			$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateSubscription', new GenericEvent(
2568 2568
 				'\OCA\DAV\CalDAV\CalDavBackend::updateSubscription',
2569 2569
 				[
@@ -2614,7 +2614,7 @@  discard block
 block discarded – undo
2614 2614
 			->executeStatement();
2615 2615
 
2616 2616
 		if ($subscriptionRow) {
2617
-			$this->dispatcher->dispatchTyped(new SubscriptionDeletedEvent((int)$subscriptionId, $subscriptionRow, []));
2617
+			$this->dispatcher->dispatchTyped(new SubscriptionDeletedEvent((int) $subscriptionId, $subscriptionRow, []));
2618 2618
 		}
2619 2619
 	}
2620 2620
 
@@ -2652,8 +2652,8 @@  discard block
 block discarded – undo
2652 2652
 			'uri' => $row['uri'],
2653 2653
 			'calendardata' => $row['calendardata'],
2654 2654
 			'lastmodified' => $row['lastmodified'],
2655
-			'etag' => '"' . $row['etag'] . '"',
2656
-			'size' => (int)$row['size'],
2655
+			'etag' => '"'.$row['etag'].'"',
2656
+			'size' => (int) $row['size'],
2657 2657
 		];
2658 2658
 	}
2659 2659
 
@@ -2681,8 +2681,8 @@  discard block
 block discarded – undo
2681 2681
 				'calendardata' => $row['calendardata'],
2682 2682
 				'uri' => $row['uri'],
2683 2683
 				'lastmodified' => $row['lastmodified'],
2684
-				'etag' => '"' . $row['etag'] . '"',
2685
-				'size' => (int)$row['size'],
2684
+				'etag' => '"'.$row['etag'].'"',
2685
+				'size' => (int) $row['size'],
2686 2686
 			];
2687 2687
 		}
2688 2688
 		$stmt->closeCursor();
@@ -2737,14 +2737,14 @@  discard block
 block discarded – undo
2737 2737
 	 * @return void
2738 2738
 	 */
2739 2739
 	protected function addChange($calendarId, $objectUri, $operation, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
2740
-		$table = $calendarType === self::CALENDAR_TYPE_CALENDAR ? 'calendars': 'calendarsubscriptions';
2740
+		$table = $calendarType === self::CALENDAR_TYPE_CALENDAR ? 'calendars' : 'calendarsubscriptions';
2741 2741
 
2742 2742
 		$query = $this->db->getQueryBuilder();
2743 2743
 		$query->select('synctoken')
2744 2744
 			->from($table)
2745 2745
 			->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)));
2746 2746
 		$result = $query->executeQuery();
2747
-		$syncToken = (int)$result->fetchOne();
2747
+		$syncToken = (int) $result->fetchOne();
2748 2748
 		$result->closeCursor();
2749 2749
 
2750 2750
 		$query = $this->db->getQueryBuilder();
@@ -2801,7 +2801,7 @@  discard block
 block discarded – undo
2801 2801
 				// Track first component type and uid
2802 2802
 				if ($uid === null) {
2803 2803
 					$componentType = $component->name;
2804
-					$uid = (string)$component->UID;
2804
+					$uid = (string) $component->UID;
2805 2805
 				}
2806 2806
 			}
2807 2807
 		}
@@ -2900,7 +2900,7 @@  discard block
 block discarded – undo
2900 2900
 			]));
2901 2901
 		$this->calendarSharingBackend->updateShares($shareable, $add, $remove);
2902 2902
 
2903
-		$this->dispatcher->dispatchTyped(new CalendarShareUpdatedEvent((int)$calendarId, $calendarRow, $oldShares, $add, $remove));
2903
+		$this->dispatcher->dispatchTyped(new CalendarShareUpdatedEvent((int) $calendarId, $calendarRow, $oldShares, $add, $remove));
2904 2904
 	}
2905 2905
 
2906 2906
 	/**
@@ -2940,7 +2940,7 @@  discard block
 block discarded – undo
2940 2940
 				]);
2941 2941
 			$query->executeStatement();
2942 2942
 
2943
-			$this->dispatcher->dispatchTyped(new CalendarPublishedEvent((int)$calendarId, $calendarData, $publicUri));
2943
+			$this->dispatcher->dispatchTyped(new CalendarPublishedEvent((int) $calendarId, $calendarData, $publicUri));
2944 2944
 			return $publicUri;
2945 2945
 		}
2946 2946
 		$query->delete('dav_shares')
@@ -2948,7 +2948,7 @@  discard block
 block discarded – undo
2948 2948
 			->andWhere($query->expr()->eq('access', $query->createNamedParameter(self::ACCESS_PUBLIC)));
2949 2949
 		$query->executeStatement();
2950 2950
 
2951
-		$this->dispatcher->dispatchTyped(new CalendarUnpublishedEvent((int)$calendarId, $calendarData));
2951
+		$this->dispatcher->dispatchTyped(new CalendarUnpublishedEvent((int) $calendarId, $calendarData));
2952 2952
 		return null;
2953 2953
 	}
2954 2954
 
@@ -3175,10 +3175,10 @@  discard block
 block discarded – undo
3175 3175
 		$result->closeCursor();
3176 3176
 
3177 3177
 		if (!isset($objectIds['id'])) {
3178
-			throw new \InvalidArgumentException('Calendarobject does not exists: ' . $uri);
3178
+			throw new \InvalidArgumentException('Calendarobject does not exists: '.$uri);
3179 3179
 		}
3180 3180
 
3181
-		return (int)$objectIds['id'];
3181
+		return (int) $objectIds['id'];
3182 3182
 	}
3183 3183
 
3184 3184
 	/**
@@ -3204,8 +3204,8 @@  discard block
 block discarded – undo
3204 3204
 	 *
3205 3205
 	 */
3206 3206
 	private function addOwnerPrincipalToCalendar(array $calendarInfo): array {
3207
-		$ownerPrincipalKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal';
3208
-		$displaynameKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}owner-displayname';
3207
+		$ownerPrincipalKey = '{'.\OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD.'}owner-principal';
3208
+		$displaynameKey = '{'.\OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD.'}owner-displayname';
3209 3209
 		if (isset($calendarInfo[$ownerPrincipalKey])) {
3210 3210
 			$uri = $calendarInfo[$ownerPrincipalKey];
3211 3211
 		} else {
Please login to merge, or discard this patch.