Passed
Push — master ( 613b2a...08e11d )
by Christoph
14:08 queued 13s
created

CalDavBackend::pruneOutdatedSyncTokens()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 7
nc 2
nop 1
dl 0
loc 9
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 * @copyright Copyright (c) 2018 Georg Ehrke
5
 * @copyright Copyright (c) 2020, leith abdulla (<[email protected]>)
6
 *
7
 * @author Chih-Hsuan Yen <[email protected]>
8
 * @author Christoph Wurst <[email protected]>
9
 * @author dartcafe <[email protected]>
10
 * @author Georg Ehrke <[email protected]>
11
 * @author Joas Schilling <[email protected]>
12
 * @author John Molakvoæ <[email protected]>
13
 * @author leith abdulla <[email protected]>
14
 * @author Lukas Reschke <[email protected]>
15
 * @author Morris Jobke <[email protected]>
16
 * @author Robin Appelman <[email protected]>
17
 * @author Roeland Jago Douma <[email protected]>
18
 * @author Simon Spannagel <[email protected]>
19
 * @author Stefan Weil <[email protected]>
20
 * @author Thomas Citharel <[email protected]>
21
 * @author Thomas Müller <[email protected]>
22
 * @author Vinicius Cubas Brand <[email protected]>
23
 *
24
 * @license AGPL-3.0
25
 *
26
 * This code is free software: you can redistribute it and/or modify
27
 * it under the terms of the GNU Affero General Public License, version 3,
28
 * as published by the Free Software Foundation.
29
 *
30
 * This program is distributed in the hope that it will be useful,
31
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
32
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
33
 * GNU Affero General Public License for more details.
34
 *
35
 * You should have received a copy of the GNU Affero General Public License, version 3,
36
 * along with this program. If not, see <http://www.gnu.org/licenses/>
37
 *
38
 */
39
namespace OCA\DAV\CalDAV;
40
41
use DateTime;
42
use DateTimeInterface;
43
use OCA\DAV\AppInfo\Application;
44
use OCA\DAV\Connector\Sabre\Principal;
45
use OCA\DAV\DAV\Sharing\Backend;
46
use OCA\DAV\DAV\Sharing\IShareable;
47
use OCA\DAV\Events\CachedCalendarObjectCreatedEvent;
48
use OCA\DAV\Events\CachedCalendarObjectDeletedEvent;
49
use OCA\DAV\Events\CachedCalendarObjectUpdatedEvent;
50
use OCA\DAV\Events\CalendarCreatedEvent;
51
use OCA\DAV\Events\CalendarDeletedEvent;
52
use OCA\DAV\Events\CalendarMovedToTrashEvent;
53
use OCA\DAV\Events\CalendarObjectCreatedEvent;
54
use OCA\DAV\Events\CalendarObjectDeletedEvent;
55
use OCA\DAV\Events\CalendarObjectMovedEvent;
56
use OCA\DAV\Events\CalendarObjectMovedToTrashEvent;
57
use OCA\DAV\Events\CalendarObjectRestoredEvent;
58
use OCA\DAV\Events\CalendarObjectUpdatedEvent;
59
use OCA\DAV\Events\CalendarPublishedEvent;
60
use OCA\DAV\Events\CalendarRestoredEvent;
61
use OCA\DAV\Events\CalendarShareUpdatedEvent;
62
use OCA\DAV\Events\CalendarUnpublishedEvent;
63
use OCA\DAV\Events\CalendarUpdatedEvent;
64
use OCA\DAV\Events\SubscriptionCreatedEvent;
65
use OCA\DAV\Events\SubscriptionDeletedEvent;
66
use OCA\DAV\Events\SubscriptionUpdatedEvent;
67
use OCP\AppFramework\Db\TTransactional;
68
use OCP\Calendar\Exceptions\CalendarException;
69
use OCP\DB\Exception;
70
use OCP\DB\QueryBuilder\IQueryBuilder;
71
use OCP\EventDispatcher\IEventDispatcher;
72
use OCP\IConfig;
73
use OCP\IDBConnection;
74
use OCP\IGroupManager;
75
use OCP\IUser;
76
use OCP\IUserManager;
77
use OCP\Security\ISecureRandom;
78
use Psr\Log\LoggerInterface;
79
use RuntimeException;
80
use Sabre\CalDAV\Backend\AbstractBackend;
81
use Sabre\CalDAV\Backend\SchedulingSupport;
82
use Sabre\CalDAV\Backend\SubscriptionSupport;
83
use Sabre\CalDAV\Backend\SyncSupport;
84
use Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp;
85
use Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet;
86
use Sabre\DAV;
87
use Sabre\DAV\Exception\BadRequest;
88
use Sabre\DAV\Exception\Forbidden;
89
use Sabre\DAV\Exception\NotFound;
90
use Sabre\DAV\PropPatch;
91
use Sabre\Uri;
92
use Sabre\VObject\Component;
93
use Sabre\VObject\Component\VCalendar;
94
use Sabre\VObject\Component\VTimeZone;
95
use Sabre\VObject\DateTimeParser;
96
use Sabre\VObject\InvalidDataException;
97
use Sabre\VObject\ParseException;
98
use Sabre\VObject\Property;
99
use Sabre\VObject\Reader;
100
use Sabre\VObject\Recur\EventIterator;
101
use function array_column;
102
use function array_merge;
103
use function array_values;
104
use function explode;
105
use function is_array;
106
use function is_resource;
107
use function pathinfo;
108
use function rewind;
109
use function settype;
110
use function sprintf;
111
use function str_replace;
112
use function strtolower;
113
use function time;
114
115
/**
116
 * Class CalDavBackend
117
 *
118
 * Code is heavily inspired by https://github.com/fruux/sabre-dav/blob/master/lib/CalDAV/Backend/PDO.php
119
 *
120
 * @package OCA\DAV\CalDAV
121
 */
122
class CalDavBackend extends AbstractBackend implements SyncSupport, SubscriptionSupport, SchedulingSupport {
123
124
	use TTransactional;
125
126
	public const CALENDAR_TYPE_CALENDAR = 0;
127
	public const CALENDAR_TYPE_SUBSCRIPTION = 1;
128
129
	public const PERSONAL_CALENDAR_URI = 'personal';
130
	public const PERSONAL_CALENDAR_NAME = 'Personal';
131
132
	public const RESOURCE_BOOKING_CALENDAR_URI = 'calendar';
133
	public const RESOURCE_BOOKING_CALENDAR_NAME = 'Calendar';
134
135
	/**
136
	 * We need to specify a max date, because we need to stop *somewhere*
137
	 *
138
	 * On 32 bit system the maximum for a signed integer is 2147483647, so
139
	 * MAX_DATE cannot be higher than date('Y-m-d', 2147483647) which results
140
	 * in 2038-01-19 to avoid problems when the date is converted
141
	 * to a unix timestamp.
142
	 */
143
	public const MAX_DATE = '2038-01-01';
144
145
	public const ACCESS_PUBLIC = 4;
146
	public const CLASSIFICATION_PUBLIC = 0;
147
	public const CLASSIFICATION_PRIVATE = 1;
148
	public const CLASSIFICATION_CONFIDENTIAL = 2;
149
150
	/**
151
	 * List of CalDAV properties, and how they map to database field names and their type
152
	 * Add your own properties by simply adding on to this array.
153
	 *
154
	 * @var array
155
	 * @psalm-var array<string, string[]>
156
	 */
157
	public array $propertyMap = [
158
		'{DAV:}displayname' => ['displayname', 'string'],
159
		'{urn:ietf:params:xml:ns:caldav}calendar-description' => ['description', 'string'],
160
		'{urn:ietf:params:xml:ns:caldav}calendar-timezone' => ['timezone', 'string'],
161
		'{http://apple.com/ns/ical/}calendar-order' => ['calendarorder', 'int'],
162
		'{http://apple.com/ns/ical/}calendar-color' => ['calendarcolor', 'string'],
163
		'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => ['deleted_at', 'int'],
164
	];
165
166
	/**
167
	 * List of subscription properties, and how they map to database field names.
168
	 *
169
	 * @var array
170
	 */
171
	public array $subscriptionPropertyMap = [
172
		'{DAV:}displayname' => ['displayname', 'string'],
173
		'{http://apple.com/ns/ical/}refreshrate' => ['refreshrate', 'string'],
174
		'{http://apple.com/ns/ical/}calendar-order' => ['calendarorder', 'int'],
175
		'{http://apple.com/ns/ical/}calendar-color' => ['calendarcolor', 'string'],
176
		'{http://calendarserver.org/ns/}subscribed-strip-todos' => ['striptodos', 'bool'],
177
		'{http://calendarserver.org/ns/}subscribed-strip-alarms' => ['stripalarms', 'string'],
178
		'{http://calendarserver.org/ns/}subscribed-strip-attachments' => ['stripattachments', 'string'],
179
	];
180
181
	/**
182
	 * properties to index
183
	 *
184
	 * This list has to be kept in sync with ICalendarQuery::SEARCH_PROPERTY_*
185
	 *
186
	 * @see \OCP\Calendar\ICalendarQuery
187
	 */
188
	private const INDEXED_PROPERTIES = [
189
		'CATEGORIES',
190
		'COMMENT',
191
		'DESCRIPTION',
192
		'LOCATION',
193
		'RESOURCES',
194
		'STATUS',
195
		'SUMMARY',
196
		'ATTENDEE',
197
		'CONTACT',
198
		'ORGANIZER'
199
	];
200
201
	/** @var array parameters to index */
202
	public static array $indexParameters = [
203
		'ATTENDEE' => ['CN'],
204
		'ORGANIZER' => ['CN'],
205
	];
206
207
	/**
208
	 * @var string[] Map of uid => display name
209
	 */
210
	protected array $userDisplayNames;
211
212
	private IDBConnection $db;
213
	private Backend $calendarSharingBackend;
214
	private Principal $principalBackend;
215
	private IUserManager $userManager;
216
	private ISecureRandom $random;
217
	private LoggerInterface $logger;
218
	private IEventDispatcher $dispatcher;
219
	private IConfig $config;
220
	private bool $legacyEndpoint;
221
	private string $dbObjectPropertiesTable = 'calendarobjects_props';
222
223
	public function __construct(IDBConnection $db,
224
								Principal $principalBackend,
225
								IUserManager $userManager,
226
								IGroupManager $groupManager,
227
								ISecureRandom $random,
228
								LoggerInterface $logger,
229
								IEventDispatcher $dispatcher,
230
								IConfig $config,
231
								bool $legacyEndpoint = false) {
232
		$this->db = $db;
233
		$this->principalBackend = $principalBackend;
234
		$this->userManager = $userManager;
235
		$this->calendarSharingBackend = new Backend($this->db, $this->userManager, $groupManager, $principalBackend, 'calendar');
236
		$this->random = $random;
237
		$this->logger = $logger;
238
		$this->dispatcher = $dispatcher;
239
		$this->config = $config;
240
		$this->legacyEndpoint = $legacyEndpoint;
241
	}
242
243
	/**
244
	 * Return the number of calendars for a principal
245
	 *
246
	 * By default this excludes the automatically generated birthday calendar
247
	 *
248
	 * @param $principalUri
249
	 * @param bool $excludeBirthday
250
	 * @return int
251
	 */
252
	public function getCalendarsForUserCount($principalUri, $excludeBirthday = true) {
253
		$principalUri = $this->convertPrincipal($principalUri, true);
254
		$query = $this->db->getQueryBuilder();
255
		$query->select($query->func()->count('*'))
256
			->from('calendars');
257
258
		if ($principalUri === '') {
259
			$query->where($query->expr()->emptyString('principaluri'));
260
		} else {
261
			$query->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
262
		}
263
264
		if ($excludeBirthday) {
265
			$query->andWhere($query->expr()->neq('uri', $query->createNamedParameter(BirthdayService::BIRTHDAY_CALENDAR_URI)));
266
		}
267
268
		$result = $query->executeQuery();
269
		$column = (int)$result->fetchOne();
270
		$result->closeCursor();
271
		return $column;
272
	}
273
274
	/**
275
	 * @return array{id: int, deleted_at: int}[]
276
	 */
277
	public function getDeletedCalendars(int $deletedBefore): array {
278
		$qb = $this->db->getQueryBuilder();
279
		$qb->select(['id', 'deleted_at'])
280
			->from('calendars')
281
			->where($qb->expr()->isNotNull('deleted_at'))
282
			->andWhere($qb->expr()->lt('deleted_at', $qb->createNamedParameter($deletedBefore)));
283
		$result = $qb->executeQuery();
284
		$raw = $result->fetchAll();
285
		$result->closeCursor();
286
		return array_map(function ($row) {
287
			return [
288
				'id' => (int) $row['id'],
289
				'deleted_at' => (int) $row['deleted_at'],
290
			];
291
		}, $raw);
292
	}
293
294
	/**
295
	 * Returns a list of calendars for a principal.
296
	 *
297
	 * Every project is an array with the following keys:
298
	 *  * id, a unique id that will be used by other functions to modify the
299
	 *    calendar. This can be the same as the uri or a database key.
300
	 *  * uri, which the basename of the uri with which the calendar is
301
	 *    accessed.
302
	 *  * principaluri. The owner of the calendar. Almost always the same as
303
	 *    principalUri passed to this method.
304
	 *
305
	 * Furthermore it can contain webdav properties in clark notation. A very
306
	 * common one is '{DAV:}displayname'.
307
	 *
308
	 * Many clients also require:
309
	 * {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
310
	 * For this property, you can just return an instance of
311
	 * Sabre\CalDAV\Property\SupportedCalendarComponentSet.
312
	 *
313
	 * If you return {http://sabredav.org/ns}read-only and set the value to 1,
314
	 * ACL will automatically be put in read-only mode.
315
	 *
316
	 * @param string $principalUri
317
	 * @return array
318
	 */
319
	public function getCalendarsForUser($principalUri) {
320
		$principalUriOriginal = $principalUri;
321
		$principalUri = $this->convertPrincipal($principalUri, true);
322
		$fields = array_column($this->propertyMap, 0);
323
		$fields[] = 'id';
324
		$fields[] = 'uri';
325
		$fields[] = 'synctoken';
326
		$fields[] = 'components';
327
		$fields[] = 'principaluri';
328
		$fields[] = 'transparent';
329
330
		// Making fields a comma-delimited list
331
		$query = $this->db->getQueryBuilder();
332
		$query->select($fields)
333
			->from('calendars')
334
			->orderBy('calendarorder', 'ASC');
335
336
		if ($principalUri === '') {
337
			$query->where($query->expr()->emptyString('principaluri'));
338
		} else {
339
			$query->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
340
		}
341
342
		$result = $query->executeQuery();
343
344
		$calendars = [];
345
		while ($row = $result->fetch()) {
346
			$row['principaluri'] = (string) $row['principaluri'];
347
			$components = [];
348
			if ($row['components']) {
349
				$components = explode(',',$row['components']);
350
			}
351
352
			$calendar = [
353
				'id' => $row['id'],
354
				'uri' => $row['uri'],
355
				'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
356
				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
357
				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
358
				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
359
				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
360
				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($principalUri, !$this->legacyEndpoint),
361
			];
362
363
			$calendar = $this->rowToCalendar($row, $calendar);
364
			$calendar = $this->addOwnerPrincipalToCalendar($calendar);
365
			$calendar = $this->addResourceTypeToCalendar($row, $calendar);
366
367
			if (!isset($calendars[$calendar['id']])) {
368
				$calendars[$calendar['id']] = $calendar;
369
			}
370
		}
371
		$result->closeCursor();
372
373
		// query for shared calendars
374
		$principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true);
375
		$principals = array_merge($principals, $this->principalBackend->getCircleMembership($principalUriOriginal));
376
377
		$principals[] = $principalUri;
378
379
		$fields = array_column($this->propertyMap, 0);
380
		$fields[] = 'a.id';
381
		$fields[] = 'a.uri';
382
		$fields[] = 'a.synctoken';
383
		$fields[] = 'a.components';
384
		$fields[] = 'a.principaluri';
385
		$fields[] = 'a.transparent';
386
		$fields[] = 's.access';
387
		$query = $this->db->getQueryBuilder();
388
		$query->select($fields)
389
			->from('dav_shares', 's')
390
			->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
391
			->where($query->expr()->in('s.principaluri', $query->createParameter('principaluri')))
392
			->andWhere($query->expr()->eq('s.type', $query->createParameter('type')))
393
			->setParameter('type', 'calendar')
394
			->setParameter('principaluri', $principals, \Doctrine\DBAL\Connection::PARAM_STR_ARRAY);
395
396
		$result = $query->executeQuery();
397
398
		$readOnlyPropertyName = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only';
399
		while ($row = $result->fetch()) {
400
			$row['principaluri'] = (string) $row['principaluri'];
401
			if ($row['principaluri'] === $principalUri) {
402
				continue;
403
			}
404
405
			$readOnly = (int) $row['access'] === Backend::ACCESS_READ;
406
			if (isset($calendars[$row['id']])) {
407
				if ($readOnly) {
408
					// New share can not have more permissions then the old one.
409
					continue;
410
				}
411
				if (isset($calendars[$row['id']][$readOnlyPropertyName]) &&
412
					$calendars[$row['id']][$readOnlyPropertyName] === 0) {
413
					// Old share is already read-write, no more permissions can be gained
414
					continue;
415
				}
416
			}
417
418
			[, $name] = Uri\split($row['principaluri']);
0 ignored issues
show
Bug introduced by
The call to split() has too few arguments starting with string. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

418
			[, $name] = /** @scrutinizer ignore-call */ Uri\split($row['principaluri']);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
419
			$uri = $row['uri'] . '_shared_by_' . $name;
420
			$row['displayname'] = $row['displayname'] . ' (' . $this->getUserDisplayName($name) . ')';
421
			$components = [];
422
			if ($row['components']) {
423
				$components = explode(',',$row['components']);
424
			}
425
			$calendar = [
426
				'id' => $row['id'],
427
				'uri' => $uri,
428
				'principaluri' => $this->convertPrincipal($principalUri, !$this->legacyEndpoint),
429
				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
430
				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
431
				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
432
				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp('transparent'),
433
				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
434
				$readOnlyPropertyName => $readOnly,
435
			];
436
437
			$calendar = $this->rowToCalendar($row, $calendar);
438
			$calendar = $this->addOwnerPrincipalToCalendar($calendar);
439
			$calendar = $this->addResourceTypeToCalendar($row, $calendar);
440
441
			$calendars[$calendar['id']] = $calendar;
442
		}
443
		$result->closeCursor();
444
445
		return array_values($calendars);
446
	}
