Passed
Push — master ( 9e596d...9f70c6 )
by Christoph
15:44 queued 10s
created

CalDavBackend::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 20
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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

How to fix   Many Parameters   

Many Parameters

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

There are several approaches to avoid long parameter lists:

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

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

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

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

1309
			$this->legacyDispatcher->/** @scrutinizer ignore-call */ 
1310
                            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...
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

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

1374
				$this->legacyDispatcher->/** @scrutinizer ignore-call */ 
1375
                             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...
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

1374
				$this->legacyDispatcher->dispatch(/** @scrutinizer ignore-type */ '\OCA\DAV\CalDAV\CalDavBackend::updateCachedCalendarObject', new GenericEvent(
Loading history...
1375
					'\OCA\DAV\CalDAV\CalDavBackend::updateCachedCalendarObject',
1376
					[
1377
						'subscriptionId' => $calendarId,
1378
						'calendarData' => $subscriptionRow,
1379
						'shares' => [],
1380
						'objectData' => $objectRow,
1381
					]
1382
				));
1383
			}
1384
		}
1385
1386
		return '"' . $extraData['etag'] . '"';
1387
	}
1388
1389
	/**
1390
	 * @param int $calendarObjectId
1391
	 * @param int $classification
1392
	 */
1393
	public function setClassification($calendarObjectId, $classification) {
1394
		if (!in_array($classification, [
1395
			self::CLASSIFICATION_PUBLIC, self::CLASSIFICATION_PRIVATE, self::CLASSIFICATION_CONFIDENTIAL
1396
		])) {
1397
			throw new \InvalidArgumentException();
1398
		}
1399
		$query = $this->db->getQueryBuilder();
1400
		$query->update('calendarobjects')
1401
			->set('classification', $query->createNamedParameter($classification))
1402
			->where($query->expr()->eq('id', $query->createNamedParameter($calendarObjectId)))
1403
			->executeStatement();
1404
	}
1405
1406
	/**
1407
	 * Deletes an existing calendar object.
1408
	 *
1409
	 * The object uri is only the basename, or filename and not a full path.
1410
	 *
1411
	 * @param mixed $calendarId
1412
	 * @param string $objectUri
1413
	 * @param int $calendarType
1414
	 * @param bool $forceDeletePermanently
1415
	 * @return void
1416
	 */
