CalDavBackend::search()   F
last analyzed

Complexity

Conditions 23
Paths 640

Size

Total Lines 132
Code Lines 89

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 23
eloc 89
nc 640
nop 6
dl 0
loc 132
rs 0.5
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\IUserManager;
76
use OCP\Security\ISecureRandom;
77
use Psr\Log\LoggerInterface;
78
use RuntimeException;
79
use Sabre\CalDAV\Backend\AbstractBackend;
80
use Sabre\CalDAV\Backend\SchedulingSupport;
81
use Sabre\CalDAV\Backend\SubscriptionSupport;
82
use Sabre\CalDAV\Backend\SyncSupport;
83
use Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp;
84
use Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet;
85
use Sabre\DAV;
86
use Sabre\DAV\Exception\BadRequest;
87
use Sabre\DAV\Exception\Forbidden;
88
use Sabre\DAV\Exception\NotFound;
89
use Sabre\DAV\PropPatch;
90
use Sabre\Uri;
91
use Sabre\VObject\Component;
92
use Sabre\VObject\Component\VCalendar;
93
use Sabre\VObject\Component\VTimeZone;
94
use Sabre\VObject\DateTimeParser;
95
use Sabre\VObject\InvalidDataException;
96
use Sabre\VObject\ParseException;
97
use Sabre\VObject\Property;
98
use Sabre\VObject\Reader;
99
use Sabre\VObject\Recur\EventIterator;
100
use function array_column;
101
use function array_merge;
102
use function array_values;
103
use function explode;
104
use function is_array;
105
use function is_resource;
106
use function pathinfo;
107
use function rewind;
108
use function settype;
109
use function sprintf;
110
use function str_replace;
111
use function strtolower;
112
use function time;
113
114
/**
115
 * Class CalDavBackend
116
 *
117
 * Code is heavily inspired by https://github.com/fruux/sabre-dav/blob/master/lib/CalDAV/Backend/PDO.php
118
 *
119
 * @package OCA\DAV\CalDAV
120
 */
121
class CalDavBackend extends AbstractBackend implements SyncSupport, SubscriptionSupport, SchedulingSupport {
122
	use TTransactional;
123
124
	public const CALENDAR_TYPE_CALENDAR = 0;
125
	public const CALENDAR_TYPE_SUBSCRIPTION = 1;
126
127
	public const PERSONAL_CALENDAR_URI = 'personal';
128
	public const PERSONAL_CALENDAR_NAME = 'Personal';
129
130
	public const RESOURCE_BOOKING_CALENDAR_URI = 'calendar';
131
	public const RESOURCE_BOOKING_CALENDAR_NAME = 'Calendar';
132
133
	/**
134
	 * We need to specify a max date, because we need to stop *somewhere*
135
	 *
136
	 * On 32 bit system the maximum for a signed integer is 2147483647, so
137
	 * MAX_DATE cannot be higher than date('Y-m-d', 2147483647) which results
138
	 * in 2038-01-19 to avoid problems when the date is converted
139
	 * to a unix timestamp.
140
	 */
141
	public const MAX_DATE = '2038-01-01';
142
143
	public const ACCESS_PUBLIC = 4;
144
	public const CLASSIFICATION_PUBLIC = 0;
145
	public const CLASSIFICATION_PRIVATE = 1;
146
	public const CLASSIFICATION_CONFIDENTIAL = 2;
147
148
	/**
149
	 * List of CalDAV properties, and how they map to database field names and their type
150
	 * Add your own properties by simply adding on to this array.
151
	 *
152
	 * @var array
153
	 * @psalm-var array<string, string[]>
154
	 */
155
	public array $propertyMap = [
156
		'{DAV:}displayname' => ['displayname', 'string'],
157
		'{urn:ietf:params:xml:ns:caldav}calendar-description' => ['description', 'string'],
158
		'{urn:ietf:params:xml:ns:caldav}calendar-timezone' => ['timezone', 'string'],
159
		'{http://apple.com/ns/ical/}calendar-order' => ['calendarorder', 'int'],
160
		'{http://apple.com/ns/ical/}calendar-color' => ['calendarcolor', 'string'],
161
		'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => ['deleted_at', 'int'],
162
	];
163
164
	/**
165
	 * List of subscription properties, and how they map to database field names.
166
	 *
167
	 * @var array
168
	 */
169
	public array $subscriptionPropertyMap = [
170
		'{DAV:}displayname' => ['displayname', 'string'],
171
		'{http://apple.com/ns/ical/}refreshrate' => ['refreshrate', 'string'],
172
		'{http://apple.com/ns/ical/}calendar-order' => ['calendarorder', 'int'],
173
		'{http://apple.com/ns/ical/}calendar-color' => ['calendarcolor', 'string'],
174
		'{http://calendarserver.org/ns/}subscribed-strip-todos' => ['striptodos', 'bool'],
175
		'{http://calendarserver.org/ns/}subscribed-strip-alarms' => ['stripalarms', 'string'],
176
		'{http://calendarserver.org/ns/}subscribed-strip-attachments' => ['stripattachments', 'string'],
177
	];
178
179
	/**
180
	 * properties to index
181
	 *
182
	 * This list has to be kept in sync with ICalendarQuery::SEARCH_PROPERTY_*
183
	 *
184
	 * @see \OCP\Calendar\ICalendarQuery
185
	 */
186
	private const INDEXED_PROPERTIES = [
187
		'CATEGORIES',
188
		'COMMENT',
189
		'DESCRIPTION',
190
		'LOCATION',
191
		'RESOURCES',
192
		'STATUS',
193
		'SUMMARY',
194
		'ATTENDEE',
195
		'CONTACT',
196
		'ORGANIZER'
197
	];
198
199
	/** @var array parameters to index */
200
	public static array $indexParameters = [
201
		'ATTENDEE' => ['CN'],
202
		'ORGANIZER' => ['CN'],
203
	];
204
205
	/**
206
	 * @var string[] Map of uid => display name
207
	 */
208
	protected array $userDisplayNames;
209
210
	private IDBConnection $db;
211
	private Backend $calendarSharingBackend;
212
	private Principal $principalBackend;
213
	private IUserManager $userManager;
214
	private ISecureRandom $random;
215
	private LoggerInterface $logger;
216
	private IEventDispatcher $dispatcher;
217
	private IConfig $config;
218
	private bool $legacyEndpoint;
219
	private string $dbObjectPropertiesTable = 'calendarobjects_props';
220
221
	public function __construct(IDBConnection $db,
222
								Principal $principalBackend,
223
								IUserManager $userManager,
224
								IGroupManager $groupManager,
225
								ISecureRandom $random,
226
								LoggerInterface $logger,
227
								IEventDispatcher $dispatcher,
228
								IConfig $config,
229
								bool $legacyEndpoint = false) {
230
		$this->db = $db;
231
		$this->principalBackend = $principalBackend;
232
		$this->userManager = $userManager;
233
		$this->calendarSharingBackend = new Backend($this->db, $this->userManager, $groupManager, $principalBackend, 'calendar');
234
		$this->random = $random;
235
		$this->logger = $logger;
236
		$this->dispatcher = $dispatcher;
237
		$this->config = $config;
238
		$this->legacyEndpoint = $legacyEndpoint;
239
	}
240
241
	/**
242
	 * Return the number of calendars for a principal
243
	 *
244
	 * By default this excludes the automatically generated birthday calendar
245
	 *
246
	 * @param $principalUri
247
	 * @param bool $excludeBirthday
248
	 * @return int
249
	 */
250
	public function getCalendarsForUserCount($principalUri, $excludeBirthday = true) {
251
		$principalUri = $this->convertPrincipal($principalUri, true);
252
		$query = $this->db->getQueryBuilder();
253
		$query->select($query->func()->count('*'))
254
			->from('calendars');
255
256
		if ($principalUri === '') {
257
			$query->where($query->expr()->emptyString('principaluri'));
258
		} else {
259
			$query->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
260
		}
261
262
		if ($excludeBirthday) {
263
			$query->andWhere($query->expr()->neq('uri', $query->createNamedParameter(BirthdayService::BIRTHDAY_CALENDAR_URI)));
264
		}
265
266
		$result = $query->executeQuery();
267
		$column = (int)$result->fetchOne();
268
		$result->closeCursor();
269
		return $column;
270
	}
271
272
	/**
273
	 * @return array{id: int, deleted_at: int}[]
274
	 */
275
	public function getDeletedCalendars(int $deletedBefore): array {
276
		$qb = $this->db->getQueryBuilder();
277
		$qb->select(['id', 'deleted_at'])
278
			->from('calendars')
279
			->where($qb->expr()->isNotNull('deleted_at'))
280
			->andWhere($qb->expr()->lt('deleted_at', $qb->createNamedParameter($deletedBefore)));
281
		$result = $qb->executeQuery();
282
		$raw = $result->fetchAll();
283
		$result->closeCursor();
284
		return array_map(function ($row) {
285
			return [
286
				'id' => (int) $row['id'],
287
				'deleted_at' => (int) $row['deleted_at'],
288
			];
289
		}, $raw);
290
	}
291
292
	/**
293
	 * Returns a list of calendars for a principal.
294
	 *
295
	 * Every project is an array with the following keys:
296
	 *  * id, a unique id that will be used by other functions to modify the
297
	 *    calendar. This can be the same as the uri or a database key.
298
	 *  * uri, which the basename of the uri with which the calendar is
299
	 *    accessed.
300
	 *  * principaluri. The owner of the calendar. Almost always the same as
301
	 *    principalUri passed to this method.
302
	 *
303
	 * Furthermore it can contain webdav properties in clark notation. A very
304
	 * common one is '{DAV:}displayname'.
305
	 *
306
	 * Many clients also require:
307
	 * {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
308
	 * For this property, you can just return an instance of
309
	 * Sabre\CalDAV\Property\SupportedCalendarComponentSet.
310
	 *
311
	 * If you return {http://sabredav.org/ns}read-only and set the value to 1,
312
	 * ACL will automatically be put in read-only mode.
313
	 *
314
	 * @param string $principalUri
315
	 * @return array
316
	 */
317
	public function getCalendarsForUser($principalUri) {
318
		return $this->atomic(function () use ($principalUri) {
319
			$principalUriOriginal = $principalUri;
320
			$principalUri = $this->convertPrincipal($principalUri, true);
321
			$fields = array_column($this->propertyMap, 0);
322
			$fields[] = 'id';
323
			$fields[] = 'uri';
324
			$fields[] = 'synctoken';
325
			$fields[] = 'components';
326
			$fields[] = 'principaluri';
327
			$fields[] = 'transparent';
328
329
			// Making fields a comma-delimited list
330
			$query = $this->db->getQueryBuilder();
331
			$query->select($fields)
332
				->from('calendars')
333
				->orderBy('calendarorder', 'ASC');
334
335
			if ($principalUri === '') {
336
				$query->where($query->expr()->emptyString('principaluri'));
337
			} else {
338
				$query->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
339
			}
340
341
			$result = $query->executeQuery();
342
343
			$calendars = [];
344
			while ($row = $result->fetch()) {
345
				$row['principaluri'] = (string) $row['principaluri'];
346
				$components = [];
347
				if ($row['components']) {
348
					$components = explode(',', $row['components']);
349
				}
350
351
				$calendar = [
352
					'id' => $row['id'],
353
					'uri' => $row['uri'],
354
					'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
355
					'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
356
					'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
357
					'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
358
					'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
359
					'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($principalUri, !$this->legacyEndpoint),
360
				];
361
362
				$calendar = $this->rowToCalendar($row, $calendar);
363
				$calendar = $this->addOwnerPrincipalToCalendar($calendar);
364
				$calendar = $this->addResourceTypeToCalendar($row, $calendar);
365
366
				if (!isset($calendars[$calendar['id']])) {
367
					$calendars[$calendar['id']] = $calendar;
368
				}
369
			}
370
			$result->closeCursor();
371
372
			// query for shared calendars
373
			$principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true);
374
			$principals = array_merge($principals, $this->principalBackend->getCircleMembership($principalUriOriginal));
375
376
			$principals[] = $principalUri;
377
378
			$fields = array_column($this->propertyMap, 0);
379
			$fields[] = 'a.id';
380
			$fields[] = 'a.uri';
381
			$fields[] = 'a.synctoken';
382
			$fields[] = 'a.components';
383
			$fields[] = 'a.principaluri';
384
			$fields[] = 'a.transparent';
385
			$fields[] = 's.access';
386
			$query = $this->db->getQueryBuilder();
387
			$query->select($fields)
388
				->from('dav_shares', 's')
389
				->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
390
				->where($query->expr()->in('s.principaluri', $query->createParameter('principaluri')))
391
				->andWhere($query->expr()->eq('s.type', $query->createParameter('type')))
392
				->setParameter('type', 'calendar')
393
				->setParameter('principaluri', $principals, \Doctrine\DBAL\Connection::PARAM_STR_ARRAY);
394
395
			$result = $query->executeQuery();
396
397
			$readOnlyPropertyName = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only';
398
			while ($row = $result->fetch()) {
399
				$row['principaluri'] = (string) $row['principaluri'];
400
				if ($row['principaluri'] === $principalUri) {
401
					continue;
402
				}
403
404
				$readOnly = (int) $row['access'] === Backend::ACCESS_READ;
405
				if (isset($calendars[$row['id']])) {
406
					if ($readOnly) {
407
						// New share can not have more permissions then the old one.
408
						continue;
409
					}
410
					if (isset($calendars[$row['id']][$readOnlyPropertyName]) &&
411
						$calendars[$row['id']][$readOnlyPropertyName] === 0) {
412
						// Old share is already read-write, no more permissions can be gained
413
						continue;
414
					}
415
				}
416
417
				[, $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

417
				[, $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...
418
				$uri = $row['uri'] . '_shared_by_' . $name;
419
				$row['displayname'] = $row['displayname'] . ' (' . ($this->userManager->getDisplayName($name) ?? ($name ?? '')) . ')';
420
				$components = [];
421
				if ($row['components']) {
422
					$components = explode(',', $row['components']);
423
				}
424
				$calendar = [
425
					'id' => $row['id'],
426
					'uri' => $uri,
427
					'principaluri' => $this->convertPrincipal($principalUri, !$this->legacyEndpoint),
428
					'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
429
					'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
430
					'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
431
					'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp('transparent'),
432
					'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
433
					$readOnlyPropertyName => $readOnly,
434
				];
435
436
				$calendar = $this->rowToCalendar($row, $calendar);
437
				$calendar = $this->addOwnerPrincipalToCalendar($calendar);
438
				$calendar = $this->addResourceTypeToCalendar($row, $calendar);
439
440
				$calendars[$calendar['id']] = $calendar;
441
			}
442
			$result->closeCursor();
443
444
			return array_values($calendars);
445
		}, $this->db);
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
	 * @return array
498
	 */
499
	public function getPublicCalendars() {
500
		$fields = array_column($this->propertyMap, 0);
501
		$fields[] = 'a.id';
502
		$fields[] = 'a.uri';
503
		$fields[] = 'a.synctoken';
504
		$fields[] = 'a.components';
505
		$fields[] = 'a.principaluri';
506
		$fields[] = 'a.transparent';
507
		$fields[] = 's.access';
508
		$fields[] = 's.publicuri';
509
		$calendars = [];
510
		$query = $this->db->getQueryBuilder();
511
		$result = $query->select($fields)
512
			->from('dav_shares', 's')
513
			->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
514
			->where($query->expr()->in('s.access', $query->createNamedParameter(self::ACCESS_PUBLIC)))
515
			->andWhere($query->expr()->eq('s.type', $query->createNamedParameter('calendar')))
516
			->executeQuery();
517
518
		while ($row = $result->fetch()) {
519
			$row['principaluri'] = (string) $row['principaluri'];
520
			[, $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

520
			[, $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...
521
			$row['displayname'] = $row['displayname'] . "($name)";
522
			$components = [];
523
			if ($row['components']) {
524
				$components = explode(',', $row['components']);
525
			}
526
			$calendar = [
527
				'id' => $row['id'],
528
				'uri' => $row['publicuri'],
529
				'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
530
				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
531
				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
532
				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
533
				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
534
				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], $this->legacyEndpoint),
535
				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ,
536
				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}public' => (int)$row['access'] === self::ACCESS_PUBLIC,
537
			];
538
539
			$calendar = $this->rowToCalendar($row, $calendar);
540
			$calendar = $this->addOwnerPrincipalToCalendar($calendar);
541
			$calendar = $this->addResourceTypeToCalendar($row, $calendar);
542
543
			if (!isset($calendars[$calendar['id']])) {
544
				$calendars[$calendar['id']] = $calendar;
545
			}
546
		}
547
		$result->closeCursor();
548
549
		return array_values($calendars);
550
	}
551
552
	/**
553
	 * @param string $uri
554
	 * @return array
555
	 * @throws NotFound
556
	 */
557
	public function getPublicCalendar($uri) {
558
		$fields = array_column($this->propertyMap, 0);
559
		$fields[] = 'a.id';
560
		$fields[] = 'a.uri';
561
		$fields[] = 'a.synctoken';
562
		$fields[] = 'a.components';
563
		$fields[] = 'a.principaluri';
564
		$fields[] = 'a.transparent';
565
		$fields[] = 's.access';
566
		$fields[] = 's.publicuri';
567
		$query = $this->db->getQueryBuilder();
568
		$result = $query->select($fields)
569
			->from('dav_shares', 's')
570
			->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
571
			->where($query->expr()->in('s.access', $query->createNamedParameter(self::ACCESS_PUBLIC)))
572
			->andWhere($query->expr()->eq('s.type', $query->createNamedParameter('calendar')))
573
			->andWhere($query->expr()->eq('s.publicuri', $query->createNamedParameter($uri)))
574
			->executeQuery();
575
576
		$row = $result->fetch();
577
578
		$result->closeCursor();
579
580
		if ($row === false) {
581
			throw new NotFound('Node with name \'' . $uri . '\' could not be found');
582
		}
583
584
		$row['principaluri'] = (string) $row['principaluri'];
585
		[, $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

585
		[, $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...
586
		$row['displayname'] = $row['displayname'] . ' ' . "($name)";
587
		$components = [];
588
		if ($row['components']) {
589
			$components = explode(',', $row['components']);
590
		}
591
		$calendar = [
592
			'id' => $row['id'],
593
			'uri' => $row['publicuri'],
594
			'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
595
			'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
596
			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
597
			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
598
			'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
599
			'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
600
			'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ,
601
			'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}public' => (int)$row['access'] === self::ACCESS_PUBLIC,
602
		];
603
604
		$calendar = $this->rowToCalendar($row, $calendar);
605
		$calendar = $this->addOwnerPrincipalToCalendar($calendar);
606
		$calendar = $this->addResourceTypeToCalendar($row, $calendar);
607
608
		return $calendar;
609
	}
610
611
	/**
612
	 * @param string $principal
613
	 * @param string $uri
614
	 * @return array|null
615
	 */
616
	public function getCalendarByUri($principal, $uri) {
617
		$fields = array_column($this->propertyMap, 0);
618
		$fields[] = 'id';
619
		$fields[] = 'uri';
620
		$fields[] = 'synctoken';
621
		$fields[] = 'components';
622
		$fields[] = 'principaluri';
623
		$fields[] = 'transparent';
624
625
		// Making fields a comma-delimited list
626
		$query = $this->db->getQueryBuilder();
627
		$query->select($fields)->from('calendars')
628
			->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
629
			->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($principal)))
630
			->setMaxResults(1);
631
		$stmt = $query->executeQuery();
632
633
		$row = $stmt->fetch();
634
		$stmt->closeCursor();
635
		if ($row === false) {
636
			return null;
637
		}
638
639
		$row['principaluri'] = (string) $row['principaluri'];
640
		$components = [];
641
		if ($row['components']) {
642
			$components = explode(',', $row['components']);
643
		}
644
645
		$calendar = [
646
			'id' => $row['id'],
647
			'uri' => $row['uri'],
648
			'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
649
			'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
650
			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
651
			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
652
			'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
653
		];
654
655
		$calendar = $this->rowToCalendar($row, $calendar);
656
		$calendar = $this->addOwnerPrincipalToCalendar($calendar);
657
		$calendar = $this->addResourceTypeToCalendar($row, $calendar);
658
659
		return $calendar;
660
	}
661
662
	/**
663
	 * @return array{id: int, uri: string, '{http://calendarserver.org/ns/}getctag': string, '{http://sabredav.org/ns}sync-token': int, '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set': SupportedCalendarComponentSet, '{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp': ScheduleCalendarTransp, '{urn:ietf:params:xml:ns:caldav}calendar-timezone': ?string }|null
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{id: int, uri: stri...ndar-timezone': ?string at position 10 could not be parsed: Expected ':' at position 10, but found '''.
Loading history...
664
	 */
665
	public function getCalendarById(int $calendarId): ?array {
666
		$fields = array_column($this->propertyMap, 0);
667
		$fields[] = 'id';
668
		$fields[] = 'uri';
669
		$fields[] = 'synctoken';
670
		$fields[] = 'components';
671
		$fields[] = 'principaluri';
672
		$fields[] = 'transparent';
673
674
		// Making fields a comma-delimited list
675
		$query = $this->db->getQueryBuilder();
676
		$query->select($fields)->from('calendars')
677
			->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)))