447
448
	/**
449
	 * @param $principalUri
450
	 * @return array
451
	 */
452
	public function getUsersOwnCalendars($principalUri) {
453
		$principalUri = $this->convertPrincipal($principalUri, true);
454
		$fields = array_column($this->propertyMap, 0);
455
		$fields[] = 'id';
456
		$fields[] = 'uri';
457
		$fields[] = 'synctoken';
458
		$fields[] = 'components';
459
		$fields[] = 'principaluri';
460
		$fields[] = 'transparent';
461
		// Making fields a comma-delimited list
462
		$query = $this->db->getQueryBuilder();
463
		$query->select($fields)->from('calendars')
464
			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
465
			->orderBy('calendarorder', 'ASC');
466
		$stmt = $query->executeQuery();
467
		$calendars = [];
468
		while ($row = $stmt->fetch()) {
469
			$row['principaluri'] = (string) $row['principaluri'];
470
			$components = [];
471
			if ($row['components']) {
472
				$components = explode(',',$row['components']);
473
			}
474
			$calendar = [
475
				'id' => $row['id'],
476
				'uri' => $row['uri'],
477
				'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
478
				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
479
				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
480
				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
481
				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
482
			];
483
484
			$calendar = $this->rowToCalendar($row, $calendar);
485
			$calendar = $this->addOwnerPrincipalToCalendar($calendar);
486
			$calendar = $this->addResourceTypeToCalendar($row, $calendar);
487
488
			if (!isset($calendars[$calendar['id']])) {
489
				$calendars[$calendar['id']] = $calendar;
490
			}
491
		}
492
		$stmt->closeCursor();
493
		return array_values($calendars);
494
	}
495
496
497
	/**
498
	 * @param $uid
499
	 * @return string
500
	 */
501
	private function getUserDisplayName($uid) {
502
		if (!isset($this->userDisplayNames[$uid])) {
503
			$user = $this->userManager->get($uid);
504
505
			if ($user instanceof IUser) {
506
				$this->userDisplayNames[$uid] = $user->getDisplayName();
507
			} else {
508
				$this->userDisplayNames[$uid] = $uid;
509
			}
510
		}
511
512
		return $this->userDisplayNames[$uid];
513
	}
514
515
	/**
516
	 * @return array
517
	 */
518
	public function getPublicCalendars() {
519
		$fields = array_column($this->propertyMap, 0);
520
		$fields[] = 'a.id';
521
		$fields[] = 'a.uri';
522
		$fields[] = 'a.synctoken';
523
		$fields[] = 'a.components';
524
		$fields[] = 'a.principaluri';
525
		$fields[] = 'a.transparent';
526
		$fields[] = 's.access';
527
		$fields[] = 's.publicuri';
528
		$calendars = [];
529
		$query = $this->db->getQueryBuilder();
530
		$result = $query->select($fields)
531
			->from('dav_shares', 's')
532
			->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
533
			->where($query->expr()->in('s.access', $query->createNamedParameter(self::ACCESS_PUBLIC)))
534
			->andWhere($query->expr()->eq('s.type', $query->createNamedParameter('calendar')))
535
			->executeQuery();
536
537
		while ($row = $result->fetch()) {
538
			$row['principaluri'] = (string) $row['principaluri'];
539
			[, $name] = Uri\split($row['principaluri']);
0 ignored issues
show
Bug introduced by
The call to split() has too few arguments starting with string. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

539
			[, $name] = /** @scrutinizer ignore-call */ Uri\split($row['principaluri']);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
540
			$row['displayname'] = $row['displayname'] . "($name)";
541
			$components = [];
542
			if ($row['components']) {
543
				$components = explode(',',$row['components']);
544
			}
545
			$calendar = [
546
				'id' => $row['id'],
547
				'uri' => $row['publicuri'],
548
				'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
549
				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
550
				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
551
				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
552
				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
553
				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], $this->legacyEndpoint),
554
				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ,
555
				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}public' => (int)$row['access'] === self::ACCESS_PUBLIC,
556
			];
557
558
			$calendar = $this->rowToCalendar($row, $calendar);
559
			$calendar = $this->addOwnerPrincipalToCalendar($calendar);
560
			$calendar = $this->addResourceTypeToCalendar($row, $calendar);
561
562
			if (!isset($calendars[$calendar['id']])) {
563
				$calendars[$calendar['id']] = $calendar;
564
			}
565
		}
566
		$result->closeCursor();
567
568
		return array_values($calendars);
569
	}
570
571
	/**
572
	 * @param string $uri
573
	 * @return array
574
	 * @throws NotFound
575
	 */
576
	public function getPublicCalendar($uri) {
577
		$fields = array_column($this->propertyMap, 0);
578
		$fields[] = 'a.id';
579
		$fields[] = 'a.uri';
580
		$fields[] = 'a.synctoken';
581
		$fields[] = 'a.components';
582
		$fields[] = 'a.principaluri';
583
		$fields[] = 'a.transparent';
584
		$fields[] = 's.access';
585
		$fields[] = 's.publicuri';
586
		$query = $this->db->getQueryBuilder();
587
		$result = $query->select($fields)
588
			->from('dav_shares', 's')
589
			->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
590
			->where($query->expr()->in('s.access', $query->createNamedParameter(self::ACCESS_PUBLIC)))
591
			->andWhere($query->expr()->eq('s.type', $query->createNamedParameter('calendar')))
592
			->andWhere($query->expr()->eq('s.publicuri', $query->createNamedParameter($uri)))
593
			->executeQuery();
594
595
		$row = $result->fetch();
596
597
		$result->closeCursor();
598
599
		if ($row === false) {
600
			throw new NotFound('Node with name \'' . $uri . '\' could not be found');
601
		}
602
603
		$row['principaluri'] = (string) $row['principaluri'];
604
		[, $name] = Uri\split($row['principaluri']);
0 ignored issues
show
Bug introduced by
The call to split() has too few arguments starting with string. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

604
		[, $name] = /** @scrutinizer ignore-call */ Uri\split($row['principaluri']);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
605
		$row['displayname'] = $row['displayname'] . ' ' . "($name)";
606
		$components = [];
607
		if ($row['components']) {
608
			$components = explode(',',$row['components']);
609
		}
610
		$calendar = [
611
			'id' => $row['id'],
612
			'uri' => $row['publicuri'],
613
			'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
614
			'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
615
			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
616
			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
617
			'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
618
			'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
619
			'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ,
620
			'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}public' => (int)$row['access'] === self::ACCESS_PUBLIC,
621
		];
622
623
		$calendar = $this->rowToCalendar($row, $calendar);
624
		$calendar = $this->addOwnerPrincipalToCalendar($calendar);
625
		$calendar = $this->addResourceTypeToCalendar($row, $calendar);
626
627
		return $calendar;
628
	}
629
630
	/**
631
	 * @param string $principal
632
	 * @param string $uri
633
	 * @return array|null
634
	 */
635
	public function getCalendarByUri($principal, $uri) {
636
		$fields = array_column($this->propertyMap, 0);
637
		$fields[] = 'id';
638
		$fields[] = 'uri';
639
		$fields[] = 'synctoken';
640
		$fields[] = 'components';
641
		$fields[] = 'principaluri';
642
		$fields[] = 'transparent';
643
644
		// Making fields a comma-delimited list
645
		$query = $this->db->getQueryBuilder();
646
		$query->select($fields)->from('calendars')
647
			->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
648
			->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($principal)))
649
			->setMaxResults(1);
650
		$stmt = $query->executeQuery();
651
652
		$row = $stmt->fetch();
653
		$stmt->closeCursor();
654
		if ($row === false) {
655
			return null;
656
		}
657
658
		$row['principaluri'] = (string) $row['principaluri'];
659
		$components = [];
660
		if ($row['components']) {
661
			$components = explode(',',$row['components']);
662
		}
663
664
		$calendar = [
665
			'id' => $row['id'],
666
			'uri' => $row['uri'],
667
			'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
668
			'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
669
			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
670
			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
671
			'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
672
		];
673
674
		$calendar = $this->rowToCalendar($row, $calendar);
675
		$calendar = $this->addOwnerPrincipalToCalendar($calendar);
676
		$calendar = $this->addResourceTypeToCalendar($row, $calendar);
677
678
		return $calendar;
679
	}
680
681
	/**
682
	 * @return array{id: int, uri: string, '{http://calendarserver.org/ns/}getctag': string, '{http://sabredav.org/ns}sync-token': int, '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set': SupportedCalendarComponentSet, '{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp': ScheduleCalendarTransp }|null
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{id: int, uri: stri... ScheduleCalendarTransp at position 10 could not be parsed: Expected ':' at position 10, but found '''.
Loading history...
683
	 */
684
	public function getCalendarById(int $calendarId): ?array {
685
		$fields = array_column($this->propertyMap, 0);
686
		$fields[] = 'id';
687
		$fields[] = 'uri';
688
		$fields[] = 'synctoken';
689
		$fields[] = 'components';
690
		$fields[] = 'principaluri';
691
		$fields[] = 'transparent';
692
693
		// Making fields a comma-delimited list
694
		$query = $this->db->getQueryBuilder();
695
		$query->select($fields)->from('calendars')
696
			->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)))
697
			->setMaxResults(1);
698
		$stmt = $query->executeQuery();
699
700
		$row = $stmt->fetch();
701
		$stmt->closeCursor();
702
		if ($row === false) {
703
			return null;
704
		}
705
706
		$row['principaluri'] = (string) $row['principaluri'];
707
		$components = [];
708
		if ($row['components']) {
709
			$components = explode(',',$row['components']);
710
		}
711
712
		$calendar = [
713
			'id' => $row['id'],
714
			'uri' => $row['uri'],
715
			'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
716
			'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
717
			'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?? 0,
718
			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
719
			'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
720
		];
721
722
		$calendar = $this->rowToCalendar($row, $calendar);
723
		$calendar = $this->addOwnerPrincipalToCalendar($calendar);
724
		$calendar = $this->addResourceTypeToCalendar($row, $calendar);
725
726
		return $calendar;
727
	}
728
729
	/**
730
	 * @param $subscriptionId
731
	 */
732
	public function getSubscriptionById($subscriptionId) {
733
		$fields = array_column($this->subscriptionPropertyMap, 0);
734
		$fields[] = 'id';
735
		$fields[] = 'uri';
736
		$fields[] = 'source';
737
		$fields[] = 'synctoken';
738
		$fields[] = 'principaluri';
739
		$fields[] = 'lastmodified';
740
741
		$query = $this->db->getQueryBuilder();
742
		$query->select($fields)
743
			->from('calendarsubscriptions')
744
			->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
745
			->orderBy('calendarorder', 'asc');
746
		$stmt = $query->executeQuery();
747
748
		$row = $stmt->fetch();
749
		$stmt->closeCursor();
750
		if ($row === false) {
751
			return null;
752
		}
753
754
		$row['principaluri'] = (string) $row['principaluri'];
755
		$subscription = [
756
			'id' => $row['id'],
757
			'uri' => $row['uri'],
758
			'principaluri' => $row['principaluri'],
759
			'source' => $row['source'],
760
			'lastmodified' => $row['lastmodified'],
761
			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
762
			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
763
		];
764
765
		return $this->rowToSubscription($row, $subscription);
766
	}
767
768
	/**
769
	 * Creates a new calendar for a principal.
770
	 *
771
	 * If the creation was a success, an id must be returned that can be used to reference
772
	 * this calendar in other methods, such as updateCalendar.
773
	 *
774
	 * @param string $principalUri
775
	 * @param string $calendarUri
776
	 * @param array $properties
777
	 * @return int
778
	 *
779
	 * @throws CalendarException
780
	 */
781
	public function createCalendar($principalUri, $calendarUri, array $properties) {
782
		if (strlen($calendarUri) > 255) {
783
			throw new CalendarException('URI too long. Calendar not created');
784
		}
785
786
		$values = [
787
			'principaluri' => $this->convertPrincipal($principalUri, true),
788
			'uri' => $calendarUri,
789
			'synctoken' => 1,
790
			'transparent' => 0,
791
			'components' => 'VEVENT,VTODO',
792
			'displayname' => $calendarUri
793
		];
794
795
		// Default value
796
		$sccs = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set';
797
		if (isset($properties[$sccs])) {
798
			if (!($properties[$sccs] instanceof SupportedCalendarComponentSet)) {
799
				throw new DAV\Exception('The ' . $sccs . ' property must be of type: \Sabre\CalDAV\Property\SupportedCalendarComponentSet');
800
			}
801
			$values['components'] = implode(',',$properties[$sccs]->getValue());
802
		} elseif (isset($properties['components'])) {
803
			// Allow to provide components internally without having
804
			// to create a SupportedCalendarComponentSet object
805
			$values['components'] = $properties['components'];
806
		}
807
808
		$transp = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp';
809
		if (isset($properties[$transp])) {
810
			$values['transparent'] = (int) ($properties[$transp]->getValue() === 'transparent');
811
		}
812
813
		foreach ($this->propertyMap as $xmlName => [$dbName, $type]) {
814
			if (isset($properties[$xmlName])) {
815
				$values[$dbName] = $properties[$xmlName];
816
			}
817
		}
818
819
		[$calendarId, $calendarData] = $this->atomic(function() use ($values) {
820
			$query = $this->db->getQueryBuilder();
821
			$query->insert('calendars');
822
			foreach ($values as $column => $value) {
823
				$query->setValue($column, $query->createNamedParameter($value));
824
			}
825
			$query->executeStatement();
826
			$calendarId = $query->getLastInsertId();
827
828
			$calendarData = $this->getCalendarById($calendarId);
829
			return [$calendarId, $calendarData];
830
		}, $this->db);
831
832
		$this->dispatcher->dispatchTyped(new CalendarCreatedEvent((int)$calendarId, $calendarData));
833
834
		return $calendarId;
835
	}
836
837
	/**
838
	 * Updates properties for a calendar.
839
	 *
840
	 * The list of mutations is stored in a Sabre\DAV\PropPatch object.
841
	 * To do the actual updates, you must tell this object which properties
842
	 * you're going to process with the handle() method.
843
	 *
844
	 * Calling the handle method is like telling the PropPatch object "I
845
	 * promise I can handle updating this property".
846
	 *
847
	 * Read the PropPatch documentation for more info and examples.
848
	 *
849
	 * @param mixed $calendarId
850
	 * @param PropPatch $propPatch
851
	 * @return void
852
	 */
