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

CalDavBackend::getCalendarObjects()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 25
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 20
nc 2
nop 2
dl 0
loc 25
rs 9.6
c 0
b 0
f 0
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