678
			->setMaxResults(1);
679
		$stmt = $query->executeQuery();
680
681
		$row = $stmt->fetch();
682
		$stmt->closeCursor();
683
		if ($row === false) {
684
			return null;
685
		}
686
687
		$row['principaluri'] = (string) $row['principaluri'];
688
		$components = [];
689
		if ($row['components']) {
690
			$components = explode(',', $row['components']);
691
		}
692
693
		$calendar = [
694
			'id' => $row['id'],
695
			'uri' => $row['uri'],
696
			'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
697
			'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
698
			'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?? 0,
699
			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
700
			'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
701
		];
702
703
		$calendar = $this->rowToCalendar($row, $calendar);
704
		$calendar = $this->addOwnerPrincipalToCalendar($calendar);
705
		$calendar = $this->addResourceTypeToCalendar($row, $calendar);
706
707
		return $calendar;
708
	}
709
710
	/**
711
	 * @param $subscriptionId
712
	 */
713
	public function getSubscriptionById($subscriptionId) {
714
		$fields = array_column($this->subscriptionPropertyMap, 0);
715
		$fields[] = 'id';
716
		$fields[] = 'uri';
717
		$fields[] = 'source';
718
		$fields[] = 'synctoken';
719
		$fields[] = 'principaluri';
720
		$fields[] = 'lastmodified';
721
722
		$query = $this->db->getQueryBuilder();
723
		$query->select($fields)
724
			->from('calendarsubscriptions')
725
			->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
726
			->orderBy('calendarorder', 'asc');
727
		$stmt = $query->executeQuery();
728
729
		$row = $stmt->fetch();
730
		$stmt->closeCursor();
731
		if ($row === false) {
732
			return null;
733
		}
734
735
		$row['principaluri'] = (string) $row['principaluri'];
736
		$subscription = [
737
			'id' => $row['id'],
738
			'uri' => $row['uri'],
739
			'principaluri' => $row['principaluri'],
740
			'source' => $row['source'],
741
			'lastmodified' => $row['lastmodified'],
742
			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
743
			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
744
		];
745
746
		return $this->rowToSubscription($row, $subscription);
747
	}
748
749
	/**
750
	 * Creates a new calendar for a principal.
751
	 *
752
	 * If the creation was a success, an id must be returned that can be used to reference
753
	 * this calendar in other methods, such as updateCalendar.
754
	 *
755
	 * @param string $principalUri
756
	 * @param string $calendarUri
757
	 * @param array $properties
758
	 * @return int
759
	 *
760
	 * @throws CalendarException
761
	 */
762
	public function createCalendar($principalUri, $calendarUri, array $properties) {
763
		if (strlen($calendarUri) > 255) {
764
			throw new CalendarException('URI too long. Calendar not created');
765
		}
766
767
		$values = [
768
			'principaluri' => $this->convertPrincipal($principalUri, true),
769
			'uri' => $calendarUri,
770
			'synctoken' => 1,
771
			'transparent' => 0,
772
			'components' => 'VEVENT,VTODO',
773
			'displayname' => $calendarUri
774
		];
775
776
		// Default value
777
		$sccs = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set';
778
		if (isset($properties[$sccs])) {
779
			if (!($properties[$sccs] instanceof SupportedCalendarComponentSet)) {
780
				throw new DAV\Exception('The ' . $sccs . ' property must be of type: \Sabre\CalDAV\Property\SupportedCalendarComponentSet');
781
			}
782
			$values['components'] = implode(',', $properties[$sccs]->getValue());
783
		} elseif (isset($properties['components'])) {
784
			// Allow to provide components internally without having
785
			// to create a SupportedCalendarComponentSet object
786
			$values['components'] = $properties['components'];
787
		}
788
789
		$transp = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp';
790
		if (isset($properties[$transp])) {
791
			$values['transparent'] = (int) ($properties[$transp]->getValue() === 'transparent');
792
		}
793
794
		foreach ($this->propertyMap as $xmlName => [$dbName, $type]) {
795
			if (isset($properties[$xmlName])) {
796
				$values[$dbName] = $properties[$xmlName];
797
			}
798
		}
799
800
		[$calendarId, $calendarData] = $this->atomic(function () use ($values) {
801
			$query = $this->db->getQueryBuilder();
802
			$query->insert('calendars');
803
			foreach ($values as $column => $value) {
804
				$query->setValue($column, $query->createNamedParameter($value));
805
			}
806
			$query->executeStatement();
807
			$calendarId = $query->getLastInsertId();
808
809
			$calendarData = $this->getCalendarById($calendarId);
810
			return [$calendarId, $calendarData];
811
		}, $this->db);
812
813
		$this->dispatcher->dispatchTyped(new CalendarCreatedEvent((int)$calendarId, $calendarData));
814
815
		return $calendarId;
816
	}
817
818
	/**
819
	 * Updates properties for a calendar.
820
	 *
821
	 * The list of mutations is stored in a Sabre\DAV\PropPatch object.
822
	 * To do the actual updates, you must tell this object which properties
823
	 * you're going to process with the handle() method.
824
	 *
825
	 * Calling the handle method is like telling the PropPatch object "I
826
	 * promise I can handle updating this property".
827
	 *
828
	 * Read the PropPatch documentation for more info and examples.
829
	 *
830
	 * @param mixed $calendarId
831
	 * @param PropPatch $propPatch
832
	 * @return void
833
	 */
834
	public function updateCalendar($calendarId, PropPatch $propPatch) {
835
		$this->atomic(function () use ($calendarId, $propPatch) {
836
			$supportedProperties = array_keys($this->propertyMap);
837
			$supportedProperties[] = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp';
838
839
			$propPatch->handle($supportedProperties, function ($mutations) use ($calendarId) {
840
				$newValues = [];
841
				foreach ($mutations as $propertyName => $propertyValue) {
842
					switch ($propertyName) {
843
						case '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp':
844
							$fieldName = 'transparent';
845
							$newValues[$fieldName] = (int) ($propertyValue->getValue() === 'transparent');
846
							break;
847
						default:
848
							$fieldName = $this->propertyMap[$propertyName][0];
849
							$newValues[$fieldName] = $propertyValue;
850
							break;
851
					}
852
				}
853
				$query = $this->db->getQueryBuilder();
854
				$query->update('calendars');
855
				foreach ($newValues as $fieldName => $value) {
856
					$query->set($fieldName, $query->createNamedParameter($value));
857
				}
858
				$query->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)));
859
				$query->executeStatement();
860
861
				$this->addChange($calendarId, "", 2);
862
863
				$calendarData = $this->getCalendarById($calendarId);
864
				$shares = $this->getShares($calendarId);
865
				$this->dispatcher->dispatchTyped(new CalendarUpdatedEvent($calendarId, $calendarData, $shares, $mutations));
866
867
				return true;
868
			});
869
		}, $this->db);
870
	}
871
872
	/**
873
	 * Delete a calendar and all it's objects
874
	 *
875
	 * @param mixed $calendarId
876
	 * @return void
877
	 */
878
	public function deleteCalendar($calendarId, bool $forceDeletePermanently = false) {
879
		$this->atomic(function () use ($calendarId, $forceDeletePermanently) {
880
			// The calendar is deleted right away if this is either enforced by the caller
881
			// or the special contacts birthday calendar or when the preference of an empty
882
			// retention (0 seconds) is set, which signals a disabled trashbin.
883
			$calendarData = $this->getCalendarById($calendarId);
884
			$isBirthdayCalendar = isset($calendarData['uri']) && $calendarData['uri'] === BirthdayService::BIRTHDAY_CALENDAR_URI;
885
			$trashbinDisabled = $this->config->getAppValue(Application::APP_ID, RetentionService::RETENTION_CONFIG_KEY) === '0';
886
			if ($forceDeletePermanently || $isBirthdayCalendar || $trashbinDisabled) {
887
				$calendarData = $this->getCalendarById($calendarId);
888
				$shares = $this->getShares($calendarId);
889
890
				$qbDeleteCalendarObjectProps = $this->db->getQueryBuilder();
891
				$qbDeleteCalendarObjectProps->delete($this->dbObjectPropertiesTable)
892
					->where($qbDeleteCalendarObjectProps->expr()->eq('calendarid', $qbDeleteCalendarObjectProps->createNamedParameter($calendarId)))
893
					->andWhere($qbDeleteCalendarObjectProps->expr()->eq('calendartype', $qbDeleteCalendarObjectProps->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)))
894
					->executeStatement();
895
896
				$qbDeleteCalendarObjects = $this->db->getQueryBuilder();
897
				$qbDeleteCalendarObjects->delete('calendarobjects')
898
					->where($qbDeleteCalendarObjects->expr()->eq('calendarid', $qbDeleteCalendarObjects->createNamedParameter($calendarId)))
899
					->andWhere($qbDeleteCalendarObjects->expr()->eq('calendartype', $qbDeleteCalendarObjects->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)))
900
					->executeStatement();
901
902
				$qbDeleteCalendarChanges = $this->db->getQueryBuilder();
903
				$qbDeleteCalendarChanges->delete('calendarchanges')
904
					->where($qbDeleteCalendarChanges->expr()->eq('calendarid', $qbDeleteCalendarChanges->createNamedParameter($calendarId)))
905
					->andWhere($qbDeleteCalendarChanges->expr()->eq('calendartype', $qbDeleteCalendarChanges->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)))
906
					->executeStatement();
907
908
				$this->calendarSharingBackend->deleteAllShares($calendarId);
909
910
				$qbDeleteCalendar = $this->db->getQueryBuilder();
911
				$qbDeleteCalendar->delete('calendars')
912
					->where($qbDeleteCalendar->expr()->eq('id', $qbDeleteCalendar->createNamedParameter($calendarId)))
913
					->executeStatement();
914
915
				// Only dispatch if we actually deleted anything
916
				if ($calendarData) {
917
					$this->dispatcher->dispatchTyped(new CalendarDeletedEvent($calendarId, $calendarData, $shares));
918
				}
919
			} else {
920
				$qbMarkCalendarDeleted = $this->db->getQueryBuilder();
921
				$qbMarkCalendarDeleted->update('calendars')
922
					->set('deleted_at', $qbMarkCalendarDeleted->createNamedParameter(time()))
923
					->where($qbMarkCalendarDeleted->expr()->eq('id', $qbMarkCalendarDeleted->createNamedParameter($calendarId)))
924
					->executeStatement();
925
926
				$calendarData = $this->getCalendarById($calendarId);
927
				$shares = $this->getShares($calendarId);
928
				if ($calendarData) {
929
					$this->dispatcher->dispatchTyped(new CalendarMovedToTrashEvent(
930
						$calendarId,
931
						$calendarData,
932
						$shares
933
					));
934
				}
935
			}