853
	public function updateCalendar($calendarId, PropPatch $propPatch) {
854
		$supportedProperties = array_keys($this->propertyMap);
855
		$supportedProperties[] = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp';
856
857
		$propPatch->handle($supportedProperties, function ($mutations) use ($calendarId) {
858
			$newValues = [];
859
			foreach ($mutations as $propertyName => $propertyValue) {
860
				switch ($propertyName) {
861
					case '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp':
862
						$fieldName = 'transparent';
863
						$newValues[$fieldName] = (int) ($propertyValue->getValue() === 'transparent');
864
						break;
865
					default:
866
						$fieldName = $this->propertyMap[$propertyName][0];
867
						$newValues[$fieldName] = $propertyValue;
868
						break;
869
				}
870
			}
871
			$query = $this->db->getQueryBuilder();
872
			$query->update('calendars');
873
			foreach ($newValues as $fieldName => $value) {
874
				$query->set($fieldName, $query->createNamedParameter($value));
875
			}
876
			$query->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)));
877
			$query->executeStatement();
878
879
			$this->addChange($calendarId, "", 2);
880
881
			$calendarData = $this->getCalendarById($calendarId);
882
			$shares = $this->getShares($calendarId);
883
			$this->dispatcher->dispatchTyped(new CalendarUpdatedEvent($calendarId, $calendarData, $shares, $mutations));
884
885
			return true;
886
		});
887
	}
888
889
	/**
890
	 * Delete a calendar and all it's objects
891
	 *
892
	 * @param mixed $calendarId
893
	 * @return void
894
	 */
895
	public function deleteCalendar($calendarId, bool $forceDeletePermanently = false) {
896
		// The calendar is deleted right away if this is either enforced by the caller
897
		// or the special contacts birthday calendar or when the preference of an empty
898
		// retention (0 seconds) is set, which signals a disabled trashbin.
899
		$calendarData = $this->getCalendarById($calendarId);
900
		$isBirthdayCalendar = isset($calendarData['uri']) && $calendarData['uri'] === BirthdayService::BIRTHDAY_CALENDAR_URI;
901
		$trashbinDisabled = $this->config->getAppValue(Application::APP_ID, RetentionService::RETENTION_CONFIG_KEY) === '0';
902
		if ($forceDeletePermanently || $isBirthdayCalendar || $trashbinDisabled) {
903
			$calendarData = $this->getCalendarById($calendarId);
904
			$shares = $this->getShares($calendarId);
905
906
			$qbDeleteCalendarObjectProps = $this->db->getQueryBuilder();
907
			$qbDeleteCalendarObjectProps->delete($this->dbObjectPropertiesTable)
908
				->where($qbDeleteCalendarObjectProps->expr()->eq('calendarid', $qbDeleteCalendarObjectProps->createNamedParameter($calendarId)))
909
				->andWhere($qbDeleteCalendarObjectProps->expr()->eq('calendartype', $qbDeleteCalendarObjectProps->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)))
910
				->executeStatement();
911
912
			$qbDeleteCalendarObjects = $this->db->getQueryBuilder();
913
			$qbDeleteCalendarObjects->delete('calendarobjects')
914
				->where($qbDeleteCalendarObjects->expr()->eq('calendarid', $qbDeleteCalendarObjects->createNamedParameter($calendarId)))
915
				->andWhere($qbDeleteCalendarObjects->expr()->eq('calendartype', $qbDeleteCalendarObjects->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)))
916
				->executeStatement();
917
918
			$qbDeleteCalendarChanges = $this->db->getQueryBuilder();
919
			$qbDeleteCalendarObjects->delete('calendarchanges')
920
				->where($qbDeleteCalendarChanges->expr()->eq('calendarid', $qbDeleteCalendarChanges->createNamedParameter($calendarId)))
921
				->andWhere($qbDeleteCalendarChanges->expr()->eq('calendartype', $qbDeleteCalendarChanges->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)))
922
				->executeStatement();
923
924
			$this->calendarSharingBackend->deleteAllShares($calendarId);
925
926
			$qbDeleteCalendar = $this->db->getQueryBuilder();
927
			$qbDeleteCalendarObjects->delete('calendars')
928
				->where($qbDeleteCalendar->expr()->eq('id', $qbDeleteCalendar->createNamedParameter($calendarId)))
929
				->executeStatement();
930
931
			// Only dispatch if we actually deleted anything
932
			if ($calendarData) {
933
				$this->dispatcher->dispatchTyped(new CalendarDeletedEvent($calendarId, $calendarData, $shares));
934
			}
935
		} else {
936
			$qbMarkCalendarDeleted = $this->db->getQueryBuilder();
937
			$qbMarkCalendarDeleted->update('calendars')
938
				->set('deleted_at', $qbMarkCalendarDeleted->createNamedParameter(time()))
939
				->where($qbMarkCalendarDeleted->expr()->eq('id', $qbMarkCalendarDeleted->createNamedParameter($calendarId)))
940
				->executeStatement();
941
942
			$calendarData = $this->getCalendarById($calendarId);
943
			$shares = $this->getShares($calendarId);
944
			if ($calendarData) {
945
				$this->dispatcher->dispatchTyped(new CalendarMovedToTrashEvent(
946
					$calendarId,
947
					$calendarData,
948
					$shares
949
				));
950
			}
951
		}
952
	}
953
954
	public function restoreCalendar(int $id): void {
955
		$qb = $this->db->getQueryBuilder();
956
		$update = $qb->update('calendars')
957
			->set('deleted_at', $qb->createNamedParameter(null))
958
			->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT));
959
		$update->executeStatement();
960
961
		$calendarData = $this->getCalendarById($id);
962
		$shares = $this->getShares($id);
963
		if ($calendarData === null) {
964
			throw new RuntimeException('Calendar data that was just written can\'t be read back. Check your database configuration.');
965
		}
966
		$this->dispatcher->dispatchTyped(new CalendarRestoredEvent(
967
			$id,
968
			$calendarData,
969
			$shares
970
		));
971
	}
972
973
	/**
974
	 * Delete all of an user's shares
975
	 *
976
	 * @param string $principaluri
977
	 * @return void
978
	 */
979
	public function deleteAllSharesByUser($principaluri) {
980
		$this->calendarSharingBackend->deleteAllSharesByUser($principaluri);
981
	}
982
983
	/**
984
	 * Returns all calendar objects within a calendar.
985
	 *
986
	 * Every item contains an array with the following keys:
987
	 *   * calendardata - The iCalendar-compatible calendar data
988
	 *   * uri - a unique key which will be used to construct the uri. This can
989
	 *     be any arbitrary string, but making sure it ends with '.ics' is a
990
	 *     good idea. This is only the basename, or filename, not the full
991
	 *     path.
992
	 *   * lastmodified - a timestamp of the last modification time
993
	 *   * etag - An arbitrary string, surrounded by double-quotes. (e.g.:
994
	 *   '"abcdef"')
995
	 *   * size - The size of the calendar objects, in bytes.
996
	 *   * component - optional, a string containing the type of object, such
997
	 *     as 'vevent' or 'vtodo'. If specified, this will be used to populate
998
	 *     the Content-Type header.
999
	 *
1000
	 * Note that the etag is optional, but it's highly encouraged to return for
1001
	 * speed reasons.
1002
	 *
1003
	 * The calendardata is also optional. If it's not returned
1004
	 * 'getCalendarObject' will be called later, which *is* expected to return
1005
	 * calendardata.
1006
	 *
1007
	 * If neither etag or size are specified, the calendardata will be
1008
	 * used/fetched to determine these numbers. If both are specified the
1009
	 * amount of times this is needed is reduced by a great degree.
1010
	 *
1011
	 * @param mixed $calendarId
1012
	 * @param int $calendarType
1013
	 * @return array
1014
	 */
1015
	public function getCalendarObjects($calendarId, $calendarType = self::CALENDAR_TYPE_CALENDAR):array {
1016
		$query = $this->db->getQueryBuilder();
1017
		$query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'componenttype', 'classification'])
1018
			->from('calendarobjects')
1019
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
1020
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)))
1021
			->andWhere($query->expr()->isNull('deleted_at'));
1022
		$stmt = $query->executeQuery();
1023
1024
		$result = [];
1025
		foreach ($stmt->fetchAll() as $row) {
1026
			$result[] = [
1027
				'id' => $row['id'],
1028
				'uri' => $row['uri'],
1029
				'lastmodified' => $row['lastmodified'],
1030
				'etag' => '"' . $row['etag'] . '"',
1031
				'calendarid' => $row['calendarid'],
1032
				'size' => (int)$row['size'],
1033
				'component' => strtolower($row['componenttype']),
1034
				'classification' => (int)$row['classification']
1035
			];
1036
		}
1037
		$stmt->closeCursor();
1038
1039
		return $result;
1040
	}
1041
1042
	public function getDeletedCalendarObjects(int $deletedBefore): array {
1043
		$query = $this->db->getQueryBuilder();
1044
		$query->select(['co.id', 'co.uri', 'co.lastmodified', 'co.etag', 'co.calendarid', 'co.calendartype', 'co.size', 'co.componenttype', 'co.classification', 'co.deleted_at'])
1045
			->from('calendarobjects', 'co')
1046
			->join('co', 'calendars', 'c', $query->expr()->eq('c.id', 'co.calendarid', IQueryBuilder::PARAM_INT))
1047
			->where($query->expr()->isNotNull('co.deleted_at'))
1048
			->andWhere($query->expr()->lt('co.deleted_at', $query->createNamedParameter($deletedBefore)));
1049
		$stmt = $query->executeQuery();
1050
1051
		$result = [];
1052
		foreach ($stmt->fetchAll() as $row) {
1053
			$result[] = [
1054
				'id' => $row['id'],
1055
				'uri' => $row['uri'],
1056
				'lastmodified' => $row['lastmodified'],
1057
				'etag' => '"' . $row['etag'] . '"',
1058
				'calendarid' => (int) $row['calendarid'],
1059
				'calendartype' => (int) $row['calendartype'],
1060
				'size' => (int) $row['size'],
1061
				'component' => strtolower($row['componenttype']),
1062
				'classification' => (int) $row['classification'],
1063
				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => $row['deleted_at'] === null ? $row['deleted_at'] : (int) $row['deleted_at'],
1064
			];
1065
		}
1066
		$stmt->closeCursor();
1067
1068
		return $result;
1069
	}
1070
1071
	/**
1072
	 * Return all deleted calendar objects by the given principal that are not
1073
	 * in deleted calendars.
1074
	 *
1075
	 * @param string $principalUri
1076
	 * @return array
1077
	 * @throws Exception
1078
	 */
1079
	public function getDeletedCalendarObjectsByPrincipal(string $principalUri): array {
1080
		$query = $this->db->getQueryBuilder();
1081
		$query->select(['co.id', 'co.uri', 'co.lastmodified', 'co.etag', 'co.calendarid', 'co.size', 'co.componenttype', 'co.classification', 'co.deleted_at'])
1082
			->selectAlias('c.uri', 'calendaruri')
1083
			->from('calendarobjects', 'co')
1084
			->join('co', 'calendars', 'c', $query->expr()->eq('c.id', 'co.calendarid', IQueryBuilder::PARAM_INT))
1085
			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
1086
			->andWhere($query->expr()->isNotNull('co.deleted_at'))
1087
			->andWhere($query->expr()->isNull('c.deleted_at'));
1088
		$stmt = $query->executeQuery();
1089
1090
		$result = [];
1091
		while ($row = $stmt->fetch()) {
1092
			$result[] = [
1093
				'id' => $row['id'],
1094
				'uri' => $row['uri'],
1095
				'lastmodified' => $row['lastmodified'],
1096
				'etag' => '"' . $row['etag'] . '"',
1097
				'calendarid' => $row['calendarid'],
1098
				'calendaruri' => $row['calendaruri'],
1099
				'size' => (int)$row['size'],
1100
				'component' => strtolower($row['componenttype']),
1101
				'classification' => (int)$row['classification'],
1102
				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => $row['deleted_at'] === null ? $row['deleted_at'] : (int) $row['deleted_at'],
1103
			];
1104
		}
1105
		$stmt->closeCursor();
1106
1107
		return $result;
1108
	}
1109
1110
	/**
1111
	 * Returns information from a single calendar object, based on it's object
1112
	 * uri.
1113
	 *
1114
	 * The object uri is only the basename, or filename and not a full path.
1115
	 *
1116
	 * The returned array must have the same keys as getCalendarObjects. The
1117
	 * 'calendardata' object is required here though, while it's not required
1118
	 * for getCalendarObjects.
1119
	 *
1120
	 * This method must return null if the object did not exist.
1121
	 *
1122
	 * @param mixed $calendarId
1123
	 * @param string $objectUri
1124
	 * @param int $calendarType
1125
	 * @return array|null
1126
	 */
1127
	public function getCalendarObject($calendarId, $objectUri, int $calendarType = self::CALENDAR_TYPE_CALENDAR) {
1128
		$query = $this->db->getQueryBuilder();
1129
		$query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification', 'deleted_at'])
1130
			->from('calendarobjects')
1131
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
1132
			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
1133
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)));
1134
		$stmt = $query->executeQuery();
1135
		$row = $stmt->fetch();
1136
		$stmt->closeCursor();
1137
1138
		if (!$row) {
1139
			return null;
1140
		}
1141
1142
		return [
1143
			'id' => $row['id'],
1144
			'uri' => $row['uri'],
1145
			'lastmodified' => $row['lastmodified'],
1146
			'etag' => '"' . $row['etag'] . '"',
1147
			'calendarid' => $row['calendarid'],
1148
			'size' => (int)$row['size'],
1149
			'calendardata' => $this->readBlob($row['calendardata']),
1150
			'component' => strtolower($row['componenttype']),
1151
			'classification' => (int)$row['classification'],
1152
			'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => $row['deleted_at'] === null ? $row['deleted_at'] : (int) $row['deleted_at'],
1153
		];
1154
	}
1155
1156
	/**
1157
	 * Returns a list of calendar objects.
1158
	 *
1159
	 * This method should work identical to getCalendarObject, but instead
1160
	 * return all the calendar objects in the list as an array.
1161
	 *
1162
	 * If the backend supports this, it may allow for some speed-ups.
1163
	 *
1164
	 * @param mixed $calendarId
1165
	 * @param string[] $uris
1166
	 * @param int $calendarType
1167
	 * @return array
1168
	 */
1169
	public function getMultipleCalendarObjects($calendarId, array $uris, $calendarType = self::CALENDAR_TYPE_CALENDAR):array {
1170
		if (empty($uris)) {
1171
			return [];
1172
		}
1173
1174
		$chunks = array_chunk($uris, 100);
1175
		$objects = [];
1176
1177
		$query = $this->db->getQueryBuilder();
1178
		$query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification'])
1179
			->from('calendarobjects')
1180
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
1181
			->andWhere($query->expr()->in('uri', $query->createParameter('uri')))
1182
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)))
1183
			->andWhere($query->expr()->isNull('deleted_at'));
1184
1185
		foreach ($chunks as $uris) {
1186
			$query->setParameter('uri', $uris, IQueryBuilder::PARAM_STR_ARRAY);
1187
			$result = $query->executeQuery();
1188
1189
			while ($row = $result->fetch()) {
1190
				$objects[] = [
1191
					'id' => $row['id'],
1192
					'uri' => $row['uri'],
1193
					'lastmodified' => $row['lastmodified'],
1194
					'etag' => '"' . $row['etag'] . '"',
1195
					'calendarid' => $row['calendarid'],
1196
					'size' => (int)$row['size'],
1197
					'calendardata' => $this->readBlob($row['calendardata']),
1198
					'component' => strtolower($row['componenttype']),
1199
					'classification' => (int)$row['classification']
1200
				];
1201
			}
1202
			$result->closeCursor();
1203
		}
1204
1205
		return $objects;
1206
	}
1207
1208
	/**
1209
	 * Creates a new calendar object.
1210
	 *
1211
	 * The object uri is only the basename, or filename and not a full path.
1212
	 *
1213
	 * It is possible return an etag from this function, which will be used in
1214
	 * the response to this PUT request. Note that the ETag must be surrounded
1215
	 * by double-quotes.
1216
	 *
1217
	 * However, you should only really return this ETag if you don't mangle the
1218
	 * calendar-data. If the result of a subsequent GET to this object is not
1219
	 * the exact same as this request body, you should omit the ETag.
1220
	 *
1221
	 * @param mixed $calendarId
1222
	 * @param string $objectUri
1223
	 * @param string $calendarData
1224
	 * @param int $calendarType
1225
	 * @return string
1226
	 */
