Passed
Push — master ( d3d534...5f2afa )
by Christoph
14:45 queued 25s
created

CalDavBackend::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 20
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 10
nc 1
nop 10
dl 0
loc 20
rs 9.9332
c 0
b 0
f 0

How to fix   Many Parameters   

Many Parameters

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

There are several approaches to avoid long parameter lists:

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

451
			[, $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...
452
			$uri = $row['uri'] . '_shared_by_' . $name;
453
			$row['displayname'] = $row['displayname'] . ' (' . $this->getUserDisplayName($name) . ')';
454
			$components = [];
455
			if ($row['components']) {
456
				$components = explode(',',$row['components']);
457
			}
458
			$calendar = [
459
				'id' => $row['id'],
460
				'uri' => $uri,
461
				'principaluri' => $this->convertPrincipal($principalUri, !$this->legacyEndpoint),
462
				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
463
				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
464
				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
465
				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp('transparent'),
466
				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
467
				$readOnlyPropertyName => $readOnly,
468
			];
469
470
			$calendar = $this->rowToCalendar($row, $calendar);
471
			$calendar = $this->addOwnerPrincipalToCalendar($calendar);
472
			$calendar = $this->addResourceTypeToCalendar($row, $calendar);
473
474
			$calendars[$calendar['id']] = $calendar;
475
		}
476
		$result->closeCursor();
477
478
		return array_values($calendars);
479
	}
480
481
	/**
482
	 * @param $principalUri
483
	 * @return array
484
	 */
485
	public function getUsersOwnCalendars($principalUri) {
486
		$principalUri = $this->convertPrincipal($principalUri, true);
487
		$fields = array_column($this->propertyMap, 0);
488
		$fields[] = 'id';
489
		$fields[] = 'uri';
490
		$fields[] = 'synctoken';
491
		$fields[] = 'components';
492
		$fields[] = 'principaluri';
493
		$fields[] = 'transparent';
494
		// Making fields a comma-delimited list
495
		$query = $this->db->getQueryBuilder();
496
		$query->select($fields)->from('calendars')
497
			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
498
			->orderBy('calendarorder', 'ASC');
499
		$stmt = $query->executeQuery();
500
		$calendars = [];
501
		while ($row = $stmt->fetch()) {
502
			$row['principaluri'] = (string) $row['principaluri'];
503
			$components = [];
504
			if ($row['components']) {
505
				$components = explode(',',$row['components']);
506
			}
507
			$calendar = [
508
				'id' => $row['id'],
509
				'uri' => $row['uri'],
510
				'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
511
				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
512
				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
513
				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
514
				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
515
			];
516
517
			$calendar = $this->rowToCalendar($row, $calendar);
518
			$calendar = $this->addOwnerPrincipalToCalendar($calendar);
519
			$calendar = $this->addResourceTypeToCalendar($row, $calendar);
520
521
			if (!isset($calendars[$calendar['id']])) {
522
				$calendars[$calendar['id']] = $calendar;
523
			}
524
		}
525
		$stmt->closeCursor();
526
		return array_values($calendars);
527
	}
528
529
530
	/**
531
	 * @param $uid
532
	 * @return string
533
	 */
534
	private function getUserDisplayName($uid) {
535
		if (!isset($this->userDisplayNames[$uid])) {
536
			$user = $this->userManager->get($uid);
537
538
			if ($user instanceof IUser) {
539
				$this->userDisplayNames[$uid] = $user->getDisplayName();
540
			} else {
541
				$this->userDisplayNames[$uid] = $uid;
542
			}
543
		}
544
545
		return $this->userDisplayNames[$uid];
546
	}
547
548
	/**
549
	 * @return array
550
	 */
551
	public function getPublicCalendars() {
552
		$fields = array_column($this->propertyMap, 0);
553
		$fields[] = 'a.id';
554
		$fields[] = 'a.uri';
555
		$fields[] = 'a.synctoken';
556
		$fields[] = 'a.components';
557
		$fields[] = 'a.principaluri';
558
		$fields[] = 'a.transparent';
559
		$fields[] = 's.access';
560
		$fields[] = 's.publicuri';
561
		$calendars = [];
562
		$query = $this->db->getQueryBuilder();
563
		$result = $query->select($fields)
564
			->from('dav_shares', 's')
565
			->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
566
			->where($query->expr()->in('s.access', $query->createNamedParameter(self::ACCESS_PUBLIC)))
567
			->andWhere($query->expr()->eq('s.type', $query->createNamedParameter('calendar')))
568
			->executeQuery();
569
570
		while ($row = $result->fetch()) {
571
			$row['principaluri'] = (string) $row['principaluri'];
572
			[, $name] = Uri\split($row['principaluri']);
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

572
			[, $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...
573
			$row['displayname'] = $row['displayname'] . "($name)";
574
			$components = [];
575
			if ($row['components']) {
576
				$components = explode(',',$row['components']);
577
			}
578
			$calendar = [
579
				'id' => $row['id'],
580
				'uri' => $row['publicuri'],
581
				'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
582
				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
583
				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
584
				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
585
				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
586
				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], $this->legacyEndpoint),
587
				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ,
588
				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}public' => (int)$row['access'] === self::ACCESS_PUBLIC,
589
			];
590
591
			$calendar = $this->rowToCalendar($row, $calendar);
592
			$calendar = $this->addOwnerPrincipalToCalendar($calendar);
593
			$calendar = $this->addResourceTypeToCalendar($row, $calendar);
594
595
			if (!isset($calendars[$calendar['id']])) {
596
				$calendars[$calendar['id']] = $calendar;
597
			}
598
		}
599
		$result->closeCursor();
600
601
		return array_values($calendars);
602
	}
603
604
	/**
605
	 * @param string $uri
606
	 * @return array
607
	 * @throws NotFound
608
	 */
609
	public function getPublicCalendar($uri) {
610
		$fields = array_column($this->propertyMap, 0);
611
		$fields[] = 'a.id';
612
		$fields[] = 'a.uri';
613
		$fields[] = 'a.synctoken';
614
		$fields[] = 'a.components';
615
		$fields[] = 'a.principaluri';
616
		$fields[] = 'a.transparent';
617
		$fields[] = 's.access';
618
		$fields[] = 's.publicuri';
619
		$query = $this->db->getQueryBuilder();
620
		$result = $query->select($fields)
621
			->from('dav_shares', 's')
622
			->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
623
			->where($query->expr()->in('s.access', $query->createNamedParameter(self::ACCESS_PUBLIC)))
624
			->andWhere($query->expr()->eq('s.type', $query->createNamedParameter('calendar')))
625
			->andWhere($query->expr()->eq('s.publicuri', $query->createNamedParameter($uri)))
626
			->executeQuery();
627
628
		$row = $result->fetch();
629
630
		$result->closeCursor();
631
632
		if ($row === false) {
633
			throw new NotFound('Node with name \'' . $uri . '\' could not be found');
634
		}
635
636
		$row['principaluri'] = (string) $row['principaluri'];
637
		[, $name] = Uri\split($row['principaluri']);
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

637
		[, $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...
638
		$row['displayname'] = $row['displayname'] . ' ' . "($name)";
639
		$components = [];
640
		if ($row['components']) {
641
			$components = explode(',',$row['components']);
642
		}
643
		$calendar = [
644
			'id' => $row['id'],
645
			'uri' => $row['publicuri'],
646
			'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
647
			'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
648
			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
649
			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
650
			'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
651
			'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
652
			'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ,
653
			'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}public' => (int)$row['access'] === self::ACCESS_PUBLIC,
654
		];
655
656
		$calendar = $this->rowToCalendar($row, $calendar);
657
		$calendar = $this->addOwnerPrincipalToCalendar($calendar);
658
		$calendar = $this->addResourceTypeToCalendar($row, $calendar);
659
660
		return $calendar;
661
	}
662
663
	/**
664
	 * @param string $principal
665
	 * @param string $uri
666
	 * @return array|null
667
	 */
668
	public function getCalendarByUri($principal, $uri) {
669
		$fields = array_column($this->propertyMap, 0);
670
		$fields[] = 'id';
671
		$fields[] = 'uri';
672
		$fields[] = 'synctoken';
673
		$fields[] = 'components';
674
		$fields[] = 'principaluri';
675
		$fields[] = 'transparent';
676
677
		// Making fields a comma-delimited list
678
		$query = $this->db->getQueryBuilder();
679
		$query->select($fields)->from('calendars')
680
			->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
681
			->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($principal)))
682
			->setMaxResults(1);
683
		$stmt = $query->executeQuery();
684
685
		$row = $stmt->fetch();
686
		$stmt->closeCursor();
687
		if ($row === false) {
688
			return null;
689
		}
690
691
		$row['principaluri'] = (string) $row['principaluri'];
692
		$components = [];
693
		if ($row['components']) {
694
			$components = explode(',',$row['components']);
695
		}
696
697
		$calendar = [
698
			'id' => $row['id'],
699
			'uri' => $row['uri'],
700
			'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
701
			'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
702
			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
703
			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
704
			'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
705
		];
706
707
		$calendar = $this->rowToCalendar($row, $calendar);
708
		$calendar = $this->addOwnerPrincipalToCalendar($calendar);
709
		$calendar = $this->addResourceTypeToCalendar($row, $calendar);
710
711
		return $calendar;
712
	}
713
714
	/**
715
	 * @param $calendarId
716
	 * @return array|null
717
	 */
718
	public function getCalendarById($calendarId) {
719
		$fields = array_column($this->propertyMap, 0);
720
		$fields[] = 'id';
721
		$fields[] = 'uri';
722
		$fields[] = 'synctoken';
723
		$fields[] = 'components';
724
		$fields[] = 'principaluri';
725
		$fields[] = 'transparent';
726
727
		// Making fields a comma-delimited list
728
		$query = $this->db->getQueryBuilder();
729
		$query->select($fields)->from('calendars')
730
			->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)))
731
			->setMaxResults(1);
732
		$stmt = $query->executeQuery();
733
734
		$row = $stmt->fetch();
735
		$stmt->closeCursor();
736
		if ($row === false) {
737
			return null;
738
		}
739
740
		$row['principaluri'] = (string) $row['principaluri'];
741
		$components = [];
742
		if ($row['components']) {
743
			$components = explode(',',$row['components']);
744
		}
745
746
		$calendar = [
747
			'id' => $row['id'],
748
			'uri' => $row['uri'],
749
			'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
750
			'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
751
			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
752
			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
753
			'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
754
		];
755
756
		$calendar = $this->rowToCalendar($row, $calendar);
757
		$calendar = $this->addOwnerPrincipalToCalendar($calendar);
758
		$calendar = $this->addResourceTypeToCalendar($row, $calendar);
759
760
		return $calendar;
761
	}
762
763
	/**
764
	 * @param $subscriptionId
765
	 */
766
	public function getSubscriptionById($subscriptionId) {
767
		$fields = array_column($this->subscriptionPropertyMap, 0);
768
		$fields[] = 'id';
769
		$fields[] = 'uri';
770
		$fields[] = 'source';
771
		$fields[] = 'synctoken';
772
		$fields[] = 'principaluri';
773
		$fields[] = 'lastmodified';
774
775
		$query = $this->db->getQueryBuilder();
776
		$query->select($fields)
777
			->from('calendarsubscriptions')
778
			->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
779
			->orderBy('calendarorder', 'asc');
780
		$stmt = $query->executeQuery();
781
782
		$row = $stmt->fetch();
783
		$stmt->closeCursor();
784
		if ($row === false) {
785
			return null;
786
		}
787
788
		$row['principaluri'] = (string) $row['principaluri'];
789
		$subscription = [
790
			'id' => $row['id'],
791
			'uri' => $row['uri'],
792
			'principaluri' => $row['principaluri'],
793
			'source' => $row['source'],
794
			'lastmodified' => $row['lastmodified'],
795
			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
796
			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
797
		];
798
799
		return $this->rowToSubscription($row, $subscription);
800
	}
801
802
	/**
803
	 * Creates a new calendar for a principal.
804
	 *
805
	 * If the creation was a success, an id must be returned that can be used to reference
806
	 * this calendar in other methods, such as updateCalendar.
807
	 *
808
	 * @param string $principalUri
809
	 * @param string $calendarUri
810
	 * @param array $properties
811
	 * @return int
812
	 */
813
	public function createCalendar($principalUri, $calendarUri, array $properties) {
814
		$values = [
815
			'principaluri' => $this->convertPrincipal($principalUri, true),
816
			'uri' => $calendarUri,
817
			'synctoken' => 1,
818
			'transparent' => 0,
819
			'components' => 'VEVENT,VTODO',
820
			'displayname' => $calendarUri
821
		];
822
823
		// Default value
824
		$sccs = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set';
825
		if (isset($properties[$sccs])) {
826
			if (!($properties[$sccs] instanceof SupportedCalendarComponentSet)) {
827
				throw new DAV\Exception('The ' . $sccs . ' property must be of type: \Sabre\CalDAV\Property\SupportedCalendarComponentSet');
828
			}
829
			$values['components'] = implode(',',$properties[$sccs]->getValue());
830
		} elseif (isset($properties['components'])) {
831
			// Allow to provide components internally without having
832
			// to create a SupportedCalendarComponentSet object
833
			$values['components'] = $properties['components'];
834
		}
835
836
		$transp = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp';
837
		if (isset($properties[$transp])) {
838
			$values['transparent'] = (int) ($properties[$transp]->getValue() === 'transparent');
839
		}
840
841
		foreach ($this->propertyMap as $xmlName => [$dbName, $type]) {
842
			if (isset($properties[$xmlName])) {
843
				$values[$dbName] = $properties[$xmlName];
844
			}
845
		}
846
847
		$query = $this->db->getQueryBuilder();
848
		$query->insert('calendars');
849
		foreach ($values as $column => $value) {
850
			$query->setValue($column, $query->createNamedParameter($value));
851
		}
852
		$query->executeStatement();
853
		$calendarId = $query->getLastInsertId();
854
855
		$calendarData = $this->getCalendarById($calendarId);
856
		$this->dispatcher->dispatchTyped(new CalendarCreatedEvent((int)$calendarId, $calendarData));
857
858
		return $calendarId;
859
	}
860
861
	/**
862
	 * Updates properties for a calendar.
863
	 *
864
	 * The list of mutations is stored in a Sabre\DAV\PropPatch object.
865
	 * To do the actual updates, you must tell this object which properties
866
	 * you're going to process with the handle() method.
867
	 *
868
	 * Calling the handle method is like telling the PropPatch object "I
869
	 * promise I can handle updating this property".
870
	 *
871
	 * Read the PropPatch documentation for more info and examples.
872
	 *
873
	 * @param mixed $calendarId
874
	 * @param PropPatch $propPatch
875
	 * @return void
876
	 */
877
	public function updateCalendar($calendarId, PropPatch $propPatch) {
878
		$supportedProperties = array_keys($this->propertyMap);
879
		$supportedProperties[] = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp';
880
881
		$propPatch->handle($supportedProperties, function ($mutations) use ($calendarId) {
882
			$newValues = [];
883
			foreach ($mutations as $propertyName => $propertyValue) {
884
				switch ($propertyName) {
885
					case '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp':
886
						$fieldName = 'transparent';
887
						$newValues[$fieldName] = (int) ($propertyValue->getValue() === 'transparent');
888
						break;
889
					default:
890
						$fieldName = $this->propertyMap[$propertyName][0];
891
						$newValues[$fieldName] = $propertyValue;
892
						break;
893
				}
894
			}
895
			$query = $this->db->getQueryBuilder();
896
			$query->update('calendars');
897
			foreach ($newValues as $fieldName => $value) {
898
				$query->set($fieldName, $query->createNamedParameter($value));
899
			}
900
			$query->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)));
