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

CalDavBackend::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 18
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 9
c 0
b 0
f 0
nc 1
nop 9
dl 0
loc 18
rs 9.9666

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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