1417
	public function deleteCalendarObject($calendarId, $objectUri, $calendarType = self::CALENDAR_TYPE_CALENDAR, bool $forceDeletePermanently = false) {
1418
		$data = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
1419
1420
		if ($data === null) {
1421
			// Nothing to delete
1422
			return;
1423
		}
1424
1425
		if ($forceDeletePermanently || $this->config->getAppValue(Application::APP_ID, RetentionService::RETENTION_CONFIG_KEY) === '0') {
1426
			$stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ? AND `uri` = ? AND `calendartype` = ?');
1427
			$stmt->execute([$calendarId, $objectUri, $calendarType]);
1428
1429
			$this->purgeProperties($calendarId, $data['id']);
1430
1431
			if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
1432
				$calendarRow = $this->getCalendarById($calendarId);
1433
				$shares = $this->getShares($calendarId);
1434
1435
				$this->dispatcher->dispatchTyped(new CalendarObjectDeletedEvent((int)$calendarId, $calendarRow, $shares, $data));
1436
			} else {
1437
				$subscriptionRow = $this->getSubscriptionById($calendarId);
1438
1439
				$this->dispatcher->dispatchTyped(new CachedCalendarObjectDeletedEvent((int)$calendarId, $subscriptionRow, [], $data));
1440
				$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteCachedCalendarObject', 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

1440
				$this->legacyDispatcher->dispatch(/** @scrutinizer ignore-type */ '\OCA\DAV\CalDAV\CalDavBackend::deleteCachedCalendarObject', 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...'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

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

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

2429
		$this->legacyDispatcher->/** @scrutinizer ignore-call */ 
2430
                           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...
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

2429
		$this->legacyDispatcher->dispatch(/** @scrutinizer ignore-type */ '\OCA\DAV\CalDAV\CalDavBackend::createSubscription', new GenericEvent(
Loading history...
2430
			'\OCA\DAV\CalDAV\CalDavBackend::createSubscription',
2431
			[
2432
				'subscriptionId' => $subscriptionId,
2433
				'subscriptionData' => $subscriptionRow,
2434
			]));
2435
2436
		return $subscriptionId;
2437
	}
2438
2439
	/**
2440
	 * Updates a subscription
2441
	 *
2442
	 * The list of mutations is stored in a Sabre\DAV\PropPatch object.
2443
	 * To do the actual updates, you must tell this object which properties
2444
	 * you're going to process with the handle() method.
2445
	 *
2446
	 * Calling the handle method is like telling the PropPatch object "I
2447
	 * promise I can handle updating this property".
2448
	 *
2449
	 * Read the PropPatch documentation for more info and examples.
2450
	 *
2451
	 * @param mixed $subscriptionId
2452
	 * @param PropPatch $propPatch
2453
	 * @return void
2454
	 */
2455
	public function updateSubscription($subscriptionId, PropPatch $propPatch) {
2456
		$supportedProperties = array_keys($this->subscriptionPropertyMap);
2457
		$supportedProperties[] = '{http://calendarserver.org/ns/}source';
2458
2459
		$propPatch->handle($supportedProperties, function ($mutations) use ($subscriptionId) {
2460
			$newValues = [];
2461
2462
			foreach ($mutations as $propertyName => $propertyValue) {
2463
				if ($propertyName === '{http://calendarserver.org/ns/}source') {
2464
					$newValues['source'] = $propertyValue->getHref();
2465
				} else {
2466
					$fieldName = $this->subscriptionPropertyMap[$propertyName];
2467
					$newValues[$fieldName] = $propertyValue;
2468
				}
2469
			}
2470
2471
			$query = $this->db->getQueryBuilder();
2472
			$query->update('calendarsubscriptions')
2473
				->set('lastmodified', $query->createNamedParameter(time()));
2474
			foreach ($newValues as $fieldName => $value) {
2475
				$query->set($fieldName, $query->createNamedParameter($value));
2476
			}
2477
			$query->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
2478
				->executeStatement();
2479
2480
			$subscriptionRow = $this->getSubscriptionById($subscriptionId);
2481
			$this->dispatcher->dispatchTyped(new SubscriptionUpdatedEvent((int)$subscriptionId, $subscriptionRow, [], $mutations));
2482
			$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

2482
			$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

2482
			$this->legacyDispatcher->/** @scrutinizer ignore-call */ 
2483
                            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...
2483
				'\OCA\DAV\CalDAV\CalDavBackend::updateSubscription',
2484
				[
2485
					'subscriptionId' => $subscriptionId,
2486
					'subscriptionData' => $subscriptionRow,
2487
					'propertyMutations' => $mutations,
2488
				]));
2489
2490
			return true;
2491
		});
2492
	}
2493
2494
	/**
2495
	 * Deletes a subscription.
2496
	 *
2497
	 * @param mixed $subscriptionId
2498
	 * @return void
2499
	 */
2500
	public function deleteSubscription($subscriptionId) {
2501
		$subscriptionRow = $this->getSubscriptionById($subscriptionId);
2502
2503
		$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

2503
		$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

2503
		$this->legacyDispatcher->/** @scrutinizer ignore-call */ 
2504
                           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...
2504
			'\OCA\DAV\CalDAV\CalDavBackend::deleteSubscription',
2505
			[
2506
				'subscriptionId' => $subscriptionId,
2507
				'subscriptionData' => $this->getSubscriptionById($subscriptionId),
2508
			]));
2509
2510
		$query = $this->db->getQueryBuilder();
2511
		$query->delete('calendarsubscriptions')
2512
			->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
2513
			->executeStatement();
2514
2515
		$query = $this->db->getQueryBuilder();
2516
		$query->delete('calendarobjects')
2517
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2518
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2519
			->executeStatement();
2520
2521
		$query->delete('calendarchanges')
2522
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2523
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2524
			->executeStatement();
2525
2526
		$query->delete($this->dbObjectPropertiesTable)
2527
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2528
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2529
			->executeStatement();
2530
2531
		if ($subscriptionRow) {
2532
			$this->dispatcher->dispatchTyped(new SubscriptionDeletedEvent((int)$subscriptionId, $subscriptionRow, []));
2533
		}
2534
	}
2535
2536
	/**
2537
	 * Returns a single scheduling object for the inbox collection.
2538
	 *
2539
	 * The returned array should contain the following elements:
2540
	 *   * uri - A unique basename for the object. This will be used to
2541
	 *           construct a full uri.
2542
	 *   * calendardata - The iCalendar object
2543
	 *   * lastmodified - The last modification date. Can be an int for a unix
2544
	 *                    timestamp, or a PHP DateTime object.
2545
	 *   * etag - A unique token that must change if the object changed.
2546
	 *   * size - The size of the object, in bytes.
2547
	 *
2548
	 * @param string $principalUri
2549
	 * @param string $objectUri
2550
	 * @return array
2551
	 */