1227
	public function createCalendarObject($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
1228
		$extraData = $this->getDenormalizedData($calendarData);
1229
1230
		// Try to detect duplicates
1231
		$qb = $this->db->getQueryBuilder();
1232
		$qb->select($qb->func()->count('*'))
1233
			->from('calendarobjects')
1234
			->where($qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)))
1235
			->andWhere($qb->expr()->eq('uid', $qb->createNamedParameter($extraData['uid'])))
1236
			->andWhere($qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType)))
1237
			->andWhere($qb->expr()->isNull('deleted_at'));
1238
		$result = $qb->executeQuery();
1239
		$count = (int) $result->fetchOne();
1240
		$result->closeCursor();
1241
1242
		if ($count !== 0) {
1243
			throw new BadRequest('Calendar object with uid already exists in this calendar collection.');
1244
		}
1245
		// For a more specific error message we also try to explicitly look up the UID but as a deleted entry
1246
		$qbDel = $this->db->getQueryBuilder();
1247
		$qbDel->select($qb->func()->count('*'))
1248
			->from('calendarobjects')
1249
			->where($qbDel->expr()->eq('calendarid', $qbDel->createNamedParameter($calendarId)))
1250
			->andWhere($qbDel->expr()->eq('uid', $qbDel->createNamedParameter($extraData['uid'])))
1251
			->andWhere($qbDel->expr()->eq('calendartype', $qbDel->createNamedParameter($calendarType)))
1252
			->andWhere($qbDel->expr()->isNotNull('deleted_at'));
1253
		$result = $qbDel->executeQuery();
1254
		$count = (int) $result->fetchOne();
1255
		$result->closeCursor();
1256
		if ($count !== 0) {
1257
			throw new BadRequest('Deleted calendar object with uid already exists in this calendar collection.');
1258
		}
1259
1260
		$query = $this->db->getQueryBuilder();
1261
		$query->insert('calendarobjects')
1262
			->values([
1263
				'calendarid' => $query->createNamedParameter($calendarId),
1264
				'uri' => $query->createNamedParameter($objectUri),
1265
				'calendardata' => $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB),
1266
				'lastmodified' => $query->createNamedParameter(time()),
1267
				'etag' => $query->createNamedParameter($extraData['etag']),
1268
				'size' => $query->createNamedParameter($extraData['size']),
1269
				'componenttype' => $query->createNamedParameter($extraData['componentType']),
1270
				'firstoccurence' => $query->createNamedParameter($extraData['firstOccurence']),
1271
				'lastoccurence' => $query->createNamedParameter($extraData['lastOccurence']),
1272
				'classification' => $query->createNamedParameter($extraData['classification']),
1273
				'uid' => $query->createNamedParameter($extraData['uid']),
1274
				'calendartype' => $query->createNamedParameter($calendarType),
1275
			])
1276
			->executeStatement();
1277
1278
		$this->updateProperties($calendarId, $objectUri, $calendarData, $calendarType);
1279
		$this->addChange($calendarId, $objectUri, 1, $calendarType);
1280
1281
		$objectRow = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
1282
		assert($objectRow !== null);
1283
1284
		if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
1285
			$calendarRow = $this->getCalendarById($calendarId);
1286
			$shares = $this->getShares($calendarId);
1287
1288
			$this->dispatcher->dispatchTyped(new CalendarObjectCreatedEvent($calendarId, $calendarRow, $shares, $objectRow));
1289
		} else {
1290
			$subscriptionRow = $this->getSubscriptionById($calendarId);
1291
1292
			$this->dispatcher->dispatchTyped(new CachedCalendarObjectCreatedEvent($calendarId, $subscriptionRow, [], $objectRow));
1293
		}
1294
1295
		return '"' . $extraData['etag'] . '"';
1296
	}
1297
1298
	/**
1299
	 * Updates an existing calendarobject, based on it's uri.
1300
	 *
1301
	 * The object uri is only the basename, or filename and not a full path.
1302
	 *
1303
	 * It is possible return an etag from this function, which will be used in
1304
	 * the response to this PUT request. Note that the ETag must be surrounded
1305
	 * by double-quotes.
1306
	 *
1307
	 * However, you should only really return this ETag if you don't mangle the
1308
	 * calendar-data. If the result of a subsequent GET to this object is not
1309
	 * the exact same as this request body, you should omit the ETag.
1310
	 *
1311
	 * @param mixed $calendarId
1312
	 * @param string $objectUri
1313
	 * @param string $calendarData
1314
	 * @param int $calendarType
1315
	 * @return string
1316
	 */
1317
	public function updateCalendarObject($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
1318
		$extraData = $this->getDenormalizedData($calendarData);
1319
		$query = $this->db->getQueryBuilder();
1320
		$query->update('calendarobjects')
1321
				->set('calendardata', $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB))
1322
				->set('lastmodified', $query->createNamedParameter(time()))
1323
				->set('etag', $query->createNamedParameter($extraData['etag']))
1324
				->set('size', $query->createNamedParameter($extraData['size']))
1325
				->set('componenttype', $query->createNamedParameter($extraData['componentType']))
1326
				->set('firstoccurence', $query->createNamedParameter($extraData['firstOccurence']))
1327
				->set('lastoccurence', $query->createNamedParameter($extraData['lastOccurence']))
1328
				->set('classification', $query->createNamedParameter($extraData['classification']))
1329
				->set('uid', $query->createNamedParameter($extraData['uid']))
1330
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
1331
			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
1332
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)))
1333
			->executeStatement();
1334
1335
		$this->updateProperties($calendarId, $objectUri, $calendarData, $calendarType);
1336
		$this->addChange($calendarId, $objectUri, 2, $calendarType);
1337
1338
		$objectRow = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
1339
		if (is_array($objectRow)) {
1340
			if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
1341
				$calendarRow = $this->getCalendarById($calendarId);
1342
				$shares = $this->getShares($calendarId);
1343
1344
				$this->dispatcher->dispatchTyped(new CalendarObjectUpdatedEvent($calendarId, $calendarRow, $shares, $objectRow));
1345
			} else {
1346
				$subscriptionRow = $this->getSubscriptionById($calendarId);
1347
1348
				$this->dispatcher->dispatchTyped(new CachedCalendarObjectUpdatedEvent($calendarId, $subscriptionRow, [], $objectRow));
1349
			}
1350
		}
1351
1352
		return '"' . $extraData['etag'] . '"';
1353
	}
1354
1355
	/**
1356
	 * Moves a calendar object from calendar to calendar.
1357
	 *
1358
	 * @param int $sourceCalendarId
1359
	 * @param int $targetCalendarId
1360
	 * @param int $objectId
1361
	 * @param string $oldPrincipalUri
1362
	 * @param string $newPrincipalUri
1363
	 * @param int $calendarType
1364
	 * @return bool
1365
	 * @throws Exception
1366
	 */
1367
	public function moveCalendarObject(int $sourceCalendarId, int $targetCalendarId, int $objectId, string $oldPrincipalUri, string $newPrincipalUri, int $calendarType = self::CALENDAR_TYPE_CALENDAR): bool {
1368
		$object = $this->getCalendarObjectById($oldPrincipalUri, $objectId);
1369
		if (empty($object)) {
1370
			return false;
1371
		}
1372
1373
		$query = $this->db->getQueryBuilder();
1374
		$query->update('calendarobjects')
1375
			->set('calendarid', $query->createNamedParameter($targetCalendarId, IQueryBuilder::PARAM_INT))
1376
			->where($query->expr()->eq('id', $query->createNamedParameter($objectId, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT))
1377
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT))
1378
			->executeStatement();
1379
1380
		$this->purgeProperties($sourceCalendarId, $objectId);
1381
		$this->updateProperties($targetCalendarId, $object['uri'], $object['calendardata'], $calendarType);
1382
1383
		$this->addChange($sourceCalendarId, $object['uri'], 1, $calendarType);
1384
		$this->addChange($targetCalendarId, $object['uri'], 3, $calendarType);
1385
1386
		$object = $this->getCalendarObjectById($newPrincipalUri, $objectId);
1387
		// Calendar Object wasn't found - possibly because it was deleted in the meantime by a different client
1388
		if (empty($object)) {
1389
			return false;
1390
		}
1391
1392
		$targetCalendarRow = $this->getCalendarById($targetCalendarId);
1393
		// the calendar this event is being moved to does not exist any longer
1394
		if (empty($targetCalendarRow)) {
1395
			return false;
1396
		}
1397
1398
		if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
1399
			$sourceShares = $this->getShares($sourceCalendarId);
1400
			$targetShares = $this->getShares($targetCalendarId);
1401
			$sourceCalendarRow = $this->getCalendarById($sourceCalendarId);
1402
			$this->dispatcher->dispatchTyped(new CalendarObjectMovedEvent($sourceCalendarId, $sourceCalendarRow, $targetCalendarId, $targetCalendarRow, $sourceShares, $targetShares, $object));
1403
		}
1404
		return true;
1405
	}
1406
1407
1408
	/**
1409
	 * @param int $calendarObjectId
1410
	 * @param int $classification
1411
	 */
1412
	public function setClassification($calendarObjectId, $classification) {
1413
		if (!in_array($classification, [
1414
			self::CLASSIFICATION_PUBLIC, self::CLASSIFICATION_PRIVATE, self::CLASSIFICATION_CONFIDENTIAL
1415
		])) {
1416
			throw new \InvalidArgumentException();
1417
		}
1418
		$query = $this->db->getQueryBuilder();
1419
		$query->update('calendarobjects')
1420
			->set('classification', $query->createNamedParameter($classification))
1421
			->where($query->expr()->eq('id', $query->createNamedParameter($calendarObjectId)))
1422
			->executeStatement();
1423
	}
1424
1425
	/**
1426
	 * Deletes an existing calendar object.
1427
	 *
1428
	 * The object uri is only the basename, or filename and not a full path.
1429
	 *
1430
	 * @param mixed $calendarId
1431
	 * @param string $objectUri
1432
	 * @param int $calendarType
1433
	 * @param bool $forceDeletePermanently
1434
	 * @return void
1435
	 */
1436
	public function deleteCalendarObject($calendarId, $objectUri, $calendarType = self::CALENDAR_TYPE_CALENDAR, bool $forceDeletePermanently = false) {
1437
		$data = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
1438
1439
		if ($data === null) {
1440
			// Nothing to delete
1441
			return;
1442
		}
1443
1444
		if ($forceDeletePermanently || $this->config->getAppValue(Application::APP_ID, RetentionService::RETENTION_CONFIG_KEY) === '0') {
1445
			$stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ? AND `uri` = ? AND `calendartype` = ?');
1446
			$stmt->execute([$calendarId, $objectUri, $calendarType]);
1447
1448
			$this->purgeProperties($calendarId, $data['id']);
1449
1450
			if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
1451
				$calendarRow = $this->getCalendarById($calendarId);
1452
				$shares = $this->getShares($calendarId);
1453
1454
				$this->dispatcher->dispatchTyped(new CalendarObjectDeletedEvent($calendarId, $calendarRow, $shares, $data));
1455
			} else {
1456
				$subscriptionRow = $this->getSubscriptionById($calendarId);
1457
1458
				$this->dispatcher->dispatchTyped(new CachedCalendarObjectDeletedEvent($calendarId, $subscriptionRow, [], $data));
1459
			}
1460
		} else {
1461
			$pathInfo = pathinfo($data['uri']);
1462
			if (!empty($pathInfo['extension'])) {
1463
				// Append a suffix to "free" the old URI for recreation
1464
				$newUri = sprintf(
1465
					"%s-deleted.%s",
1466
					$pathInfo['filename'],
1467
					$pathInfo['extension']
1468
				);
1469
			} else {
1470
				$newUri = sprintf(
1471
					"%s-deleted",
1472
					$pathInfo['filename']
1473
				);
1474
			}
1475
1476
			// Try to detect conflicts before the DB does
1477
			// As unlikely as it seems, this can happen when the user imports, then deletes, imports and deletes again
1478
			$newObject = $this->getCalendarObject($calendarId, $newUri, $calendarType);
1479
			if ($newObject !== null) {
1480
				throw new Forbidden("A calendar object with URI $newUri already exists in calendar $calendarId, therefore this object can't be moved into the trashbin");
1481
			}
1482
1483
			$qb = $this->db->getQueryBuilder();
1484
			$markObjectDeletedQuery = $qb->update('calendarobjects')
1485
				->set('deleted_at', $qb->createNamedParameter(time(), IQueryBuilder::PARAM_INT))
1486
				->set('uri', $qb->createNamedParameter($newUri))
1487
				->where(
1488
					$qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)),
1489
					$qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT),
1490
					$qb->expr()->eq('uri', $qb->createNamedParameter($objectUri))
1491
				);
1492
			$markObjectDeletedQuery->executeStatement();
1493
1494
			$calendarData = $this->getCalendarById($calendarId);
1495
			if ($calendarData !== null) {
1496
				$this->dispatcher->dispatchTyped(
1497
					new CalendarObjectMovedToTrashEvent(
1498
						$calendarId,
1499
						$calendarData,
1500
						$this->getShares($calendarId),
1501
						$data
1502
					)
1503
				);
1504
			}
1505
		}
1506
1507
		$this->addChange($calendarId, $objectUri, 3, $calendarType);
1508
	}
1509
1510
	/**
1511
	 * @param mixed $objectData
1512
	 *
1513
	 * @throws Forbidden
1514
	 */
1515
	public function restoreCalendarObject(array $objectData): void {
1516
		$id = (int) $objectData['id'];
1517
		$restoreUri = str_replace("-deleted.ics", ".ics", $objectData['uri']);
1518
		$targetObject = $this->getCalendarObject(
1519
			$objectData['calendarid'],
1520
			$restoreUri
1521
		);
1522
		if ($targetObject !== null) {
1523
			throw new Forbidden("Can not restore calendar $id because a calendar object with the URI $restoreUri already exists");
1524
		}
1525
1526
		$qb = $this->db->getQueryBuilder();
1527
		$update = $qb->update('calendarobjects')
1528
			->set('uri', $qb->createNamedParameter($restoreUri))
1529
			->set('deleted_at', $qb->createNamedParameter(null))
1530
			->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT));
1531
		$update->executeStatement();
1532
1533
		// Make sure this change is tracked in the changes table
1534
		$qb2 = $this->db->getQueryBuilder();
1535
		$selectObject = $qb2->select('calendardata', 'uri', 'calendarid', 'calendartype')
1536
			->selectAlias('componenttype', 'component')
1537
			->from('calendarobjects')
1538
			->where($qb2->expr()->eq('id', $qb2->createNamedParameter($id, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT));
1539
		$result = $selectObject->executeQuery();
1540
		$row = $result->fetch();
1541
		$result->closeCursor();
1542
		if ($row === false) {
1543
			// Welp, this should possibly not have happened, but let's ignore
1544
			return;
1545
		}
1546
		$this->addChange($row['calendarid'], $row['uri'], 1, (int) $row['calendartype']);
1547
1548
		$calendarRow = $this->getCalendarById((int) $row['calendarid']);
1549
		if ($calendarRow === null) {
1550
			throw new RuntimeException('Calendar object data that was just written can\'t be read back. Check your database configuration.');
1551
		}
1552
		$this->dispatcher->dispatchTyped(
1553
			new CalendarObjectRestoredEvent(
1554
				(int) $objectData['calendarid'],
1555
				$calendarRow,
1556
				$this->getShares((int) $row['calendarid']),
1557
				$row
1558
			)
1559
		);
1560
	}