901
			$query->executeStatement();
902
903
			$this->addChange($calendarId, "", 2);
904
905
			$calendarData = $this->getCalendarById($calendarId);
906
			$shares = $this->getShares($calendarId);
907
			$this->dispatcher->dispatchTyped(new CalendarUpdatedEvent((int)$calendarId, $calendarData, $shares, $mutations));
908
909
			return true;
910
		});
911
	}
912
913
	/**
914
	 * Delete a calendar and all it's objects
915
	 *
916
	 * @param mixed $calendarId
917
	 * @return void
918
	 */
919
	public function deleteCalendar($calendarId, bool $forceDeletePermanently = false) {
920
		// The calendar is deleted right away if this is either enforced by the caller
921
		// or the special contacts birthday calendar or when the preference of an empty
922
		// retention (0 seconds) is set, which signals a disabled trashbin.
923
		$calendarData = $this->getCalendarById($calendarId);
924
		$isBirthdayCalendar = isset($calendarData['uri']) && $calendarData['uri'] === BirthdayService::BIRTHDAY_CALENDAR_URI;
925
		$trashbinDisabled = $this->config->getAppValue(Application::APP_ID, RetentionService::RETENTION_CONFIG_KEY) === '0';
926
		if ($forceDeletePermanently || $isBirthdayCalendar || $trashbinDisabled) {
927
			$calendarData = $this->getCalendarById($calendarId);
928
			$shares = $this->getShares($calendarId);
929
930
			$qbDeleteCalendarObjectProps = $this->db->getQueryBuilder();
931
			$qbDeleteCalendarObjectProps->delete($this->dbObjectPropertiesTable)
932
				->where($qbDeleteCalendarObjectProps->expr()->eq('calendarid', $qbDeleteCalendarObjectProps->createNamedParameter($calendarId)))
933
				->andWhere($qbDeleteCalendarObjectProps->expr()->eq('calendartype', $qbDeleteCalendarObjectProps->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)))
934
				->executeStatement();
935
936
			$qbDeleteCalendarObjects = $this->db->getQueryBuilder();
937
			$qbDeleteCalendarObjects->delete('calendarobjects')
938
				->where($qbDeleteCalendarObjects->expr()->eq('calendarid', $qbDeleteCalendarObjects->createNamedParameter($calendarId)))
939
				->andWhere($qbDeleteCalendarObjects->expr()->eq('calendartype', $qbDeleteCalendarObjects->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)))
940
				->executeStatement();
941
942
			$qbDeleteCalendarChanges = $this->db->getQueryBuilder();
943
			$qbDeleteCalendarObjects->delete('calendarchanges')
944
				->where($qbDeleteCalendarChanges->expr()->eq('calendarid', $qbDeleteCalendarChanges->createNamedParameter($calendarId)))
945
				->andWhere($qbDeleteCalendarChanges->expr()->eq('calendartype', $qbDeleteCalendarChanges->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)))
946
				->executeStatement();
947
948
			$this->calendarSharingBackend->deleteAllShares($calendarId);
949
950
			$qbDeleteCalendar = $this->db->getQueryBuilder();
951
			$qbDeleteCalendarObjects->delete('calendars')
952
				->where($qbDeleteCalendar->expr()->eq('id', $qbDeleteCalendar->createNamedParameter($calendarId)))
953
				->executeStatement();
954
955
			// Only dispatch if we actually deleted anything
956
			if ($calendarData) {
957
				$this->dispatcher->dispatchTyped(new CalendarDeletedEvent((int)$calendarId, $calendarData, $shares));
958
			}
959
		} else {
960
			$qbMarkCalendarDeleted = $this->db->getQueryBuilder();
961
			$qbMarkCalendarDeleted->update('calendars')
962
				->set('deleted_at', $qbMarkCalendarDeleted->createNamedParameter(time()))
963
				->where($qbMarkCalendarDeleted->expr()->eq('id', $qbMarkCalendarDeleted->createNamedParameter($calendarId)))
964
				->executeStatement();
965
966
			$calendarData = $this->getCalendarById($calendarId);
967
			$shares = $this->getShares($calendarId);
968
			if ($calendarData) {
969
				$this->dispatcher->dispatchTyped(new CalendarMovedToTrashEvent(
970
					(int)$calendarId,
971
					$calendarData,
972
					$shares
973
				));
974
			}
975
		}
976
	}
977
978
	public function restoreCalendar(int $id): void {
979
		$qb = $this->db->getQueryBuilder();
980
		$update = $qb->update('calendars')
981
			->set('deleted_at', $qb->createNamedParameter(null))
982
			->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT));
983
		$update->executeStatement();
984
985
		$calendarData = $this->getCalendarById($id);
986
		$shares = $this->getShares($id);
987
		if ($calendarData === null) {
988
			throw new RuntimeException('Calendar data that was just written can\'t be read back. Check your database configuration.');
989
		}
990
		$this->dispatcher->dispatchTyped(new CalendarRestoredEvent(
991
			$id,
992
			$calendarData,
993
			$shares
994
		));
995
	}
996
997
	/**
998
	 * Delete all of an user's shares
999
	 *
1000
	 * @param string $principaluri
1001
	 * @return void
1002
	 */
1003
	public function deleteAllSharesByUser($principaluri) {
1004
		$this->calendarSharingBackend->deleteAllSharesByUser($principaluri);
1005
	}
1006
1007
	/**
1008
	 * Returns all calendar objects within a calendar.
1009
	 *
1010
	 * Every item contains an array with the following keys:
1011
	 *   * calendardata - The iCalendar-compatible calendar data
1012
	 *   * uri - a unique key which will be used to construct the uri. This can
1013
	 *     be any arbitrary string, but making sure it ends with '.ics' is a
1014
	 *     good idea. This is only the basename, or filename, not the full
1015
	 *     path.
1016
	 *   * lastmodified - a timestamp of the last modification time
1017
	 *   * etag - An arbitrary string, surrounded by double-quotes. (e.g.:
1018
	 *   '"abcdef"')
1019
	 *   * size - The size of the calendar objects, in bytes.
1020
	 *   * component - optional, a string containing the type of object, such
1021
	 *     as 'vevent' or 'vtodo'. If specified, this will be used to populate
1022
	 *     the Content-Type header.
1023
	 *
1024
	 * Note that the etag is optional, but it's highly encouraged to return for
1025
	 * speed reasons.
1026
	 *
1027
	 * The calendardata is also optional. If it's not returned
1028
	 * 'getCalendarObject' will be called later, which *is* expected to return
1029
	 * calendardata.
1030
	 *
1031
	 * If neither etag or size are specified, the calendardata will be
1032
	 * used/fetched to determine these numbers. If both are specified the
1033
	 * amount of times this is needed is reduced by a great degree.
1034
	 *
1035
	 * @param mixed $calendarId
1036
	 * @param int $calendarType
1037
	 * @return array
1038
	 */
1039
	public function getCalendarObjects($calendarId, $calendarType = self::CALENDAR_TYPE_CALENDAR):array {
1040
		$query = $this->db->getQueryBuilder();
1041
		$query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'componenttype', 'classification'])
1042
			->from('calendarobjects')
1043
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
1044
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)))
1045
			->andWhere($query->expr()->isNull('deleted_at'));
1046
		$stmt = $query->executeQuery();
1047
1048
		$result = [];
1049
		foreach ($stmt->fetchAll() as $row) {
1050
			$result[] = [
1051
				'id' => $row['id'],
1052
				'uri' => $row['uri'],
1053
				'lastmodified' => $row['lastmodified'],
1054
				'etag' => '"' . $row['etag'] . '"',
1055
				'calendarid' => $row['calendarid'],
1056
				'size' => (int)$row['size'],
1057
				'component' => strtolower($row['componenttype']),
1058
				'classification' => (int)$row['classification']
1059
			];
1060
		}
1061
		$stmt->closeCursor();
1062
1063
		return $result;
1064
	}
1065
1066
	public function getDeletedCalendarObjects(int $deletedBefore): array {
1067
		$query = $this->db->getQueryBuilder();
1068
		$query->select(['co.id', 'co.uri', 'co.lastmodified', 'co.etag', 'co.calendarid', 'co.calendartype', 'co.size', 'co.componenttype', 'co.classification', 'co.deleted_at'])
1069
			->from('calendarobjects', 'co')
1070
			->join('co', 'calendars', 'c', $query->expr()->eq('c.id', 'co.calendarid', IQueryBuilder::PARAM_INT))
1071
			->where($query->expr()->isNotNull('co.deleted_at'))
1072
			->andWhere($query->expr()->lt('co.deleted_at', $query->createNamedParameter($deletedBefore)));
1073
		$stmt = $query->executeQuery();
1074
1075
		$result = [];
1076
		foreach ($stmt->fetchAll() as $row) {
1077
			$result[] = [
1078
				'id' => $row['id'],
1079
				'uri' => $row['uri'],
1080
				'lastmodified' => $row['lastmodified'],
1081
				'etag' => '"' . $row['etag'] . '"',
1082
				'calendarid' => (int) $row['calendarid'],
1083
				'calendartype' => (int) $row['calendartype'],
1084
				'size' => (int) $row['size'],
1085
				'component' => strtolower($row['componenttype']),
1086
				'classification' => (int) $row['classification'],
1087
				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => $row['deleted_at'] === null ? $row['deleted_at'] : (int) $row['deleted_at'],
1088
			];
1089
		}
1090
		$stmt->closeCursor();
1091
1092
		return $result;
1093
	}
1094
1095
	/**
1096
	 * Return all deleted calendar objects by the given principal that are not
1097
	 * in deleted calendars.
1098
	 *
1099
	 * @param string $principalUri
1100
	 * @return array
1101
	 * @throws Exception
1102
	 */
1103
	public function getDeletedCalendarObjectsByPrincipal(string $principalUri): array {
1104
		$query = $this->db->getQueryBuilder();
1105
		$query->select(['co.id', 'co.uri', 'co.lastmodified', 'co.etag', 'co.calendarid', 'co.size', 'co.componenttype', 'co.classification', 'co.deleted_at'])
1106
			->selectAlias('c.uri', 'calendaruri')
1107
			->from('calendarobjects', 'co')
1108
			->join('co', 'calendars', 'c', $query->expr()->eq('c.id', 'co.calendarid', IQueryBuilder::PARAM_INT))
1109
			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
1110
			->andWhere($query->expr()->isNotNull('co.deleted_at'))
1111
			->andWhere($query->expr()->isNull('c.deleted_at'));
1112
		$stmt = $query->executeQuery();
1113
1114
		$result = [];
1115
		while ($row = $stmt->fetch()) {
1116
			$result[] = [
1117
				'id' => $row['id'],
1118
				'uri' => $row['uri'],
1119
				'lastmodified' => $row['lastmodified'],
1120
				'etag' => '"' . $row['etag'] . '"',
1121
				'calendarid' => $row['calendarid'],
1122
				'calendaruri' => $row['calendaruri'],
1123
				'size' => (int)$row['size'],
1124
				'component' => strtolower($row['componenttype']),
1125
				'classification' => (int)$row['classification'],
1126
				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => $row['deleted_at'] === null ? $row['deleted_at'] : (int) $row['deleted_at'],
1127
			];
1128
		}
1129
		$stmt->closeCursor();
1130
1131
		return $result;
1132
	}
1133
1134
	/**
1135
	 * Returns information from a single calendar object, based on it's object
1136
	 * uri.
1137
	 *
1138
	 * The object uri is only the basename, or filename and not a full path.
1139
	 *
1140
	 * The returned array must have the same keys as getCalendarObjects. The
1141
	 * 'calendardata' object is required here though, while it's not required
1142
	 * for getCalendarObjects.
1143
	 *
1144
	 * This method must return null if the object did not exist.
1145
	 *
1146
	 * @param mixed $calendarId
1147
	 * @param string $objectUri
1148
	 * @param int $calendarType
1149
	 * @return array|null
1150
	 */
1151
	public function getCalendarObject($calendarId, $objectUri, int $calendarType = self::CALENDAR_TYPE_CALENDAR) {
1152
		$query = $this->db->getQueryBuilder();
1153
		$query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification'])
1154
			->from('calendarobjects')
1155
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
1156
			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
1157
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)));
1158
		$stmt = $query->executeQuery();
1159
		$row = $stmt->fetch();
1160
		$stmt->closeCursor();
1161
1162
		if (!$row) {
1163
			return null;
1164
		}
1165
1166
		return [
1167
			'id' => $row['id'],
1168
			'uri' => $row['uri'],
1169
			'lastmodified' => $row['lastmodified'],
1170
			'etag' => '"' . $row['etag'] . '"',
1171
			'calendarid' => $row['calendarid'],
1172
			'size' => (int)$row['size'],
1173
			'calendardata' => $this->readBlob($row['calendardata']),
1174
			'component' => strtolower($row['componenttype']),
1175
			'classification' => (int)$row['classification']
1176
		];
1177
	}
1178
1179
	/**
1180
	 * Returns a list of calendar objects.
1181
	 *
1182
	 * This method should work identical to getCalendarObject, but instead
1183
	 * return all the calendar objects in the list as an array.
1184
	 *
1185
	 * If the backend supports this, it may allow for some speed-ups.
1186
	 *
1187
	 * @param mixed $calendarId
1188
	 * @param string[] $uris
1189
	 * @param int $calendarType
1190
	 * @return array
1191
	 */
1192
	public function getMultipleCalendarObjects($calendarId, array $uris, $calendarType = self::CALENDAR_TYPE_CALENDAR):array {
1193
		if (empty($uris)) {
1194
			return [];
1195
		}
1196
1197
		$chunks = array_chunk($uris, 100);
1198
		$objects = [];
1199
1200
		$query = $this->db->getQueryBuilder();
1201
		$query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification'])
1202
			->from('calendarobjects')
1203
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
1204
			->andWhere($query->expr()->in('uri', $query->createParameter('uri')))
1205
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)))
1206
			->andWhere($query->expr()->isNull('deleted_at'));
1207
1208
		foreach ($chunks as $uris) {
1209
			$query->setParameter('uri', $uris, IQueryBuilder::PARAM_STR_ARRAY);
1210
			$result = $query->executeQuery();
1211
1212
			while ($row = $result->fetch()) {
1213
				$objects[] = [
1214
					'id' => $row['id'],
1215
					'uri' => $row['uri'],
1216
					'lastmodified' => $row['lastmodified'],
1217
					'etag' => '"' . $row['etag'] . '"',
1218
					'calendarid' => $row['calendarid'],
1219
					'size' => (int)$row['size'],
1220
					'calendardata' => $this->readBlob($row['calendardata']),
1221
					'component' => strtolower($row['componenttype']),
1222
					'classification' => (int)$row['classification']
1223
				];
1224
			}
1225
			$result->closeCursor();
1226
		}
