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

CalDavBackend::rowToSubscription()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 6
c 1
b 0
f 0
nc 3
nop 2
dl 0
loc 9
rs 10
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