936
		}, $this->db);
937
	}
938
939
	public function restoreCalendar(int $id): void {
940
		$this->atomic(function () use ($id) {
941
			$qb = $this->db->getQueryBuilder();
942
			$update = $qb->update('calendars')
943
				->set('deleted_at', $qb->createNamedParameter(null))
944
				->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT));
945
			$update->executeStatement();
946
947
			$calendarData = $this->getCalendarById($id);
948
			$shares = $this->getShares($id);
949
			if ($calendarData === null) {
950
				throw new RuntimeException('Calendar data that was just written can\'t be read back. Check your database configuration.');
951
			}
952
			$this->dispatcher->dispatchTyped(new CalendarRestoredEvent(
953
				$id,
954
				$calendarData,
955
				$shares
956
			));
957
		}, $this->db);
958
	}
959
960
	/**
961
	 * Delete all of an user's shares
962
	 *
963
	 * @param string $principaluri
964
	 * @return void
965
	 */
966
	public function deleteAllSharesByUser($principaluri) {
967
		$this->calendarSharingBackend->deleteAllSharesByUser($principaluri);
968
	}
969
970
	/**
971
	 * Returns all calendar objects within a calendar.
972
	 *
973
	 * Every item contains an array with the following keys:
974
	 *   * calendardata - The iCalendar-compatible calendar data
975
	 *   * uri - a unique key which will be used to construct the uri. This can
976
	 *     be any arbitrary string, but making sure it ends with '.ics' is a
977
	 *     good idea. This is only the basename, or filename, not the full
978
	 *     path.
979
	 *   * lastmodified - a timestamp of the last modification time
980
	 *   * etag - An arbitrary string, surrounded by double-quotes. (e.g.:
981
	 *   '"abcdef"')
982
	 *   * size - The size of the calendar objects, in bytes.
983
	 *   * component - optional, a string containing the type of object, such
984
	 *     as 'vevent' or 'vtodo'. If specified, this will be used to populate
985
	 *     the Content-Type header.
986
	 *
987
	 * Note that the etag is optional, but it's highly encouraged to return for
988
	 * speed reasons.
989
	 *
990
	 * The calendardata is also optional. If it's not returned
991
	 * 'getCalendarObject' will be called later, which *is* expected to return
992
	 * calendardata.
993
	 *
994
	 * If neither etag or size are specified, the calendardata will be
995
	 * used/fetched to determine these numbers. If both are specified the
996
	 * amount of times this is needed is reduced by a great degree.
997
	 *
998
	 * @param mixed $calendarId
999
	 * @param int $calendarType
1000
	 * @return array
1001
	 */
1002
	public function getCalendarObjects($calendarId, $calendarType = self::CALENDAR_TYPE_CALENDAR):array {
1003
		$query = $this->db->getQueryBuilder();
1004
		$query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'componenttype', 'classification'])
1005
			->from('calendarobjects')
1006
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
1007
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)))
1008
			->andWhere($query->expr()->isNull('deleted_at'));
1009
		$stmt = $query->executeQuery();
1010
1011
		$result = [];
1012
		foreach ($stmt->fetchAll() as $row) {
1013
			$result[] = [
1014
				'id' => $row['id'],
1015
				'uri' => $row['uri'],
1016
				'lastmodified' => $row['lastmodified'],
1017
				'etag' => '"' . $row['etag'] . '"',
1018
				'calendarid' => $row['calendarid'],
1019
				'size' => (int)$row['size'],
1020
				'component' => strtolower($row['componenttype']),
1021
				'classification' => (int)$row['classification']
1022
			];
1023
		}
1024
		$stmt->closeCursor();
1025
1026
		return $result;
1027
	}
1028
1029
	public function getDeletedCalendarObjects(int $deletedBefore): array {
1030
		$query = $this->db->getQueryBuilder();
1031
		$query->select(['co.id', 'co.uri', 'co.lastmodified', 'co.etag', 'co.calendarid', 'co.calendartype', 'co.size', 'co.componenttype', 'co.classification', 'co.deleted_at'])
1032
			->from('calendarobjects', 'co')
1033
			->join('co', 'calendars', 'c', $query->expr()->eq('c.id', 'co.calendarid', IQueryBuilder::PARAM_INT))
1034
			->where($query->expr()->isNotNull('co.deleted_at'))
1035
			->andWhere($query->expr()->lt('co.deleted_at', $query->createNamedParameter($deletedBefore)));
1036
		$stmt = $query->executeQuery();
1037
1038
		$result = [];
1039
		foreach ($stmt->fetchAll() as $row) {
1040
			$result[] = [
1041
				'id' => $row['id'],
1042
				'uri' => $row['uri'],
1043
				'lastmodified' => $row['lastmodified'],
1044
				'etag' => '"' . $row['etag'] . '"',
1045
				'calendarid' => (int) $row['calendarid'],
1046
				'calendartype' => (int) $row['calendartype'],
1047
				'size' => (int) $row['size'],
1048
				'component' => strtolower($row['componenttype']),
1049
				'classification' => (int) $row['classification'],
1050
				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => $row['deleted_at'] === null ? $row['deleted_at'] : (int) $row['deleted_at'],
1051
			];
1052
		}
1053
		$stmt->closeCursor();
1054
1055
		return $result;
1056
	}
1057
1058
	/**
1059
	 * Return all deleted calendar objects by the given principal that are not
1060
	 * in deleted calendars.
1061
	 *
1062
	 * @param string $principalUri
1063
	 * @return array
1064
	 * @throws Exception
1065
	 */
1066
	public function getDeletedCalendarObjectsByPrincipal(string $principalUri): array {
1067
		$query = $this->db->getQueryBuilder();
1068
		$query->select(['co.id', 'co.uri', 'co.lastmodified', 'co.etag', 'co.calendarid', 'co.size', 'co.componenttype', 'co.classification', 'co.deleted_at'])
1069
			->selectAlias('c.uri', 'calendaruri')
1070
			->from('calendarobjects', 'co')
1071
			->join('co', 'calendars', 'c', $query->expr()->eq('c.id', 'co.calendarid', IQueryBuilder::PARAM_INT))
1072
			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
1073
			->andWhere($query->expr()->isNotNull('co.deleted_at'))
1074
			->andWhere($query->expr()->isNull('c.deleted_at'));
1075
		$stmt = $query->executeQuery();
1076
1077
		$result = [];
1078
		while ($row = $stmt->fetch()) {
1079
			$result[] = [
1080
				'id' => $row['id'],
1081
				'uri' => $row['uri'],
1082
				'lastmodified' => $row['lastmodified'],
1083
				'etag' => '"' . $row['etag'] . '"',
1084
				'calendarid' => $row['calendarid'],
1085
				'calendaruri' => $row['calendaruri'],
1086
				'size' => (int)$row['size'],
1087
				'component' => strtolower($row['componenttype']),
1088
				'classification' => (int)$row['classification'],
1089
				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => $row['deleted_at'] === null ? $row['deleted_at'] : (int) $row['deleted_at'],
1090
			];
1091
		}
1092
		$stmt->closeCursor();
1093
1094
		return $result;
1095
	}
1096
1097
	/**
1098
	 * Returns information from a single calendar object, based on it's object
1099
	 * uri.
1100
	 *
1101
	 * The object uri is only the basename, or filename and not a full path.
1102
	 *
1103
	 * The returned array must have the same keys as getCalendarObjects. The
1104
	 * 'calendardata' object is required here though, while it's not required
1105
	 * for getCalendarObjects.
1106
	 *
1107
	 * This method must return null if the object did not exist.
1108
	 *
1109
	 * @param mixed $calendarId
1110
	 * @param string $objectUri
1111
	 * @param int $calendarType
1112
	 * @return array|null
1113
	 */
1114
	public function getCalendarObject($calendarId, $objectUri, int $calendarType = self::CALENDAR_TYPE_CALENDAR) {
1115
		$query = $this->db->getQueryBuilder();
1116
		$query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification', 'deleted_at'])
1117
			->from('calendarobjects')
1118
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
1119
			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
1120
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)));
1121
		$stmt = $query->executeQuery();
1122
		$row = $stmt->fetch();
1123
		$stmt->closeCursor();
1124
1125
		if (!$row) {
1126
			return null;
1127
		}
1128
1129
		return [
1130
			'id' => $row['id'],
1131
			'uri' => $row['uri'],
1132
			'lastmodified' => $row['lastmodified'],
1133
			'etag' => '"' . $row['etag'] . '"',
1134
			'calendarid' => $row['calendarid'],
1135
			'size' => (int)$row['size'],
1136
			'calendardata' => $this->readBlob($row['calendardata']),
1137
			'component' => strtolower($row['componenttype']),
1138
			'classification' => (int)$row['classification'],
1139
			'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => $row['deleted_at'] === null ? $row['deleted_at'] : (int) $row['deleted_at'],
1140
		];
1141
	}
1142
1143
	/**
1144
	 * Returns a list of calendar objects.
1145
	 *
1146
	 * This method should work identical to getCalendarObject, but instead
1147
	 * return all the calendar objects in the list as an array.
1148
	 *
1149
	 * If the backend supports this, it may allow for some speed-ups.
1150
	 *
1151
	 * @param mixed $calendarId
1152
	 * @param string[] $uris
1153
	 * @param int $calendarType
1154
	 * @return array
1155
	 */
1156
	public function getMultipleCalendarObjects($calendarId, array $uris, $calendarType = self::CALENDAR_TYPE_CALENDAR):array {
1157
		if (empty($uris)) {
1158
			return [];
1159
		}
1160
1161
		$chunks = array_chunk($uris, 100);
1162
		$objects = [];
1163
1164
		$query = $this->db->getQueryBuilder();
1165
		$query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification'])
1166
			->from('calendarobjects')
1167
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
1168
			->andWhere($query->expr()->in('uri', $query->createParameter('uri')))
1169
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)))
1170
			->andWhere($query->expr()->isNull('deleted_at'));
1171
1172
		foreach ($chunks as $uris) {
1173
			$query->setParameter('uri', $uris, IQueryBuilder::PARAM_STR_ARRAY);
1174
			$result = $query->executeQuery();
1175
1176
			while ($row = $result->fetch()) {
1177
				$objects[] = [
1178
					'id' => $row['id'],
1179
					'uri' => $row['uri'],
1180
					'lastmodified' => $row['lastmodified'],
1181
					'etag' => '"' . $row['etag'] . '"',
1182
					'calendarid' => $row['calendarid'],
1183
					'size' => (int)$row['size'],
1184
					'calendardata' => $this->readBlob($row['calendardata']),
1185
					'component' => strtolower($row['componenttype']),
1186
					'classification' => (int)$row['classification']
1187
				];
1188
			}
1189
			$result->closeCursor();
1190
		}
1191
1192
		return $objects;
1193
	}
1194
1195
	/**
1196
	 * Creates a new calendar object.
1197
	 *
1198
	 * The object uri is only the basename, or filename and not a full path.
1199
	 *
1200
	 * It is possible return an etag from this function, which will be used in
1201
	 * the response to this PUT request. Note that the ETag must be surrounded
1202
	 * by double-quotes.
1203
	 *
1204
	 * However, you should only really return this ETag if you don't mangle the
1205
	 * calendar-data. If the result of a subsequent GET to this object is not
1206
	 * the exact same as this request body, you should omit the ETag.
1207
	 *
1208
	 * @param mixed $calendarId
1209
	 * @param string $objectUri
1210
	 * @param string $calendarData
1211
	 * @param int $calendarType
1212
	 * @return string
1213
	 */
1214
	public function createCalendarObject($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
1215
		$extraData = $this->getDenormalizedData($calendarData);
1216
1217
		return $this->atomic(function () use ($calendarId, $objectUri, $calendarData, $extraData, $calendarType) {
1218
			// Try to detect duplicates
1219
			$qb = $this->db->getQueryBuilder();
1220
			$qb->select($qb->func()->count('*'))
1221
				->from('calendarobjects')
1222
				->where($qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)))
1223
				->andWhere($qb->expr()->eq('uid', $qb->createNamedParameter($extraData['uid'])))
1224
				->andWhere($qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType)))
1225
				->andWhere($qb->expr()->isNull('deleted_at'));
1226
			$result = $qb->executeQuery();
1227
			$count = (int) $result->fetchOne();
1228
			$result->closeCursor();
1229
1230
			if ($count !== 0) {
1231
				throw new BadRequest('Calendar object with uid already exists in this calendar collection.');
1232
			}
1233
			// For a more specific error message we also try to explicitly look up the UID but as a deleted entry
1234
			$qbDel = $this->db->getQueryBuilder();
1235
			$qbDel->select('*')
1236
				->from('calendarobjects')
1237
				->where($qbDel->expr()->eq('calendarid', $qbDel->createNamedParameter($calendarId)))
1238
				->andWhere($qbDel->expr()->eq('uid', $qbDel->createNamedParameter($extraData['uid'])))
1239
				->andWhere($qbDel->expr()->eq('calendartype', $qbDel->createNamedParameter($calendarType)))
1240
				->andWhere($qbDel->expr()->isNotNull('deleted_at'));
1241
			$result = $qbDel->executeQuery();
1242
			$found = $result->fetch();
1243
			$result->closeCursor();
1244
			if ($found !== false) {
1245
				// the object existed previously but has been deleted
1246
				// remove the trashbin entry and continue as if it was a new object
1247
				$this->deleteCalendarObject($calendarId, $found['uri']);
1248
			}
1249
1250
			$query = $this->db->getQueryBuilder();
1251
			$query->insert('calendarobjects')