1227
1228
		return $objects;
1229
	}
1230
1231
	/**
1232
	 * Creates a new calendar object.
1233
	 *
1234
	 * The object uri is only the basename, or filename and not a full path.
1235
	 *
1236
	 * It is possible return an etag from this function, which will be used in
1237
	 * the response to this PUT request. Note that the ETag must be surrounded
1238
	 * by double-quotes.
1239
	 *
1240
	 * However, you should only really return this ETag if you don't mangle the
1241
	 * calendar-data. If the result of a subsequent GET to this object is not
1242
	 * the exact same as this request body, you should omit the ETag.
1243
	 *
1244
	 * @param mixed $calendarId
1245
	 * @param string $objectUri
1246
	 * @param string $calendarData
1247
	 * @param int $calendarType
1248
	 * @return string
1249
	 */
1250
	public function createCalendarObject($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
1251
		$extraData = $this->getDenormalizedData($calendarData);
1252
1253
		// Try to detect duplicates
1254
		$qb = $this->db->getQueryBuilder();
1255
		$qb->select($qb->func()->count('*'))
1256
			->from('calendarobjects')
1257
			->where($qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)))
1258
			->andWhere($qb->expr()->eq('uid', $qb->createNamedParameter($extraData['uid'])))
1259
			->andWhere($qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType)))
1260
			->andWhere($qb->expr()->isNull('deleted_at'));
1261
		$result = $qb->executeQuery();
1262
		$count = (int) $result->fetchOne();
1263
		$result->closeCursor();
1264
1265
		if ($count !== 0) {
1266
			throw new BadRequest('Calendar object with uid already exists in this calendar collection.');
1267
		}
1268
		// For a more specific error message we also try to explicitly look up the UID but as a deleted entry
1269
		$qbDel = $this->db->getQueryBuilder();
1270
		$qbDel->select($qb->func()->count('*'))
1271
			->from('calendarobjects')
1272
			->where($qbDel->expr()->eq('calendarid', $qbDel->createNamedParameter($calendarId)))
1273
			->andWhere($qbDel->expr()->eq('uid', $qbDel->createNamedParameter($extraData['uid'])))
1274
			->andWhere($qbDel->expr()->eq('calendartype', $qbDel->createNamedParameter($calendarType)))
1275
			->andWhere($qbDel->expr()->isNotNull('deleted_at'));
1276
		$result = $qbDel->executeQuery();
1277
		$count = (int) $result->fetchOne();
1278
		$result->closeCursor();
1279
		if ($count !== 0) {
1280
			throw new BadRequest('Deleted calendar object with uid already exists in this calendar collection.');
1281
		}
1282
1283
		$query = $this->db->getQueryBuilder();
1284
		$query->insert('calendarobjects')
1285
			->values([
1286
				'calendarid' => $query->createNamedParameter($calendarId),
1287
				'uri' => $query->createNamedParameter($objectUri),
1288
				'calendardata' => $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB),
1289
				'lastmodified' => $query->createNamedParameter(time()),
1290
				'etag' => $query->createNamedParameter($extraData['etag']),
1291
				'size' => $query->createNamedParameter($extraData['size']),
1292
				'componenttype' => $query->createNamedParameter($extraData['componentType']),
1293
				'firstoccurence' => $query->createNamedParameter($extraData['firstOccurence']),
1294
				'lastoccurence' => $query->createNamedParameter($extraData['lastOccurence']),
1295
				'classification' => $query->createNamedParameter($extraData['classification']),
1296
				'uid' => $query->createNamedParameter($extraData['uid']),
1297
				'calendartype' => $query->createNamedParameter($calendarType),
1298
			])
1299
			->executeStatement();
1300
1301
		$this->updateProperties($calendarId, $objectUri, $calendarData, $calendarType);
1302
		$this->addChange($calendarId, $objectUri, 1, $calendarType);
1303
1304
		$objectRow = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
1305
		if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
1306
			$calendarRow = $this->getCalendarById($calendarId);
1307
			$shares = $this->getShares($calendarId);
1308
1309
			$this->dispatcher->dispatchTyped(new CalendarObjectCreatedEvent((int)$calendarId, $calendarRow, $shares, $objectRow));
1310
		} else {
1311
			$subscriptionRow = $this->getSubscriptionById($calendarId);
1312
1313
			$this->dispatcher->dispatchTyped(new CachedCalendarObjectCreatedEvent((int)$calendarId, $subscriptionRow, [], $objectRow));
1314
			$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createCachedCalendarObject', new GenericEvent(
0 ignored issues
show
Bug introduced by
'\OCA\DAV\CalDAV\CalDavB...teCachedCalendarObject' of type string is incompatible with the type object expected by parameter $event of Symfony\Contracts\EventD...erInterface::dispatch(). ( Ignorable by Annotation )

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

1314
			$this->legacyDispatcher->dispatch(/** @scrutinizer ignore-type */ '\OCA\DAV\CalDAV\CalDavBackend::createCachedCalendarObject', new GenericEvent(
Loading history...
Unused Code introduced by
The call to Symfony\Contracts\EventD...erInterface::dispatch() has too many arguments starting with new Symfony\Component\Ev...ctData' => $objectRow)). ( Ignorable by Annotation )

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

1314
			$this->legacyDispatcher->/** @scrutinizer ignore-call */ 
1315
                            dispatch('\OCA\DAV\CalDAV\CalDavBackend::createCachedCalendarObject', new GenericEvent(

This check compares calls to functions or methods with their respective definitions. If the call has more 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...
1315
				'\OCA\DAV\CalDAV\CalDavBackend::createCachedCalendarObject',
1316
				[
1317
					'subscriptionId' => $calendarId,
1318
					'calendarData' => $subscriptionRow,
1319
					'shares' => [],
1320
					'objectData' => $objectRow,
1321
				]
1322
			));
1323
		}
1324
1325
		return '"' . $extraData['etag'] . '"';
1326
	}
1327
1328
	/**
1329
	 * Updates an existing calendarobject, based on it's uri.
1330
	 *
1331
	 * The object uri is only the basename, or filename and not a full path.
1332
	 *
1333
	 * It is possible return an etag from this function, which will be used in
1334
	 * the response to this PUT request. Note that the ETag must be surrounded
1335
	 * by double-quotes.
1336
	 *
1337
	 * However, you should only really return this ETag if you don't mangle the
1338
	 * calendar-data. If the result of a subsequent GET to this object is not
1339
	 * the exact same as this request body, you should omit the ETag.
1340
	 *
1341
	 * @param mixed $calendarId
1342
	 * @param string $objectUri
1343
	 * @param string $calendarData
1344
	 * @param int $calendarType
1345
	 * @return string
1346
	 */
1347
	public function updateCalendarObject($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
1348
		$extraData = $this->getDenormalizedData($calendarData);
1349
		$query = $this->db->getQueryBuilder();
1350
		$query->update('calendarobjects')
1351
				->set('calendardata', $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB))
1352
				->set('lastmodified', $query->createNamedParameter(time()))
1353
				->set('etag', $query->createNamedParameter($extraData['etag']))
1354
				->set('size', $query->createNamedParameter($extraData['size']))
1355
				->set('componenttype', $query->createNamedParameter($extraData['componentType']))
1356
				->set('firstoccurence', $query->createNamedParameter($extraData['firstOccurence']))
1357
				->set('lastoccurence', $query->createNamedParameter($extraData['lastOccurence']))
1358
				->set('classification', $query->createNamedParameter($extraData['classification']))
1359
				->set('uid', $query->createNamedParameter($extraData['uid']))
1360
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
1361
			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
1362
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)))
1363
			->executeStatement();
1364
1365
		$this->updateProperties($calendarId, $objectUri, $calendarData, $calendarType);
1366
		$this->addChange($calendarId, $objectUri, 2, $calendarType);
1367
1368
		$objectRow = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
1369
		if (is_array($objectRow)) {
1370
			if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
1371
				$calendarRow = $this->getCalendarById($calendarId);
1372
				$shares = $this->getShares($calendarId);
1373
1374
				$this->dispatcher->dispatchTyped(new CalendarObjectUpdatedEvent((int)$calendarId, $calendarRow, $shares, $objectRow));
1375
			} else {
1376
				$subscriptionRow = $this->getSubscriptionById($calendarId);
1377
1378
				$this->dispatcher->dispatchTyped(new CachedCalendarObjectUpdatedEvent((int)$calendarId, $subscriptionRow, [], $objectRow));
1379
				$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateCachedCalendarObject', new GenericEvent(
0 ignored issues
show
Bug introduced by
'\OCA\DAV\CalDAV\CalDavB...teCachedCalendarObject' of type string is incompatible with the type object expected by parameter $event of Symfony\Contracts\EventD...erInterface::dispatch(). ( Ignorable by Annotation )

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

1379
				$this->legacyDispatcher->dispatch(/** @scrutinizer ignore-type */ '\OCA\DAV\CalDAV\CalDavBackend::updateCachedCalendarObject', new GenericEvent(
Loading history...
Unused Code introduced by
The call to Symfony\Contracts\EventD...erInterface::dispatch() has too many arguments starting with new Symfony\Component\Ev...ctData' => $objectRow)). ( Ignorable by Annotation )

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

1379
				$this->legacyDispatcher->/** @scrutinizer ignore-call */ 
1380
                             dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateCachedCalendarObject', new GenericEvent(

This check compares calls to functions or methods with their respective definitions. If the call has more 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...
1380
					'\OCA\DAV\CalDAV\CalDavBackend::updateCachedCalendarObject',
1381
					[
1382
						'subscriptionId' => $calendarId,
1383
						'calendarData' => $subscriptionRow,
1384
						'shares' => [],
1385
						'objectData' => $objectRow,
1386
					]
1387
				));
1388
			}
1389
		}
1390
1391
		return '"' . $extraData['etag'] . '"';
1392
	}
1393
1394
	/**
1395
	 * Moves a calendar object from calendar to calendar.
1396
	 *
1397
	 * @param int $sourceCalendarId
1398
	 * @param int $targetCalendarId
1399
	 * @param int $objectId
1400
	 * @param string $principalUri
1401
	 * @param int $calendarType
1402
	 * @return bool
1403
	 * @throws Exception
1404
	 */
1405
	public function moveCalendarObject(int $sourceCalendarId, int $targetCalendarId, int $objectId, string $principalUri, int $calendarType = self::CALENDAR_TYPE_CALENDAR): bool {
1406
		$object = $this->getCalendarObjectById($principalUri, $objectId);
1407
		if (empty($object)) {
1408
			return false;
1409
		}
1410
1411
		$query = $this->db->getQueryBuilder();
1412
		$query->update('calendarobjects')
1413
			->set('calendarid', $query->createNamedParameter($targetCalendarId, IQueryBuilder::PARAM_INT))
1414
			->where($query->expr()->eq('id', $query->createNamedParameter($objectId, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT))
1415
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT))
1416
			->executeStatement();
1417
1418
		$this->purgeProperties($sourceCalendarId, $objectId);
1419
		$this->updateProperties($targetCalendarId, $object['uri'], $object['calendardata'], $calendarType);
1420
1421
		$this->addChange($sourceCalendarId, $object['uri'], 1, $calendarType);
1422
		$this->addChange($targetCalendarId, $object['uri'], 3, $calendarType);
1423
1424
		$object = $this->getCalendarObjectById($principalUri, $objectId);
1425
		// Calendar Object wasn't found - possibly because it was deleted in the meantime by a different client
1426
		if (empty($object)) {
1427
			return false;
1428
		}
1429
1430
		$calendarRow = $this->getCalendarById($targetCalendarId);
1431
		// the calendar this event is being moved to does not exist any longer
1432
		if (empty($calendarRow)) {
1433
			return false;
1434
		}
1435
1436
		if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
1437
			$shares = $this->getShares($targetCalendarId);
1438
			$this->dispatcher->dispatchTyped(new CalendarObjectUpdatedEvent($targetCalendarId, $calendarRow, $shares, $object));
1439
		}
1440
		return true;
1441
	}
1442
1443
1444
	/**
1445
	 * @param int $calendarObjectId
1446
	 * @param int $classification
1447
	 */
1448
	public function setClassification($calendarObjectId, $classification) {
1449
		if (!in_array($classification, [
1450
			self::CLASSIFICATION_PUBLIC, self::CLASSIFICATION_PRIVATE, self::CLASSIFICATION_CONFIDENTIAL
1451
		])) {
1452
			throw new \InvalidArgumentException();
1453
		}
1454
		$query = $this->db->getQueryBuilder();
1455
		$query->update('calendarobjects')
1456
			->set('classification', $query->createNamedParameter($classification))
1457
			->where($query->expr()->eq('id', $query->createNamedParameter($calendarObjectId)))
1458
			->executeStatement();
1459
	}
1460
1461
	/**
1462
	 * Deletes an existing calendar object.
1463
	 *
1464
	 * The object uri is only the basename, or filename and not a full path.
1465
	 *
1466
	 * @param mixed $calendarId
1467
	 * @param string $objectUri
1468
	 * @param int $calendarType
1469
	 * @param bool $forceDeletePermanently
1470
	 * @return void
1471
	 */
1472
	public function deleteCalendarObject($calendarId, $objectUri, $calendarType = self::CALENDAR_TYPE_CALENDAR, bool $forceDeletePermanently = false) {
1473
		$data = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
1474
1475
		if ($data === null) {
1476
			// Nothing to delete
1477
			return;
1478
		}
1479
1480
		if ($forceDeletePermanently || $this->config->getAppValue(Application::APP_ID, RetentionService::RETENTION_CONFIG_KEY) === '0') {
1481
			$stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ? AND `uri` = ? AND `calendartype` = ?');
1482
			$stmt->execute([$calendarId, $objectUri, $calendarType]);
1483
1484
			$this->purgeProperties($calendarId, $data['id']);
1485
1486
			if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
1487
				$calendarRow = $this->getCalendarById($calendarId);
1488
				$shares = $this->getShares($calendarId);
1489
1490
				$this->dispatcher->dispatchTyped(new CalendarObjectDeletedEvent((int)$calendarId, $calendarRow, $shares, $data));
1491
			} else {
1492
				$subscriptionRow = $this->getSubscriptionById($calendarId);
1493
1494
				$this->dispatcher->dispatchTyped(new CachedCalendarObjectDeletedEvent((int)$calendarId, $subscriptionRow, [], $data));
1495
				$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteCachedCalendarObject', new GenericEvent(
0 ignored issues
show
Unused Code introduced by
The call to Symfony\Contracts\EventD...erInterface::dispatch() has too many arguments starting with new Symfony\Component\Ev...'objectData' => $data)). ( Ignorable by Annotation )

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

1495
				$this->legacyDispatcher->/** @scrutinizer ignore-call */ 
1496
                             dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteCachedCalendarObject', new GenericEvent(

This check compares calls to functions or methods with their respective definitions. If the call has more 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...
Bug introduced by
'\OCA\DAV\CalDAV\CalDavB...teCachedCalendarObject' of type string is incompatible with the type object expected by parameter $event of Symfony\Contracts\EventD...erInterface::dispatch(). ( Ignorable by Annotation )

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