2552
	public function getSchedulingObject($principalUri, $objectUri) {
2553
		$query = $this->db->getQueryBuilder();
2554
		$stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size'])
2555
			->from('schedulingobjects')
2556
			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
2557
			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
2558
			->executeQuery();
2559
2560
		$row = $stmt->fetch();
2561
2562
		if (!$row) {
2563
			return null;
2564
		}
2565
2566
		return [
2567
			'uri' => $row['uri'],
2568
			'calendardata' => $row['calendardata'],
2569
			'lastmodified' => $row['lastmodified'],
2570
			'etag' => '"' . $row['etag'] . '"',
2571
			'size' => (int)$row['size'],
2572
		];
2573
	}
2574
2575
	/**
2576
	 * Returns all scheduling objects for the inbox collection.
2577
	 *
2578
	 * These objects should be returned as an array. Every item in the array
2579
	 * should follow the same structure as returned from getSchedulingObject.
2580
	 *
2581
	 * The main difference is that 'calendardata' is optional.
2582
	 *
2583
	 * @param string $principalUri
2584
	 * @return array
2585
	 */
2586
	public function getSchedulingObjects($principalUri) {
2587
		$query = $this->db->getQueryBuilder();
2588
		$stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size'])
2589
				->from('schedulingobjects')
2590
				->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
2591
				->executeQuery();
2592
2593
		$result = [];
2594
		foreach ($stmt->fetchAll() as $row) {
2595
			$result[] = [
2596
				'calendardata' => $row['calendardata'],
2597
				'uri' => $row['uri'],
2598
				'lastmodified' => $row['lastmodified'],
2599
				'etag' => '"' . $row['etag'] . '"',
2600
				'size' => (int)$row['size'],
2601
			];
2602
		}
2603
2604
		return $result;
2605
	}
2606
2607
	/**
2608
	 * Deletes a scheduling object from the inbox collection.
2609
	 *
2610
	 * @param string $principalUri
2611
	 * @param string $objectUri
2612
	 * @return void
2613
	 */
2614
	public function deleteSchedulingObject($principalUri, $objectUri) {
2615
		$query = $this->db->getQueryBuilder();
2616
		$query->delete('schedulingobjects')
2617
				->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
2618
				->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
2619
				->executeStatement();
2620
	}
2621
2622
	/**
2623
	 * Creates a new scheduling object. This should land in a users' inbox.
2624
	 *
2625
	 * @param string $principalUri
2626
	 * @param string $objectUri
2627
	 * @param string $objectData
2628
	 * @return void
2629
	 */
2630
	public function createSchedulingObject($principalUri, $objectUri, $objectData) {
2631
		$query = $this->db->getQueryBuilder();
2632
		$query->insert('schedulingobjects')
2633
			->values([
2634
				'principaluri' => $query->createNamedParameter($principalUri),
2635
				'calendardata' => $query->createNamedParameter($objectData, IQueryBuilder::PARAM_LOB),
2636
				'uri' => $query->createNamedParameter($objectUri),
2637
				'lastmodified' => $query->createNamedParameter(time()),
2638
				'etag' => $query->createNamedParameter(md5($objectData)),
2639
				'size' => $query->createNamedParameter(strlen($objectData))
2640
			])
2641
			->executeStatement();
2642
	}
2643
2644
	/**
2645
	 * Adds a change record to the calendarchanges table.
2646
	 *
2647
	 * @param mixed $calendarId
2648
	 * @param string $objectUri
2649
	 * @param int $operation 1 = add, 2 = modify, 3 = delete.
2650
	 * @param int $calendarType
2651
	 * @return void
2652
	 */
2653
	protected function addChange($calendarId, $objectUri, $operation, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
2654
		$table = $calendarType === self::CALENDAR_TYPE_CALENDAR ? 'calendars': 'calendarsubscriptions';
2655
2656
		$query = $this->db->getQueryBuilder();
2657
		$query->select('synctoken')
2658
			->from($table)
2659
			->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)));
2660
		$result = $query->executeQuery();
2661
		$syncToken = (int)$result->fetchOne();
2662
		$result->closeCursor();
2663
2664
		$query = $this->db->getQueryBuilder();
2665
		$query->insert('calendarchanges')