1252
				->values([
1253
					'calendarid' => $query->createNamedParameter($calendarId),
1254
					'uri' => $query->createNamedParameter($objectUri),
1255
					'calendardata' => $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB),
1256
					'lastmodified' => $query->createNamedParameter(time()),
1257
					'etag' => $query->createNamedParameter($extraData['etag']),
1258
					'size' => $query->createNamedParameter($extraData['size']),
1259
					'componenttype' => $query->createNamedParameter($extraData['componentType']),
1260
					'firstoccurence' => $query->createNamedParameter($extraData['firstOccurence']),
1261
					'lastoccurence' => $query->createNamedParameter($extraData['lastOccurence']),
1262
					'classification' => $query->createNamedParameter($extraData['classification']),
1263
					'uid' => $query->createNamedParameter($extraData['uid']),
1264
					'calendartype' => $query->createNamedParameter($calendarType),
1265
				])
1266
				->executeStatement();
1267
1268
			$this->updateProperties($calendarId, $objectUri, $calendarData, $calendarType);
1269
			$this->addChange($calendarId, $objectUri, 1, $calendarType);
1270
1271
			$objectRow = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
1272
			assert($objectRow !== null);
1273
1274
			if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
1275
				$calendarRow = $this->getCalendarById($calendarId);
1276
				$shares = $this->getShares($calendarId);
1277
1278
				$this->dispatcher->dispatchTyped(new CalendarObjectCreatedEvent($calendarId, $calendarRow, $shares, $objectRow));
1279
			} else {
1280
				$subscriptionRow = $this->getSubscriptionById($calendarId);
1281
1282
				$this->dispatcher->dispatchTyped(new CachedCalendarObjectCreatedEvent($calendarId, $subscriptionRow, [], $objectRow));
1283
			}
1284
1285
			return '"' . $extraData['etag'] . '"';
1286
		}, $this->db);
1287
	}
1288
1289
	/**
1290
	 * Updates an existing calendarobject, based on it's uri.
1291
	 *
1292
	 * The object uri is only the basename, or filename and not a full path.
1293
	 *
1294
	 * It is possible return an etag from this function, which will be used in
1295
	 * the response to this PUT request. Note that the ETag must be surrounded
1296
	 * by double-quotes.
1297
	 *
1298
	 * However, you should only really return this ETag if you don't mangle the
1299
	 * calendar-data. If the result of a subsequent GET to this object is not
1300
	 * the exact same as this request body, you should omit the ETag.
1301
	 *
1302
	 * @param mixed $calendarId
1303
	 * @param string $objectUri
1304
	 * @param string $calendarData
1305
	 * @param int $calendarType
1306
	 * @return string
1307
	 */
1308
	public function updateCalendarObject($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
1309
		$extraData = $this->getDenormalizedData($calendarData);
1310
1311
		return $this->atomic(function () use ($calendarId, $objectUri, $calendarData, $extraData, $calendarType) {
1312
			$query = $this->db->getQueryBuilder();
1313
			$query->update('calendarobjects')
1314
					->set('calendardata', $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB))
1315
					->set('lastmodified', $query->createNamedParameter(time()))
1316
					->set('etag', $query->createNamedParameter($extraData['etag']))
1317
					->set('size', $query->createNamedParameter($extraData['size']))
1318
					->set('componenttype', $query->createNamedParameter($extraData['componentType']))
1319
					->set('firstoccurence', $query->createNamedParameter($extraData['firstOccurence']))
1320
					->set('lastoccurence', $query->createNamedParameter($extraData['lastOccurence']))
1321
					->set('classification', $query->createNamedParameter($extraData['classification']))
1322
					->set('uid', $query->createNamedParameter($extraData['uid']))
1323
				->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
1324
				->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
1325
				->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)))
1326
				->executeStatement();
1327
1328
			$this->updateProperties($calendarId, $objectUri, $calendarData, $calendarType);
1329
			$this->addChange($calendarId, $objectUri, 2, $calendarType);
1330
1331
			$objectRow = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
1332
			if (is_array($objectRow)) {
1333
				if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
1334
					$calendarRow = $this->getCalendarById($calendarId);
1335
					$shares = $this->getShares($calendarId);
1336
1337
					$this->dispatcher->dispatchTyped(new CalendarObjectUpdatedEvent($calendarId, $calendarRow, $shares, $objectRow));
1338
				} else {
1339
					$subscriptionRow = $this->getSubscriptionById($calendarId);
1340
1341
					$this->dispatcher->dispatchTyped(new CachedCalendarObjectUpdatedEvent($calendarId, $subscriptionRow, [], $objectRow));
1342
				}
1343
			}
1344
1345
			return '"' . $extraData['etag'] . '"';
1346
		}, $this->db);
1347
	}
1348
1349
	/**
1350
	 * Moves a calendar object from calendar to calendar.
1351
	 *
1352
	 * @param int $sourceCalendarId
1353
	 * @param int $targetCalendarId
1354
	 * @param int $objectId
1355
	 * @param string $oldPrincipalUri
1356
	 * @param string $newPrincipalUri
1357
	 * @param int $calendarType
1358
	 * @return bool
1359
	 * @throws Exception
1360
	 */
1361
	public function moveCalendarObject(int $sourceCalendarId, int $targetCalendarId, int $objectId, string $oldPrincipalUri, string $newPrincipalUri, int $calendarType = self::CALENDAR_TYPE_CALENDAR): bool {
1362
		return $this->atomic(function () use ($sourceCalendarId, $targetCalendarId, $objectId, $oldPrincipalUri, $newPrincipalUri, $calendarType) {
1363
			$object = $this->getCalendarObjectById($oldPrincipalUri, $objectId);
1364
			if (empty($object)) {
1365
				return false;
1366
			}
1367
1368
			$query = $this->db->getQueryBuilder();
1369
			$query->update('calendarobjects')
1370
				->set('calendarid', $query->createNamedParameter($targetCalendarId, IQueryBuilder::PARAM_INT))
1371
				->where($query->expr()->eq('id', $query->createNamedParameter($objectId, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT))
1372
				->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT))
1373
				->executeStatement();
1374
1375
			$this->purgeProperties($sourceCalendarId, $objectId);
1376
			$this->updateProperties($targetCalendarId, $object['uri'], $object['calendardata'], $calendarType);
1377
1378
			$this->addChange($sourceCalendarId, $object['uri'], 3, $calendarType);
1379
			$this->addChange($targetCalendarId, $object['uri'], 1, $calendarType);
1380
1381
			$object = $this->getCalendarObjectById($newPrincipalUri, $objectId);
1382
			// Calendar Object wasn't found - possibly because it was deleted in the meantime by a different client
1383
			if (empty($object)) {
1384
				return false;
1385
			}
1386
1387
			$targetCalendarRow = $this->getCalendarById($targetCalendarId);
1388
			// the calendar this event is being moved to does not exist any longer
1389
			if (empty($targetCalendarRow)) {
1390
				return false;
1391
			}
1392
1393
			if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
1394
				$sourceShares = $this->getShares($sourceCalendarId);
1395
				$targetShares = $this->getShares($targetCalendarId);
1396
				$sourceCalendarRow = $this->getCalendarById($sourceCalendarId);
1397
				$this->dispatcher->dispatchTyped(new CalendarObjectMovedEvent($sourceCalendarId, $sourceCalendarRow, $targetCalendarId, $targetCalendarRow, $sourceShares, $targetShares, $object));
1398
			}
1399
			return true;
1400
		}, $this->db);
1401
	}
1402
1403
1404
	/**
1405
	 * @param int $calendarObjectId
1406
	 * @param int $classification
1407
	 */
1408
	public function setClassification($calendarObjectId, $classification) {
1409
		if (!in_array($classification, [
1410
			self::CLASSIFICATION_PUBLIC, self::CLASSIFICATION_PRIVATE, self::CLASSIFICATION_CONFIDENTIAL
1411
		])) {
1412
			throw new \InvalidArgumentException();
1413
		}
1414
		$query = $this->db->getQueryBuilder();
1415
		$query->update('calendarobjects')
1416
			->set('classification', $query->createNamedParameter($classification))
1417
			->where($query->expr()->eq('id', $query->createNamedParameter($calendarObjectId)))
1418
			->executeStatement();
1419
	}
1420
1421
	/**
1422
	 * Deletes an existing calendar object.
1423
	 *
1424
	 * The object uri is only the basename, or filename and not a full path.
1425
	 *
1426
	 * @param mixed $calendarId
1427
	 * @param string $objectUri
1428
	 * @param int $calendarType
1429
	 * @param bool $forceDeletePermanently
1430
	 * @return void
1431
	 */
1432
	public function deleteCalendarObject($calendarId, $objectUri, $calendarType = self::CALENDAR_TYPE_CALENDAR, bool $forceDeletePermanently = false) {
1433
		$this->atomic(function () use ($calendarId, $objectUri, $calendarType, $forceDeletePermanently) {
1434
			$data = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
1435
1436
			if ($data === null) {
1437
				// Nothing to delete
1438
				return;
1439
			}
1440
1441
			if ($forceDeletePermanently || $this->config->getAppValue(Application::APP_ID, RetentionService::RETENTION_CONFIG_KEY) === '0') {
1442
				$stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ? AND `uri` = ? AND `calendartype` = ?');
1443
				$stmt->execute([$calendarId, $objectUri, $calendarType]);
1444
1445
				$this->purgeProperties($calendarId, $data['id']);
1446
1447
				if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
1448
					$calendarRow = $this->getCalendarById($calendarId);
1449
					$shares = $this->getShares($calendarId);
1450
1451
					$this->dispatcher->dispatchTyped(new CalendarObjectDeletedEvent($calendarId, $calendarRow, $shares, $data));
1452
				} else {
1453
					$subscriptionRow = $this->getSubscriptionById($calendarId);
1454
1455
					$this->dispatcher->dispatchTyped(new CachedCalendarObjectDeletedEvent($calendarId, $subscriptionRow, [], $data));
1456
				}
1457
			} else {
1458
				$pathInfo = pathinfo($data['uri']);
1459
				if (!empty($pathInfo['extension'])) {
1460
					// Append a suffix to "free" the old URI for recreation
1461
					$newUri = sprintf(
1462
						"%s-deleted.%s",
1463
						$pathInfo['filename'],
1464
						$pathInfo['extension']
1465
					);
1466
				} else {
1467
					$newUri = sprintf(
1468
						"%s-deleted",
1469
						$pathInfo['filename']
1470
					);
1471
				}
1472
1473
				// Try to detect conflicts before the DB does
1474
				// As unlikely as it seems, this can happen when the user imports, then deletes, imports and deletes again
1475
				$newObject = $this->getCalendarObject($calendarId, $newUri, $calendarType);
1476
				if ($newObject !== null) {
1477
					throw new Forbidden("A calendar object with URI $newUri already exists in calendar $calendarId, therefore this object can't be moved into the trashbin");
1478
				}
1479
1480
				$qb = $this->db->getQueryBuilder();
1481
				$markObjectDeletedQuery = $qb->update('calendarobjects')
1482
					->set('deleted_at', $qb->createNamedParameter(time(), IQueryBuilder::PARAM_INT))
1483
					->set('uri', $qb->createNamedParameter($newUri))
1484
					->where(
1485
						$qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)),
1486
						$qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT),
1487
						$qb->expr()->eq('uri', $qb->createNamedParameter($objectUri))
1488
					);
1489
				$markObjectDeletedQuery->executeStatement();
1490
1491
				$calendarData = $this->getCalendarById($calendarId);
1492
				if ($calendarData !== null) {
1493
					$this->dispatcher->dispatchTyped(
1494
						new CalendarObjectMovedToTrashEvent(
1495
							$calendarId,
1496
							$calendarData,
1497
							$this->getShares($calendarId),
1498
							$data
1499
						)
1500
					);
1501
				}
1502
			}
1503
1504
			$this->addChange($calendarId, $objectUri, 3, $calendarType);
1505
		}, $this->db);
1506
	}
1507
1508
	/**
1509
	 * @param mixed $objectData
1510
	 *
1511
	 * @throws Forbidden
1512
	 */