1495
				$this->legacyDispatcher->dispatch(/** @scrutinizer ignore-type */ '\OCA\DAV\CalDAV\CalDavBackend::deleteCachedCalendarObject', new GenericEvent(
Loading history...
1496
					'\OCA\DAV\CalDAV\CalDavBackend::deleteCachedCalendarObject',
1497
					[
1498
						'subscriptionId' => $calendarId,
1499
						'calendarData' => $subscriptionRow,
1500
						'shares' => [],
1501
						'objectData' => $data,
1502
					]
1503
				));
1504
			}
1505
		} else {
1506
			$pathInfo = pathinfo($data['uri']);
1507
			if (!empty($pathInfo['extension'])) {
1508
				// Append a suffix to "free" the old URI for recreation
1509
				$newUri = sprintf(
1510
					"%s-deleted.%s",
1511
					$pathInfo['filename'],
1512
					$pathInfo['extension']
1513
				);
1514
			} else {
1515
				$newUri = sprintf(
1516
					"%s-deleted",
1517
					$pathInfo['filename']
1518
				);
1519
			}
1520
1521
			// Try to detect conflicts before the DB does
1522
			// As unlikely as it seems, this can happen when the user imports, then deletes, imports and deletes again
1523
			$newObject = $this->getCalendarObject($calendarId, $newUri, $calendarType);
1524
			if ($newObject !== null) {
1525
				throw new Forbidden("A calendar object with URI $newUri already exists in calendar $calendarId, therefore this object can't be moved into the trashbin");
1526
			}
1527
1528
			$qb = $this->db->getQueryBuilder();
1529
			$markObjectDeletedQuery = $qb->update('calendarobjects')
1530
				->set('deleted_at', $qb->createNamedParameter(time(), IQueryBuilder::PARAM_INT))
1531
				->set('uri', $qb->createNamedParameter($newUri))
1532
				->where(
1533
					$qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)),
1534
					$qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT),
1535
					$qb->expr()->eq('uri', $qb->createNamedParameter($objectUri))
1536
				);
1537
			$markObjectDeletedQuery->executeStatement();
1538
1539
			$calendarData = $this->getCalendarById($calendarId);
1540
			if ($calendarData !== null) {
1541
				$this->dispatcher->dispatchTyped(
1542
					new CalendarObjectMovedToTrashEvent(
1543
						(int)$calendarId,
1544
						$calendarData,
1545
						$this->getShares($calendarId),
1546
						$data
1547
					)
1548
				);
1549
			}
1550
		}
1551
1552
		$this->addChange($calendarId, $objectUri, 3, $calendarType);
1553
	}
1554
1555
	/**
1556
	 * @param mixed $objectData
1557
	 *
1558
	 * @throws Forbidden
1559
	 */
1560
	public function restoreCalendarObject(array $objectData): void {
1561
		$id = (int) $objectData['id'];
1562
		$restoreUri = str_replace("-deleted.ics", ".ics", $objectData['uri']);
1563
		$targetObject = $this->getCalendarObject(
1564
			$objectData['calendarid'],
1565
			$restoreUri
1566
		);
1567
		if ($targetObject !== null) {
1568
			throw new Forbidden("Can not restore calendar $id because a calendar object with the URI $restoreUri already exists");
1569
		}
1570
1571
		$qb = $this->db->getQueryBuilder();
1572
		$update = $qb->update('calendarobjects')
1573
			->set('uri', $qb->createNamedParameter($restoreUri))
1574
			->set('deleted_at', $qb->createNamedParameter(null))
1575
			->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT));
1576
		$update->executeStatement();
1577
1578
		// Make sure this change is tracked in the changes table
1579
		$qb2 = $this->db->getQueryBuilder();
1580
		$selectObject = $qb2->select('calendardata', 'uri', 'calendarid', 'calendartype')
1581
			->selectAlias('componenttype', 'component')
1582
			->from('calendarobjects')
1583
			->where($qb2->expr()->eq('id', $qb2->createNamedParameter($id, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT));
1584
		$result = $selectObject->executeQuery();
1585
		$row = $result->fetch();
1586
		$result->closeCursor();
1587
		if ($row === false) {
1588
			// Welp, this should possibly not have happened, but let's ignore
1589
			return;
1590
		}
1591
		$this->addChange($row['calendarid'], $row['uri'], 1, (int) $row['calendartype']);
1592
1593
		$calendarRow = $this->getCalendarById((int) $row['calendarid']);
1594
		if ($calendarRow === null) {
1595
			throw new RuntimeException('Calendar object data that was just written can\'t be read back. Check your database configuration.');
1596
		}
1597
		$this->dispatcher->dispatchTyped(
1598
			new CalendarObjectRestoredEvent(
1599
				(int) $objectData['calendarid'],
1600
				$calendarRow,
1601
				$this->getShares((int) $row['calendarid']),
1602
				$row
1603
			)
1604
		);
1605
	}
1606
1607
	/**
1608
	 * Performs a calendar-query on the contents of this calendar.
1609
	 *
1610
	 * The calendar-query is defined in RFC4791 : CalDAV. Using the
1611
	 * calendar-query it is possible for a client to request a specific set of
1612
	 * object, based on contents of iCalendar properties, date-ranges and
1613
	 * iCalendar component types (VTODO, VEVENT).
1614
	 *
1615
	 * This method should just return a list of (relative) urls that match this
1616
	 * query.
1617
	 *
1618
	 * The list of filters are specified as an array. The exact array is
1619
	 * documented by Sabre\CalDAV\CalendarQueryParser.
1620
	 *
1621
	 * Note that it is extremely likely that getCalendarObject for every path
1622
	 * returned from this method will be called almost immediately after. You
1623
	 * may want to anticipate this to speed up these requests.
1624
	 *
1625
	 * This method provides a default implementation, which parses *all* the
1626
	 * iCalendar objects in the specified calendar.
1627
	 *
1628
	 * This default may well be good enough for personal use, and calendars
1629
	 * that aren't very large. But if you anticipate high usage, big calendars
1630
	 * or high loads, you are strongly advised to optimize certain paths.
1631
	 *
1632
	 * The best way to do so is override this method and to optimize
1633
	 * specifically for 'common filters'.
1634
	 *
1635
	 * Requests that are extremely common are:
1636
	 *   * requests for just VEVENTS
1637
	 *   * requests for just VTODO
1638
	 *   * requests with a time-range-filter on either VEVENT or VTODO.
1639
	 *
1640
	 * ..and combinations of these requests. It may not be worth it to try to
1641
	 * handle every possible situation and just rely on the (relatively
1642
	 * easy to use) CalendarQueryValidator to handle the rest.
1643
	 *
1644
	 * Note that especially time-range-filters may be difficult to parse. A
1645
	 * time-range filter specified on a VEVENT must for instance also handle
1646
	 * recurrence rules correctly.
1647
	 * A good example of how to interprete all these filters can also simply
1648
	 * be found in Sabre\CalDAV\CalendarQueryFilter. This class is as correct
1649
	 * as possible, so it gives you a good idea on what type of stuff you need
1650
	 * to think of.
1651
	 *
1652
	 * @param mixed $calendarId
1653
	 * @param array $filters
1654
	 * @param int $calendarType
1655
	 * @return array
1656
	 */
1657
	public function calendarQuery($calendarId, array $filters, $calendarType = self::CALENDAR_TYPE_CALENDAR):array {
1658
		$componentType = null;
1659
		$requirePostFilter = true;
1660
		$timeRange = null;
1661
1662
		// if no filters were specified, we don't need to filter after a query
1663
		if (!$filters['prop-filters'] && !$filters['comp-filters']) {
1664
			$requirePostFilter = false;
1665
		}
1666
1667
		// Figuring out if there's a component filter
1668
		if (count($filters['comp-filters']) > 0 && !$filters['comp-filters'][0]['is-not-defined']) {
1669
			$componentType = $filters['comp-filters'][0]['name'];
1670
1671
			// Checking if we need post-filters
1672
			if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['time-range'] && !$filters['comp-filters'][0]['prop-filters']) {
1673
				$requirePostFilter = false;
1674
			}
1675
			// There was a time-range filter
1676
			if ($componentType === 'VEVENT' && isset($filters['comp-filters'][0]['time-range']) && is_array($filters['comp-filters'][0]['time-range'])) {
1677
				$timeRange = $filters['comp-filters'][0]['time-range'];
1678
1679
				// If start time OR the end time is not specified, we can do a
1680
				// 100% accurate mysql query.
1681
				if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['prop-filters'] && (!$timeRange['start'] || !$timeRange['end'])) {
1682
					$requirePostFilter = false;
1683
				}
1684
			}
1685
		}
1686
		$columns = ['uri'];
1687
		if ($requirePostFilter) {
1688
			$columns = ['uri', 'calendardata'];
1689
		}
1690
		$query = $this->db->getQueryBuilder();
1691
		$query->select($columns)
1692
			->from('calendarobjects')
1693
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
1694
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)))
1695
			->andWhere($query->expr()->isNull('deleted_at'));
1696
1697
		if ($componentType) {
1698
			$query->andWhere($query->expr()->eq('componenttype', $query->createNamedParameter($componentType)));
1699
		}
1700
1701
		if ($timeRange && $timeRange['start']) {
1702
			$query->andWhere($query->expr()->gt('lastoccurence', $query->createNamedParameter($timeRange['start']->getTimeStamp())));
1703
		}
1704
		if ($timeRange && $timeRange['end']) {
1705
			$query->andWhere($query->expr()->lt('firstoccurence', $query->createNamedParameter($timeRange['end']->getTimeStamp())));
1706
		}
1707
1708
		$stmt = $query->executeQuery();
1709
1710
		$result = [];
1711
		while ($row = $stmt->fetch()) {
1712
			if ($requirePostFilter) {
1713
				// validateFilterForObject will parse the calendar data
1714
				// catch parsing errors
1715
				try {
1716
					$matches = $this->validateFilterForObject($row, $filters);
1717
				} catch (ParseException $ex) {
1718
					$this->logger->logException($ex, [
1719
						'app' => 'dav',
1720
						'message' => 'Caught parsing exception for calendar data. This usually indicates invalid calendar data. calendar-id:'.$calendarId.' uri:'.$row['uri']
1721
					]);
1722
					continue;
1723
				} catch (InvalidDataException $ex) {
1724
					$this->logger->logException($ex, [
1725
						'app' => 'dav',
1726
						'message' => 'Caught invalid data exception for calendar data. This usually indicates invalid calendar data. calendar-id:'.$calendarId.' uri:'.$row['uri']
1727
					]);
1728
					continue;
1729
				}
1730
1731
				if (!$matches) {
1732
					continue;
1733
				}
1734
			}
1735
			$result[] = $row['uri'];
1736
		}
1737
1738
		return $result;
1739
	}
1740
1741
	/**
1742
	 * custom Nextcloud search extension for CalDAV
1743
	 *
1744
	 * TODO - this should optionally cover cached calendar objects as well
1745
	 *
1746
	 * @param string $principalUri
1747
	 * @param array $filters
1748
	 * @param integer|null $limit
1749
	 * @param integer|null $offset
1750
	 * @return array
1751
	 */