1561
1562
	/**
1563
	 * Performs a calendar-query on the contents of this calendar.
1564
	 *
1565
	 * The calendar-query is defined in RFC4791 : CalDAV. Using the
1566
	 * calendar-query it is possible for a client to request a specific set of
1567
	 * object, based on contents of iCalendar properties, date-ranges and
1568
	 * iCalendar component types (VTODO, VEVENT).
1569
	 *
1570
	 * This method should just return a list of (relative) urls that match this
1571
	 * query.
1572
	 *
1573
	 * The list of filters are specified as an array. The exact array is
1574
	 * documented by Sabre\CalDAV\CalendarQueryParser.
1575
	 *
1576
	 * Note that it is extremely likely that getCalendarObject for every path
1577
	 * returned from this method will be called almost immediately after. You
1578
	 * may want to anticipate this to speed up these requests.
1579
	 *
1580
	 * This method provides a default implementation, which parses *all* the
1581
	 * iCalendar objects in the specified calendar.
1582
	 *
1583
	 * This default may well be good enough for personal use, and calendars
1584
	 * that aren't very large. But if you anticipate high usage, big calendars
1585
	 * or high loads, you are strongly advised to optimize certain paths.
1586
	 *
1587
	 * The best way to do so is override this method and to optimize
1588
	 * specifically for 'common filters'.
1589
	 *
1590
	 * Requests that are extremely common are:
1591
	 *   * requests for just VEVENTS
1592
	 *   * requests for just VTODO
1593
	 *   * requests with a time-range-filter on either VEVENT or VTODO.
1594
	 *
1595
	 * ..and combinations of these requests. It may not be worth it to try to
1596
	 * handle every possible situation and just rely on the (relatively
1597
	 * easy to use) CalendarQueryValidator to handle the rest.
1598
	 *
1599
	 * Note that especially time-range-filters may be difficult to parse. A
1600
	 * time-range filter specified on a VEVENT must for instance also handle
1601
	 * recurrence rules correctly.
1602
	 * A good example of how to interpret all these filters can also simply
1603
	 * be found in Sabre\CalDAV\CalendarQueryFilter. This class is as correct
1604
	 * as possible, so it gives you a good idea on what type of stuff you need
1605
	 * to think of.
1606
	 *
1607
	 * @param mixed $calendarId
1608
	 * @param array $filters
1609
	 * @param int $calendarType
1610
	 * @return array
1611
	 */
1612
	public function calendarQuery($calendarId, array $filters, $calendarType = self::CALENDAR_TYPE_CALENDAR):array {
1613
		$componentType = null;
1614
		$requirePostFilter = true;
1615
		$timeRange = null;
1616
1617
		// if no filters were specified, we don't need to filter after a query
1618
		if (!$filters['prop-filters'] && !$filters['comp-filters']) {
1619
			$requirePostFilter = false;
1620
		}
1621
1622
		// Figuring out if there's a component filter
1623
		if (count($filters['comp-filters']) > 0 && !$filters['comp-filters'][0]['is-not-defined']) {
1624
			$componentType = $filters['comp-filters'][0]['name'];
1625
1626
			// Checking if we need post-filters
1627
			if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['time-range'] && !$filters['comp-filters'][0]['prop-filters']) {
1628
				$requirePostFilter = false;
1629
			}
1630
			// There was a time-range filter
1631
			if ($componentType === 'VEVENT' && isset($filters['comp-filters'][0]['time-range']) && is_array($filters['comp-filters'][0]['time-range'])) {
1632
				$timeRange = $filters['comp-filters'][0]['time-range'];
1633
1634
				// If start time OR the end time is not specified, we can do a
1635
				// 100% accurate mysql query.
1636
				if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['prop-filters'] && (!$timeRange['start'] || !$timeRange['end'])) {
1637
					$requirePostFilter = false;
1638
				}
1639
			}
1640
		}
1641
		$columns = ['uri'];
1642
		if ($requirePostFilter) {
1643
			$columns = ['uri', 'calendardata'];
1644
		}
1645
		$query = $this->db->getQueryBuilder();
1646
		$query->select($columns)
1647
			->from('calendarobjects')
1648
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
1649
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)))
1650
			->andWhere($query->expr()->isNull('deleted_at'));
1651
1652
		if ($componentType) {
1653
			$query->andWhere($query->expr()->eq('componenttype', $query->createNamedParameter($componentType)));
1654
		}
1655
1656
		if ($timeRange && $timeRange['start']) {
1657
			$query->andWhere($query->expr()->gt('lastoccurence', $query->createNamedParameter($timeRange['start']->getTimeStamp())));
1658
		}
1659
		if ($timeRange && $timeRange['end']) {
1660
			$query->andWhere($query->expr()->lt('firstoccurence', $query->createNamedParameter($timeRange['end']->getTimeStamp())));
1661
		}
1662
1663
		$stmt = $query->executeQuery();
1664
1665
		$result = [];
1666
		while ($row = $stmt->fetch()) {
1667
			if ($requirePostFilter) {
1668
				// validateFilterForObject will parse the calendar data
1669
				// catch parsing errors
1670
				try {
1671
					$matches = $this->validateFilterForObject($row, $filters);
1672
				} catch (ParseException $ex) {
1673
					$this->logger->error('Caught parsing exception for calendar data. This usually indicates invalid calendar data. calendar-id:'.$calendarId.' uri:'.$row['uri'], [
1674
						'app' => 'dav',
1675
						'exception' => $ex,
1676
					]);
1677
					continue;
1678
				} catch (InvalidDataException $ex) {
1679
					$this->logger->error('Caught invalid data exception for calendar data. This usually indicates invalid calendar data. calendar-id:'.$calendarId.' uri:'.$row['uri'], [
1680
						'app' => 'dav',
1681
						'exception' => $ex,
1682
					]);
1683
					continue;
1684
				}
1685
1686
				if (!$matches) {
1687
					continue;
1688
				}
1689
			}
1690
			$result[] = $row['uri'];
1691
		}
1692
1693
		return $result;
1694
	}
1695
1696
	/**
1697
	 * custom Nextcloud search extension for CalDAV
1698
	 *
1699
	 * TODO - this should optionally cover cached calendar objects as well
1700
	 *
1701
	 * @param string $principalUri
1702
	 * @param array $filters
1703
	 * @param integer|null $limit
1704
	 * @param integer|null $offset
1705
	 * @return array
1706
	 */
1707
	public function calendarSearch($principalUri, array $filters, $limit = null, $offset = null) {
1708
		$calendars = $this->getCalendarsForUser($principalUri);
1709
		$ownCalendars = [];
1710
		$sharedCalendars = [];
1711
1712
		$uriMapper = [];
1713
1714
		foreach ($calendars as $calendar) {
1715
			if ($calendar['{http://owncloud.org/ns}owner-principal'] === $principalUri) {
1716
				$ownCalendars[] = $calendar['id'];
1717
			} else {
1718
				$sharedCalendars[] = $calendar['id'];
1719
			}
1720
			$uriMapper[$calendar['id']] = $calendar['uri'];
1721
		}
1722
		if (count($ownCalendars) === 0 && count($sharedCalendars) === 0) {
1723
			return [];
1724
		}
1725
1726
		$query = $this->db->getQueryBuilder();
1727
		// Calendar id expressions
1728
		$calendarExpressions = [];
1729
		foreach ($ownCalendars as $id) {
1730
			$calendarExpressions[] = $query->expr()->andX(
1731
				$query->expr()->eq('c.calendarid',
1732
					$query->createNamedParameter($id)),
1733
				$query->expr()->eq('c.calendartype',
1734
						$query->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
1735
		}
1736
		foreach ($sharedCalendars as $id) {
1737
			$calendarExpressions[] = $query->expr()->andX(
1738
				$query->expr()->eq('c.calendarid',
1739
					$query->createNamedParameter($id)),
1740
				$query->expr()->eq('c.classification',
1741
					$query->createNamedParameter(self::CLASSIFICATION_PUBLIC)),
1742
				$query->expr()->eq('c.calendartype',
1743
					$query->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
1744
		}
1745
1746
		if (count($calendarExpressions) === 1) {
1747
			$calExpr = $calendarExpressions[0];
1748
		} else {
1749
			$calExpr = call_user_func_array([$query->expr(), 'orX'], $calendarExpressions);
1750
		}
1751
1752
		// Component expressions
1753
		$compExpressions = [];
1754
		foreach ($filters['comps'] as $comp) {
1755
			$compExpressions[] = $query->expr()
1756
				->eq('c.componenttype', $query->createNamedParameter($comp));
1757
		}
1758
1759
		if (count($compExpressions) === 1) {
1760
			$compExpr = $compExpressions[0];
1761
		} else {
1762
			$compExpr = call_user_func_array([$query->expr(), 'orX'], $compExpressions);
1763
		}
1764
1765
		if (!isset($filters['props'])) {
1766
			$filters['props'] = [];
1767
		}
1768
		if (!isset($filters['params'])) {
1769
			$filters['params'] = [];
1770
		}
1771
1772
		$propParamExpressions = [];
1773
		foreach ($filters['props'] as $prop) {
1774
			$propParamExpressions[] = $query->expr()->andX(
1775
				$query->expr()->eq('i.name', $query->createNamedParameter($prop)),
1776
				$query->expr()->isNull('i.parameter')
1777
			);
1778
		}
1779
		foreach ($filters['params'] as $param) {
1780
			$propParamExpressions[] = $query->expr()->andX(
1781
				$query->expr()->eq('i.name', $query->createNamedParameter($param['property'])),
1782
				$query->expr()->eq('i.parameter', $query->createNamedParameter($param['parameter']))
1783
			);
1784
		}
1785
1786
		if (count($propParamExpressions) === 1) {
1787
			$propParamExpr = $propParamExpressions[0];
1788
		} else {
1789
			$propParamExpr = call_user_func_array([$query->expr(), 'orX'], $propParamExpressions);
1790
		}
1791
1792
		$query->select(['c.calendarid', 'c.uri'])
1793
			->from($this->dbObjectPropertiesTable, 'i')
1794
			->join('i', 'calendarobjects', 'c', $query->expr()->eq('i.objectid', 'c.id'))
1795
			->where($calExpr)
1796
			->andWhere($compExpr)
1797
			->andWhere($propParamExpr)
1798
			->andWhere($query->expr()->iLike('i.value',
1799
				$query->createNamedParameter('%'.$this->db->escapeLikeParameter($filters['search-term']).'%')))
1800
			->andWhere($query->expr()->isNull('deleted_at'));
1801
1802
		if ($offset) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $offset of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1803
			$query->setFirstResult($offset);
1804
		}
1805
		if ($limit) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $limit of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1806
			$query->setMaxResults($limit);
1807
		}
1808
1809
		$stmt = $query->executeQuery();
1810
1811
		$result = [];
1812
		while ($row = $stmt->fetch()) {
1813
			$path = $uriMapper[$row['calendarid']] . '/' . $row['uri'];
1814
			if (!in_array($path, $result)) {
1815
				$result[] = $path;
1816
			}
1817
		}
1818
1819
		return $result;
1820
	}
1821
1822
	/**
1823
	 * used for Nextcloud's calendar API
1824
	 *
1825
	 * @param array $calendarInfo
1826
	 * @param string $pattern
1827
	 * @param array $searchProperties
1828
	 * @param array $options
1829
	 * @param integer|null $limit
1830
	 * @param integer|null $offset
1831
	 *
1832
	 * @return array
1833
	 */
1834
	public function search(array $calendarInfo, $pattern, array $searchProperties,
1835
						   array $options, $limit, $offset) {
1836
		$outerQuery = $this->db->getQueryBuilder();
1837
		$innerQuery = $this->db->getQueryBuilder();
1838
1839
		$innerQuery->selectDistinct('op.objectid')
1840
			->from($this->dbObjectPropertiesTable, 'op')
1841
			->andWhere($innerQuery->expr()->eq('op.calendarid',
1842
				$outerQuery->createNamedParameter($calendarInfo['id'])))
1843
			->andWhere($innerQuery->expr()->eq('op.calendartype',
1844
				$outerQuery->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
1845
1846
		// only return public items for shared calendars for now
1847
		if (isset($calendarInfo['{http://owncloud.org/ns}owner-principal']) === false || $calendarInfo['principaluri'] !== $calendarInfo['{http://owncloud.org/ns}owner-principal']) {
1848
			$innerQuery->andWhere($innerQuery->expr()->eq('c.classification',
1849
				$outerQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
1850
		}
1851
1852
		if (!empty($searchProperties)) {
1853
			$or = $innerQuery->expr()->orX();
1854
			foreach ($searchProperties as $searchProperty) {
1855
				$or->add($innerQuery->expr()->eq('op.name',
1856
					$outerQuery->createNamedParameter($searchProperty)));
1857
			}
1858
			$innerQuery->andWhere($or);
1859
		}
1860
1861
		if ($pattern !== '') {
1862
			$innerQuery->andWhere($innerQuery->expr()->iLike('op.value',
1863
				$outerQuery->createNamedParameter('%' .
1864
					$this->db->escapeLikeParameter($pattern) . '%')));
1865
		}
1866
1867
		$outerQuery->select('c.id', 'c.calendardata', 'c.componenttype', 'c.uid', 'c.uri')
1868
			->from('calendarobjects', 'c')
1869
			->where($outerQuery->expr()->isNull('deleted_at'));
1870
1871
		if (isset($options['timerange'])) {
1872
			if (isset($options['timerange']['start']) && $options['timerange']['start'] instanceof DateTimeInterface) {
1873
				$outerQuery->andWhere($outerQuery->expr()->gt('lastoccurence',
1874
					$outerQuery->createNamedParameter($options['timerange']['start']->getTimeStamp())));
1875
			}
1876
			if (isset($options['timerange']['end']) && $options['timerange']['end'] instanceof DateTimeInterface) {
1877
				$outerQuery->andWhere($outerQuery->expr()->lt('firstoccurence',
1878
					$outerQuery->createNamedParameter($options['timerange']['end']->getTimeStamp())));
1879
			}
1880
		}
1881
1882
		if(isset($options['uid'])) {
1883
			$outerQuery->andWhere($outerQuery->expr()->eq('uid', $outerQuery->createNamedParameter($options['uid'])));
1884
		}
1885
1886
		if (!empty($options['types'])) {
1887
			$or = $outerQuery->expr()->orX();
1888
			foreach ($options['types'] as $type) {
1889
				$or->add($outerQuery->expr()->eq('componenttype',
1890
					$outerQuery->createNamedParameter($type)));
1891
			}
1892
			$outerQuery->andWhere($or);
1893
		}
1894
1895
		$outerQuery->andWhere($outerQuery->expr()->in('c.id', $outerQuery->createFunction($innerQuery->getSQL())));
1896
1897
		if ($offset) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $offset of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1898
			$outerQuery->setFirstResult($offset);
1899
		}
1900
		if ($limit) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $limit of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1901
			$outerQuery->setMaxResults($limit);
1902
		}
1903
1904
		$result = $outerQuery->executeQuery();
1905
		$calendarObjects = array_filter($result->fetchAll(), function (array $row) use ($options) {
1906
			$start = $options['timerange']['start'] ?? null;
1907
			$end = $options['timerange']['end'] ?? null;
1908
1909
			if ($start === null || !($start instanceof DateTimeInterface) || $end === null || !($end instanceof DateTimeInterface)) {
1910
				// No filter required
1911
				return true;
1912
			}
1913
1914
			$isValid = $this->validateFilterForObject($row, [
1915
				'name' => 'VCALENDAR',
1916
				'comp-filters' => [
1917
					[
1918
						'name' => 'VEVENT',
1919
						'comp-filters' => [],
1920
						'prop-filters' => [],
1921
						'is-not-defined' => false,
1922
						'time-range' => [
1923
							'start' => $start,
1924
							'end' => $end,
1925
						],
1926
					],
1927
				],
1928
				'prop-filters' => [],
1929
				'is-not-defined' => false,
1930
				'time-range' => null,
1931
			]);
1932
			if (is_resource($row['calendardata'])) {
1933
				// Put the stream back to the beginning so it can be read another time
1934
				rewind($row['calendardata']);
1935
			}
1936
			return $isValid;
1937
		});
1938
		$result->closeCursor();
1939
1940
		return array_map(function ($o) {
1941
			$calendarData = Reader::read($o['calendardata']);
1942
			$comps = $calendarData->getComponents();
1943
			$objects = [];
1944
			$timezones = [];
1945
			foreach ($comps as $comp) {
1946
				if ($comp instanceof VTimeZone) {
1947
					$timezones[] = $comp;
1948
				} else {
1949
					$objects[] = $comp;
1950
				}
1951
			}
1952
1953
			return [
1954
				'id' => $o['id'],
1955
				'type' => $o['componenttype'],
1956
				'uid' => $o['uid'],
1957
				'uri' => $o['uri'],
1958
				'objects' => array_map(function ($c) {
1959
					return $this->transformSearchData($c);
1960
				}, $objects),
1961
				'timezones' => array_map(function ($c) {
1962
					return $this->transformSearchData($c);
1963
				}, $timezones),
1964
			];
1965
		}, $calendarObjects);
1966
	}
1967
1968
	/**
1969
	 * @param Component $comp
1970
	 * @return array
1971
	 */
1972
	private function transformSearchData(Component $comp) {
1973
		$data = [];
1974
		/** @var Component[] $subComponents */
1975
		$subComponents = $comp->getComponents();
1976
		/** @var Property[] $properties */
1977
		$properties = array_filter($comp->children(), function ($c) {
1978
			return $c instanceof Property;
1979
		});
1980
		$validationRules = $comp->getValidationRules();
1981
1982
		foreach ($subComponents as $subComponent) {
1983
			$name = $subComponent->name;
1984
			if (!isset($data[$name])) {
1985
				$data[$name] = [];
1986
			}
1987
			$data[$name][] = $this->transformSearchData($subComponent);
1988
		}
1989
1990
		foreach ($properties as $property) {
1991
			$name = $property->name;
1992
			if (!isset($validationRules[$name])) {
1993
				$validationRules[$name] = '*';
1994
			}
1995
1996
			$rule = $validationRules[$property->name];
1997
			if ($rule === '+' || $rule === '*') { // multiple
1998
				if (!isset($data[$name])) {
1999
					$data[$name] = [];
2000
				}
2001
2002
				$data[$name][] = $this->transformSearchProperty($property);
2003
			} else { // once
2004
				$data[$name] = $this->transformSearchProperty($property);
2005
			}
2006
		}
2007
2008
		return $data;
2009
	}
2010
2011
	/**
2012
	 * @param Property $prop
2013
	 * @return array
2014
	 */
2015
	private function transformSearchProperty(Property $prop) {
2016
		// No need to check Date, as it extends DateTime
2017
		if ($prop instanceof Property\ICalendar\DateTime) {
2018
			$value = $prop->getDateTime();
2019
		} else {
2020
			$value = $prop->getValue();
2021
		}
2022
2023
		return [
2024
			$value,
2025
			$prop->parameters()
2026
		];
2027
	}
2028
2029
	/**
2030
	 * @param string $principalUri
2031
	 * @param string $pattern
2032
	 * @param array $componentTypes
2033
	 * @param array $searchProperties
2034
	 * @param array $searchParameters
2035
	 * @param array $options
2036
	 * @return array
2037
	 */
2038
	public function searchPrincipalUri(string $principalUri,
2039
									   string $pattern,
2040
									   array $componentTypes,
2041
									   array $searchProperties,
2042
									   array $searchParameters,
2043
									   array $options = []): array {
2044
		$escapePattern = !\array_key_exists('escape_like_param', $options) || $options['escape_like_param'] !== false;
2045
2046
		$calendarObjectIdQuery = $this->db->getQueryBuilder();
2047
		$calendarOr = $calendarObjectIdQuery->expr()->orX();
2048
		$searchOr = $calendarObjectIdQuery->expr()->orX();
2049
2050
		// Fetch calendars and subscription
2051
		$calendars = $this->getCalendarsForUser($principalUri);
2052
		$subscriptions = $this->getSubscriptionsForUser($principalUri);
2053
		foreach ($calendars as $calendar) {
2054
			$calendarAnd = $calendarObjectIdQuery->expr()->andX();
2055
			$calendarAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int)$calendar['id'])));
2056
			$calendarAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendartype', $calendarObjectIdQuery->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
2057
2058
			// If it's shared, limit search to public events
2059
			if (isset($calendar['{http://owncloud.org/ns}owner-principal'])
2060
				&& $calendar['principaluri'] !== $calendar['{http://owncloud.org/ns}owner-principal']) {
2061
				$calendarAnd->add($calendarObjectIdQuery->expr()->eq('co.classification', $calendarObjectIdQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
2062
			}
2063
2064
			$calendarOr->add($calendarAnd);
2065
		}
2066
		foreach ($subscriptions as $subscription) {
2067
			$subscriptionAnd = $calendarObjectIdQuery->expr()->andX();
2068
			$subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int)$subscription['id'])));