1513
	public function restoreCalendarObject(array $objectData): void {
1514
		$this->atomic(function () use ($objectData) {
1515
			$id = (int) $objectData['id'];
1516
			$restoreUri = str_replace("-deleted.ics", ".ics", $objectData['uri']);
1517
			$targetObject = $this->getCalendarObject(
1518
				$objectData['calendarid'],
1519
				$restoreUri
1520
			);
1521
			if ($targetObject !== null) {
1522
				throw new Forbidden("Can not restore calendar $id because a calendar object with the URI $restoreUri already exists");
1523
			}
1524
1525
			$qb = $this->db->getQueryBuilder();
1526
			$update = $qb->update('calendarobjects')
1527
				->set('uri', $qb->createNamedParameter($restoreUri))
1528
				->set('deleted_at', $qb->createNamedParameter(null))
1529
				->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT));
1530
			$update->executeStatement();
1531
1532
			// Make sure this change is tracked in the changes table
1533
			$qb2 = $this->db->getQueryBuilder();
1534
			$selectObject = $qb2->select('calendardata', 'uri', 'calendarid', 'calendartype')
1535
				->selectAlias('componenttype', 'component')
1536
				->from('calendarobjects')
1537
				->where($qb2->expr()->eq('id', $qb2->createNamedParameter($id, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT));
1538
			$result = $selectObject->executeQuery();
1539
			$row = $result->fetch();
1540
			$result->closeCursor();
1541
			if ($row === false) {
1542
				// Welp, this should possibly not have happened, but let's ignore
1543
				return;
1544
			}
1545
			$this->addChange($row['calendarid'], $row['uri'], 1, (int) $row['calendartype']);
1546
1547
			$calendarRow = $this->getCalendarById((int) $row['calendarid']);
1548
			if ($calendarRow === null) {
1549
				throw new RuntimeException('Calendar object data that was just written can\'t be read back. Check your database configuration.');
1550
			}
1551
			$this->dispatcher->dispatchTyped(
1552
				new CalendarObjectRestoredEvent(
1553
					(int) $objectData['calendarid'],
1554
					$calendarRow,
1555
					$this->getShares((int) $row['calendarid']),
1556
					$row
1557
				)
1558
			);
1559
		}, $this->db);
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
		return $this->atomic(function () use ($principalUri, $filters, $limit, $offset) {
1709
			$calendars = $this->getCalendarsForUser($principalUri);
1710
			$ownCalendars = [];
1711
			$sharedCalendars = [];
1712
1713
			$uriMapper = [];
1714
1715
			foreach ($calendars as $calendar) {
1716
				if ($calendar['{http://owncloud.org/ns}owner-principal'] === $principalUri) {
1717
					$ownCalendars[] = $calendar['id'];
1718
				} else {
1719
					$sharedCalendars[] = $calendar['id'];
1720
				}
1721
				$uriMapper[$calendar['id']] = $calendar['uri'];
1722
			}
1723
			if (count($ownCalendars) === 0 && count($sharedCalendars) === 0) {
1724
				return [];
1725
			}
1726
1727
			$query = $this->db->getQueryBuilder();
1728
			// Calendar id expressions
1729
			$calendarExpressions = [];
1730
			foreach ($ownCalendars as $id) {
1731
				$calendarExpressions[] = $query->expr()->andX(
1732
					$query->expr()->eq('c.calendarid',
1733
						$query->createNamedParameter($id)),
1734
					$query->expr()->eq('c.calendartype',
1735
						$query->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
1736
			}
1737
			foreach ($sharedCalendars as $id) {
1738
				$calendarExpressions[] = $query->expr()->andX(
1739
					$query->expr()->eq('c.calendarid',
1740
						$query->createNamedParameter($id)),
1741
					$query->expr()->eq('c.classification',
1742
						$query->createNamedParameter(self::CLASSIFICATION_PUBLIC)),
1743
					$query->expr()->eq('c.calendartype',
1744
						$query->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
1745
			}
1746
1747
			if (count($calendarExpressions) === 1) {
1748
				$calExpr = $calendarExpressions[0];
1749
			} else {
1750
				$calExpr = call_user_func_array([$query->expr(), 'orX'], $calendarExpressions);
1751
			}
1752
1753
			// Component expressions
1754
			$compExpressions = [];
1755
			foreach ($filters['comps'] as $comp) {
1756
				$compExpressions[] = $query->expr()
1757
					->eq('c.componenttype', $query->createNamedParameter($comp));
1758
			}
1759
1760
			if (count($compExpressions) === 1) {
1761
				$compExpr = $compExpressions[0];
1762
			} else {
1763
				$compExpr = call_user_func_array([$query->expr(), 'orX'], $compExpressions);
1764
			}
1765
1766
			if (!isset($filters['props'])) {
1767
				$filters['props'] = [];
1768
			}
1769
			if (!isset($filters['params'])) {
1770
				$filters['params'] = [];
1771
			}
1772
1773
			$propParamExpressions = [];
1774
			foreach ($filters['props'] as $prop) {
1775
				$propParamExpressions[] = $query->expr()->andX(
1776
					$query->expr()->eq('i.name', $query->createNamedParameter($prop)),
1777
					$query->expr()->isNull('i.parameter')
1778
				);
1779
			}
1780
			foreach ($filters['params'] as $param) {
1781
				$propParamExpressions[] = $query->expr()->andX(
1782
					$query->expr()->eq('i.name', $query->createNamedParameter($param['property'])),
1783
					$query->expr()->eq('i.parameter', $query->createNamedParameter($param['parameter']))
1784
				);
1785
			}
1786
1787
			if (count($propParamExpressions) === 1) {
1788
				$propParamExpr = $propParamExpressions[0];
1789
			} else {
1790
				$propParamExpr = call_user_func_array([$query->expr(), 'orX'], $propParamExpressions);
1791
			}
1792
1793
			$query->select(['c.calendarid', 'c.uri'])
1794
				->from($this->dbObjectPropertiesTable, 'i')
1795
				->join('i', 'calendarobjects', 'c', $query->expr()->eq('i.objectid', 'c.id'))
1796
				->where($calExpr)
1797
				->andWhere($compExpr)
1798
				->andWhere($propParamExpr)
1799
				->andWhere($query->expr()->iLike('i.value',
1800
					$query->createNamedParameter('%'.$this->db->escapeLikeParameter($filters['search-term']).'%')))
1801
				->andWhere($query->expr()->isNull('deleted_at'));
1802
1803
			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...
1804
				$query->setFirstResult($offset);
1805
			}
1806
			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...
1807
				$query->setMaxResults($limit);
1808
			}
1809
1810
			$stmt = $query->executeQuery();
1811
1812
			$result = [];
1813
			while ($row = $stmt->fetch()) {
1814
				$path = $uriMapper[$row['calendarid']] . '/' . $row['uri'];
1815
				if (!in_array($path, $result)) {
1816
					$result[] = $path;
1817
				}
1818
			}
1819
1820
			return $result;
1821
		}, $this->db);
1822
	}
1823
1824
	/**
1825
	 * used for Nextcloud's calendar API
1826
	 *
1827
	 * @param array $calendarInfo
1828
	 * @param string $pattern
1829
	 * @param array $searchProperties
1830
	 * @param array $options
1831
	 * @param integer|null $limit
1832
	 * @param integer|null $offset
1833
	 *
1834
	 * @return array
1835
	 */
1836
	public function search(array $calendarInfo, $pattern, array $searchProperties,
1837
						   array $options, $limit, $offset) {
1838
		$outerQuery = $this->db->getQueryBuilder();
1839
		$innerQuery = $this->db->getQueryBuilder();
1840
1841
		$innerQuery->selectDistinct('op.objectid')
1842
			->from($this->dbObjectPropertiesTable, 'op')
1843
			->andWhere($innerQuery->expr()->eq('op.calendarid',
1844
				$outerQuery->createNamedParameter($calendarInfo['id'])))
1845
			->andWhere($innerQuery->expr()->eq('op.calendartype',
1846
				$outerQuery->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
1847
1848
		// only return public items for shared calendars for now
1849
		if (isset($calendarInfo['{http://owncloud.org/ns}owner-principal']) === false || $calendarInfo['principaluri'] !== $calendarInfo['{http://owncloud.org/ns}owner-principal']) {
1850
			$innerQuery->andWhere($innerQuery->expr()->eq('c.classification',
1851
				$outerQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
1852
		}
1853
1854
		if (!empty($searchProperties)) {
1855
			$or = $innerQuery->expr()->orX();
1856
			foreach ($searchProperties as $searchProperty) {
1857
				$or->add($innerQuery->expr()->eq('op.name',
1858
					$outerQuery->createNamedParameter($searchProperty)));
1859
			}
1860
			$innerQuery->andWhere($or);
1861
		}
1862
1863
		if ($pattern !== '') {
1864
			$innerQuery->andWhere($innerQuery->expr()->iLike('op.value',
1865
				$outerQuery->createNamedParameter('%' .
1866
					$this->db->escapeLikeParameter($pattern) . '%')));
1867
		}
1868
1869
		$outerQuery->select('c.id', 'c.calendardata', 'c.componenttype', 'c.uid', 'c.uri')
1870
			->from('calendarobjects', 'c')
1871
			->where($outerQuery->expr()->isNull('deleted_at'));
1872
1873
		if (isset($options['timerange'])) {
1874
			if (isset($options['timerange']['start']) && $options['timerange']['start'] instanceof DateTimeInterface) {
1875
				$outerQuery->andWhere($outerQuery->expr()->gt('lastoccurence',
1876
					$outerQuery->createNamedParameter($options['timerange']['start']->getTimeStamp())));
1877
			}
1878
			if (isset($options['timerange']['end']) && $options['timerange']['end'] instanceof DateTimeInterface) {
1879
				$outerQuery->andWhere($outerQuery->expr()->lt('firstoccurence',
1880
					$outerQuery->createNamedParameter($options['timerange']['end']->getTimeStamp())));
1881
			}
1882
		}
1883
1884
		if (isset($options['uid'])) {
1885
			$outerQuery->andWhere($outerQuery->expr()->eq('uid', $outerQuery->createNamedParameter($options['uid'])));
1886
		}
1887
1888
		if (!empty($options['types'])) {
1889
			$or = $outerQuery->expr()->orX();
1890
			foreach ($options['types'] as $type) {
1891
				$or->add($outerQuery->expr()->eq('componenttype',
1892
					$outerQuery->createNamedParameter($type)));
1893
			}
1894
			$outerQuery->andWhere($or);
1895
		}
1896
1897
		$outerQuery->andWhere($outerQuery->expr()->in('c.id', $outerQuery->createFunction($innerQuery->getSQL())));
1898
1899
		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...
1900
			$outerQuery->setFirstResult($offset);
1901
		}
1902
		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...
1903
			$outerQuery->setMaxResults($limit);
1904
		}
1905
1906
		$result = $outerQuery->executeQuery();
1907
		$calendarObjects = array_filter($result->fetchAll(), function (array $row) use ($options) {
1908
			$start = $options['timerange']['start'] ?? null;
1909
			$end = $options['timerange']['end'] ?? null;
1910
1911
			if ($start === null || !($start instanceof DateTimeInterface) || $end === null || !($end instanceof DateTimeInterface)) {
1912
				// No filter required
1913
				return true;
1914
			}
1915
1916
			$isValid = $this->validateFilterForObject($row, [
1917
				'name' => 'VCALENDAR',
1918
				'comp-filters' => [
1919
					[
1920
						'name' => 'VEVENT',
1921
						'comp-filters' => [],
1922
						'prop-filters' => [],
1923
						'is-not-defined' => false,
1924
						'time-range' => [
1925
							'start' => $start,
1926
							'end' => $end,
1927
						],
1928
					],
1929
				],
1930
				'prop-filters' => [],
1931
				'is-not-defined' => false,
1932
				'time-range' => null,
1933
			]);
1934
			if (is_resource($row['calendardata'])) {
1935
				// Put the stream back to the beginning so it can be read another time
1936
				rewind($row['calendardata']);
1937
			}
1938
			return $isValid;
1939
		});
1940
		$result->closeCursor();
1941
1942
		return array_map(function ($o) {
1943
			$calendarData = Reader::read($o['calendardata']);
1944
			$comps = $calendarData->getComponents();
1945
			$objects = [];
1946
			$timezones = [];
1947
			foreach ($comps as $comp) {
1948
				if ($comp instanceof VTimeZone) {
1949
					$timezones[] = $comp;
1950
				} else {
1951
					$objects[] = $comp;
1952
				}
1953
			}
1954
1955
			return [
1956
				'id' => $o['id'],
1957
				'type' => $o['componenttype'],
1958
				'uid' => $o['uid'],
1959
				'uri' => $o['uri'],
1960
				'objects' => array_map(function ($c) {
1961
					return $this->transformSearchData($c);
1962
				}, $objects),
1963
				'timezones' => array_map(function ($c) {
1964
					return $this->transformSearchData($c);
1965
				}, $timezones),
1966
			];
1967
		}, $calendarObjects);
1968
	}
1969
1970
	/**
1971
	 * @param Component $comp
1972
	 * @return array
1973
	 */
1974
	private function transformSearchData(Component $comp) {
1975
		$data = [];
1976
		/** @var Component[] $subComponents */
1977
		$subComponents = $comp->getComponents();
1978
		/** @var Property[] $properties */
1979
		$properties = array_filter($comp->children(), function ($c) {
1980
			return $c instanceof Property;
1981
		});
1982
		$validationRules = $comp->getValidationRules();
1983
1984
		foreach ($subComponents as $subComponent) {
1985
			$name = $subComponent->name;
1986
			if (!isset($data[$name])) {
1987
				$data[$name] = [];
1988
			}
1989
			$data[$name][] = $this->transformSearchData($subComponent);
1990
		}
1991
1992
		foreach ($properties as $property) {
1993
			$name = $property->name;
1994
			if (!isset($validationRules[$name])) {
1995
				$validationRules[$name] = '*';
1996
			}
1997
1998
			$rule = $validationRules[$property->name];
1999
			if ($rule === '+' || $rule === '*') { // multiple
2000
				if (!isset($data[$name])) {
2001
					$data[$name] = [];
2002
				}
2003
2004
				$data[$name][] = $this->transformSearchProperty($property);
2005
			} else { // once
2006
				$data[$name] = $this->transformSearchProperty($property);
2007
			}
2008
		}
2009
2010
		return $data;
2011
	}
2012
2013
	/**
2014
	 * @param Property $prop
2015
	 * @return array
2016
	 */
2017
	private function transformSearchProperty(Property $prop) {
2018
		// No need to check Date, as it extends DateTime
2019
		if ($prop instanceof Property\ICalendar\DateTime) {
2020
			$value = $prop->getDateTime();
2021
		} else {
2022
			$value = $prop->getValue();
2023
		}
2024
2025
		return [
2026
			$value,
2027
			$prop->parameters()
2028
		];
2029
	}
2030
2031
	/**
2032
	 * @param string $principalUri
2033
	 * @param string $pattern
2034
	 * @param array $componentTypes
2035
	 * @param array $searchProperties
2036
	 * @param array $searchParameters
2037
	 * @param array $options
2038
	 * @return array
2039
	 */
2040
	public function searchPrincipalUri(string $principalUri,
2041
									   string $pattern,
2042
									   array $componentTypes,
2043
									   array $searchProperties,
2044
									   array $searchParameters,
2045
									   array $options = []): array {
2046
		return $this->atomic(function () use ($principalUri, $pattern, $componentTypes, $searchProperties, $searchParameters, $options) {
2047
			$escapePattern = !\array_key_exists('escape_like_param', $options) || $options['escape_like_param'] !== false;
2048
2049
			$calendarObjectIdQuery = $this->db->getQueryBuilder();
2050
			$calendarOr = $calendarObjectIdQuery->expr()->orX();
2051
			$searchOr = $calendarObjectIdQuery->expr()->orX();
2052
2053
			// Fetch calendars and subscription
2054
			$calendars = $this->getCalendarsForUser($principalUri);
2055
			$subscriptions = $this->getSubscriptionsForUser($principalUri);
2056
			foreach ($calendars as $calendar) {
2057
				$calendarAnd = $calendarObjectIdQuery->expr()->andX();
2058
				$calendarAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int)$calendar['id'])));
2059
				$calendarAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendartype', $calendarObjectIdQuery->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
2060
2061
				// If it's shared, limit search to public events
2062
				if (isset($calendar['{http://owncloud.org/ns}owner-principal'])
2063
					&& $calendar['principaluri'] !== $calendar['{http://owncloud.org/ns}owner-principal']) {
2064
					$calendarAnd->add($calendarObjectIdQuery->expr()->eq('co.classification', $calendarObjectIdQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
2065
				}
2066
2067
				$calendarOr->add($calendarAnd);
2068
			}
2069
			foreach ($subscriptions as $subscription) {
2070
				$subscriptionAnd = $calendarObjectIdQuery->expr()->andX();
2071
				$subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int)$subscription['id'])));
2072
				$subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendartype', $calendarObjectIdQuery->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)));
2073
2074
				// If it's shared, limit search to public events
2075
				if (isset($subscription['{http://owncloud.org/ns}owner-principal'])
2076
					&& $subscription['principaluri'] !== $subscription['{http://owncloud.org/ns}owner-principal']) {
2077
					$subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('co.classification', $calendarObjectIdQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
2078
				}
2079
2080
				$calendarOr->add($subscriptionAnd);
2081
			}
2082
2083
			foreach ($searchProperties as $property) {
2084
				$propertyAnd = $calendarObjectIdQuery->expr()->andX();
2085
				$propertyAnd->add($calendarObjectIdQuery->expr()->eq('cob.name', $calendarObjectIdQuery->createNamedParameter($property, IQueryBuilder::PARAM_STR)));
2086
				$propertyAnd->add($calendarObjectIdQuery->expr()->isNull('cob.parameter'));
2087
2088
				$searchOr->add($propertyAnd);
2089
			}
2090
			foreach ($searchParameters as $property => $parameter) {
2091
				$parameterAnd = $calendarObjectIdQuery->expr()->andX();
2092
				$parameterAnd->add($calendarObjectIdQuery->expr()->eq('cob.name', $calendarObjectIdQuery->createNamedParameter($property, IQueryBuilder::PARAM_STR)));
2093
				$parameterAnd->add($calendarObjectIdQuery->expr()->eq('cob.parameter', $calendarObjectIdQuery->createNamedParameter($parameter, IQueryBuilder::PARAM_STR_ARRAY)));
2094
2095
				$searchOr->add($parameterAnd);
2096
			}
2097
2098
			if ($calendarOr->count() === 0) {
2099
				return [];
2100
			}
2101
			if ($searchOr->count() === 0) {
2102
				return [];
2103
			}
2104
2105
			$calendarObjectIdQuery->selectDistinct('cob.objectid')
2106
				->from($this->dbObjectPropertiesTable, 'cob')
2107
				->leftJoin('cob', 'calendarobjects', 'co', $calendarObjectIdQuery->expr()->eq('co.id', 'cob.objectid'))
2108
				->andWhere($calendarObjectIdQuery->expr()->in('co.componenttype', $calendarObjectIdQuery->createNamedParameter($componentTypes, IQueryBuilder::PARAM_STR_ARRAY)))
2109
				->andWhere($calendarOr)
2110
				->andWhere($searchOr)
2111
				->andWhere($calendarObjectIdQuery->expr()->isNull('deleted_at'));
2112
2113
			if ('' !== $pattern) {
2114
				if (!$escapePattern) {
2115
					$calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->ilike('cob.value', $calendarObjectIdQuery->createNamedParameter($pattern)));
2116
				} else {
2117
					$calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->ilike('cob.value', $calendarObjectIdQuery->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%')));
2118
				}
2119
			}
2120
2121
			if (isset($options['limit'])) {
2122
				$calendarObjectIdQuery->setMaxResults($options['limit']);
2123
			}
2124
			if (isset($options['offset'])) {
2125
				$calendarObjectIdQuery->setFirstResult($options['offset']);
2126
			}
2127
2128
			$result = $calendarObjectIdQuery->executeQuery();
2129
			$matches = $result->fetchAll();
2130
			$result->closeCursor();
2131
			$matches = array_map(static function (array $match):int {
2132
				return (int) $match['objectid'];
2133
			}, $matches);
2134
2135
			$query = $this->db->getQueryBuilder();
2136
			$query->select('calendardata', 'uri', 'calendarid', 'calendartype')
2137
				->from('calendarobjects')
2138
				->where($query->expr()->in('id', $query->createNamedParameter($matches, IQueryBuilder::PARAM_INT_ARRAY)));
2139
2140
			$result = $query->executeQuery();
2141
			$calendarObjects = $result->fetchAll();
2142
			$result->closeCursor();
2143
2144
			return array_map(function (array $array): array {
2145
				$array['calendarid'] = (int)$array['calendarid'];
2146
				$array['calendartype'] = (int)$array['calendartype'];
2147
				$array['calendardata'] = $this->readBlob($array['calendardata']);
2148
2149
				return $array;
2150
			}, $calendarObjects);
2151
		}, $this->db);