1752
	public function calendarSearch($principalUri, array $filters, $limit = null, $offset = null) {
1753
		$calendars = $this->getCalendarsForUser($principalUri);
1754
		$ownCalendars = [];
1755
		$sharedCalendars = [];
1756
1757
		$uriMapper = [];
1758
1759
		foreach ($calendars as $calendar) {
1760
			if ($calendar['{http://owncloud.org/ns}owner-principal'] === $principalUri) {
1761
				$ownCalendars[] = $calendar['id'];
1762
			} else {
1763
				$sharedCalendars[] = $calendar['id'];
1764
			}
1765
			$uriMapper[$calendar['id']] = $calendar['uri'];
1766
		}
1767
		if (count($ownCalendars) === 0 && count($sharedCalendars) === 0) {
1768
			return [];
1769
		}
1770
1771
		$query = $this->db->getQueryBuilder();
1772
		// Calendar id expressions
1773
		$calendarExpressions = [];
1774
		foreach ($ownCalendars as $id) {
1775
			$calendarExpressions[] = $query->expr()->andX(
1776
				$query->expr()->eq('c.calendarid',
1777
					$query->createNamedParameter($id)),
1778
				$query->expr()->eq('c.calendartype',
1779
						$query->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
1780
		}
1781
		foreach ($sharedCalendars as $id) {
1782
			$calendarExpressions[] = $query->expr()->andX(
1783
				$query->expr()->eq('c.calendarid',
1784
					$query->createNamedParameter($id)),
1785
				$query->expr()->eq('c.classification',
1786
					$query->createNamedParameter(self::CLASSIFICATION_PUBLIC)),
1787
				$query->expr()->eq('c.calendartype',
1788
					$query->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
1789
		}
1790
1791
		if (count($calendarExpressions) === 1) {
1792
			$calExpr = $calendarExpressions[0];
1793
		} else {
1794
			$calExpr = call_user_func_array([$query->expr(), 'orX'], $calendarExpressions);
1795
		}
1796
1797
		// Component expressions
1798
		$compExpressions = [];
1799
		foreach ($filters['comps'] as $comp) {
1800
			$compExpressions[] = $query->expr()
1801
				->eq('c.componenttype', $query->createNamedParameter($comp));
1802
		}
1803
1804
		if (count($compExpressions) === 1) {
1805
			$compExpr = $compExpressions[0];
1806
		} else {
1807
			$compExpr = call_user_func_array([$query->expr(), 'orX'], $compExpressions);
1808
		}
1809
1810
		if (!isset($filters['props'])) {
1811
			$filters['props'] = [];
1812
		}
1813
		if (!isset($filters['params'])) {
1814
			$filters['params'] = [];
1815
		}
1816
1817
		$propParamExpressions = [];
1818
		foreach ($filters['props'] as $prop) {
1819
			$propParamExpressions[] = $query->expr()->andX(
1820
				$query->expr()->eq('i.name', $query->createNamedParameter($prop)),
1821
				$query->expr()->isNull('i.parameter')
1822
			);
1823
		}
1824
		foreach ($filters['params'] as $param) {
1825
			$propParamExpressions[] = $query->expr()->andX(
1826
				$query->expr()->eq('i.name', $query->createNamedParameter($param['property'])),
1827
				$query->expr()->eq('i.parameter', $query->createNamedParameter($param['parameter']))
1828
			);
1829
		}
1830
1831
		if (count($propParamExpressions) === 1) {
1832
			$propParamExpr = $propParamExpressions[0];
1833
		} else {
1834
			$propParamExpr = call_user_func_array([$query->expr(), 'orX'], $propParamExpressions);
1835
		}
1836
1837
		$query->select(['c.calendarid', 'c.uri'])
1838
			->from($this->dbObjectPropertiesTable, 'i')
1839
			->join('i', 'calendarobjects', 'c', $query->expr()->eq('i.objectid', 'c.id'))
1840
			->where($calExpr)
1841
			->andWhere($compExpr)
1842
			->andWhere($propParamExpr)
1843
			->andWhere($query->expr()->iLike('i.value',
1844
				$query->createNamedParameter('%'.$this->db->escapeLikeParameter($filters['search-term']).'%')))
1845
			->andWhere($query->expr()->isNull('deleted_at'));
1846
1847
		if ($offset) {
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...
1848
			$query->setFirstResult($offset);
1849
		}
1850
		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...
1851
			$query->setMaxResults($limit);
1852
		}
1853
1854
		$stmt = $query->executeQuery();
1855
1856
		$result = [];
1857
		while ($row = $stmt->fetch()) {
1858
			$path = $uriMapper[$row['calendarid']] . '/' . $row['uri'];
1859
			if (!in_array($path, $result)) {
1860
				$result[] = $path;
1861
			}
1862
		}
1863
1864
		return $result;
1865
	}
1866
1867
	/**
1868
	 * used for Nextcloud's calendar API
1869
	 *
1870
	 * @param array $calendarInfo
1871
	 * @param string $pattern
1872
	 * @param array $searchProperties
1873
	 * @param array $options
1874
	 * @param integer|null $limit
1875
	 * @param integer|null $offset
1876
	 *
1877
	 * @return array
1878
	 */
1879
	public function search(array $calendarInfo, $pattern, array $searchProperties,
1880
						   array $options, $limit, $offset) {
1881
		$outerQuery = $this->db->getQueryBuilder();
1882
		$innerQuery = $this->db->getQueryBuilder();
1883
1884
		$innerQuery->selectDistinct('op.objectid')
1885
			->from($this->dbObjectPropertiesTable, 'op')
1886
			->andWhere($innerQuery->expr()->eq('op.calendarid',
1887
				$outerQuery->createNamedParameter($calendarInfo['id'])))
1888
			->andWhere($innerQuery->expr()->eq('op.calendartype',
1889
				$outerQuery->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
1890
1891
		// only return public items for shared calendars for now
1892
		if (isset($calendarInfo['{http://owncloud.org/ns}owner-principal']) === false || $calendarInfo['principaluri'] !== $calendarInfo['{http://owncloud.org/ns}owner-principal']) {
1893
			$innerQuery->andWhere($innerQuery->expr()->eq('c.classification',
1894
				$outerQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
1895
		}
1896
1897
		if (!empty($searchProperties)) {
1898
			$or = $innerQuery->expr()->orX();
1899
			foreach ($searchProperties as $searchProperty) {
1900
				$or->add($innerQuery->expr()->eq('op.name',
1901
					$outerQuery->createNamedParameter($searchProperty)));
1902
			}
1903
			$innerQuery->andWhere($or);
1904
		}
1905
1906
		if ($pattern !== '') {
1907
			$innerQuery->andWhere($innerQuery->expr()->iLike('op.value',
1908
				$outerQuery->createNamedParameter('%' .
1909
					$this->db->escapeLikeParameter($pattern) . '%')));
1910
		}
1911
1912
		$outerQuery->select('c.id', 'c.calendardata', 'c.componenttype', 'c.uid', 'c.uri')
1913
			->from('calendarobjects', 'c')
1914
			->where($outerQuery->expr()->isNull('deleted_at'));
1915
1916
		if (isset($options['timerange'])) {
1917
			if (isset($options['timerange']['start']) && $options['timerange']['start'] instanceof DateTimeInterface) {
1918
				$outerQuery->andWhere($outerQuery->expr()->gt('lastoccurence',
1919
					$outerQuery->createNamedParameter($options['timerange']['start']->getTimeStamp())));
1920
			}
1921
			if (isset($options['timerange']['end']) && $options['timerange']['end'] instanceof DateTimeInterface) {
1922
				$outerQuery->andWhere($outerQuery->expr()->lt('firstoccurence',
1923
					$outerQuery->createNamedParameter($options['timerange']['end']->getTimeStamp())));
1924
			}
1925
		}
1926
1927
		if (!empty($options['types'])) {
1928
			$or = $outerQuery->expr()->orX();
1929
			foreach ($options['types'] as $type) {
1930
				$or->add($outerQuery->expr()->eq('componenttype',
1931
					$outerQuery->createNamedParameter($type)));
1932
			}
1933
			$outerQuery->andWhere($or);
1934
		}
1935
1936
		$outerQuery->andWhere($outerQuery->expr()->in('c.id', $outerQuery->createFunction($innerQuery->getSQL())));
1937
1938
		if ($offset) {
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...
1939
			$outerQuery->setFirstResult($offset);
1940
		}
1941
		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...
1942
			$outerQuery->setMaxResults($limit);
1943
		}
1944
1945
		$result = $outerQuery->executeQuery();
1946
		$calendarObjects = array_filter($result->fetchAll(), function (array $row) use ($options) {
1947
			$start = $options['timerange']['start'] ?? null;
1948
			$end = $options['timerange']['end'] ?? null;
1949
1950
			if ($start === null || !($start instanceof DateTimeInterface) || $end === null || !($end instanceof DateTimeInterface)) {
1951
				// No filter required
1952
				return true;
1953
			}
1954
1955
			$isValid = $this->validateFilterForObject($row, [
1956
				'name' => 'VCALENDAR',
1957
				'comp-filters' => [
1958
					[
1959
						'name' => 'VEVENT',
1960
						'comp-filters' => [],
1961
						'prop-filters' => [],
1962
						'is-not-defined' => false,
1963
						'time-range' => [
1964
							'start' => $start,
1965
							'end' => $end,
1966
						],
1967
					],
1968
				],
1969
				'prop-filters' => [],
1970
				'is-not-defined' => false,
1971
				'time-range' => null,
1972
			]);
1973
			if (is_resource($row['calendardata'])) {
1974
				// Put the stream back to the beginning so it can be read another time
1975
				rewind($row['calendardata']);
1976
			}
1977
			return $isValid;
1978
		});
1979
		$result->closeCursor();
1980
1981
		return array_map(function ($o) {
1982
			$calendarData = Reader::read($o['calendardata']);
1983
			$comps = $calendarData->getComponents();
1984
			$objects = [];
1985
			$timezones = [];
1986
			foreach ($comps as $comp) {
1987
				if ($comp instanceof VTimeZone) {
1988
					$timezones[] = $comp;
1989
				} else {
1990
					$objects[] = $comp;
1991
				}
1992
			}
1993
1994
			return [
1995
				'id' => $o['id'],
1996
				'type' => $o['componenttype'],
1997
				'uid' => $o['uid'],
1998
				'uri' => $o['uri'],
1999
				'objects' => array_map(function ($c) {
2000
					return $this->transformSearchData($c);
2001
				}, $objects),
2002
				'timezones' => array_map(function ($c) {
2003
					return $this->transformSearchData($c);
2004
				}, $timezones),
2005
			];
2006
		}, $calendarObjects);
2007
	}
2008
2009
	/**
2010
	 * @param Component $comp
2011
	 * @return array
2012
	 */
2013
	private function transformSearchData(Component $comp) {
2014
		$data = [];
2015
		/** @var Component[] $subComponents */
2016
		$subComponents = $comp->getComponents();
2017
		/** @var Property[] $properties */
2018
		$properties = array_filter($comp->children(), function ($c) {
2019
			return $c instanceof Property;
2020
		});
2021
		$validationRules = $comp->getValidationRules();
2022
2023
		foreach ($subComponents as $subComponent) {
2024
			$name = $subComponent->name;
2025
			if (!isset($data[$name])) {
2026
				$data[$name] = [];
2027
			}
2028
			$data[$name][] = $this->transformSearchData($subComponent);
2029
		}
2030
2031
		foreach ($properties as $property) {
2032
			$name = $property->name;
2033
			if (!isset($validationRules[$name])) {
2034
				$validationRules[$name] = '*';
2035
			}
2036
2037
			$rule = $validationRules[$property->name];
2038
			if ($rule === '+' || $rule === '*') { // multiple
2039
				if (!isset($data[$name])) {
2040
					$data[$name] = [];
2041
				}
2042
2043
				$data[$name][] = $this->transformSearchProperty($property);
2044
			} else { // once
2045
				$data[$name] = $this->transformSearchProperty($property);
2046
			}
2047
		}
2048
2049
		return $data;
2050
	}
2051
2052
	/**
2053
	 * @param Property $prop
2054
	 * @return array
2055
	 */
2056
	private function transformSearchProperty(Property $prop) {
2057
		// No need to check Date, as it extends DateTime
2058
		if ($prop instanceof Property\ICalendar\DateTime) {
2059
			$value = $prop->getDateTime();
2060
		} else {
2061
			$value = $prop->getValue();
2062
		}
2063
2064
		return [
2065
			$value,
2066
			$prop->parameters()
2067
		];
2068
	}
2069
2070
	/**
2071
	 * @param string $principalUri
2072
	 * @param string $pattern
2073
	 * @param array $componentTypes
2074
	 * @param array $searchProperties
2075
	 * @param array $searchParameters
2076
	 * @param array $options
2077
	 * @return array
2078
	 */
2079
	public function searchPrincipalUri(string $principalUri,
2080
									   string $pattern,
2081
									   array $componentTypes,
2082
									   array $searchProperties,
2083
									   array $searchParameters,
2084
									   array $options = []): array {
2085
		$escapePattern = !\array_key_exists('escape_like_param', $options) || $options['escape_like_param'] !== false;
2086
2087
		$calendarObjectIdQuery = $this->db->getQueryBuilder();
2088
		$calendarOr = $calendarObjectIdQuery->expr()->orX();
2089
		$searchOr = $calendarObjectIdQuery->expr()->orX();
2090
2091
		// Fetch calendars and subscription
2092
		$calendars = $this->getCalendarsForUser($principalUri);
2093
		$subscriptions = $this->getSubscriptionsForUser($principalUri);
2094
		foreach ($calendars as $calendar) {
2095
			$calendarAnd = $calendarObjectIdQuery->expr()->andX();
2096
			$calendarAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int)$calendar['id'])));
2097
			$calendarAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendartype', $calendarObjectIdQuery->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
2098
2099
			// If it's shared, limit search to public events
2100
			if (isset($calendar['{http://owncloud.org/ns}owner-principal'])
2101
				&& $calendar['principaluri'] !== $calendar['{http://owncloud.org/ns}owner-principal']) {
2102
				$calendarAnd->add($calendarObjectIdQuery->expr()->eq('co.classification', $calendarObjectIdQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
2103
			}
2104
2105
			$calendarOr->add($calendarAnd);
2106
		}
2107
		foreach ($subscriptions as $subscription) {
2108
			$subscriptionAnd = $calendarObjectIdQuery->expr()->andX();
2109
			$subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int)$subscription['id'])));
2110
			$subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendartype', $calendarObjectIdQuery->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)));
2111
2112
			// If it's shared, limit search to public events
2113
			if (isset($subscription['{http://owncloud.org/ns}owner-principal'])
2114
				&& $subscription['principaluri'] !== $subscription['{http://owncloud.org/ns}owner-principal']) {
2115
				$subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('co.classification', $calendarObjectIdQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
2116
			}
2117
2118
			$calendarOr->add($subscriptionAnd);
2119
		}
2120
2121
		foreach ($searchProperties as $property) {
2122
			$propertyAnd = $calendarObjectIdQuery->expr()->andX();
2123
			$propertyAnd->add($calendarObjectIdQuery->expr()->eq('cob.name', $calendarObjectIdQuery->createNamedParameter($property, IQueryBuilder::PARAM_STR)));
2124
			$propertyAnd->add($calendarObjectIdQuery->expr()->isNull('cob.parameter'));
2125
2126
			$searchOr->add($propertyAnd);
2127
		}
2128
		foreach ($searchParameters as $property => $parameter) {
2129
			$parameterAnd = $calendarObjectIdQuery->expr()->andX();
2130
			$parameterAnd->add($calendarObjectIdQuery->expr()->eq('cob.name', $calendarObjectIdQuery->createNamedParameter($property, IQueryBuilder::PARAM_STR)));
2131
			$parameterAnd->add($calendarObjectIdQuery->expr()->eq('cob.parameter', $calendarObjectIdQuery->createNamedParameter($parameter, IQueryBuilder::PARAM_STR_ARRAY)));
2132
2133
			$searchOr->add($parameterAnd);
2134
		}
2135
2136
		if ($calendarOr->count() === 0) {
2137
			return [];
2138
		}
2139
		if ($searchOr->count() === 0) {
2140
			return [];
2141
		}
2142
2143
		$calendarObjectIdQuery->selectDistinct('cob.objectid')
2144
			->from($this->dbObjectPropertiesTable, 'cob')
2145
			->leftJoin('cob', 'calendarobjects', 'co', $calendarObjectIdQuery->expr()->eq('co.id', 'cob.objectid'))
2146
			->andWhere($calendarObjectIdQuery->expr()->in('co.componenttype', $calendarObjectIdQuery->createNamedParameter($componentTypes, IQueryBuilder::PARAM_STR_ARRAY)))
2147
			->andWhere($calendarOr)
2148
			->andWhere($searchOr)
2149
			->andWhere($calendarObjectIdQuery->expr()->isNull('deleted_at'));
2150
2151
		if ('' !== $pattern) {
2152
			if (!$escapePattern) {
2153
				$calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->ilike('cob.value', $calendarObjectIdQuery->createNamedParameter($pattern)));
2154
			} else {
2155
				$calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->ilike('cob.value', $calendarObjectIdQuery->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%')));
2156
			}
2157
		}
2158
2159
		if (isset($options['limit'])) {
2160
			$calendarObjectIdQuery->setMaxResults($options['limit']);
2161
		}
2162
		if (isset($options['offset'])) {
2163
			$calendarObjectIdQuery->setFirstResult($options['offset']);
2164
		}
2165
2166
		$result = $calendarObjectIdQuery->executeQuery();
2167
		$matches = $result->fetchAll();
2168
		$result->closeCursor();
2169
		$matches = array_map(static function (array $match):int {
2170
			return (int) $match['objectid'];
2171
		}, $matches);
2172
2173
		$query = $this->db->getQueryBuilder();
2174
		$query->select('calendardata', 'uri', 'calendarid', 'calendartype')
2175
			->from('calendarobjects')
2176
			->where($query->expr()->in('id', $query->createNamedParameter($matches, IQueryBuilder::PARAM_INT_ARRAY)));
2177
2178
		$result = $query->executeQuery();
2179
		$calendarObjects = $result->fetchAll();
2180
		$result->closeCursor();
2181
2182
		return array_map(function (array $array): array {
2183
			$array['calendarid'] = (int)$array['calendarid'];
2184
			$array['calendartype'] = (int)$array['calendartype'];
2185
			$array['calendardata'] = $this->readBlob($array['calendardata']);
2186
2187
			return $array;
2188
		}, $calendarObjects);
2189
	}
2190
2191
	/**
2192
	 * Searches through all of a users calendars and calendar objects to find
2193
	 * an object with a specific UID.
2194
	 *
2195
	 * This method should return the path to this object, relative to the
2196
	 * calendar home, so this path usually only contains two parts:
2197
	 *
2198
	 * calendarpath/objectpath.ics
2199
	 *
2200
	 * If the uid is not found, return null.
2201
	 *
2202
	 * This method should only consider * objects that the principal owns, so
2203
	 * any calendars owned by other principals that also appear in this
2204
	 * collection should be ignored.
2205
	 *
2206
	 * @param string $principalUri
2207
	 * @param string $uid
2208
	 * @return string|null
2209
	 */
2210
	public function getCalendarObjectByUID($principalUri, $uid) {
2211
		$query = $this->db->getQueryBuilder();
2212
		$query->selectAlias('c.uri', 'calendaruri')->selectAlias('co.uri', 'objecturi')
2213
			->from('calendarobjects', 'co')
2214
			->leftJoin('co', 'calendars', 'c', $query->expr()->eq('co.calendarid', 'c.id'))
2215
			->where($query->expr()->eq('c.principaluri', $query->createNamedParameter($principalUri)))
2216
			->andWhere($query->expr()->eq('co.uid', $query->createNamedParameter($uid)))
2217
			->andWhere($query->expr()->isNull('co.deleted_at'));
2218
		$stmt = $query->executeQuery();
2219
		$row = $stmt->fetch();
2220
		$stmt->closeCursor();
2221
		if ($row) {
2222
			return $row['calendaruri'] . '/' . $row['objecturi'];
2223
		}
2224
2225
		return null;
2226
	}
2227
2228
	public function getCalendarObjectById(string $principalUri, int $id): ?array {
2229
		$query = $this->db->getQueryBuilder();
2230
		$query->select(['co.id', 'co.uri', 'co.lastmodified', 'co.etag', 'co.calendarid', 'co.size', 'co.calendardata', 'co.componenttype', 'co.classification', 'co.deleted_at'])
2231
			->selectAlias('c.uri', 'calendaruri')
2232
			->from('calendarobjects', 'co')
2233
			->join('co', 'calendars', 'c', $query->expr()->eq('c.id', 'co.calendarid', IQueryBuilder::PARAM_INT))
2234
			->where($query->expr()->eq('c.principaluri', $query->createNamedParameter($principalUri)))
2235
			->andWhere($query->expr()->eq('co.id', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT));
2236
		$stmt = $query->executeQuery();
2237
		$row = $stmt->fetch();
2238
		$stmt->closeCursor();
2239
2240
		if (!$row) {
2241
			return null;
2242
		}
2243
2244
		return [
2245
			'id' => $row['id'],
2246
			'uri' => $row['uri'],
2247
			'lastmodified' => $row['lastmodified'],
2248
			'etag' => '"' . $row['etag'] . '"',
2249
			'calendarid' => $row['calendarid'],
2250
			'calendaruri' => $row['calendaruri'],
2251
			'size' => (int)$row['size'],
2252
			'calendardata' => $this->readBlob($row['calendardata']),
2253
			'component' => strtolower($row['componenttype']),
2254
			'classification' => (int)$row['classification'],
2255
			'deleted_at' => isset($row['deleted_at']) ? ((int) $row['deleted_at']) : null,
2256
		];
2257
	}
2258
2259
	/**
2260
	 * The getChanges method returns all the changes that have happened, since
2261
	 * the specified syncToken in the specified calendar.
2262
	 *
2263
	 * This function should return an array, such as the following:
2264
	 *
2265
	 * [
2266
	 *   'syncToken' => 'The current synctoken',
2267
	 *   'added'   => [
2268
	 *      'new.txt',
2269
	 *   ],
2270
	 *   'modified'   => [
2271
	 *      'modified.txt',
2272
	 *   ],
2273
	 *   'deleted' => [
2274
	 *      'foo.php.bak',
2275
	 *      'old.txt'
2276
	 *   ]
2277
	 * );
2278
	 *
2279
	 * The returned syncToken property should reflect the *current* syncToken
2280
	 * of the calendar, as reported in the {http://sabredav.org/ns}sync-token
2281
	 * property This is * needed here too, to ensure the operation is atomic.
2282
	 *
2283
	 * If the $syncToken argument is specified as null, this is an initial
2284
	 * sync, and all members should be reported.
2285
	 *
2286
	 * The modified property is an array of nodenames that have changed since
2287
	 * the last token.
2288
	 *
2289
	 * The deleted property is an array with nodenames, that have been deleted
2290
	 * from collection.
2291
	 *
2292
	 * The $syncLevel argument is basically the 'depth' of the report. If it's
2293
	 * 1, you only have to report changes that happened only directly in
2294
	 * immediate descendants. If it's 2, it should also include changes from
2295
	 * the nodes below the child collections. (grandchildren)
2296
	 *
2297
	 * The $limit argument allows a client to specify how many results should
2298
	 * be returned at most. If the limit is not specified, it should be treated
2299
	 * as infinite.
2300
	 *
2301
	 * If the limit (infinite or not) is higher than you're willing to return,
2302
	 * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
2303
	 *
2304
	 * If the syncToken is expired (due to data cleanup) or unknown, you must
2305
	 * return null.
2306
	 *
2307
	 * The limit is 'suggestive'. You are free to ignore it.
2308
	 *
2309
	 * @param string $calendarId
2310
	 * @param string $syncToken
2311
	 * @param int $syncLevel
2312
	 * @param int|null $limit
2313
	 * @param int $calendarType
2314
	 * @return array
2315
	 */
2316
	public function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit = null, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
2317
		// Current synctoken
2318
		$qb = $this->db->getQueryBuilder();
2319
		$qb->select('synctoken')
2320
			->from('calendars')
2321
			->where(
2322
				$qb->expr()->eq('id', $qb->createNamedParameter($calendarId))
2323
			);
2324
		$stmt = $qb->executeQuery();
2325
		$currentToken = $stmt->fetchOne();
2326
2327
		if ($currentToken === false) {
2328
			return null;
2329
		}
2330
2331
		$result = [
2332
			'syncToken' => $currentToken,
2333
			'added' => [],
2334
			'modified' => [],
2335
			'deleted' => [],
2336
		];
2337
2338
		if ($syncToken) {
2339
			$qb = $this->db->getQueryBuilder();
2340
2341
			$qb->select('uri', 'operation')
2342
				->from('calendarchanges')
2343
				->where(
2344
					$qb->expr()->andX(
2345
						$qb->expr()->gte('synctoken', $qb->createNamedParameter($syncToken)),
2346
						$qb->expr()->lt('synctoken', $qb->createNamedParameter($currentToken)),
2347
						$qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)),
2348
						$qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType))