2069
			$subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendartype', $calendarObjectIdQuery->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)));
2070
2071
			// If it's shared, limit search to public events
2072
			if (isset($subscription['{http://owncloud.org/ns}owner-principal'])
2073
				&& $subscription['principaluri'] !== $subscription['{http://owncloud.org/ns}owner-principal']) {
2074
				$subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('co.classification', $calendarObjectIdQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
2075
			}
2076
2077
			$calendarOr->add($subscriptionAnd);
2078
		}
2079
2080
		foreach ($searchProperties as $property) {
2081
			$propertyAnd = $calendarObjectIdQuery->expr()->andX();
2082
			$propertyAnd->add($calendarObjectIdQuery->expr()->eq('cob.name', $calendarObjectIdQuery->createNamedParameter($property, IQueryBuilder::PARAM_STR)));
2083
			$propertyAnd->add($calendarObjectIdQuery->expr()->isNull('cob.parameter'));
2084
2085
			$searchOr->add($propertyAnd);
2086
		}
2087
		foreach ($searchParameters as $property => $parameter) {
2088
			$parameterAnd = $calendarObjectIdQuery->expr()->andX();
2089
			$parameterAnd->add($calendarObjectIdQuery->expr()->eq('cob.name', $calendarObjectIdQuery->createNamedParameter($property, IQueryBuilder::PARAM_STR)));
2090
			$parameterAnd->add($calendarObjectIdQuery->expr()->eq('cob.parameter', $calendarObjectIdQuery->createNamedParameter($parameter, IQueryBuilder::PARAM_STR_ARRAY)));
2091
2092
			$searchOr->add($parameterAnd);
2093
		}
2094
2095
		if ($calendarOr->count() === 0) {
2096
			return [];
2097
		}
2098
		if ($searchOr->count() === 0) {
2099
			return [];
2100
		}
2101
2102
		$calendarObjectIdQuery->selectDistinct('cob.objectid')
2103
			->from($this->dbObjectPropertiesTable, 'cob')
2104
			->leftJoin('cob', 'calendarobjects', 'co', $calendarObjectIdQuery->expr()->eq('co.id', 'cob.objectid'))
2105
			->andWhere($calendarObjectIdQuery->expr()->in('co.componenttype', $calendarObjectIdQuery->createNamedParameter($componentTypes, IQueryBuilder::PARAM_STR_ARRAY)))
2106
			->andWhere($calendarOr)
2107
			->andWhere($searchOr)
2108
			->andWhere($calendarObjectIdQuery->expr()->isNull('deleted_at'));
2109
2110
		if ('' !== $pattern) {
2111
			if (!$escapePattern) {
2112
				$calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->ilike('cob.value', $calendarObjectIdQuery->createNamedParameter($pattern)));
2113
			} else {
2114
				$calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->ilike('cob.value', $calendarObjectIdQuery->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%')));
2115
			}
2116
		}
2117
2118
		if (isset($options['limit'])) {
2119
			$calendarObjectIdQuery->setMaxResults($options['limit']);
2120
		}
2121
		if (isset($options['offset'])) {
2122
			$calendarObjectIdQuery->setFirstResult($options['offset']);
2123
		}
2124
2125
		$result = $calendarObjectIdQuery->executeQuery();
2126
		$matches = $result->fetchAll();
2127
		$result->closeCursor();
2128
		$matches = array_map(static function (array $match):int {
2129
			return (int) $match['objectid'];
2130
		}, $matches);
2131
2132
		$query = $this->db->getQueryBuilder();
2133
		$query->select('calendardata', 'uri', 'calendarid', 'calendartype')
2134
			->from('calendarobjects')
2135
			->where($query->expr()->in('id', $query->createNamedParameter($matches, IQueryBuilder::PARAM_INT_ARRAY)));
2136
2137
		$result = $query->executeQuery();
2138
		$calendarObjects = $result->fetchAll();
2139
		$result->closeCursor();
2140
2141
		return array_map(function (array $array): array {
2142
			$array['calendarid'] = (int)$array['calendarid'];
2143
			$array['calendartype'] = (int)$array['calendartype'];
2144
			$array['calendardata'] = $this->readBlob($array['calendardata']);
2145
2146
			return $array;
2147
		}, $calendarObjects);
2148
	}
2149
2150
	/**
2151
	 * Searches through all of a users calendars and calendar objects to find
2152
	 * an object with a specific UID.
2153
	 *
2154
	 * This method should return the path to this object, relative to the
2155
	 * calendar home, so this path usually only contains two parts:
2156
	 *
2157
	 * calendarpath/objectpath.ics
2158
	 *
2159
	 * If the uid is not found, return null.
2160
	 *
2161
	 * This method should only consider * objects that the principal owns, so
2162
	 * any calendars owned by other principals that also appear in this
2163
	 * collection should be ignored.
2164
	 *
2165
	 * @param string $principalUri
2166
	 * @param string $uid
2167
	 * @return string|null
2168
	 */
2169
	public function getCalendarObjectByUID($principalUri, $uid) {
2170
		$query = $this->db->getQueryBuilder();
2171
		$query->selectAlias('c.uri', 'calendaruri')->selectAlias('co.uri', 'objecturi')
2172
			->from('calendarobjects', 'co')
2173
			->leftJoin('co', 'calendars', 'c', $query->expr()->eq('co.calendarid', 'c.id'))
2174
			->where($query->expr()->eq('c.principaluri', $query->createNamedParameter($principalUri)))
2175
			->andWhere($query->expr()->eq('co.uid', $query->createNamedParameter($uid)))
2176
			->andWhere($query->expr()->isNull('co.deleted_at'));
2177
		$stmt = $query->executeQuery();
2178
		$row = $stmt->fetch();
2179
		$stmt->closeCursor();
2180
		if ($row) {
2181
			return $row['calendaruri'] . '/' . $row['objecturi'];
2182
		}
2183
2184
		return null;
2185
	}
2186
2187
	public function getCalendarObjectById(string $principalUri, int $id): ?array {
2188
		$query = $this->db->getQueryBuilder();
2189
		$query->select(['co.id', 'co.uri', 'co.lastmodified', 'co.etag', 'co.calendarid', 'co.size', 'co.calendardata', 'co.componenttype', 'co.classification', 'co.deleted_at'])
2190
			->selectAlias('c.uri', 'calendaruri')
2191
			->from('calendarobjects', 'co')
2192
			->join('co', 'calendars', 'c', $query->expr()->eq('c.id', 'co.calendarid', IQueryBuilder::PARAM_INT))
2193
			->where($query->expr()->eq('c.principaluri', $query->createNamedParameter($principalUri)))
2194
			->andWhere($query->expr()->eq('co.id', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT));
2195
		$stmt = $query->executeQuery();
2196
		$row = $stmt->fetch();
2197
		$stmt->closeCursor();
2198
2199
		if (!$row) {
2200
			return null;
2201
		}
2202
2203
		return [
2204
			'id' => $row['id'],
2205
			'uri' => $row['uri'],
2206
			'lastmodified' => $row['lastmodified'],
2207
			'etag' => '"' . $row['etag'] . '"',
2208
			'calendarid' => $row['calendarid'],
2209
			'calendaruri' => $row['calendaruri'],
2210
			'size' => (int)$row['size'],
2211
			'calendardata' => $this->readBlob($row['calendardata']),
2212
			'component' => strtolower($row['componenttype']),
2213
			'classification' => (int)$row['classification'],
2214
			'deleted_at' => isset($row['deleted_at']) ? ((int) $row['deleted_at']) : null,
2215
		];
2216
	}
2217
2218
	/**
2219
	 * The getChanges method returns all the changes that have happened, since
2220
	 * the specified syncToken in the specified calendar.
2221
	 *
2222
	 * This function should return an array, such as the following:
2223
	 *
2224
	 * [
2225
	 *   'syncToken' => 'The current synctoken',
2226
	 *   'added'   => [
2227
	 *      'new.txt',
2228
	 *   ],
2229
	 *   'modified'   => [
2230
	 *      'modified.txt',
2231
	 *   ],
2232
	 *   'deleted' => [
2233
	 *      'foo.php.bak',
2234
	 *      'old.txt'
2235
	 *   ]
2236
	 * );
2237
	 *
2238
	 * The returned syncToken property should reflect the *current* syncToken
2239
	 * of the calendar, as reported in the {http://sabredav.org/ns}sync-token
2240
	 * property This is * needed here too, to ensure the operation is atomic.
2241
	 *
2242
	 * If the $syncToken argument is specified as null, this is an initial
2243
	 * sync, and all members should be reported.
2244
	 *
2245
	 * The modified property is an array of nodenames that have changed since
2246
	 * the last token.
2247
	 *
2248
	 * The deleted property is an array with nodenames, that have been deleted
2249
	 * from collection.
2250
	 *
2251
	 * The $syncLevel argument is basically the 'depth' of the report. If it's
2252
	 * 1, you only have to report changes that happened only directly in
2253
	 * immediate descendants. If it's 2, it should also include changes from
2254
	 * the nodes below the child collections. (grandchildren)
2255
	 *
2256
	 * The $limit argument allows a client to specify how many results should
2257
	 * be returned at most. If the limit is not specified, it should be treated
2258
	 * as infinite.
2259
	 *
2260
	 * If the limit (infinite or not) is higher than you're willing to return,
2261
	 * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
2262
	 *
2263
	 * If the syncToken is expired (due to data cleanup) or unknown, you must
2264
	 * return null.
2265
	 *
2266
	 * The limit is 'suggestive'. You are free to ignore it.
2267
	 *
2268
	 * @param string $calendarId
2269
	 * @param string $syncToken
2270
	 * @param int $syncLevel
2271
	 * @param int|null $limit
2272
	 * @param int $calendarType
2273
	 * @return array
2274
	 */
2275
	public function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit = null, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
2276
		// Current synctoken
2277
		$qb = $this->db->getQueryBuilder();
2278
		$qb->select('synctoken')
2279
			->from('calendars')
2280
			->where(
2281
				$qb->expr()->eq('id', $qb->createNamedParameter($calendarId))
2282
			);
2283
		$stmt = $qb->executeQuery();
2284
		$currentToken = $stmt->fetchOne();
2285
2286
		if ($currentToken === false) {
2287
			return null;
2288
		}
2289
2290
		$result = [
2291
			'syncToken' => $currentToken,
2292
			'added' => [],
2293
			'modified' => [],
2294
			'deleted' => [],
2295
		];
2296
2297
		if ($syncToken) {
2298
			$qb = $this->db->getQueryBuilder();
2299
2300
			$qb->select('uri', 'operation')
2301
				->from('calendarchanges')
2302
				->where(
2303
					$qb->expr()->andX(
2304
						$qb->expr()->gte('synctoken', $qb->createNamedParameter($syncToken)),
2305
						$qb->expr()->lt('synctoken', $qb->createNamedParameter($currentToken)),
2306
						$qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)),
2307
						$qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType))
2308
					)
2309
				)->orderBy('synctoken');
2310
			if (is_int($limit) && $limit > 0) {
2311
				$qb->setMaxResults($limit);
2312
			}
2313
2314
			// Fetching all changes
2315
			$stmt = $qb->executeQuery();
2316
			$changes = [];
2317
2318
			// This loop ensures that any duplicates are overwritten, only the
2319
			// last change on a node is relevant.
2320
			while ($row = $stmt->fetch()) {
2321
				$changes[$row['uri']] = $row['operation'];
2322
			}
2323
			$stmt->closeCursor();
2324
2325
			foreach ($changes as $uri => $operation) {
2326
				switch ($operation) {
2327
					case 1:
2328
						$result['added'][] = $uri;
2329
						break;
2330
					case 2:
2331
						$result['modified'][] = $uri;
2332
						break;
2333
					case 3:
2334
						$result['deleted'][] = $uri;
2335
						break;
2336
				}
2337
			}
2338
		} else {
2339
			// No synctoken supplied, this is the initial sync.
2340
			$qb = $this->db->getQueryBuilder();
2341
			$qb->select('uri')
2342
				->from('calendarobjects')
2343
				->where(
2344
					$qb->expr()->andX(
2345
						$qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)),
2346
						$qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType))
2347
					)
2348
				);
2349
			$stmt = $qb->executeQuery();
2350
			$result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
2351
			$stmt->closeCursor();
2352
		}
2353
		return $result;
2354
	}