2152
	}
2153
2154
	/**
2155
	 * Searches through all of a users calendars and calendar objects to find
2156
	 * an object with a specific UID.
2157
	 *
2158
	 * This method should return the path to this object, relative to the
2159
	 * calendar home, so this path usually only contains two parts:
2160
	 *
2161
	 * calendarpath/objectpath.ics
2162
	 *
2163
	 * If the uid is not found, return null.
2164
	 *
2165
	 * This method should only consider * objects that the principal owns, so
2166
	 * any calendars owned by other principals that also appear in this
2167
	 * collection should be ignored.
2168
	 *
2169
	 * @param string $principalUri
2170
	 * @param string $uid
2171
	 * @return string|null
2172
	 */
2173
	public function getCalendarObjectByUID($principalUri, $uid) {
2174
		$query = $this->db->getQueryBuilder();
2175
		$query->selectAlias('c.uri', 'calendaruri')->selectAlias('co.uri', 'objecturi')
2176
			->from('calendarobjects', 'co')
2177
			->leftJoin('co', 'calendars', 'c', $query->expr()->eq('co.calendarid', 'c.id'))
2178
			->where($query->expr()->eq('c.principaluri', $query->createNamedParameter($principalUri)))
2179
			->andWhere($query->expr()->eq('co.uid', $query->createNamedParameter($uid)))
2180
			->andWhere($query->expr()->isNull('co.deleted_at'));
2181
		$stmt = $query->executeQuery();
2182
		$row = $stmt->fetch();
2183
		$stmt->closeCursor();
2184
		if ($row) {
2185
			return $row['calendaruri'] . '/' . $row['objecturi'];
2186
		}
2187
2188
		return null;
2189
	}
2190
2191
	public function getCalendarObjectById(string $principalUri, int $id): ?array {
2192
		$query = $this->db->getQueryBuilder();
2193
		$query->select(['co.id', 'co.uri', 'co.lastmodified', 'co.etag', 'co.calendarid', 'co.size', 'co.calendardata', 'co.componenttype', 'co.classification', 'co.deleted_at'])
2194
			->selectAlias('c.uri', 'calendaruri')
2195
			->from('calendarobjects', 'co')
2196
			->join('co', 'calendars', 'c', $query->expr()->eq('c.id', 'co.calendarid', IQueryBuilder::PARAM_INT))
2197
			->where($query->expr()->eq('c.principaluri', $query->createNamedParameter($principalUri)))
2198
			->andWhere($query->expr()->eq('co.id', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT));
2199
		$stmt = $query->executeQuery();
2200
		$row = $stmt->fetch();
2201
		$stmt->closeCursor();
2202
2203
		if (!$row) {
2204
			return null;
2205
		}
2206
2207
		return [
2208
			'id' => $row['id'],
2209
			'uri' => $row['uri'],
2210
			'lastmodified' => $row['lastmodified'],
2211
			'etag' => '"' . $row['etag'] . '"',
2212
			'calendarid' => $row['calendarid'],
2213
			'calendaruri' => $row['calendaruri'],
2214
			'size' => (int)$row['size'],
2215
			'calendardata' => $this->readBlob($row['calendardata']),
2216
			'component' => strtolower($row['componenttype']),
2217
			'classification' => (int)$row['classification'],
2218
			'deleted_at' => isset($row['deleted_at']) ? ((int) $row['deleted_at']) : null,
2219
		];
2220
	}
2221
2222
	/**
2223
	 * The getChanges method returns all the changes that have happened, since
2224
	 * the specified syncToken in the specified calendar.
2225
	 *
2226
	 * This function should return an array, such as the following:
2227
	 *
2228
	 * [
2229
	 *   'syncToken' => 'The current synctoken',
2230
	 *   'added'   => [
2231
	 *      'new.txt',
2232
	 *   ],
2233
	 *   'modified'   => [
2234
	 *      'modified.txt',
2235
	 *   ],
2236
	 *   'deleted' => [
2237
	 *      'foo.php.bak',
2238
	 *      'old.txt'
2239
	 *   ]
2240
	 * );
2241
	 *
2242
	 * The returned syncToken property should reflect the *current* syncToken
2243
	 * of the calendar, as reported in the {http://sabredav.org/ns}sync-token
2244
	 * property This is * needed here too, to ensure the operation is atomic.
2245
	 *
2246
	 * If the $syncToken argument is specified as null, this is an initial
2247
	 * sync, and all members should be reported.
2248
	 *
2249
	 * The modified property is an array of nodenames that have changed since
2250
	 * the last token.
2251
	 *
2252
	 * The deleted property is an array with nodenames, that have been deleted
2253
	 * from collection.
2254
	 *
2255
	 * The $syncLevel argument is basically the 'depth' of the report. If it's
2256
	 * 1, you only have to report changes that happened only directly in
2257
	 * immediate descendants. If it's 2, it should also include changes from
2258
	 * the nodes below the child collections. (grandchildren)
2259
	 *
2260
	 * The $limit argument allows a client to specify how many results should
2261
	 * be returned at most. If the limit is not specified, it should be treated
2262
	 * as infinite.
2263
	 *
2264
	 * If the limit (infinite or not) is higher than you're willing to return,
2265
	 * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
2266
	 *
2267
	 * If the syncToken is expired (due to data cleanup) or unknown, you must
2268
	 * return null.
2269
	 *
2270
	 * The limit is 'suggestive'. You are free to ignore it.
2271
	 *
2272
	 * @param string $calendarId
2273
	 * @param string $syncToken
2274
	 * @param int $syncLevel
2275
	 * @param int|null $limit
2276
	 * @param int $calendarType
2277
	 * @return array
2278
	 */
2279
	public function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit = null, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
2280
		return $this->atomic(function () use ($calendarId, $syncToken, $syncLevel, $limit, $calendarType) {
0 ignored issues
show
Unused Code introduced by
The import $syncLevel is not used and could be removed.

This check looks for imports that have been defined, but are not used in the scope.

Loading history...
2281
			// Current synctoken
2282
			$qb = $this->db->getQueryBuilder();
2283
			$qb->select('synctoken')
2284
				->from('calendars')
2285
				->where(
2286
					$qb->expr()->eq('id', $qb->createNamedParameter($calendarId))
2287
				);
2288
			$stmt = $qb->executeQuery();
2289
			$currentToken = $stmt->fetchOne();
2290
2291
			if ($currentToken === false) {
2292
				return null;
2293
			}
2294
2295
			$result = [
2296
				'syncToken' => $currentToken,
2297
				'added' => [],
2298
				'modified' => [],
2299
				'deleted' => [],
2300
			];
2301
2302
			if ($syncToken) {
2303
				$qb = $this->db->getQueryBuilder();
2304
2305
				$qb->select('uri', 'operation')
2306
					->from('calendarchanges')
2307
					->where(
2308
						$qb->expr()->andX(
2309
							$qb->expr()->gte('synctoken', $qb->createNamedParameter($syncToken)),
2310
							$qb->expr()->lt('synctoken', $qb->createNamedParameter($currentToken)),
2311
							$qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)),
2312
							$qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType))
2313
						)
2314
					)->orderBy('synctoken');
2315
				if (is_int($limit) && $limit > 0) {
2316
					$qb->setMaxResults($limit);
2317
				}
2318
2319
				// Fetching all changes
2320
				$stmt = $qb->executeQuery();
2321
				$changes = [];
2322
2323
				// This loop ensures that any duplicates are overwritten, only the
2324
				// last change on a node is relevant.
2325
				while ($row = $stmt->fetch()) {
2326
					$changes[$row['uri']] = $row['operation'];
2327
				}
2328
				$stmt->closeCursor();
2329
2330
				foreach ($changes as $uri => $operation) {
2331
					switch ($operation) {
2332
						case 1:
2333
							$result['added'][] = $uri;
2334
							break;
2335
						case 2:
2336
							$result['modified'][] = $uri;
2337
							break;
2338
						case 3:
2339
							$result['deleted'][] = $uri;
2340
							break;
2341
					}
2342
				}
2343
			} else {
2344
				// No synctoken supplied, this is the initial sync.
2345
				$qb = $this->db->getQueryBuilder();
2346
				$qb->select('uri')
2347
					->from('calendarobjects')
2348
					->where(
2349
						$qb->expr()->andX(
2350
							$qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)),
2351
							$qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType))
2352
						)
2353
					);
2354
				$stmt = $qb->executeQuery();
2355
				$result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
2356
				$stmt->closeCursor();
2357
			}
2358
			return $result;
2359
		}, $this->db);
2360
	}
2361
2362
	/**
2363
	 * Returns a list of subscriptions for a principal.
2364
	 *
2365
	 * Every subscription is an array with the following keys:
2366
	 *  * id, a unique id that will be used by other functions to modify the
2367
	 *    subscription. This can be the same as the uri or a database key.
2368
	 *  * uri. This is just the 'base uri' or 'filename' of the subscription.
2369
	 *  * principaluri. The owner of the subscription. Almost always the same as
2370
	 *    principalUri passed to this method.
2371
	 *
2372
	 * Furthermore, all the subscription info must be returned too:
2373
	 *
2374
	 * 1. {DAV:}displayname
2375
	 * 2. {http://apple.com/ns/ical/}refreshrate
2376
	 * 3. {http://calendarserver.org/ns/}subscribed-strip-todos (omit if todos
2377
	 *    should not be stripped).
2378
	 * 4. {http://calendarserver.org/ns/}subscribed-strip-alarms (omit if alarms
2379
	 *    should not be stripped).
2380
	 * 5. {http://calendarserver.org/ns/}subscribed-strip-attachments (omit if
2381
	 *    attachments should not be stripped).
2382
	 * 6. {http://calendarserver.org/ns/}source (Must be a
2383
	 *     Sabre\DAV\Property\Href).
2384
	 * 7. {http://apple.com/ns/ical/}calendar-color
2385
	 * 8. {http://apple.com/ns/ical/}calendar-order
2386
	 * 9. {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
2387
	 *    (should just be an instance of
2388
	 *    Sabre\CalDAV\Property\SupportedCalendarComponentSet, with a bunch of
2389
	 *    default components).
2390
	 *
2391
	 * @param string $principalUri
2392
	 * @return array
2393
	 */
2394
	public function getSubscriptionsForUser($principalUri) {
2395
		$fields = array_column($this->subscriptionPropertyMap, 0);
2396
		$fields[] = 'id';
2397
		$fields[] = 'uri';
2398
		$fields[] = 'source';
2399
		$fields[] = 'principaluri';
2400
		$fields[] = 'lastmodified';
2401
		$fields[] = 'synctoken';
2402
2403
		$query = $this->db->getQueryBuilder();
2404
		$query->select($fields)
2405
			->from('calendarsubscriptions')
2406
			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
2407
			->orderBy('calendarorder', 'asc');
2408
		$stmt = $query->executeQuery();
2409
2410
		$subscriptions = [];
2411
		while ($row = $stmt->fetch()) {
2412
			$subscription = [
2413
				'id' => $row['id'],
2414
				'uri' => $row['uri'],
2415
				'principaluri' => $row['principaluri'],
2416
				'source' => $row['source'],
2417
				'lastmodified' => $row['lastmodified'],
2418
2419
				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
2420
				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
2421
			];
2422
2423
			$subscriptions[] = $this->rowToSubscription($row, $subscription);
2424
		}
2425
2426
		return $subscriptions;
2427
	}
2428
2429
	/**
2430
	 * Creates a new subscription for a principal.
2431
	 *
2432
	 * If the creation was a success, an id must be returned that can be used to reference
2433
	 * this subscription in other methods, such as updateSubscription.
2434
	 *
2435
	 * @param string $principalUri
2436
	 * @param string $uri
2437
	 * @param array $properties
2438
	 * @return mixed
2439
	 */