2349
					)
2350
				)->orderBy('synctoken');
2351
			if (is_int($limit) && $limit > 0) {
2352
				$qb->setMaxResults($limit);
2353
			}
2354
2355
			// Fetching all changes
2356
			$stmt = $qb->executeQuery();
2357
			$changes = [];
2358
2359
			// This loop ensures that any duplicates are overwritten, only the
2360
			// last change on a node is relevant.
2361
			while ($row = $stmt->fetch()) {
2362
				$changes[$row['uri']] = $row['operation'];
2363
			}
2364
			$stmt->closeCursor();
2365
2366
			foreach ($changes as $uri => $operation) {
2367
				switch ($operation) {
2368
					case 1:
2369
						$result['added'][] = $uri;
2370
						break;
2371
					case 2:
2372
						$result['modified'][] = $uri;
2373
						break;
2374
					case 3:
2375
						$result['deleted'][] = $uri;
2376
						break;
2377
				}
2378
			}
2379
		} else {
2380
			// No synctoken supplied, this is the initial sync.
2381
			$qb = $this->db->getQueryBuilder();
2382
			$qb->select('uri')
2383
				->from('calendarobjects')
2384
				->where(
2385
					$qb->expr()->andX(
2386
						$qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)),
2387
						$qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType))
2388
					)
2389
				);
2390
			$stmt = $qb->executeQuery();
2391
			$result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
2392
			$stmt->closeCursor();
2393
		}
2394
		return $result;
2395
	}
2396
2397
	/**
2398
	 * Returns a list of subscriptions for a principal.
2399
	 *
2400
	 * Every subscription is an array with the following keys:
2401
	 *  * id, a unique id that will be used by other functions to modify the
2402
	 *    subscription. This can be the same as the uri or a database key.
2403
	 *  * uri. This is just the 'base uri' or 'filename' of the subscription.
2404
	 *  * principaluri. The owner of the subscription. Almost always the same as
2405
	 *    principalUri passed to this method.
2406
	 *
2407
	 * Furthermore, all the subscription info must be returned too:
2408
	 *
2409
	 * 1. {DAV:}displayname
2410
	 * 2. {http://apple.com/ns/ical/}refreshrate
2411
	 * 3. {http://calendarserver.org/ns/}subscribed-strip-todos (omit if todos
2412
	 *    should not be stripped).
2413
	 * 4. {http://calendarserver.org/ns/}subscribed-strip-alarms (omit if alarms
2414
	 *    should not be stripped).
2415
	 * 5. {http://calendarserver.org/ns/}subscribed-strip-attachments (omit if
2416
	 *    attachments should not be stripped).
2417
	 * 6. {http://calendarserver.org/ns/}source (Must be a
2418
	 *     Sabre\DAV\Property\Href).
2419
	 * 7. {http://apple.com/ns/ical/}calendar-color
2420
	 * 8. {http://apple.com/ns/ical/}calendar-order
2421
	 * 9. {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
2422
	 *    (should just be an instance of
2423
	 *    Sabre\CalDAV\Property\SupportedCalendarComponentSet, with a bunch of
2424
	 *    default components).
2425
	 *
2426
	 * @param string $principalUri
2427
	 * @return array
2428
	 */
2429
	public function getSubscriptionsForUser($principalUri) {
2430
		$fields = array_column($this->subscriptionPropertyMap, 0);
2431
		$fields[] = 'id';
2432
		$fields[] = 'uri';
2433
		$fields[] = 'source';
2434
		$fields[] = 'principaluri';
2435
		$fields[] = 'lastmodified';
2436
		$fields[] = 'synctoken';
2437
2438
		$query = $this->db->getQueryBuilder();
2439
		$query->select($fields)
2440
			->from('calendarsubscriptions')
2441
			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
2442
			->orderBy('calendarorder', 'asc');
2443
		$stmt = $query->executeQuery();
2444
2445
		$subscriptions = [];
2446
		while ($row = $stmt->fetch()) {
2447
			$subscription = [
2448
				'id' => $row['id'],
2449
				'uri' => $row['uri'],
2450
				'principaluri' => $row['principaluri'],
2451
				'source' => $row['source'],
2452
				'lastmodified' => $row['lastmodified'],
2453
2454
				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
2455
				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
2456
			];
2457
2458
			$subscriptions[] = $this->rowToSubscription($row, $subscription);
2459
		}
2460
2461
		return $subscriptions;
2462
	}
2463
2464
	/**
2465
	 * Creates a new subscription for a principal.
2466
	 *
2467
	 * If the creation was a success, an id must be returned that can be used to reference
2468
	 * this subscription in other methods, such as updateSubscription.
2469
	 *
2470
	 * @param string $principalUri
2471
	 * @param string $uri
2472
	 * @param array $properties
2473
	 * @return mixed
2474
	 */
2475
	public function createSubscription($principalUri, $uri, array $properties) {
2476
		if (!isset($properties['{http://calendarserver.org/ns/}source'])) {
2477
			throw new Forbidden('The {http://calendarserver.org/ns/}source property is required when creating subscriptions');
2478
		}
2479
2480
		$values = [
2481
			'principaluri' => $principalUri,
2482
			'uri' => $uri,
2483
			'source' => $properties['{http://calendarserver.org/ns/}source']->getHref(),
2484
			'lastmodified' => time(),
2485
		];
2486
2487
		$propertiesBoolean = ['striptodos', 'stripalarms', 'stripattachments'];
2488
2489
		foreach ($this->subscriptionPropertyMap as $xmlName => [$dbName, $type]) {
2490
			if (array_key_exists($xmlName, $properties)) {
2491
				$values[$dbName] = $properties[$xmlName];
2492
				if (in_array($dbName, $propertiesBoolean)) {
2493
					$values[$dbName] = true;
2494
				}
2495
			}
2496
		}
2497
2498
		$valuesToInsert = [];
2499
2500
		$query = $this->db->getQueryBuilder();
2501
2502
		foreach (array_keys($values) as $name) {
2503
			$valuesToInsert[$name] = $query->createNamedParameter($values[$name]);
2504
		}
2505
2506
		$query->insert('calendarsubscriptions')
2507
			->values($valuesToInsert)
2508
			->executeStatement();
2509
2510
		$subscriptionId = $query->getLastInsertId();
2511
2512
		$subscriptionRow = $this->getSubscriptionById($subscriptionId);
2513
		$this->dispatcher->dispatchTyped(new SubscriptionCreatedEvent($subscriptionId, $subscriptionRow));
2514
		$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createSubscription', new GenericEvent(
0 ignored issues
show
Bug introduced by
'\OCA\DAV\CalDAV\CalDavB...nd::createSubscription' of type string is incompatible with the type object expected by parameter $event of Symfony\Contracts\EventD...erInterface::dispatch(). ( Ignorable by Annotation )

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

2514
		$this->legacyDispatcher->dispatch(/** @scrutinizer ignore-type */ '\OCA\DAV\CalDAV\CalDavBackend::createSubscription', new GenericEvent(
Loading history...
Unused Code introduced by
The call to Symfony\Contracts\EventD...erInterface::dispatch() has too many arguments starting with new Symfony\Component\Ev...' => $subscriptionRow)). ( Ignorable by Annotation )

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

2514
		$this->legacyDispatcher->/** @scrutinizer ignore-call */ 
2515
                           dispatch('\OCA\DAV\CalDAV\CalDavBackend::createSubscription', new GenericEvent(

This check compares calls to functions or methods with their respective definitions. If the call has more 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...
2515
			'\OCA\DAV\CalDAV\CalDavBackend::createSubscription',
2516
			[
2517
				'subscriptionId' => $subscriptionId,
2518
				'subscriptionData' => $subscriptionRow,
2519
			]));
2520
2521
		return $subscriptionId;
2522
	}
2523
2524
	/**
2525
	 * Updates a subscription
2526
	 *
2527
	 * The list of mutations is stored in a Sabre\DAV\PropPatch object.
2528
	 * To do the actual updates, you must tell this object which properties
2529
	 * you're going to process with the handle() method.
2530
	 *
2531
	 * Calling the handle method is like telling the PropPatch object "I
2532
	 * promise I can handle updating this property".
2533
	 *
2534
	 * Read the PropPatch documentation for more info and examples.
2535
	 *
2536
	 * @param mixed $subscriptionId
2537
	 * @param PropPatch $propPatch
2538
	 * @return void
2539
	 */
2540
	public function updateSubscription($subscriptionId, PropPatch $propPatch) {
2541
		$supportedProperties = array_keys($this->subscriptionPropertyMap);
2542
		$supportedProperties[] = '{http://calendarserver.org/ns/}source';
2543
2544
		$propPatch->handle($supportedProperties, function ($mutations) use ($subscriptionId) {
2545
			$newValues = [];
2546
2547
			foreach ($mutations as $propertyName => $propertyValue) {
2548
				if ($propertyName === '{http://calendarserver.org/ns/}source') {
2549
					$newValues['source'] = $propertyValue->getHref();
2550
				} else {
2551
					$fieldName = $this->subscriptionPropertyMap[$propertyName][0];
2552
					$newValues[$fieldName] = $propertyValue;
2553
				}
2554
			}
2555
2556
			$query = $this->db->getQueryBuilder();
2557
			$query->update('calendarsubscriptions')
2558
				->set('lastmodified', $query->createNamedParameter(time()));
2559
			foreach ($newValues as $fieldName => $value) {
2560
				$query->set($fieldName, $query->createNamedParameter($value));
2561
			}
2562
			$query->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
2563
				->executeStatement();
2564
2565
			$subscriptionRow = $this->getSubscriptionById($subscriptionId);
2566
			$this->dispatcher->dispatchTyped(new SubscriptionUpdatedEvent((int)$subscriptionId, $subscriptionRow, [], $mutations));
2567
			$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateSubscription', new GenericEvent(
0 ignored issues
show
Bug introduced by
'\OCA\DAV\CalDAV\CalDavB...nd::updateSubscription' of type string is incompatible with the type object expected by parameter $event of Symfony\Contracts\EventD...erInterface::dispatch(). ( Ignorable by Annotation )

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

2567
			$this->legacyDispatcher->dispatch(/** @scrutinizer ignore-type */ '\OCA\DAV\CalDAV\CalDavBackend::updateSubscription', new GenericEvent(
Loading history...
Unused Code introduced by
The call to Symfony\Contracts\EventD...erInterface::dispatch() has too many arguments starting with new Symfony\Component\Ev...ations' => $mutations)). ( Ignorable by Annotation )

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

2567
			$this->legacyDispatcher->/** @scrutinizer ignore-call */ 
2568
                            dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateSubscription', new GenericEvent(

This check compares calls to functions or methods with their respective definitions. If the call has more 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...
2568
				'\OCA\DAV\CalDAV\CalDavBackend::updateSubscription',
2569
				[
2570
					'subscriptionId' => $subscriptionId,
2571
					'subscriptionData' => $subscriptionRow,
2572
					'propertyMutations' => $mutations,
2573
				]));
2574
2575
			return true;
2576
		});
2577
	}
2578
2579
	/**
2580
	 * Deletes a subscription.
2581
	 *
2582
	 * @param mixed $subscriptionId
2583
	 * @return void
2584
	 */
2585
	public function deleteSubscription($subscriptionId) {
2586
		$subscriptionRow = $this->getSubscriptionById($subscriptionId);
2587
2588
		$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteSubscription', new GenericEvent(
0 ignored issues
show
Bug introduced by
'\OCA\DAV\CalDAV\CalDavB...nd::deleteSubscription' of type string is incompatible with the type object expected by parameter $event of Symfony\Contracts\EventD...erInterface::dispatch(). ( Ignorable by Annotation )

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

2588
		$this->legacyDispatcher->dispatch(/** @scrutinizer ignore-type */ '\OCA\DAV\CalDAV\CalDavBackend::deleteSubscription', new GenericEvent(
Loading history...
Unused Code introduced by
The call to Symfony\Contracts\EventD...erInterface::dispatch() has too many arguments starting with new Symfony\Component\Ev...ById($subscriptionId))). ( Ignorable by Annotation )

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

2588
		$this->legacyDispatcher->/** @scrutinizer ignore-call */ 
2589
                           dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteSubscription', new GenericEvent(

This check compares calls to functions or methods with their respective definitions. If the call has more 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...
2589
			'\OCA\DAV\CalDAV\CalDavBackend::deleteSubscription',
2590
			[
2591
				'subscriptionId' => $subscriptionId,
2592
				'subscriptionData' => $this->getSubscriptionById($subscriptionId),
2593
			]));
2594
2595
		$query = $this->db->getQueryBuilder();
2596
		$query->delete('calendarsubscriptions')
2597
			->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
2598
			->executeStatement();
2599
2600
		$query = $this->db->getQueryBuilder();
2601
		$query->delete('calendarobjects')
2602
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2603
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2604
			->executeStatement();
2605
2606
		$query->delete('calendarchanges')
2607
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2608
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2609
			->executeStatement();
2610
2611
		$query->delete($this->dbObjectPropertiesTable)
2612
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2613
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2614
			->executeStatement();
2615
2616
		if ($subscriptionRow) {
2617
			$this->dispatcher->dispatchTyped(new SubscriptionDeletedEvent((int)$subscriptionId, $subscriptionRow, []));
2618
		}
2619
	}