2666
			->values([
2667
				'uri' => $query->createNamedParameter($objectUri),
2668
				'synctoken' => $query->createNamedParameter($syncToken),
2669
				'calendarid' => $query->createNamedParameter($calendarId),
2670
				'operation' => $query->createNamedParameter($operation),
2671
				'calendartype' => $query->createNamedParameter($calendarType),
2672
			])
2673
			->executeStatement();
2674
2675
		$stmt = $this->db->prepare("UPDATE `*PREFIX*$table` SET `synctoken` = `synctoken` + 1 WHERE `id` = ?");
2676
		$stmt->execute([
2677
			$calendarId
2678
		]);
2679
	}
2680
2681
	/**
2682
	 * Parses some information from calendar objects, used for optimized
2683
	 * calendar-queries.
2684
	 *
2685
	 * Returns an array with the following keys:
2686
	 *   * etag - An md5 checksum of the object without the quotes.
2687
	 *   * size - Size of the object in bytes
2688
	 *   * componentType - VEVENT, VTODO or VJOURNAL
2689
	 *   * firstOccurence
2690
	 *   * lastOccurence
2691
	 *   * uid - value of the UID property
2692
	 *
2693
	 * @param string $calendarData
2694
	 * @return array
2695
	 */
2696
	public function getDenormalizedData($calendarData) {
2697
		$vObject = Reader::read($calendarData);
2698
		$vEvents = [];
2699
		$componentType = null;
2700
		$component = null;
2701
		$firstOccurrence = null;
2702
		$lastOccurrence = null;
2703
		$uid = null;
2704
		$classification = self::CLASSIFICATION_PUBLIC;
2705
		$hasDTSTART = false;
2706
		foreach ($vObject->getComponents() as $component) {
2707
			if ($component->name !== 'VTIMEZONE') {
2708
				// Finding all VEVENTs, and track them
2709
				if ($component->name === 'VEVENT') {
2710
					array_push($vEvents, $component);
2711
					if ($component->DTSTART) {
2712
						$hasDTSTART = true;
2713
					}
2714
				}
2715
				// Track first component type and uid
2716
				if ($uid === null) {
2717
					$componentType = $component->name;
2718
					$uid = (string)$component->UID;
2719
				}
2720
			}
2721
		}
2722
		if (!$componentType) {
2723
			throw new BadRequest('Calendar objects must have a VJOURNAL, VEVENT or VTODO component');
2724
		}
2725
2726
		if ($hasDTSTART) {
2727
			$component = $vEvents[0];
2728
2729
			// Finding the last occurrence is a bit harder
2730
			if (!isset($component->RRULE) && count($vEvents) === 1) {
2731
				$firstOccurrence = $component->DTSTART->getDateTime()->getTimeStamp();
2732
				if (isset($component->DTEND)) {
2733
					$lastOccurrence = $component->DTEND->getDateTime()->getTimeStamp();
2734
				} elseif (isset($component->DURATION)) {
2735
					$endDate = clone $component->DTSTART->getDateTime();
2736
					$endDate->add(DateTimeParser::parse($component->DURATION->getValue()));
2737
					$lastOccurrence = $endDate->getTimeStamp();
2738
				} elseif (!$component->DTSTART->hasTime()) {
2739
					$endDate = clone $component->DTSTART->getDateTime();
2740
					$endDate->modify('+1 day');
2741
					$lastOccurrence = $endDate->getTimeStamp();
2742
				} else {
2743
					$lastOccurrence = $firstOccurrence;
2744
				}
2745
			} else {
2746
				$it = new EventIterator($vEvents);
2747
				$maxDate = new DateTime(self::MAX_DATE);
2748
				$firstOccurrence = $it->getDtStart()->getTimestamp();
2749
				if ($it->isInfinite()) {
2750
					$lastOccurrence = $maxDate->getTimestamp();
2751
				} else {
2752
					$end = $it->getDtEnd();
2753
					while ($it->valid() && $end < $maxDate) {
2754
						$end = $it->getDtEnd();
2755
						$it->next();
2756
					}
2757
					$lastOccurrence = $end->getTimestamp();
2758
				}
2759
			}
2760
		}
2761
2762
		if ($component->CLASS) {
2763
			$classification = CalDavBackend::CLASSIFICATION_PRIVATE;
2764
			switch ($component->CLASS->getValue()) {
2765
				case 'PUBLIC':
2766
					$classification = CalDavBackend::CLASSIFICATION_PUBLIC;
2767
					break;
2768
				case 'CONFIDENTIAL':
2769
					$classification = CalDavBackend::CLASSIFICATION_CONFIDENTIAL;
2770
					break;
2771
			}
2772
		}
2773
		return [
2774
			'etag' => md5($calendarData),
2775
			'size' => strlen($calendarData),
2776
			'componentType' => $componentType,
2777
			'firstOccurence' => is_null($firstOccurrence) ? null : max(0, $firstOccurrence),
2778
			'lastOccurence' => $lastOccurrence,
2779
			'uid' => $uid,
2780
			'classification' => $classification
2781
		];
2782
	}