2440
	public function createSubscription($principalUri, $uri, array $properties) {
2441
		if (!isset($properties['{http://calendarserver.org/ns/}source'])) {
2442
			throw new Forbidden('The {http://calendarserver.org/ns/}source property is required when creating subscriptions');
2443
		}
2444
2445
		$values = [
2446
			'principaluri' => $principalUri,
2447
			'uri' => $uri,
2448
			'source' => $properties['{http://calendarserver.org/ns/}source']->getHref(),
2449
			'lastmodified' => time(),
2450
		];
2451
2452
		$propertiesBoolean = ['striptodos', 'stripalarms', 'stripattachments'];
2453
2454
		foreach ($this->subscriptionPropertyMap as $xmlName => [$dbName, $type]) {
2455
			if (array_key_exists($xmlName, $properties)) {
2456
				$values[$dbName] = $properties[$xmlName];
2457
				if (in_array($dbName, $propertiesBoolean)) {
2458
					$values[$dbName] = true;
2459
				}
2460
			}
2461
		}
2462
2463
		[$subscriptionId, $subscriptionRow] = $this->atomic(function () use ($values) {
2464
			$valuesToInsert = [];
2465
			$query = $this->db->getQueryBuilder();
2466
			foreach (array_keys($values) as $name) {
2467
				$valuesToInsert[$name] = $query->createNamedParameter($values[$name]);
2468
			}
2469
			$query->insert('calendarsubscriptions')
2470
				->values($valuesToInsert)
2471
				->executeStatement();
2472
2473
			$subscriptionId = $query->getLastInsertId();
2474
2475
			$subscriptionRow = $this->getSubscriptionById($subscriptionId);
2476
			return [$subscriptionId, $subscriptionRow];
2477
		}, $this->db);
2478
2479
		$this->dispatcher->dispatchTyped(new SubscriptionCreatedEvent($subscriptionId, $subscriptionRow));
2480
2481
		return $subscriptionId;
2482
	}
2483
2484
	/**
2485
	 * Updates a subscription
2486
	 *
2487
	 * The list of mutations is stored in a Sabre\DAV\PropPatch object.
2488
	 * To do the actual updates, you must tell this object which properties
2489
	 * you're going to process with the handle() method.
2490
	 *
2491
	 * Calling the handle method is like telling the PropPatch object "I
2492
	 * promise I can handle updating this property".
2493
	 *
2494
	 * Read the PropPatch documentation for more info and examples.
2495
	 *
2496
	 * @param mixed $subscriptionId
2497
	 * @param PropPatch $propPatch
2498
	 * @return void
2499
	 */
2500
	public function updateSubscription($subscriptionId, PropPatch $propPatch) {
2501
		$this->atomic(function () use ($subscriptionId, $propPatch) {
2502
			$supportedProperties = array_keys($this->subscriptionPropertyMap);
2503
			$supportedProperties[] = '{http://calendarserver.org/ns/}source';
2504
2505
			$propPatch->handle($supportedProperties, function ($mutations) use ($subscriptionId) {
2506
				$newValues = [];
2507
2508
				foreach ($mutations as $propertyName => $propertyValue) {
2509
					if ($propertyName === '{http://calendarserver.org/ns/}source') {
2510
						$newValues['source'] = $propertyValue->getHref();
2511
					} else {
2512
						$fieldName = $this->subscriptionPropertyMap[$propertyName][0];
2513
						$newValues[$fieldName] = $propertyValue;
2514
					}
2515
				}
2516
2517
				$query = $this->db->getQueryBuilder();
2518
				$query->update('calendarsubscriptions')
2519
					->set('lastmodified', $query->createNamedParameter(time()));
2520
				foreach ($newValues as $fieldName => $value) {
2521
					$query->set($fieldName, $query->createNamedParameter($value));
2522
				}
2523
				$query->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
2524
					->executeStatement();
2525
2526
				$subscriptionRow = $this->getSubscriptionById($subscriptionId);
2527
				$this->dispatcher->dispatchTyped(new SubscriptionUpdatedEvent((int)$subscriptionId, $subscriptionRow, [], $mutations));
2528
2529
				return true;
2530
			});
2531
		}, $this->db);
2532
	}
2533
2534
	/**
2535
	 * Deletes a subscription.
2536
	 *
2537
	 * @param mixed $subscriptionId
2538
	 * @return void
2539
	 */
2540
	public function deleteSubscription($subscriptionId) {
2541
		$this->atomic(function () use ($subscriptionId) {
2542
			$subscriptionRow = $this->getSubscriptionById($subscriptionId);
2543
2544
			$query = $this->db->getQueryBuilder();
2545
			$query->delete('calendarsubscriptions')
2546
				->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
2547
				->executeStatement();
2548
2549
			$query = $this->db->getQueryBuilder();
2550
			$query->delete('calendarobjects')
2551
				->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2552
				->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2553
				->executeStatement();
2554
2555
			$query->delete('calendarchanges')
2556
				->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2557
				->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2558
				->executeStatement();
2559
2560
			$query->delete($this->dbObjectPropertiesTable)
2561
				->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2562
				->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2563
				->executeStatement();
2564
2565
			if ($subscriptionRow) {
2566
				$this->dispatcher->dispatchTyped(new SubscriptionDeletedEvent((int)$subscriptionId, $subscriptionRow, []));
2567
			}
2568
		}, $this->db);
2569
	}
2570
2571
	/**
2572
	 * Returns a single scheduling object for the inbox collection.
2573
	 *
2574
	 * The returned array should contain the following elements:
2575
	 *   * uri - A unique basename for the object. This will be used to
2576
	 *           construct a full uri.
2577
	 *   * calendardata - The iCalendar object
2578
	 *   * lastmodified - The last modification date. Can be an int for a unix
2579
	 *                    timestamp, or a PHP DateTime object.
2580
	 *   * etag - A unique token that must change if the object changed.
2581
	 *   * size - The size of the object, in bytes.
2582
	 *
2583
	 * @param string $principalUri
2584
	 * @param string $objectUri
2585
	 * @return array
2586
	 */
2587
	public function getSchedulingObject($principalUri, $objectUri) {
2588
		$query = $this->db->getQueryBuilder();
2589
		$stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size'])
2590
			->from('schedulingobjects')
2591
			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
2592
			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
2593
			->executeQuery();
2594
2595
		$row = $stmt->fetch();
2596
2597
		if (!$row) {
2598
			return null;
2599
		}
2600
2601
		return [
2602
			'uri' => $row['uri'],
2603
			'calendardata' => $row['calendardata'],
2604
			'lastmodified' => $row['lastmodified'],
2605
			'etag' => '"' . $row['etag'] . '"',
2606
			'size' => (int)$row['size'],
2607
		];
2608
	}
2609
2610
	/**
2611
	 * Returns all scheduling objects for the inbox collection.
2612
	 *
2613
	 * These objects should be returned as an array. Every item in the array
2614
	 * should follow the same structure as returned from getSchedulingObject.
2615
	 *
2616
	 * The main difference is that 'calendardata' is optional.
2617
	 *
2618
	 * @param string $principalUri
2619
	 * @return array
2620
	 */
2621
	public function getSchedulingObjects($principalUri) {
2622
		$query = $this->db->getQueryBuilder();
2623
		$stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size'])
2624
				->from('schedulingobjects')
2625
				->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
2626
				->executeQuery();
2627
2628
		$result = [];
2629
		foreach ($stmt->fetchAll() as $row) {
2630
			$result[] = [
2631
				'calendardata' => $row['calendardata'],
2632
				'uri' => $row['uri'],
2633
				'lastmodified' => $row['lastmodified'],
2634
				'etag' => '"' . $row['etag'] . '"',
2635
				'size' => (int)$row['size'],
2636
			];
2637
		}
2638
		$stmt->closeCursor();
2639
2640
		return $result;
2641
	}
2642
2643
	/**
2644
	 * Deletes a scheduling object from the inbox collection.
2645
	 *
2646
	 * @param string $principalUri
2647
	 * @param string $objectUri
2648
	 * @return void
2649
	 */
2650
	public function deleteSchedulingObject($principalUri, $objectUri) {
2651
		$query = $this->db->getQueryBuilder();
2652
		$query->delete('schedulingobjects')
2653
				->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
2654
				->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
2655
				->executeStatement();
2656
	}
2657
2658
	/**
2659
	 * Creates a new scheduling object. This should land in a users' inbox.
2660
	 *
2661
	 * @param string $principalUri
2662
	 * @param string $objectUri
2663
	 * @param string $objectData
2664
	 * @return void
2665
	 */
2666
	public function createSchedulingObject($principalUri, $objectUri, $objectData) {
2667
		$query = $this->db->getQueryBuilder();
2668
		$query->insert('schedulingobjects')
2669
			->values([
2670
				'principaluri' => $query->createNamedParameter($principalUri),
2671
				'calendardata' => $query->createNamedParameter($objectData, IQueryBuilder::PARAM_LOB),
2672
				'uri' => $query->createNamedParameter($objectUri),
2673
				'lastmodified' => $query->createNamedParameter(time()),
2674
				'etag' => $query->createNamedParameter(md5($objectData)),
2675
				'size' => $query->createNamedParameter(strlen($objectData))
2676
			])
2677
			->executeStatement();
2678
	}
2679
2680
	/**
2681
	 * Adds a change record to the calendarchanges table.
2682
	 *
2683
	 * @param mixed $calendarId
2684
	 * @param string $objectUri
2685
	 * @param int $operation 1 = add, 2 = modify, 3 = delete.
2686
	 * @param int $calendarType
2687
	 * @return void
2688
	 */
2689
	protected function addChange(int $calendarId, string $objectUri, int $operation, int $calendarType = self::CALENDAR_TYPE_CALENDAR): void {
2690
		$table = $calendarType === self::CALENDAR_TYPE_CALENDAR ? 'calendars': 'calendarsubscriptions';
2691
2692
		$this->atomic(function () use ($calendarId, $objectUri, $operation, $calendarType, $table) {
2693
			$query = $this->db->getQueryBuilder();
2694
			$query->select('synctoken')
2695
				->from($table)
2696
				->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)));
2697
			$result = $query->executeQuery();
2698
			$syncToken = (int)$result->fetchOne();
2699
			$result->closeCursor();
2700
2701
			$query = $this->db->getQueryBuilder();
2702
			$query->insert('calendarchanges')
2703
				->values([
2704
					'uri' => $query->createNamedParameter($objectUri),
2705
					'synctoken' => $query->createNamedParameter($syncToken),
2706
					'calendarid' => $query->createNamedParameter($calendarId),
2707
					'operation' => $query->createNamedParameter($operation),
2708
					'calendartype' => $query->createNamedParameter($calendarType),
2709
				])
2710
				->executeStatement();
2711
2712
			$query = $this->db->getQueryBuilder();
2713
			$query->update($table)
2714
				->set('synctoken', $query->createNamedParameter($syncToken + 1, IQueryBuilder::PARAM_INT))
2715
				->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)))
2716
				->executeStatement();
2717
		}, $this->db);
2718
	}
2719
2720
	/**
2721
	 * Parses some information from calendar objects, used for optimized
2722
	 * calendar-queries.
2723
	 *
2724
	 * Returns an array with the following keys:
2725
	 *   * etag - An md5 checksum of the object without the quotes.
2726
	 *   * size - Size of the object in bytes
2727
	 *   * componentType - VEVENT, VTODO or VJOURNAL
2728
	 *   * firstOccurence
2729
	 *   * lastOccurence
2730
	 *   * uid - value of the UID property
2731
	 *
2732
	 * @param string $calendarData
2733
	 * @return array
2734
	 */
2735
	public function getDenormalizedData($calendarData) {
2736
		$vObject = Reader::read($calendarData);
2737
		$vEvents = [];
2738
		$componentType = null;
2739
		$component = null;
2740
		$firstOccurrence = null;
2741
		$lastOccurrence = null;
2742
		$uid = null;
2743
		$classification = self::CLASSIFICATION_PUBLIC;
2744
		$hasDTSTART = false;
2745
		foreach ($vObject->getComponents() as $component) {
2746
			if ($component->name !== 'VTIMEZONE') {
2747
				// Finding all VEVENTs, and track them
2748
				if ($component->name === 'VEVENT') {
2749
					array_push($vEvents, $component);
2750
					if ($component->DTSTART) {
2751
						$hasDTSTART = true;
2752
					}
2753
				}
2754
				// Track first component type and uid
2755
				if ($uid === null) {
2756
					$componentType = $component->name;
2757
					$uid = (string)$component->UID;
2758
				}
2759
			}
2760
		}
2761
		if (!$componentType) {
2762
			throw new BadRequest('Calendar objects must have a VJOURNAL, VEVENT or VTODO component');
2763
		}
2764
2765
		if ($hasDTSTART) {
2766
			$component = $vEvents[0];
2767
2768
			// Finding the last occurrence is a bit harder
2769
			if (!isset($component->RRULE) && count($vEvents) === 1) {
2770
				$firstOccurrence = $component->DTSTART->getDateTime()->getTimeStamp();
2771
				if (isset($component->DTEND)) {
2772
					$lastOccurrence = $component->DTEND->getDateTime()->getTimeStamp();
2773
				} elseif (isset($component->DURATION)) {
2774
					$endDate = clone $component->DTSTART->getDateTime();
2775
					$endDate->add(DateTimeParser::parse($component->DURATION->getValue()));
2776
					$lastOccurrence = $endDate->getTimeStamp();
2777
				} elseif (!$component->DTSTART->hasTime()) {
2778
					$endDate = clone $component->DTSTART->getDateTime();
2779
					$endDate->modify('+1 day');
2780
					$lastOccurrence = $endDate->getTimeStamp();
2781
				} else {
2782
					$lastOccurrence = $firstOccurrence;
2783
				}
2784
			} else {
2785
				$it = new EventIterator($vEvents);
2786
				$maxDate = new DateTime(self::MAX_DATE);
2787
				$firstOccurrence = $it->getDtStart()->getTimestamp();
2788
				if ($it->isInfinite()) {
2789
					$lastOccurrence = $maxDate->getTimestamp();
2790
				} else {
2791
					$end = $it->getDtEnd();
2792
					while ($it->valid() && $end < $maxDate) {
2793
						$end = $it->getDtEnd();
2794
						$it->next();
2795
					}
2796
					$lastOccurrence = $end->getTimestamp();
2797
				}
2798
			}
2799
		}
2800
2801
		if ($component->CLASS) {
2802
			$classification = CalDavBackend::CLASSIFICATION_PRIVATE;
2803
			switch ($component->CLASS->getValue()) {
2804
				case 'PUBLIC':
2805
					$classification = CalDavBackend::CLASSIFICATION_PUBLIC;
2806
					break;
2807
				case 'CONFIDENTIAL':
2808
					$classification = CalDavBackend::CLASSIFICATION_CONFIDENTIAL;
2809
					break;
2810
			}
2811
		}
2812
		return [
2813
			'etag' => md5($calendarData),
2814
			'size' => strlen($calendarData),
2815
			'componentType' => $componentType,
2816
			'firstOccurence' => is_null($firstOccurrence) ? null : max(0, $firstOccurrence),
2817
			'lastOccurence' => $lastOccurrence,
2818
			'uid' => $uid,
2819
			'classification' => $classification
2820
		];
2821
	}
2822
2823
	/**
2824
	 * @param $cardData
2825
	 * @return bool|string
2826
	 */
2827
	private function readBlob($cardData) {
2828
		if (is_resource($cardData)) {
2829
			return stream_get_contents($cardData);
2830
		}
2831
2832
		return $cardData;
2833
	}
2834
2835
	/**
2836
	 * @param list<array{href: string, commonName: string, readOnly: bool}> $add
2837
	 * @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...
2838
	 */