2620
2621
	/**
2622
	 * Returns a single scheduling object for the inbox collection.
2623
	 *
2624
	 * The returned array should contain the following elements:
2625
	 *   * uri - A unique basename for the object. This will be used to
2626
	 *           construct a full uri.
2627
	 *   * calendardata - The iCalendar object
2628
	 *   * lastmodified - The last modification date. Can be an int for a unix
2629
	 *                    timestamp, or a PHP DateTime object.
2630
	 *   * etag - A unique token that must change if the object changed.
2631
	 *   * size - The size of the object, in bytes.
2632
	 *
2633
	 * @param string $principalUri
2634
	 * @param string $objectUri
2635
	 * @return array
2636
	 */
2637
	public function getSchedulingObject($principalUri, $objectUri) {
2638
		$query = $this->db->getQueryBuilder();
2639
		$stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size'])
2640
			->from('schedulingobjects')
2641
			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
2642
			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
2643
			->executeQuery();
2644
2645
		$row = $stmt->fetch();
2646
2647
		if (!$row) {
2648
			return null;
2649
		}
2650
2651
		return [
2652
			'uri' => $row['uri'],
2653
			'calendardata' => $row['calendardata'],
2654
			'lastmodified' => $row['lastmodified'],
2655
			'etag' => '"' . $row['etag'] . '"',
2656
			'size' => (int)$row['size'],
2657
		];
2658
	}
2659
2660
	/**
2661
	 * Returns all scheduling objects for the inbox collection.
2662
	 *
2663
	 * These objects should be returned as an array. Every item in the array
2664
	 * should follow the same structure as returned from getSchedulingObject.
2665
	 *
2666
	 * The main difference is that 'calendardata' is optional.
2667
	 *
2668
	 * @param string $principalUri
2669
	 * @return array
2670
	 */
2671
	public function getSchedulingObjects($principalUri) {
2672
		$query = $this->db->getQueryBuilder();
2673
		$stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size'])
2674
				->from('schedulingobjects')
2675
				->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
2676
				->executeQuery();
2677
2678
		$result = [];
2679
		foreach ($stmt->fetchAll() as $row) {
2680
			$result[] = [
2681
				'calendardata' => $row['calendardata'],
2682
				'uri' => $row['uri'],
2683
				'lastmodified' => $row['lastmodified'],
2684
				'etag' => '"' . $row['etag'] . '"',
2685
				'size' => (int)$row['size'],
2686
			];
2687
		}
2688
		$stmt->closeCursor();
2689
2690
		return $result;
2691
	}
2692
2693
	/**
2694
	 * Deletes a scheduling object from the inbox collection.
2695
	 *
2696
	 * @param string $principalUri
2697
	 * @param string $objectUri
2698
	 * @return void
2699
	 */
2700
	public function deleteSchedulingObject($principalUri, $objectUri) {
2701
		$query = $this->db->getQueryBuilder();
2702
		$query->delete('schedulingobjects')
2703
				->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
2704
				->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
2705
				->executeStatement();
2706
	}
2707
2708
	/**
2709
	 * Creates a new scheduling object. This should land in a users' inbox.
2710
	 *
2711
	 * @param string $principalUri
2712
	 * @param string $objectUri
2713
	 * @param string $objectData
2714
	 * @return void
2715
	 */
2716
	public function createSchedulingObject($principalUri, $objectUri, $objectData) {
2717
		$query = $this->db->getQueryBuilder();
2718
		$query->insert('schedulingobjects')
2719
			->values([
2720
				'principaluri' => $query->createNamedParameter($principalUri),
2721
				'calendardata' => $query->createNamedParameter($objectData, IQueryBuilder::PARAM_LOB),
2722
				'uri' => $query->createNamedParameter($objectUri),
2723
				'lastmodified' => $query->createNamedParameter(time()),
2724
				'etag' => $query->createNamedParameter(md5($objectData)),
2725
				'size' => $query->createNamedParameter(strlen($objectData))
2726
			])
2727
			->executeStatement();
2728
	}
2729
2730
	/**
2731
	 * Adds a change record to the calendarchanges table.
2732
	 *
2733
	 * @param mixed $calendarId
2734
	 * @param string $objectUri
2735
	 * @param int $operation 1 = add, 2 = modify, 3 = delete.
2736
	 * @param int $calendarType
2737
	 * @return void
2738
	 */
2739
	protected function addChange($calendarId, $objectUri, $operation, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
2740
		$table = $calendarType === self::CALENDAR_TYPE_CALENDAR ? 'calendars': 'calendarsubscriptions';
2741
2742
		$query = $this->db->getQueryBuilder();
2743
		$query->select('synctoken')
2744
			->from($table)
2745
			->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)));
2746
		$result = $query->executeQuery();
2747
		$syncToken = (int)$result->fetchOne();
2748
		$result->closeCursor();
2749
2750
		$query = $this->db->getQueryBuilder();
2751
		$query->insert('calendarchanges')
2752
			->values([
2753
				'uri' => $query->createNamedParameter($objectUri),
2754
				'synctoken' => $query->createNamedParameter($syncToken),
2755
				'calendarid' => $query->createNamedParameter($calendarId),
2756
				'operation' => $query->createNamedParameter($operation),
2757
				'calendartype' => $query->createNamedParameter($calendarType),
2758
			])
2759
			->executeStatement();
2760
2761
		$stmt = $this->db->prepare("UPDATE `*PREFIX*$table` SET `synctoken` = `synctoken` + 1 WHERE `id` = ?");
2762
		$stmt->execute([
2763
			$calendarId
2764
		]);
2765
	}
2766
2767
	/**
2768
	 * Parses some information from calendar objects, used for optimized
2769
	 * calendar-queries.
2770
	 *
2771
	 * Returns an array with the following keys:
2772
	 *   * etag - An md5 checksum of the object without the quotes.
2773
	 *   * size - Size of the object in bytes
2774
	 *   * componentType - VEVENT, VTODO or VJOURNAL
2775
	 *   * firstOccurence
2776
	 *   * lastOccurence
2777
	 *   * uid - value of the UID property
2778
	 *
2779
	 * @param string $calendarData
2780
	 * @return array
2781
	 */
2782
	public function getDenormalizedData($calendarData) {
2783
		$vObject = Reader::read($calendarData);
2784
		$vEvents = [];
2785
		$componentType = null;
2786
		$component = null;
2787
		$firstOccurrence = null;
2788
		$lastOccurrence = null;
2789
		$uid = null;
2790
		$classification = self::CLASSIFICATION_PUBLIC;
2791
		$hasDTSTART = false;
2792
		foreach ($vObject->getComponents() as $component) {
2793
			if ($component->name !== 'VTIMEZONE') {
2794
				// Finding all VEVENTs, and track them
2795
				if ($component->name === 'VEVENT') {
2796
					array_push($vEvents, $component);
2797
					if ($component->DTSTART) {
2798
						$hasDTSTART = true;
2799
					}
2800
				}
2801
				// Track first component type and uid
2802
				if ($uid === null) {
2803
					$componentType = $component->name;
2804
					$uid = (string)$component->UID;
2805
				}
2806
			}
2807
		}
2808
		if (!$componentType) {
2809
			throw new BadRequest('Calendar objects must have a VJOURNAL, VEVENT or VTODO component');
2810
		}
2811
2812
		if ($hasDTSTART) {
2813
			$component = $vEvents[0];
2814
2815
			// Finding the last occurrence is a bit harder
2816
			if (!isset($component->RRULE) && count($vEvents) === 1) {
2817
				$firstOccurrence = $component->DTSTART->getDateTime()->getTimeStamp();
2818
				if (isset($component->DTEND)) {
2819
					$lastOccurrence = $component->DTEND->getDateTime()->getTimeStamp();
2820
				} elseif (isset($component->DURATION)) {
2821
					$endDate = clone $component->DTSTART->getDateTime();
2822
					$endDate->add(DateTimeParser::parse($component->DURATION->getValue()));
2823
					$lastOccurrence = $endDate->getTimeStamp();
2824
				} elseif (!$component->DTSTART->hasTime()) {
2825
					$endDate = clone $component->DTSTART->getDateTime();
2826
					$endDate->modify('+1 day');
2827
					$lastOccurrence = $endDate->getTimeStamp();
2828
				} else {
2829
					$lastOccurrence = $firstOccurrence;
2830
				}
2831
			} else {
2832
				$it = new EventIterator($vEvents);
2833
				$maxDate = new DateTime(self::MAX_DATE);
2834
				$firstOccurrence = $it->getDtStart()->getTimestamp();
2835
				if ($it->isInfinite()) {
2836
					$lastOccurrence = $maxDate->getTimestamp();
2837
				} else {
2838
					$end = $it->getDtEnd();
2839
					while ($it->valid() && $end < $maxDate) {
2840
						$end = $it->getDtEnd();
2841
						$it->next();
2842
					}
2843
					$lastOccurrence = $end->getTimestamp();
2844
				}
2845
			}
2846
		}
2847
2848
		if ($component->CLASS) {
2849
			$classification = CalDavBackend::CLASSIFICATION_PRIVATE;
2850
			switch ($component->CLASS->getValue()) {
2851
				case 'PUBLIC':
2852
					$classification = CalDavBackend::CLASSIFICATION_PUBLIC;
2853
					break;
2854
				case 'CONFIDENTIAL':
2855
					$classification = CalDavBackend::CLASSIFICATION_CONFIDENTIAL;
2856
					break;
2857
			}
2858
		}
2859
		return [
2860
			'etag' => md5($calendarData),
2861
			'size' => strlen($calendarData),
2862
			'componentType' => $componentType,
2863
			'firstOccurence' => is_null($firstOccurrence) ? null : max(0, $firstOccurrence),
2864
			'lastOccurence' => $lastOccurrence,
2865
			'uid' => $uid,
2866
			'classification' => $classification
2867
		];
2868
	}
2869
2870
	/**
2871
	 * @param $cardData
2872
	 * @return bool|string
2873
	 */
2874
	private function readBlob($cardData) {
2875
		if (is_resource($cardData)) {
2876
			return stream_get_contents($cardData);
2877
		}
2878
2879
		return $cardData;
2880
	}
2881
2882
	/**
2883
	 * @param IShareable $shareable
2884
	 * @param array $add
2885
	 * @param array $remove
2886
	 */
2887
	public function updateShares($shareable, $add, $remove) {
2888
		$calendarId = $shareable->getResourceId();
2889
		$calendarRow = $this->getCalendarById($calendarId);
2890
		$oldShares = $this->getShares($calendarId);
2891
2892
		$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateShares', new GenericEvent(
0 ignored issues
show
Unused Code introduced by
The call to Symfony\Contracts\EventD...erInterface::dispatch() has too many arguments starting with new Symfony\Component\Ev..., 'remove' => $remove)). ( Ignorable by Annotation )

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

2892
		$this->legacyDispatcher->/** @scrutinizer ignore-call */ 
2893
                           dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateShares', new GenericEvent(

This check compares calls to functions or methods with their respective definitions. If the call has more 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...
Bug introduced by
'\OCA\DAV\CalDAV\CalDavBackend::updateShares' of type string is incompatible with the type object expected by parameter $event of Symfony\Contracts\EventD...erInterface::dispatch(). ( Ignorable by Annotation )

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