2355
2356
	/**
2357
	 * Returns a list of subscriptions for a principal.
2358
	 *
2359
	 * Every subscription is an array with the following keys:
2360
	 *  * id, a unique id that will be used by other functions to modify the
2361
	 *    subscription. This can be the same as the uri or a database key.
2362
	 *  * uri. This is just the 'base uri' or 'filename' of the subscription.
2363
	 *  * principaluri. The owner of the subscription. Almost always the same as
2364
	 *    principalUri passed to this method.
2365
	 *
2366
	 * Furthermore, all the subscription info must be returned too:
2367
	 *
2368
	 * 1. {DAV:}displayname
2369
	 * 2. {http://apple.com/ns/ical/}refreshrate
2370
	 * 3. {http://calendarserver.org/ns/}subscribed-strip-todos (omit if todos
2371
	 *    should not be stripped).
2372
	 * 4. {http://calendarserver.org/ns/}subscribed-strip-alarms (omit if alarms
2373
	 *    should not be stripped).
2374
	 * 5. {http://calendarserver.org/ns/}subscribed-strip-attachments (omit if
2375
	 *    attachments should not be stripped).
2376
	 * 6. {http://calendarserver.org/ns/}source (Must be a
2377
	 *     Sabre\DAV\Property\Href).
2378
	 * 7. {http://apple.com/ns/ical/}calendar-color
2379
	 * 8. {http://apple.com/ns/ical/}calendar-order
2380
	 * 9. {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
2381
	 *    (should just be an instance of
2382
	 *    Sabre\CalDAV\Property\SupportedCalendarComponentSet, with a bunch of
2383
	 *    default components).
2384
	 *
2385
	 * @param string $principalUri
2386
	 * @return array
2387
	 */
2388
	public function getSubscriptionsForUser($principalUri) {
2389
		$fields = array_column($this->subscriptionPropertyMap, 0);
2390
		$fields[] = 'id';
2391
		$fields[] = 'uri';
2392
		$fields[] = 'source';
2393
		$fields[] = 'principaluri';
2394
		$fields[] = 'lastmodified';
2395
		$fields[] = 'synctoken';
2396
2397
		$query = $this->db->getQueryBuilder();
2398
		$query->select($fields)
2399
			->from('calendarsubscriptions')
2400
			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
2401
			->orderBy('calendarorder', 'asc');
2402
		$stmt = $query->executeQuery();
2403
2404
		$subscriptions = [];
2405
		while ($row = $stmt->fetch()) {
2406
			$subscription = [
2407
				'id' => $row['id'],
2408
				'uri' => $row['uri'],
2409
				'principaluri' => $row['principaluri'],
2410
				'source' => $row['source'],
2411
				'lastmodified' => $row['lastmodified'],
2412
2413
				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
2414
				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
2415
			];
2416
2417
			$subscriptions[] = $this->rowToSubscription($row, $subscription);
2418
		}
2419
2420
		return $subscriptions;
2421
	}
2422
2423
	/**
2424
	 * Creates a new subscription for a principal.
2425
	 *
2426
	 * If the creation was a success, an id must be returned that can be used to reference
2427
	 * this subscription in other methods, such as updateSubscription.
2428
	 *
2429
	 * @param string $principalUri
2430
	 * @param string $uri
2431
	 * @param array $properties
2432
	 * @return mixed
2433
	 */
2434
	public function createSubscription($principalUri, $uri, array $properties) {
2435
		if (!isset($properties['{http://calendarserver.org/ns/}source'])) {
2436
			throw new Forbidden('The {http://calendarserver.org/ns/}source property is required when creating subscriptions');
2437
		}
2438
2439
		$values = [
2440
			'principaluri' => $principalUri,
2441
			'uri' => $uri,
2442
			'source' => $properties['{http://calendarserver.org/ns/}source']->getHref(),
2443
			'lastmodified' => time(),
2444
		];
2445
2446
		$propertiesBoolean = ['striptodos', 'stripalarms', 'stripattachments'];
2447
2448
		foreach ($this->subscriptionPropertyMap as $xmlName => [$dbName, $type]) {
2449
			if (array_key_exists($xmlName, $properties)) {
2450
				$values[$dbName] = $properties[$xmlName];
2451
				if (in_array($dbName, $propertiesBoolean)) {
2452
					$values[$dbName] = true;
2453
				}
2454
			}
2455
		}
2456
2457
		[$subscriptionId, $subscriptionRow] = $this->atomic(function() use ($values) {
2458
			$valuesToInsert = [];
2459
			$query = $this->db->getQueryBuilder();
2460
			foreach (array_keys($values) as $name) {
2461
				$valuesToInsert[$name] = $query->createNamedParameter($values[$name]);
2462
			}
2463
			$query->insert('calendarsubscriptions')
2464
				->values($valuesToInsert)
2465
				->executeStatement();
2466
2467
			$subscriptionId = $query->getLastInsertId();
2468
2469
			$subscriptionRow = $this->getSubscriptionById($subscriptionId);
2470
			return [$subscriptionId, $subscriptionRow];
2471
		}, $this->db);
2472
2473
		$this->dispatcher->dispatchTyped(new SubscriptionCreatedEvent($subscriptionId, $subscriptionRow));
2474
2475
		return $subscriptionId;
2476
	}
2477
2478
	/**
2479
	 * Updates a subscription
2480
	 *
2481
	 * The list of mutations is stored in a Sabre\DAV\PropPatch object.
2482
	 * To do the actual updates, you must tell this object which properties
2483
	 * you're going to process with the handle() method.
2484
	 *
2485
	 * Calling the handle method is like telling the PropPatch object "I
2486
	 * promise I can handle updating this property".
2487
	 *
2488
	 * Read the PropPatch documentation for more info and examples.
2489
	 *
2490
	 * @param mixed $subscriptionId
2491
	 * @param PropPatch $propPatch
2492
	 * @return void
2493
	 */
2494
	public function updateSubscription($subscriptionId, PropPatch $propPatch) {
2495
		$supportedProperties = array_keys($this->subscriptionPropertyMap);
2496
		$supportedProperties[] = '{http://calendarserver.org/ns/}source';
2497
2498
		$propPatch->handle($supportedProperties, function ($mutations) use ($subscriptionId) {
2499
			$newValues = [];
2500
2501
			foreach ($mutations as $propertyName => $propertyValue) {
2502
				if ($propertyName === '{http://calendarserver.org/ns/}source') {
2503
					$newValues['source'] = $propertyValue->getHref();
2504
				} else {
2505
					$fieldName = $this->subscriptionPropertyMap[$propertyName][0];
2506
					$newValues[$fieldName] = $propertyValue;
2507
				}
2508
			}
2509
2510
			$query = $this->db->getQueryBuilder();
2511
			$query->update('calendarsubscriptions')
2512
				->set('lastmodified', $query->createNamedParameter(time()));
2513
			foreach ($newValues as $fieldName => $value) {
2514
				$query->set($fieldName, $query->createNamedParameter($value));
2515
			}
2516
			$query->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
2517
				->executeStatement();
2518
2519
			$subscriptionRow = $this->getSubscriptionById($subscriptionId);
2520
			$this->dispatcher->dispatchTyped(new SubscriptionUpdatedEvent((int)$subscriptionId, $subscriptionRow, [], $mutations));
2521
2522
			return true;
2523
		});
2524
	}
2525
2526
	/**
2527
	 * Deletes a subscription.
2528
	 *
2529
	 * @param mixed $subscriptionId
2530
	 * @return void
2531
	 */
2532
	public function deleteSubscription($subscriptionId) {
2533
		$subscriptionRow = $this->getSubscriptionById($subscriptionId);
2534
2535
		$query = $this->db->getQueryBuilder();
2536
		$query->delete('calendarsubscriptions')
2537
			->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
2538
			->executeStatement();
2539
2540
		$query = $this->db->getQueryBuilder();
2541
		$query->delete('calendarobjects')
2542
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2543
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2544
			->executeStatement();
2545
2546
		$query->delete('calendarchanges')
2547
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2548
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2549
			->executeStatement();
2550
2551
		$query->delete($this->dbObjectPropertiesTable)
2552
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2553
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2554
			->executeStatement();
2555
2556
		if ($subscriptionRow) {
2557
			$this->dispatcher->dispatchTyped(new SubscriptionDeletedEvent((int)$subscriptionId, $subscriptionRow, []));
2558
		}
2559
	}
2560
2561
	/**
2562
	 * Returns a single scheduling object for the inbox collection.
2563
	 *
2564
	 * The returned array should contain the following elements:
2565
	 *   * uri - A unique basename for the object. This will be used to
2566
	 *           construct a full uri.
2567
	 *   * calendardata - The iCalendar object
2568
	 *   * lastmodified - The last modification date. Can be an int for a unix
2569
	 *                    timestamp, or a PHP DateTime object.
2570
	 *   * etag - A unique token that must change if the object changed.
2571
	 *   * size - The size of the object, in bytes.
2572
	 *
2573
	 * @param string $principalUri
2574
	 * @param string $objectUri
2575
	 * @return array
2576
	 */
2577
	public function getSchedulingObject($principalUri, $objectUri) {
2578
		$query = $this->db->getQueryBuilder();
2579
		$stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size'])
2580
			->from('schedulingobjects')
2581
			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
2582
			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
2583
			->executeQuery();
2584
2585
		$row = $stmt->fetch();
2586
2587
		if (!$row) {
2588
			return null;
2589
		}
2590
2591
		return [
2592
			'uri' => $row['uri'],
2593
			'calendardata' => $row['calendardata'],
2594
			'lastmodified' => $row['lastmodified'],
2595
			'etag' => '"' . $row['etag'] . '"',
2596
			'size' => (int)$row['size'],
2597
		];
2598
	}
2599
2600
	/**
2601
	 * Returns all scheduling objects for the inbox collection.
2602
	 *
2603
	 * These objects should be returned as an array. Every item in the array
2604
	 * should follow the same structure as returned from getSchedulingObject.
2605
	 *
2606
	 * The main difference is that 'calendardata' is optional.
2607
	 *
2608
	 * @param string $principalUri
2609
	 * @return array
2610
	 */
2611
	public function getSchedulingObjects($principalUri) {
2612
		$query = $this->db->getQueryBuilder();
2613
		$stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size'])
2614
				->from('schedulingobjects')
2615
				->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
2616
				->executeQuery();
2617
2618
		$result = [];
2619
		foreach ($stmt->fetchAll() as $row) {
2620
			$result[] = [
2621
				'calendardata' => $row['calendardata'],
2622
				'uri' => $row['uri'],
2623
				'lastmodified' => $row['lastmodified'],
2624
				'etag' => '"' . $row['etag'] . '"',
2625
				'size' => (int)$row['size'],
2626
			];
2627
		}
2628
		$stmt->closeCursor();
2629
2630
		return $result;
2631
	}
2632
2633
	/**
2634
	 * Deletes a scheduling object from the inbox collection.
2635
	 *
2636
	 * @param string $principalUri
2637
	 * @param string $objectUri
2638
	 * @return void
2639
	 */
2640
	public function deleteSchedulingObject($principalUri, $objectUri) {
2641
		$query = $this->db->getQueryBuilder();
2642
		$query->delete('schedulingobjects')
2643
				->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
2644
				->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
2645
				->executeStatement();
2646
	}
2647
2648
	/**
2649
	 * Creates a new scheduling object. This should land in a users' inbox.
2650
	 *
2651
	 * @param string $principalUri
2652
	 * @param string $objectUri
2653
	 * @param string $objectData
2654
	 * @return void
2655
	 */
2656
	public function createSchedulingObject($principalUri, $objectUri, $objectData) {
2657
		$query = $this->db->getQueryBuilder();
2658
		$query->insert('schedulingobjects')
2659
			->values([
2660
				'principaluri' => $query->createNamedParameter($principalUri),
2661
				'calendardata' => $query->createNamedParameter($objectData, IQueryBuilder::PARAM_LOB),
2662
				'uri' => $query->createNamedParameter($objectUri),
2663
				'lastmodified' => $query->createNamedParameter(time()),
2664
				'etag' => $query->createNamedParameter(md5($objectData)),
2665
				'size' => $query->createNamedParameter(strlen($objectData))
2666
			])
2667
			->executeStatement();
2668
	}
2669
2670
	/**
2671
	 * Adds a change record to the calendarchanges table.
2672
	 *
2673
	 * @param mixed $calendarId
2674
	 * @param string $objectUri
2675
	 * @param int $operation 1 = add, 2 = modify, 3 = delete.
2676
	 * @param int $calendarType
2677
	 * @return void
2678
	 */
2679
	protected function addChange($calendarId, $objectUri, $operation, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
2680
		$table = $calendarType === self::CALENDAR_TYPE_CALENDAR ? 'calendars': 'calendarsubscriptions';
2681
2682
		$query = $this->db->getQueryBuilder();
2683
		$query->select('synctoken')
2684
			->from($table)
2685
			->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)));
2686
		$result = $query->executeQuery();
2687
		$syncToken = (int)$result->fetchOne();
2688
		$result->closeCursor();
2689
2690
		$query = $this->db->getQueryBuilder();
2691
		$query->insert('calendarchanges')
2692
			->values([
2693
				'uri' => $query->createNamedParameter($objectUri),
2694
				'synctoken' => $query->createNamedParameter($syncToken),
2695
				'calendarid' => $query->createNamedParameter($calendarId),
2696
				'operation' => $query->createNamedParameter($operation),
2697
				'calendartype' => $query->createNamedParameter($calendarType),
2698
			])
2699
			->executeStatement();
2700
2701
		$stmt = $this->db->prepare("UPDATE `*PREFIX*$table` SET `synctoken` = `synctoken` + 1 WHERE `id` = ?");
2702
		$stmt->execute([
2703
			$calendarId
2704
		]);
2705
	}
2706
2707
	/**
2708
	 * Parses some information from calendar objects, used for optimized
2709
	 * calendar-queries.
2710
	 *
2711
	 * Returns an array with the following keys:
2712
	 *   * etag - An md5 checksum of the object without the quotes.
2713
	 *   * size - Size of the object in bytes
2714
	 *   * componentType - VEVENT, VTODO or VJOURNAL
2715
	 *   * firstOccurence
2716
	 *   * lastOccurence
2717
	 *   * uid - value of the UID property
2718
	 *
2719
	 * @param string $calendarData
2720
	 * @return array
2721
	 */
2722
	public function getDenormalizedData($calendarData) {
2723
		$vObject = Reader::read($calendarData);
2724
		$vEvents = [];
2725
		$componentType = null;
2726
		$component = null;
2727
		$firstOccurrence = null;
2728
		$lastOccurrence = null;
2729
		$uid = null;
2730
		$classification = self::CLASSIFICATION_PUBLIC;
2731
		$hasDTSTART = false;
2732
		foreach ($vObject->getComponents() as $component) {
2733
			if ($component->name !== 'VTIMEZONE') {
2734
				// Finding all VEVENTs, and track them
2735
				if ($component->name === 'VEVENT') {
2736
					array_push($vEvents, $component);
2737
					if ($component->DTSTART) {
2738
						$hasDTSTART = true;
2739
					}
2740
				}
2741
				// Track first component type and uid
2742
				if ($uid === null) {
2743
					$componentType = $component->name;
2744
					$uid = (string)$component->UID;
2745
				}
2746
			}
2747
		}
2748
		if (!$componentType) {
2749
			throw new BadRequest('Calendar objects must have a VJOURNAL, VEVENT or VTODO component');
2750
		}
2751
2752
		if ($hasDTSTART) {
2753
			$component = $vEvents[0];
2754
2755
			// Finding the last occurrence is a bit harder
2756
			if (!isset($component->RRULE) && count($vEvents) === 1) {
2757
				$firstOccurrence = $component->DTSTART->getDateTime()->getTimeStamp();
2758
				if (isset($component->DTEND)) {
2759
					$lastOccurrence = $component->DTEND->getDateTime()->getTimeStamp();
2760
				} elseif (isset($component->DURATION)) {
2761
					$endDate = clone $component->DTSTART->getDateTime();
2762
					$endDate->add(DateTimeParser::parse($component->DURATION->getValue()));
2763
					$lastOccurrence = $endDate->getTimeStamp();
2764
				} elseif (!$component->DTSTART->hasTime()) {
2765
					$endDate = clone $component->DTSTART->getDateTime();
2766
					$endDate->modify('+1 day');
2767
					$lastOccurrence = $endDate->getTimeStamp();
2768
				} else {
2769
					$lastOccurrence = $firstOccurrence;
2770
				}
2771
			} else {
2772
				$it = new EventIterator($vEvents);
2773
				$maxDate = new DateTime(self::MAX_DATE);
2774
				$firstOccurrence = $it->getDtStart()->getTimestamp();
2775
				if ($it->isInfinite()) {
2776
					$lastOccurrence = $maxDate->getTimestamp();
2777
				} else {
2778
					$end = $it->getDtEnd();
2779
					while ($it->valid() && $end < $maxDate) {
2780
						$end = $it->getDtEnd();
2781
						$it->next();
2782
					}
2783
					$lastOccurrence = $end->getTimestamp();
2784
				}
2785
			}
2786
		}
2787
2788
		if ($component->CLASS) {
2789
			$classification = CalDavBackend::CLASSIFICATION_PRIVATE;
2790
			switch ($component->CLASS->getValue()) {
2791
				case 'PUBLIC':
2792
					$classification = CalDavBackend::CLASSIFICATION_PUBLIC;
2793
					break;
2794
				case 'CONFIDENTIAL':
2795
					$classification = CalDavBackend::CLASSIFICATION_CONFIDENTIAL;
2796
					break;
2797
			}
2798
		}
2799
		return [
2800
			'etag' => md5($calendarData),
2801
			'size' => strlen($calendarData),
2802
			'componentType' => $componentType,
2803
			'firstOccurence' => is_null($firstOccurrence) ? null : max(0, $firstOccurrence),
2804
			'lastOccurence' => $lastOccurrence,
2805
			'uid' => $uid,
2806
			'classification' => $classification
2807
		];
2808
	}