2839
	public function updateShares(IShareable $shareable, array $add, array $remove): void {
2840
		$this->atomic(function () use ($shareable, $add, $remove) {
2841
			$calendarId = $shareable->getResourceId();
2842
			$calendarRow = $this->getCalendarById($calendarId);
2843
			if ($calendarRow === null) {
2844
				throw new \RuntimeException('Trying to update shares for innexistant calendar: ' . $calendarId);
2845
			}
2846
			$oldShares = $this->getShares($calendarId);
2847
2848
			$this->calendarSharingBackend->updateShares($shareable, $add, $remove);
2849
2850
			$this->dispatcher->dispatchTyped(new CalendarShareUpdatedEvent($calendarId, $calendarRow, $oldShares, $add, $remove));
2851
		}, $this->db);
2852
	}
2853
2854
	/**
2855
	 * @return list<array{href: string, commonName: string, status: int, readOnly: bool, '{http://owncloud.org/ns}principal': string, '{http://owncloud.org/ns}group-share': bool}>
2856
	 */
2857
	public function getShares(int $resourceId): array {
2858
		return $this->calendarSharingBackend->getShares($resourceId);
2859
	}
2860
2861
	/**
2862
	 * @param boolean $value
2863
	 * @param \OCA\DAV\CalDAV\Calendar $calendar
2864
	 * @return string|null
2865
	 */
2866
	public function setPublishStatus($value, $calendar) {
2867
		return $this->atomic(function () use ($value, $calendar) {
2868
			$calendarId = $calendar->getResourceId();
2869
			$calendarData = $this->getCalendarById($calendarId);
2870
2871
			$query = $this->db->getQueryBuilder();
2872
			if ($value) {
2873
				$publicUri = $this->random->generate(16, ISecureRandom::CHAR_HUMAN_READABLE);
2874
				$query->insert('dav_shares')
2875
					->values([
2876
						'principaluri' => $query->createNamedParameter($calendar->getPrincipalURI()),
2877
						'type' => $query->createNamedParameter('calendar'),
2878
						'access' => $query->createNamedParameter(self::ACCESS_PUBLIC),
2879
						'resourceid' => $query->createNamedParameter($calendar->getResourceId()),
2880
						'publicuri' => $query->createNamedParameter($publicUri)
2881
					]);
2882
				$query->executeStatement();
2883
2884
				$this->dispatcher->dispatchTyped(new CalendarPublishedEvent($calendarId, $calendarData, $publicUri));
2885
				return $publicUri;
2886
			}
2887
			$query->delete('dav_shares')
2888
				->where($query->expr()->eq('resourceid', $query->createNamedParameter($calendar->getResourceId())))
2889
				->andWhere($query->expr()->eq('access', $query->createNamedParameter(self::ACCESS_PUBLIC)));
2890
			$query->executeStatement();
2891
2892
			$this->dispatcher->dispatchTyped(new CalendarUnpublishedEvent($calendarId, $calendarData));
2893
			return null;
2894
		}, $this->db);
2895
	}
2896
2897
	/**
2898
	 * @param \OCA\DAV\CalDAV\Calendar $calendar
2899
	 * @return mixed
2900
	 */
2901
	public function getPublishStatus($calendar) {
2902
		$query = $this->db->getQueryBuilder();
2903
		$result = $query->select('publicuri')
2904
			->from('dav_shares')
2905
			->where($query->expr()->eq('resourceid', $query->createNamedParameter($calendar->getResourceId())))
2906
			->andWhere($query->expr()->eq('access', $query->createNamedParameter(self::ACCESS_PUBLIC)))
2907
			->executeQuery();
2908
2909
		$row = $result->fetch();
2910
		$result->closeCursor();
2911
		return $row ? reset($row) : false;
2912
	}
2913
2914
	/**
2915
	 * @param int $resourceId
2916
	 * @param list<array{privilege: string, principal: string, protected: bool}> $acl
2917
	 * @return list<array{privilege: string, principal: string, protected: bool}>
2918
	 */
2919
	public function applyShareAcl(int $resourceId, array $acl): array {
2920
		return $this->calendarSharingBackend->applyShareAcl($resourceId, $acl);
2921
	}
2922
2923
	/**
2924
	 * update properties table
2925
	 *
2926
	 * @param int $calendarId
2927
	 * @param string $objectUri
2928
	 * @param string $calendarData
2929
	 * @param int $calendarType
2930
	 */
2931
	public function updateProperties($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
2932
		$this->atomic(function () use ($calendarId, $objectUri, $calendarData, $calendarType) {
2933
			$objectId = $this->getCalendarObjectId($calendarId, $objectUri, $calendarType);
2934
2935
			try {
2936
				$vCalendar = $this->readCalendarData($calendarData);
2937
			} catch (\Exception $ex) {
2938
				return;
2939
			}
2940
2941
			$this->purgeProperties($calendarId, $objectId);
2942
2943
			$query = $this->db->getQueryBuilder();
2944
			$query->insert($this->dbObjectPropertiesTable)
2945
				->values(
2946
					[
2947
						'calendarid' => $query->createNamedParameter($calendarId),
2948
						'calendartype' => $query->createNamedParameter($calendarType),
2949
						'objectid' => $query->createNamedParameter($objectId),
2950
						'name' => $query->createParameter('name'),
2951
						'parameter' => $query->createParameter('parameter'),
2952
						'value' => $query->createParameter('value'),
2953
					]
2954
				);
2955
2956
			$indexComponents = ['VEVENT', 'VJOURNAL', 'VTODO'];
2957
			foreach ($vCalendar->getComponents() as $component) {
2958
				if (!in_array($component->name, $indexComponents)) {
2959
					continue;
2960
				}
2961
2962
				foreach ($component->children() as $property) {
2963
					if (in_array($property->name, self::INDEXED_PROPERTIES, true)) {
2964
						$value = $property->getValue();
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
						$value = mb_strcut($value, 0, 254);
2970
2971
						$query->setParameter('name', $property->name);
2972
						$query->setParameter('parameter', null);
2973
						$query->setParameter('value', $value);
2974
						$query->executeStatement();
2975
					}
2976
2977
					if (array_key_exists($property->name, self::$indexParameters)) {
2978
						$parameters = $property->parameters();
2979
						$indexedParametersForProperty = self::$indexParameters[$property->name];
2980
2981
						foreach ($parameters as $key => $value) {
2982
							if (in_array($key, $indexedParametersForProperty)) {
2983
								// is this a shitty db?
2984
								if ($this->db->supports4ByteText()) {
2985
									$value = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $value);
2986
								}
2987
2988
								$query->setParameter('name', $property->name);
2989
								$query->setParameter('parameter', mb_strcut($key, 0, 254));
2990
								$query->setParameter('value', mb_strcut($value, 0, 254));
2991
								$query->executeStatement();
2992
							}
2993
						}
2994
					}
2995
				}
2996
			}
2997
		}, $this->db);
2998
	}
2999
3000
	/**
3001
	 * deletes all birthday calendars
3002
	 */
3003
	public function deleteAllBirthdayCalendars() {
3004
		$this->atomic(function () {
3005
			$query = $this->db->getQueryBuilder();
3006
			$result = $query->select(['id'])->from('calendars')
3007
				->where($query->expr()->eq('uri', $query->createNamedParameter(BirthdayService::BIRTHDAY_CALENDAR_URI)))
3008
				->executeQuery();
3009
3010
			$ids = $result->fetchAll();
3011
			$result->closeCursor();
3012
			foreach ($ids as $id) {
3013
				$this->deleteCalendar(
3014
					$id['id'],
3015
					true // No data to keep in the trashbin, if the user re-enables then we regenerate
3016
				);
3017
			}
3018
		}, $this->db);
3019
	}
3020
3021
	/**
3022
	 * @param $subscriptionId
3023
	 */
3024
	public function purgeAllCachedEventsForSubscription($subscriptionId) {
3025
		$this->atomic(function () use ($subscriptionId) {
3026
			$query = $this->db->getQueryBuilder();
3027
			$query->select('uri')
3028
				->from('calendarobjects')
3029
				->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
3030
				->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)));
3031
			$stmt = $query->executeQuery();
3032
3033
			$uris = [];
3034
			foreach ($stmt->fetchAll() as $row) {
3035
				$uris[] = $row['uri'];
3036
			}
3037
			$stmt->closeCursor();
3038
3039
			$query = $this->db->getQueryBuilder();
3040
			$query->delete('calendarobjects')
3041
				->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
3042
				->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
3043
				->executeStatement();
3044
3045
			$query->delete('calendarchanges')
3046
				->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
3047
				->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
3048
				->executeStatement();
3049
3050
			$query->delete($this->dbObjectPropertiesTable)
3051
				->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
3052
				->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
3053
				->executeStatement();
3054
3055
			foreach ($uris as $uri) {
3056
				$this->addChange($subscriptionId, $uri, 3, self::CALENDAR_TYPE_SUBSCRIPTION);
3057
			}
3058
		}, $this->db);
3059
	}
3060
3061
	/**
3062
	 * Move a calendar from one user to another
3063
	 *
3064
	 * @param string $uriName
3065
	 * @param string $uriOrigin
3066
	 * @param string $uriDestination
3067
	 * @param string $newUriName (optional) the new uriName
3068
	 */
3069
	public function moveCalendar($uriName, $uriOrigin, $uriDestination, $newUriName = null) {
3070
		$query = $this->db->getQueryBuilder();
3071
		$query->update('calendars')
3072
			->set('principaluri', $query->createNamedParameter($uriDestination))
3073
			->set('uri', $query->createNamedParameter($newUriName ?: $uriName))
3074
			->where($query->expr()->eq('principaluri', $query->createNamedParameter($uriOrigin)))
3075
			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($uriName)))
3076
			->executeStatement();
3077
	}
3078
3079
	/**
3080
	 * read VCalendar data into a VCalendar object
3081
	 *
3082
	 * @param string $objectData
3083
	 * @return VCalendar
3084
	 */
3085
	protected function readCalendarData($objectData) {
3086
		return Reader::read($objectData);
3087
	}
3088
3089
	/**
3090
	 * delete all properties from a given calendar object
3091
	 *
3092
	 * @param int $calendarId
3093
	 * @param int $objectId
3094
	 */
3095
	protected function purgeProperties($calendarId, $objectId) {
3096
		$query = $this->db->getQueryBuilder();
3097
		$query->delete($this->dbObjectPropertiesTable)
3098
			->where($query->expr()->eq('objectid', $query->createNamedParameter($objectId)))
3099
			->andWhere($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)));
3100
		$query->executeStatement();
3101
	}
3102
3103
	/**
3104
	 * get ID from a given calendar object
3105
	 *
3106
	 * @param int $calendarId
3107
	 * @param string $uri
3108
	 * @param int $calendarType
3109
	 * @return int
3110
	 */
3111
	protected function getCalendarObjectId($calendarId, $uri, $calendarType):int {
3112
		$query = $this->db->getQueryBuilder();
3113
		$query->select('id')
3114
			->from('calendarobjects')
3115
			->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
3116
			->andWhere($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
3117
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)));
3118
3119
		$result = $query->executeQuery();
3120
		$objectIds = $result->fetch();
3121
		$result->closeCursor();
3122
3123
		if (!isset($objectIds['id'])) {
3124
			throw new \InvalidArgumentException('Calendarobject does not exists: ' . $uri);
3125
		}
3126
3127
		return (int)$objectIds['id'];
3128
	}
3129
3130
	/**
3131
	 * @throws \InvalidArgumentException
3132
	 */
3133
	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...
3134
		if ($keep < 0) {
3135
			throw new \InvalidArgumentException();
3136
		}
3137
3138
		$query = $this->db->getQueryBuilder();
3139
		$query->select($query->func()->max('id'))
3140
			->from('calendarchanges');
3141
3142
		$maxId =  $query->executeQuery()->fetchOne();
3143
		if (!$maxId || $maxId < $keep) {
3144
		    return 0;
3145
		}
3146
3147
		$query = $this->db->getQueryBuilder();
3148
		$query->delete('calendarchanges')
3149
			->where($query->expr()->lte('id', $query->createNamedParameter($maxId - $keep, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT));
3150
		return $query->executeStatement();
3151
	}
3152
3153
	/**
3154
	 * return legacy endpoint principal name to new principal name
3155
	 *
3156
	 * @param $principalUri
3157
	 * @param $toV2
3158
	 * @return string
3159
	 */
3160
	private function convertPrincipal($principalUri, $toV2) {
3161
		if ($this->principalBackend->getPrincipalPrefix() === 'principals') {
3162
			[, $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

3162
			[, $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...
3163
			if ($toV2 === true) {
3164
				return "principals/users/$name";
3165
			}
3166
			return "principals/$name";
3167
		}
3168
		return $principalUri;
3169
	}
3170
3171
	/**
3172
	 * adds information about an owner to the calendar data
3173
	 *
3174
	 */
3175
	private function addOwnerPrincipalToCalendar(array $calendarInfo): array {
3176
		$ownerPrincipalKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal';
3177
		$displaynameKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}owner-displayname';
3178
		if (isset($calendarInfo[$ownerPrincipalKey])) {
3179
			$uri = $calendarInfo[$ownerPrincipalKey];
3180
		} else {
3181
			$uri = $calendarInfo['principaluri'];
3182
		}
3183
3184
		$principalInformation = $this->principalBackend->getPrincipalByPath($uri);
3185
		if (isset($principalInformation['{DAV:}displayname'])) {
3186
			$calendarInfo[$displaynameKey] = $principalInformation['{DAV:}displayname'];
3187
		}
3188
		return $calendarInfo;
3189
	}
3190
3191
	private function addResourceTypeToCalendar(array $row, array $calendar): array {
3192
		if (isset($row['deleted_at'])) {
3193
			// Columns is set and not null -> this is a deleted calendar
3194
			// we send a custom resourcetype to hide the deleted calendar
3195
			// from ordinary DAV clients, but the Calendar app will know
3196
			// how to handle this special resource.
3197
			$calendar['{DAV:}resourcetype'] = new DAV\Xml\Property\ResourceType([
3198
				'{DAV:}collection',
3199
				sprintf('{%s}deleted-calendar', \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD),
3200
			]);
3201
		}
3202
		return $calendar;
3203
	}
3204
3205
	/**
3206
	 * Amend the calendar info with database row data
3207
	 *
3208
	 * @param array $row
3209
	 * @param array $calendar
3210
	 *
3211
	 * @return array
3212
	 */
3213
	private function rowToCalendar($row, array $calendar): array {
3214
		foreach ($this->propertyMap as $xmlName => [$dbName, $type]) {
3215
			$value = $row[$dbName];
3216
			if ($value !== null) {
3217
				settype($value, $type);
3218
			}
3219
			$calendar[$xmlName] = $value;
3220
		}
3221
		return $calendar;
3222
	}
3223
3224
	/**
3225
	 * Amend the subscription info with database row data
3226
	 *
3227
	 * @param array $row
3228
	 * @param array $subscription
3229
	 *
3230
	 * @return array
3231
	 */
3232
	private function rowToSubscription($row, array $subscription): array {
3233
		foreach ($this->subscriptionPropertyMap as $xmlName => [$dbName, $type]) {
3234
			$value = $row[$dbName];
3235
			if ($value !== null) {
3236
				settype($value, $type);
3237
			}
3238
			$subscription[$xmlName] = $value;
3239
		}
3240
		return $subscription;
3241
	}
3242
}
3243