2892
		$this->legacyDispatcher->dispatch(/** @scrutinizer ignore-type */ '\OCA\DAV\CalDAV\CalDavBackend::updateShares', new GenericEvent(
Loading history...
2893
			'\OCA\DAV\CalDAV\CalDavBackend::updateShares',
2894
			[
2895
				'calendarId' => $calendarId,
2896
				'calendarData' => $calendarRow,
2897
				'shares' => $oldShares,
2898
				'add' => $add,
2899
				'remove' => $remove,
2900
			]));
2901
		$this->calendarSharingBackend->updateShares($shareable, $add, $remove);
2902
2903
		$this->dispatcher->dispatchTyped(new CalendarShareUpdatedEvent((int)$calendarId, $calendarRow, $oldShares, $add, $remove));
2904
	}
2905
2906
	/**
2907
	 * @param int $resourceId
2908
	 * @return array
2909
	 */
2910
	public function getShares($resourceId) {
2911
		return $this->calendarSharingBackend->getShares($resourceId);
2912
	}
2913
2914
	/**
2915
	 * @param boolean $value
2916
	 * @param \OCA\DAV\CalDAV\Calendar $calendar
2917
	 * @return string|null
2918
	 */
2919
	public function setPublishStatus($value, $calendar) {
2920
		$calendarId = $calendar->getResourceId();
2921
		$calendarData = $this->getCalendarById($calendarId);
2922
		$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::publishCalendar', new GenericEvent(
0 ignored issues
show
Bug introduced by
'\OCA\DAV\CalDAV\CalDavBackend::publishCalendar' of type string is incompatible with the type object expected by parameter $event of Symfony\Contracts\EventD...erInterface::dispatch(). ( Ignorable by Annotation )

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

2922
		$this->legacyDispatcher->dispatch(/** @scrutinizer ignore-type */ '\OCA\DAV\CalDAV\CalDavBackend::publishCalendar', new GenericEvent(
Loading history...
Unused Code introduced by
The call to Symfony\Contracts\EventD...erInterface::dispatch() has too many arguments starting with new Symfony\Component\Ev...a, 'public' => $value)). ( Ignorable by Annotation )

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

2922
		$this->legacyDispatcher->/** @scrutinizer ignore-call */ 
2923
                           dispatch('\OCA\DAV\CalDAV\CalDavBackend::publishCalendar', new GenericEvent(

This check compares calls to functions or methods with their respective definitions. If the call has more 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...
2923
			'\OCA\DAV\CalDAV\CalDavBackend::updateShares',
2924
			[
2925
				'calendarId' => $calendarId,
2926
				'calendarData' => $calendarData,
2927
				'public' => $value,
2928
			]));
2929
2930
		$query = $this->db->getQueryBuilder();
2931
		if ($value) {
2932
			$publicUri = $this->random->generate(16, ISecureRandom::CHAR_HUMAN_READABLE);
2933
			$query->insert('dav_shares')
2934
				->values([
2935
					'principaluri' => $query->createNamedParameter($calendar->getPrincipalURI()),
2936
					'type' => $query->createNamedParameter('calendar'),
2937
					'access' => $query->createNamedParameter(self::ACCESS_PUBLIC),
2938
					'resourceid' => $query->createNamedParameter($calendar->getResourceId()),
2939
					'publicuri' => $query->createNamedParameter($publicUri)
2940
				]);
2941
			$query->executeStatement();
2942
2943
			$this->dispatcher->dispatchTyped(new CalendarPublishedEvent((int)$calendarId, $calendarData, $publicUri));
2944
			return $publicUri;
2945
		}
2946
		$query->delete('dav_shares')
2947
			->where($query->expr()->eq('resourceid', $query->createNamedParameter($calendar->getResourceId())))
2948
			->andWhere($query->expr()->eq('access', $query->createNamedParameter(self::ACCESS_PUBLIC)));
2949
		$query->executeStatement();
2950
2951
		$this->dispatcher->dispatchTyped(new CalendarUnpublishedEvent((int)$calendarId, $calendarData));
2952
		return null;
2953
	}
2954
2955
	/**
2956
	 * @param \OCA\DAV\CalDAV\Calendar $calendar
2957
	 * @return mixed
2958
	 */
2959
	public function getPublishStatus($calendar) {
2960
		$query = $this->db->getQueryBuilder();
2961
		$result = $query->select('publicuri')
2962
			->from('dav_shares')
2963
			->where($query->expr()->eq('resourceid', $query->createNamedParameter($calendar->getResourceId())))
2964
			->andWhere($query->expr()->eq('access', $query->createNamedParameter(self::ACCESS_PUBLIC)))
2965
			->executeQuery();
2966
2967
		$row = $result->fetch();
2968
		$result->closeCursor();
2969
		return $row ? reset($row) : false;
2970
	}
2971
2972
	/**
2973
	 * @param int $resourceId
2974
	 * @param array $acl
2975
	 * @return array
2976
	 */
2977
	public function applyShareAcl($resourceId, $acl) {
2978
		return $this->calendarSharingBackend->applyShareAcl($resourceId, $acl);
2979
	}
2980
2981
2982
2983
	/**
2984
	 * update properties table
2985
	 *
2986
	 * @param int $calendarId
2987
	 * @param string $objectUri
2988
	 * @param string $calendarData
2989
	 * @param int $calendarType
2990
	 */
2991
	public function updateProperties($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
2992
		$objectId = $this->getCalendarObjectId($calendarId, $objectUri, $calendarType);
2993
2994
		try {
2995
			$vCalendar = $this->readCalendarData($calendarData);
2996
		} catch (\Exception $ex) {
2997
			return;
2998
		}
2999
3000
		$this->purgeProperties($calendarId, $objectId);
3001
3002
		$query = $this->db->getQueryBuilder();
3003
		$query->insert($this->dbObjectPropertiesTable)
3004
			->values(
3005
				[
3006
					'calendarid' => $query->createNamedParameter($calendarId),
3007
					'calendartype' => $query->createNamedParameter($calendarType),
3008
					'objectid' => $query->createNamedParameter($objectId),
3009
					'name' => $query->createParameter('name'),
3010
					'parameter' => $query->createParameter('parameter'),
3011
					'value' => $query->createParameter('value'),
3012
				]
3013
			);
3014
3015
		$indexComponents = ['VEVENT', 'VJOURNAL', 'VTODO'];
3016
		foreach ($vCalendar->getComponents() as $component) {
3017
			if (!in_array($component->name, $indexComponents)) {
3018
				continue;
3019
			}
3020
3021
			foreach ($component->children() as $property) {
3022
				if (in_array($property->name, self::INDEXED_PROPERTIES, true)) {
3023
					$value = $property->getValue();
3024
					// is this a shitty db?
3025
					if (!$this->db->supports4ByteText()) {
3026
						$value = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $value);
3027
					}
3028
					$value = mb_strcut($value, 0, 254);
3029
3030
					$query->setParameter('name', $property->name);
3031
					$query->setParameter('parameter', null);
3032
					$query->setParameter('value', $value);
3033
					$query->executeStatement();
3034
				}
3035
3036
				if (array_key_exists($property->name, self::$indexParameters)) {
3037
					$parameters = $property->parameters();
3038
					$indexedParametersForProperty = self::$indexParameters[$property->name];
3039
3040
					foreach ($parameters as $key => $value) {
3041
						if (in_array($key, $indexedParametersForProperty)) {
3042
							// is this a shitty db?
3043
							if ($this->db->supports4ByteText()) {
3044
								$value = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $value);
3045
							}
3046
3047
							$query->setParameter('name', $property->name);
3048
							$query->setParameter('parameter', mb_strcut($key, 0, 254));
3049
							$query->setParameter('value', mb_strcut($value, 0, 254));
3050
							$query->executeStatement();
3051
						}
3052
					}
3053
				}
3054
			}
3055
		}
3056
	}
3057
3058
	/**
3059
	 * deletes all birthday calendars
3060
	 */
3061
	public function deleteAllBirthdayCalendars() {
3062
		$query = $this->db->getQueryBuilder();
3063
		$result = $query->select(['id'])->from('calendars')
3064
			->where($query->expr()->eq('uri', $query->createNamedParameter(BirthdayService::BIRTHDAY_CALENDAR_URI)))
3065
			->executeQuery();
3066
3067
		$ids = $result->fetchAll();
3068
		$result->closeCursor();
3069
		foreach ($ids as $id) {
3070
			$this->deleteCalendar(
3071
				$id['id'],
3072
				true // No data to keep in the trashbin, if the user re-enables then we regenerate
3073
			);
3074
		}
3075
	}
3076
3077
	/**
3078
	 * @param $subscriptionId
3079
	 */
3080
	public function purgeAllCachedEventsForSubscription($subscriptionId) {
3081
		$query = $this->db->getQueryBuilder();
3082
		$query->select('uri')
3083
			->from('calendarobjects')
3084
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
3085
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)));
3086
		$stmt = $query->executeQuery();
3087
3088
		$uris = [];
3089
		foreach ($stmt->fetchAll() as $row) {
3090
			$uris[] = $row['uri'];
3091
		}
3092
		$stmt->closeCursor();
3093
3094
		$query = $this->db->getQueryBuilder();
3095
		$query->delete('calendarobjects')
3096
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
3097
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
3098
			->executeStatement();
3099
3100
		$query->delete('calendarchanges')
3101
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
3102
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
3103
			->executeStatement();
3104
3105
		$query->delete($this->dbObjectPropertiesTable)
3106
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
3107
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
3108
			->executeStatement();
3109
3110
		foreach ($uris as $uri) {
3111
			$this->addChange($subscriptionId, $uri, 3, self::CALENDAR_TYPE_SUBSCRIPTION);
3112
		}
3113
	}
3114
3115
	/**
3116
	 * Move a calendar from one user to another
3117
	 *
3118
	 * @param string $uriName
3119
	 * @param string $uriOrigin
3120
	 * @param string $uriDestination
3121
	 * @param string $newUriName (optional) the new uriName
3122
	 */
3123
	public function moveCalendar($uriName, $uriOrigin, $uriDestination, $newUriName = null) {
3124
		$query = $this->db->getQueryBuilder();
3125
		$query->update('calendars')
3126
			->set('principaluri', $query->createNamedParameter($uriDestination))
3127
			->set('uri', $query->createNamedParameter($newUriName ?: $uriName))
3128
			->where($query->expr()->eq('principaluri', $query->createNamedParameter($uriOrigin)))
3129
			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($uriName)))
3130
			->executeStatement();
3131
	}
3132
3133
	/**
3134
	 * read VCalendar data into a VCalendar object
3135
	 *
3136
	 * @param string $objectData
3137
	 * @return VCalendar
3138
	 */
3139
	protected function readCalendarData($objectData) {
3140
		return Reader::read($objectData);
3141
	}
3142
3143
	/**
3144
	 * delete all properties from a given calendar object
3145
	 *
3146
	 * @param int $calendarId
3147
	 * @param int $objectId
3148
	 */
3149
	protected function purgeProperties($calendarId, $objectId) {
3150
		$query = $this->db->getQueryBuilder();
3151
		$query->delete($this->dbObjectPropertiesTable)
3152
			->where($query->expr()->eq('objectid', $query->createNamedParameter($objectId)))
3153
			->andWhere($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)));
3154
		$query->executeStatement();
3155
	}
3156
3157
	/**
3158
	 * get ID from a given calendar object
3159
	 *
3160
	 * @param int $calendarId
3161
	 * @param string $uri
3162
	 * @param int $calendarType
3163
	 * @return int
3164
	 */
3165
	protected function getCalendarObjectId($calendarId, $uri, $calendarType):int {
3166
		$query = $this->db->getQueryBuilder();
3167
		$query->select('id')
3168
			->from('calendarobjects')
3169
			->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
3170
			->andWhere($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
3171
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)));
3172
3173
		$result = $query->executeQuery();
3174
		$objectIds = $result->fetch();
3175
		$result->closeCursor();
3176
3177
		if (!isset($objectIds['id'])) {
3178
			throw new \InvalidArgumentException('Calendarobject does not exists: ' . $uri);
3179
		}
3180
3181
		return (int)$objectIds['id'];
3182
	}
3183
3184
	/**
3185
	 * return legacy endpoint principal name to new principal name
3186
	 *
3187
	 * @param $principalUri
3188
	 * @param $toV2
3189
	 * @return string
3190
	 */
3191
	private function convertPrincipal($principalUri, $toV2) {
3192
		if ($this->principalBackend->getPrincipalPrefix() === 'principals') {
3193
			[, $name] = Uri\split($principalUri);
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

3193
			[, $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...
3194
			if ($toV2 === true) {
3195
				return "principals/users/$name";
3196
			}
3197
			return "principals/$name";
3198
		}
3199
		return $principalUri;
3200
	}
3201
3202
	/**
3203
	 * adds information about an owner to the calendar data
3204
	 *
3205
	 */
3206
	private function addOwnerPrincipalToCalendar(array $calendarInfo): array {
3207
		$ownerPrincipalKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal';
3208
		$displaynameKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}owner-displayname';
3209
		if (isset($calendarInfo[$ownerPrincipalKey])) {
3210
			$uri = $calendarInfo[$ownerPrincipalKey];
3211
		} else {
3212
			$uri = $calendarInfo['principaluri'];
3213
		}
3214
3215
		$principalInformation = $this->principalBackend->getPrincipalByPath($uri);
3216
		if (isset($principalInformation['{DAV:}displayname'])) {
3217
			$calendarInfo[$displaynameKey] = $principalInformation['{DAV:}displayname'];
3218
		}
3219
		return $calendarInfo;
3220
	}
3221
3222
	private function addResourceTypeToCalendar(array $row, array $calendar): array {
3223
		if (isset($row['deleted_at'])) {
3224
			// Columns is set and not null -> this is a deleted calendar
3225
			// we send a custom resourcetype to hide the deleted calendar
3226
			// from ordinary DAV clients, but the Calendar app will know
3227
			// how to handle this special resource.
3228
			$calendar['{DAV:}resourcetype'] = new DAV\Xml\Property\ResourceType([
3229
				'{DAV:}collection',
3230
				sprintf('{%s}deleted-calendar', \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD),
3231
			]);
3232
		}
3233
		return $calendar;
3234
	}
3235
3236
	/**
3237
	 * Amend the calendar info with database row data
3238
	 *
3239
	 * @param array $row
3240
	 * @param array $calendar
3241
	 *
3242
	 * @return array
3243
	 */
3244
	private function rowToCalendar($row, array $calendar): array {
3245
		foreach ($this->propertyMap as $xmlName => [$dbName, $type]) {
3246
			$value = $row[$dbName];
3247
			if ($value !== null) {
3248
				settype($value, $type);
3249
			}
3250
			$calendar[$xmlName] = $value;
3251
		}
3252
		return $calendar;
3253
	}
3254
3255
	/**
3256
	 * Amend the subscription info with database row data
3257
	 *
3258
	 * @param array $row
3259
	 * @param array $subscription
3260
	 *
3261
	 * @return array
3262
	 */
3263
	private function rowToSubscription($row, array $subscription): array {
3264
		foreach ($this->subscriptionPropertyMap as $xmlName => [$dbName, $type]) {
3265
			$value = $row[$dbName];
3266
			if ($value !== null) {
3267
				settype($value, $type);
3268
			}
3269
			$subscription[$xmlName] = $value;
3270
		}
3271
		return $subscription;
3272
	}
3273
}
3274