2783
2784
	/**
2785
	 * @param $cardData
2786
	 * @return bool|string
2787
	 */
2788
	private function readBlob($cardData) {
2789
		if (is_resource($cardData)) {
2790
			return stream_get_contents($cardData);
2791
		}
2792
2793
		return $cardData;
2794
	}
2795
2796
	/**
2797
	 * @param IShareable $shareable
2798
	 * @param array $add
2799
	 * @param array $remove
2800
	 */
2801
	public function updateShares($shareable, $add, $remove) {
2802
		$calendarId = $shareable->getResourceId();
2803
		$calendarRow = $this->getCalendarById($calendarId);
2804
		$oldShares = $this->getShares($calendarId);
2805
2806
		$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

2806
		$this->legacyDispatcher->/** @scrutinizer ignore-call */ 
2807
                           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

2806
		$this->legacyDispatcher->dispatch(/** @scrutinizer ignore-type */ '\OCA\DAV\CalDAV\CalDavBackend::updateShares', new GenericEvent(
Loading history...
2807
			'\OCA\DAV\CalDAV\CalDavBackend::updateShares',
2808
			[
2809
				'calendarId' => $calendarId,
2810
				'calendarData' => $calendarRow,
2811
				'shares' => $oldShares,
2812
				'add' => $add,
2813
				'remove' => $remove,
2814
			]));
2815
		$this->calendarSharingBackend->updateShares($shareable, $add, $remove);
2816
2817
		$this->dispatcher->dispatchTyped(new CalendarShareUpdatedEvent((int)$calendarId, $calendarRow, $oldShares, $add, $remove));
2818
	}
2819
2820
	/**
2821
	 * @param int $resourceId
2822
	 * @return array
2823
	 */
2824
	public function getShares($resourceId) {
2825
		return $this->calendarSharingBackend->getShares($resourceId);
2826
	}
2827
2828
	/**
2829
	 * @param boolean $value
2830
	 * @param \OCA\DAV\CalDAV\Calendar $calendar
2831
	 * @return string|null
2832
	 */
2833
	public function setPublishStatus($value, $calendar) {
2834
		$calendarId = $calendar->getResourceId();
2835
		$calendarData = $this->getCalendarById($calendarId);
2836
		$this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::publishCalendar', 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...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

2836
		$this->legacyDispatcher->/** @scrutinizer ignore-call */ 
2837
                           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...
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

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

3106
			[, $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...
3107
			if ($toV2 === true) {
3108
				return "principals/users/$name";
3109
			}
3110
			return "principals/$name";
3111
		}
3112
		return $principalUri;
3113
	}
3114
3115
	/**
3116
	 * adds information about an owner to the calendar data
3117
	 *
3118
	 */
3119
	private function addOwnerPrincipalToCalendar(array $calendarInfo): array {
3120
		$ownerPrincipalKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal';
3121
		$displaynameKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}owner-displayname';
3122
		if (isset($calendarInfo[$ownerPrincipalKey])) {
3123
			$uri = $calendarInfo[$ownerPrincipalKey];
3124
		} else {
3125
			$uri = $calendarInfo['principaluri'];
3126
		}
3127
3128
		$principalInformation = $this->principalBackend->getPrincipalByPath($uri);
3129
		if (isset($principalInformation['{DAV:}displayname'])) {
3130
			$calendarInfo[$displaynameKey] = $principalInformation['{DAV:}displayname'];
3131
		}
3132
		return $calendarInfo;
3133
	}
3134
3135
	private function addResourceTypeToCalendar(array $row, array $calendar): array {
3136
		if (isset($row['deleted_at'])) {
3137
			// Columns is set and not null -> this is a deleted calendar
3138
			// we send a custom resourcetype to hide the deleted calendar
3139
			// from ordinary DAV clients, but the Calendar app will know
3140
			// how to handle this special resource.
3141
			$calendar['{DAV:}resourcetype'] = new DAV\Xml\Property\ResourceType([
3142
				'{DAV:}collection',
3143
				sprintf('{%s}deleted-calendar', \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD),
3144
			]);
3145
		}
3146
		return $calendar;
3147
	}
3148
}
3149