2809
2810
	/**
2811
	 * @param $cardData
2812
	 * @return bool|string
2813
	 */
2814
	private function readBlob($cardData) {
2815
		if (is_resource($cardData)) {
2816
			return stream_get_contents($cardData);
2817
		}
2818
2819
		return $cardData;
2820
	}
2821
2822
	/**
2823
	 * @param list<array{href: string, commonName: string, readOnly: bool}> $add
2824
	 * @param list<string> $remove
0 ignored issues
show
Bug introduced by
The type OCA\DAV\CalDAV\list was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
2825
	 */
2826
	public function updateShares(IShareable $shareable, array $add, array $remove): void {
2827
		$calendarId = $shareable->getResourceId();
2828
		$calendarRow = $this->getCalendarById($calendarId);
2829
		if ($calendarRow === null) {
2830
			throw new \RuntimeException('Trying to update shares for innexistant calendar: ' . $calendarId);
2831
		}
2832
		$oldShares = $this->getShares($calendarId);
2833
2834
		$this->calendarSharingBackend->updateShares($shareable, $add, $remove);
2835
2836
		$this->dispatcher->dispatchTyped(new CalendarShareUpdatedEvent($calendarId, $calendarRow, $oldShares, $add, $remove));
2837
	}
2838
2839
	/**
2840
	 * @return list<array{href: string, commonName: string, status: int, readOnly: bool, '{http://owncloud.org/ns}principal': string, '{http://owncloud.org/ns}group-share': bool}>
2841
	 */
2842
	public function getShares(int $resourceId): array {
2843
		return $this->calendarSharingBackend->getShares($resourceId);
2844
	}
2845
2846
	/**
2847
	 * @param boolean $value
2848
	 * @param \OCA\DAV\CalDAV\Calendar $calendar
2849
	 * @return string|null
2850
	 */
2851
	public function setPublishStatus($value, $calendar) {
2852
		$calendarId = $calendar->getResourceId();
2853
		$calendarData = $this->getCalendarById($calendarId);
2854
2855
		$query = $this->db->getQueryBuilder();
2856
		if ($value) {
2857
			$publicUri = $this->random->generate(16, ISecureRandom::CHAR_HUMAN_READABLE);
2858
			$query->insert('dav_shares')
2859
				->values([
2860
					'principaluri' => $query->createNamedParameter($calendar->getPrincipalURI()),
2861
					'type' => $query->createNamedParameter('calendar'),
2862
					'access' => $query->createNamedParameter(self::ACCESS_PUBLIC),
2863
					'resourceid' => $query->createNamedParameter($calendar->getResourceId()),
2864
					'publicuri' => $query->createNamedParameter($publicUri)
2865
				]);
2866
			$query->executeStatement();
2867
2868
			$this->dispatcher->dispatchTyped(new CalendarPublishedEvent($calendarId, $calendarData, $publicUri));
2869
			return $publicUri;
2870
		}
2871
		$query->delete('dav_shares')
2872
			->where($query->expr()->eq('resourceid', $query->createNamedParameter($calendar->getResourceId())))
2873
			->andWhere($query->expr()->eq('access', $query->createNamedParameter(self::ACCESS_PUBLIC)));
2874
		$query->executeStatement();
2875
2876
		$this->dispatcher->dispatchTyped(new CalendarUnpublishedEvent($calendarId, $calendarData));
2877
		return null;
2878
	}
2879
2880
	/**
2881
	 * @param \OCA\DAV\CalDAV\Calendar $calendar
2882
	 * @return mixed
2883
	 */
2884
	public function getPublishStatus($calendar) {
2885
		$query = $this->db->getQueryBuilder();
2886
		$result = $query->select('publicuri')
2887
			->from('dav_shares')
2888
			->where($query->expr()->eq('resourceid', $query->createNamedParameter($calendar->getResourceId())))
2889
			->andWhere($query->expr()->eq('access', $query->createNamedParameter(self::ACCESS_PUBLIC)))
2890
			->executeQuery();
2891
2892
		$row = $result->fetch();
2893
		$result->closeCursor();
2894
		return $row ? reset($row) : false;
2895
	}
2896
2897
	/**
2898
	 * @param int $resourceId
2899
	 * @param list<array{privilege: string, principal: string, protected: bool}> $acl
2900
	 * @return list<array{privilege: string, principal: string, protected: bool}>
2901
	 */
2902
	public function applyShareAcl(int $resourceId, array $acl): array {
2903
		return $this->calendarSharingBackend->applyShareAcl($resourceId, $acl);
2904
	}
2905
2906
	/**
2907
	 * update properties table
2908
	 *
2909
	 * @param int $calendarId
2910
	 * @param string $objectUri
2911
	 * @param string $calendarData
2912
	 * @param int $calendarType
2913
	 */
2914
	public function updateProperties($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
2915
		$objectId = $this->getCalendarObjectId($calendarId, $objectUri, $calendarType);
2916
2917
		try {
2918
			$vCalendar = $this->readCalendarData($calendarData);
2919
		} catch (\Exception $ex) {
2920
			return;
2921
		}
2922
2923
		$this->purgeProperties($calendarId, $objectId);
2924
2925
		$query = $this->db->getQueryBuilder();
2926
		$query->insert($this->dbObjectPropertiesTable)
2927
			->values(
2928
				[
2929
					'calendarid' => $query->createNamedParameter($calendarId),
2930
					'calendartype' => $query->createNamedParameter($calendarType),
2931
					'objectid' => $query->createNamedParameter($objectId),
2932
					'name' => $query->createParameter('name'),
2933
					'parameter' => $query->createParameter('parameter'),
2934
					'value' => $query->createParameter('value'),
2935
				]
2936
			);
2937
2938
		$indexComponents = ['VEVENT', 'VJOURNAL', 'VTODO'];
2939
		foreach ($vCalendar->getComponents() as $component) {
2940
			if (!in_array($component->name, $indexComponents)) {
2941
				continue;
2942
			}
2943
2944
			foreach ($component->children() as $property) {
2945
				if (in_array($property->name, self::INDEXED_PROPERTIES, true)) {
2946
					$value = $property->getValue();
2947
					// is this a shitty db?
2948
					if (!$this->db->supports4ByteText()) {
2949
						$value = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $value);
2950
					}
2951
					$value = mb_strcut($value, 0, 254);
2952
2953
					$query->setParameter('name', $property->name);
2954
					$query->setParameter('parameter', null);
2955
					$query->setParameter('value', $value);
2956
					$query->executeStatement();
2957
				}
2958
2959
				if (array_key_exists($property->name, self::$indexParameters)) {
2960
					$parameters = $property->parameters();
2961
					$indexedParametersForProperty = self::$indexParameters[$property->name];
2962
2963
					foreach ($parameters as $key => $value) {
2964
						if (in_array($key, $indexedParametersForProperty)) {
2965
							// is this a shitty db?
2966
							if ($this->db->supports4ByteText()) {
2967
								$value = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $value);
2968
							}
2969
2970
							$query->setParameter('name', $property->name);
2971
							$query->setParameter('parameter', mb_strcut($key, 0, 254));
2972
							$query->setParameter('value', mb_strcut($value, 0, 254));
2973
							$query->executeStatement();
2974
						}
2975
					}
2976
				}
2977
			}
2978
		}
2979
	}
2980
2981
	/**
2982
	 * deletes all birthday calendars
2983
	 */
2984
	public function deleteAllBirthdayCalendars() {
2985
		$query = $this->db->getQueryBuilder();
2986
		$result = $query->select(['id'])->from('calendars')
2987
			->where($query->expr()->eq('uri', $query->createNamedParameter(BirthdayService::BIRTHDAY_CALENDAR_URI)))
2988
			->executeQuery();
2989
2990
		$ids = $result->fetchAll();
2991
		$result->closeCursor();
2992
		foreach ($ids as $id) {
2993
			$this->deleteCalendar(
2994
				$id['id'],
2995
				true // No data to keep in the trashbin, if the user re-enables then we regenerate
2996
			);
2997
		}
2998
	}
2999
3000
	/**
3001
	 * @param $subscriptionId
3002
	 */
3003
	public function purgeAllCachedEventsForSubscription($subscriptionId) {
3004
		$query = $this->db->getQueryBuilder();
3005
		$query->select('uri')
3006
			->from('calendarobjects')
3007
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
3008
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)));
3009
		$stmt = $query->executeQuery();
3010
3011
		$uris = [];
3012
		foreach ($stmt->fetchAll() as $row) {
3013
			$uris[] = $row['uri'];
3014
		}
3015
		$stmt->closeCursor();
3016
3017
		$query = $this->db->getQueryBuilder();
3018
		$query->delete('calendarobjects')
3019
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
3020
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
3021
			->executeStatement();
3022
3023
		$query->delete('calendarchanges')
3024
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
3025
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
3026
			->executeStatement();
3027
3028
		$query->delete($this->dbObjectPropertiesTable)
3029
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
3030
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
3031
			->executeStatement();
3032
3033
		foreach ($uris as $uri) {
3034
			$this->addChange($subscriptionId, $uri, 3, self::CALENDAR_TYPE_SUBSCRIPTION);
3035
		}
3036
	}
3037
3038
	/**
3039
	 * Move a calendar from one user to another
3040
	 *
3041
	 * @param string $uriName
3042
	 * @param string $uriOrigin
3043
	 * @param string $uriDestination
3044
	 * @param string $newUriName (optional) the new uriName
3045
	 */
3046
	public function moveCalendar($uriName, $uriOrigin, $uriDestination, $newUriName = null) {
3047
		$query = $this->db->getQueryBuilder();
3048
		$query->update('calendars')
3049
			->set('principaluri', $query->createNamedParameter($uriDestination))
3050
			->set('uri', $query->createNamedParameter($newUriName ?: $uriName))
3051
			->where($query->expr()->eq('principaluri', $query->createNamedParameter($uriOrigin)))
3052
			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($uriName)))
3053
			->executeStatement();
3054
	}
3055
3056
	/**
3057
	 * read VCalendar data into a VCalendar object
3058
	 *
3059
	 * @param string $objectData
3060
	 * @return VCalendar
3061
	 */
3062
	protected function readCalendarData($objectData) {
3063
		return Reader::read($objectData);
3064
	}
3065
3066
	/**
3067
	 * delete all properties from a given calendar object
3068
	 *
3069
	 * @param int $calendarId
3070
	 * @param int $objectId
3071
	 */
3072
	protected function purgeProperties($calendarId, $objectId) {
3073
		$query = $this->db->getQueryBuilder();
3074
		$query->delete($this->dbObjectPropertiesTable)
3075
			->where($query->expr()->eq('objectid', $query->createNamedParameter($objectId)))
3076
			->andWhere($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)));
3077
		$query->executeStatement();
3078
	}
3079
3080
	/**
3081
	 * get ID from a given calendar object
3082
	 *
3083
	 * @param int $calendarId
3084
	 * @param string $uri
3085
	 * @param int $calendarType
3086
	 * @return int
3087
	 */
3088
	protected function getCalendarObjectId($calendarId, $uri, $calendarType):int {
3089
		$query = $this->db->getQueryBuilder();
3090
		$query->select('id')
3091
			->from('calendarobjects')
3092
			->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
3093
			->andWhere($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
3094
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)));
3095
3096
		$result = $query->executeQuery();
3097
		$objectIds = $result->fetch();
3098
		$result->closeCursor();
3099
3100
		if (!isset($objectIds['id'])) {
3101
			throw new \InvalidArgumentException('Calendarobject does not exists: ' . $uri);
3102
		}
3103
3104
		return (int)$objectIds['id'];
3105
	}
3106
3107
	/**
3108
	 * @throws \InvalidArgumentException
3109
	 */
3110
	public function pruneOutdatedSyncTokens(int $keep = 10_000): int {
0 ignored issues
show
Bug introduced by
The constant OCA\DAV\CalDAV\10_000 was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
3111
		if ($keep < 0) {
3112
			throw new \InvalidArgumentException();
3113
		}
3114
		$query = $this->db->getQueryBuilder();
3115
		$query->delete('calendarchanges')
3116
			->orderBy('id', 'DESC')
3117
			->setFirstResult($keep);
3118
		return $query->executeStatement();
3119
	}
3120
3121
	/**
3122
	 * return legacy endpoint principal name to new principal name
3123
	 *
3124
	 * @param $principalUri
3125
	 * @param $toV2
3126
	 * @return string
3127
	 */
3128
	private function convertPrincipal($principalUri, $toV2) {
3129
		if ($this->principalBackend->getPrincipalPrefix() === 'principals') {
3130
			[, $name] = Uri\split($principalUri);
0 ignored issues
show
Bug introduced by
The call to split() has too few arguments starting with string. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

3130
			[, $name] = /** @scrutinizer ignore-call */ Uri\split($principalUri);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
3131
			if ($toV2 === true) {
3132
				return "principals/users/$name";
3133
			}
3134
			return "principals/$name";
3135
		}
3136
		return $principalUri;
3137
	}
3138
3139
	/**
3140
	 * adds information about an owner to the calendar data
3141
	 *
3142
	 */
3143
	private function addOwnerPrincipalToCalendar(array $calendarInfo): array {
3144
		$ownerPrincipalKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal';
3145
		$displaynameKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}owner-displayname';
3146
		if (isset($calendarInfo[$ownerPrincipalKey])) {
3147
			$uri = $calendarInfo[$ownerPrincipalKey];
3148
		} else {
3149
			$uri = $calendarInfo['principaluri'];
3150
		}
3151
3152
		$principalInformation = $this->principalBackend->getPrincipalByPath($uri);
3153
		if (isset($principalInformation['{DAV:}displayname'])) {
3154
			$calendarInfo[$displaynameKey] = $principalInformation['{DAV:}displayname'];
3155
		}
3156
		return $calendarInfo;
3157
	}
3158
3159
	private function addResourceTypeToCalendar(array $row, array $calendar): array {
3160
		if (isset($row['deleted_at'])) {
3161
			// Columns is set and not null -> this is a deleted calendar
3162
			// we send a custom resourcetype to hide the deleted calendar
3163
			// from ordinary DAV clients, but the Calendar app will know
3164
			// how to handle this special resource.
3165
			$calendar['{DAV:}resourcetype'] = new DAV\Xml\Property\ResourceType([
3166
				'{DAV:}collection',
3167
				sprintf('{%s}deleted-calendar', \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD),
3168
			]);
3169
		}
3170
		return $calendar;
3171
	}
3172
3173
	/**
3174
	 * Amend the calendar info with database row data
3175
	 *
3176
	 * @param array $row
3177
	 * @param array $calendar
3178
	 *
3179
	 * @return array
3180
	 */
3181
	private function rowToCalendar($row, array $calendar): array {
3182
		foreach ($this->propertyMap as $xmlName => [$dbName, $type]) {
3183
			$value = $row[$dbName];
3184
			if ($value !== null) {
3185
				settype($value, $type);
3186
			}
3187
			$calendar[$xmlName] = $value;
3188
		}
3189
		return $calendar;
3190
	}
3191
3192
	/**
3193
	 * Amend the subscription info with database row data
3194
	 *
3195
	 * @param array $row
3196
	 * @param array $subscription
3197
	 *
3198
	 * @return array
3199
	 */
3200
	private function rowToSubscription($row, array $subscription): array {
3201
		foreach ($this->subscriptionPropertyMap as $xmlName => [$dbName, $type]) {
3202
			$value = $row[$dbName];
3203
			if ($value !== null) {
3204
				settype($value, $type);
3205
			}
3206
			$subscription[$xmlName] = $value;
3207
		}
3208
		return $subscription;
3209
	}
3210
}
3211