Passed
Push — master ( cfdece...554c78 )
by Morris
12:32
created

CalDavBackend::getSchedulingObject()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 20
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 15
nc 2
nop 2
dl 0
loc 20
rs 9.7666
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 * @copyright Copyright (c) 2018 Georg Ehrke
5
 *
6
 * @author Georg Ehrke <[email protected]>
7
 * @author Joas Schilling <[email protected]>
8
 * @author Lukas Reschke <[email protected]>
9
 * @author nhirokinet <[email protected]>
10
 * @author Robin Appelman <[email protected]>
11
 * @author Roeland Jago Douma <[email protected]>
12
 * @author Stefan Weil <[email protected]>
13
 * @author Thomas Citharel <[email protected]>
14
 * @author Thomas Müller <[email protected]>
15
 *
16
 * @license AGPL-3.0
17
 *
18
 * This code is free software: you can redistribute it and/or modify
19
 * it under the terms of the GNU Affero General Public License, version 3,
20
 * as published by the Free Software Foundation.
21
 *
22
 * This program is distributed in the hope that it will be useful,
23
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
24
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25
 * GNU Affero General Public License for more details.
26
 *
27
 * You should have received a copy of the GNU Affero General Public License, version 3,
28
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
29
 *
30
 */
31
32
namespace OCA\DAV\CalDAV;
33
34
use OCA\DAV\DAV\Sharing\IShareable;
35
use OCP\DB\QueryBuilder\IQueryBuilder;
36
use OCA\DAV\Connector\Sabre\Principal;
37
use OCA\DAV\DAV\Sharing\Backend;
38
use OCP\IDBConnection;
39
use OCP\IGroupManager;
40
use OCP\ILogger;
41
use OCP\IUser;
42
use OCP\IUserManager;
43
use OCP\Security\ISecureRandom;
44
use Sabre\CalDAV\Backend\AbstractBackend;
45
use Sabre\CalDAV\Backend\SchedulingSupport;
46
use Sabre\CalDAV\Backend\SubscriptionSupport;
47
use Sabre\CalDAV\Backend\SyncSupport;
48
use Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp;
49
use Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet;
50
use Sabre\DAV;
51
use Sabre\DAV\Exception\Forbidden;
52
use Sabre\DAV\Exception\NotFound;
53
use Sabre\DAV\PropPatch;
54
use Sabre\VObject\Component;
55
use Sabre\VObject\Component\VCalendar;
56
use Sabre\VObject\Component\VTimeZone;
57
use Sabre\VObject\DateTimeParser;
58
use Sabre\VObject\InvalidDataException;
59
use Sabre\VObject\ParseException;
60
use Sabre\VObject\Property;
61
use Sabre\VObject\Reader;
62
use Sabre\VObject\Recur\EventIterator;
63
use Sabre\Uri;
64
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
65
use Symfony\Component\EventDispatcher\GenericEvent;
66
67
/**
68
 * Class CalDavBackend
69
 *
70
 * Code is heavily inspired by https://github.com/fruux/sabre-dav/blob/master/lib/CalDAV/Backend/PDO.php
71
 *
72
 * @package OCA\DAV\CalDAV
73
 */
74
class CalDavBackend extends AbstractBackend implements SyncSupport, SubscriptionSupport, SchedulingSupport {
75
76
	const CALENDAR_TYPE_CALENDAR = 0;
77
	const CALENDAR_TYPE_SUBSCRIPTION = 1;
78
79
	const PERSONAL_CALENDAR_URI = 'personal';
80
	const PERSONAL_CALENDAR_NAME = 'Personal';
81
82
	const RESOURCE_BOOKING_CALENDAR_URI = 'calendar';
83
	const RESOURCE_BOOKING_CALENDAR_NAME = 'Calendar';
84
85
	/**
86
	 * We need to specify a max date, because we need to stop *somewhere*
87
	 *
88
	 * On 32 bit system the maximum for a signed integer is 2147483647, so
89
	 * MAX_DATE cannot be higher than date('Y-m-d', 2147483647) which results
90
	 * in 2038-01-19 to avoid problems when the date is converted
91
	 * to a unix timestamp.
92
	 */
93
	const MAX_DATE = '2038-01-01';
94
95
	const ACCESS_PUBLIC = 4;
96
	const CLASSIFICATION_PUBLIC = 0;
97
	const CLASSIFICATION_PRIVATE = 1;
98
	const CLASSIFICATION_CONFIDENTIAL = 2;
99
100
	/**
101
	 * List of CalDAV properties, and how they map to database field names
102
	 * Add your own properties by simply adding on to this array.
103
	 *
104
	 * Note that only string-based properties are supported here.
105
	 *
106
	 * @var array
107
	 */
108
	public $propertyMap = [
109
		'{DAV:}displayname'                          => 'displayname',
110
		'{urn:ietf:params:xml:ns:caldav}calendar-description' => 'description',
111
		'{urn:ietf:params:xml:ns:caldav}calendar-timezone'    => 'timezone',
112
		'{http://apple.com/ns/ical/}calendar-order'  => 'calendarorder',
113
		'{http://apple.com/ns/ical/}calendar-color'  => 'calendarcolor',
114
	];
115
116
	/**
117
	 * List of subscription properties, and how they map to database field names.
118
	 *
119
	 * @var array
120
	 */
121
	public $subscriptionPropertyMap = [
122
		'{DAV:}displayname'                                           => 'displayname',
123
		'{http://apple.com/ns/ical/}refreshrate'                      => 'refreshrate',
124
		'{http://apple.com/ns/ical/}calendar-order'                   => 'calendarorder',
125
		'{http://apple.com/ns/ical/}calendar-color'                   => 'calendarcolor',
126
		'{http://calendarserver.org/ns/}subscribed-strip-todos'       => 'striptodos',
127
		'{http://calendarserver.org/ns/}subscribed-strip-alarms'      => 'stripalarms',
128
		'{http://calendarserver.org/ns/}subscribed-strip-attachments' => 'stripattachments',
129
	];
130
131
	/** @var array properties to index */
132
	public static $indexProperties = ['CATEGORIES', 'COMMENT', 'DESCRIPTION',
133
		'LOCATION', 'RESOURCES', 'STATUS', 'SUMMARY', 'ATTENDEE', 'CONTACT',
134
		'ORGANIZER'];
135
136
	/** @var array parameters to index */
137
	public static $indexParameters = [
138
		'ATTENDEE' => ['CN'],
139
		'ORGANIZER' => ['CN'],
140
	];
141
142
	/**
143
	 * @var string[] Map of uid => display name
144
	 */
145
	protected $userDisplayNames;
146
147
	/** @var IDBConnection */
148
	private $db;
149
150
	/** @var Backend */
151
	private $calendarSharingBackend;
152
153
	/** @var Principal */
154
	private $principalBackend;
155
156
	/** @var IUserManager */
157
	private $userManager;
158
159
	/** @var ISecureRandom */
160
	private $random;
161
162
	/** @var ILogger */
163
	private $logger;
164
165
	/** @var EventDispatcherInterface */
166
	private $dispatcher;
167
168
	/** @var bool */
169
	private $legacyEndpoint;
170
171
	/** @var string */
172
	private $dbObjectPropertiesTable = 'calendarobjects_props';
173
174
	/**
175
	 * CalDavBackend constructor.
176
	 *
177
	 * @param IDBConnection $db
178
	 * @param Principal $principalBackend
179
	 * @param IUserManager $userManager
180
	 * @param IGroupManager $groupManager
181
	 * @param ISecureRandom $random
182
	 * @param ILogger $logger
183
	 * @param EventDispatcherInterface $dispatcher
184
	 * @param bool $legacyEndpoint
185
	 */
186
	public function __construct(IDBConnection $db,
187
								Principal $principalBackend,
188
								IUserManager $userManager,
189
								IGroupManager $groupManager,
190
								ISecureRandom $random,
191
								ILogger $logger,
192
								EventDispatcherInterface $dispatcher,
193
								$legacyEndpoint = false) {
194
		$this->db = $db;
195
		$this->principalBackend = $principalBackend;
196
		$this->userManager = $userManager;
197
		$this->calendarSharingBackend = new Backend($this->db, $this->userManager, $groupManager, $principalBackend, 'calendar');
198
		$this->random = $random;
199
		$this->logger = $logger;
200
		$this->dispatcher = $dispatcher;
201
		$this->legacyEndpoint = $legacyEndpoint;
202
	}
203
204
	/**
205
	 * Return the number of calendars for a principal
206
	 *
207
	 * By default this excludes the automatically generated birthday calendar
208
	 *
209
	 * @param $principalUri
210
	 * @param bool $excludeBirthday
211
	 * @return int
212
	 */
213
	public function getCalendarsForUserCount($principalUri, $excludeBirthday = true) {
214
		$principalUri = $this->convertPrincipal($principalUri, true);
215
		$query = $this->db->getQueryBuilder();
216
		$query->select($query->func()->count('*'))
217
			->from('calendars')
218
			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
219
220
		if ($excludeBirthday) {
221
			$query->andWhere($query->expr()->neq('uri', $query->createNamedParameter(BirthdayService::BIRTHDAY_CALENDAR_URI)));
222
		}
223
224
		return (int)$query->execute()->fetchColumn();
225
	}
226
227
	/**
228
	 * Returns a list of calendars for a principal.
229
	 *
230
	 * Every project is an array with the following keys:
231
	 *  * id, a unique id that will be used by other functions to modify the
232
	 *    calendar. This can be the same as the uri or a database key.
233
	 *  * uri, which the basename of the uri with which the calendar is
234
	 *    accessed.
235
	 *  * principaluri. The owner of the calendar. Almost always the same as
236
	 *    principalUri passed to this method.
237
	 *
238
	 * Furthermore it can contain webdav properties in clark notation. A very
239
	 * common one is '{DAV:}displayname'.
240
	 *
241
	 * Many clients also require:
242
	 * {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
243
	 * For this property, you can just return an instance of
244
	 * Sabre\CalDAV\Property\SupportedCalendarComponentSet.
245
	 *
246
	 * If you return {http://sabredav.org/ns}read-only and set the value to 1,
247
	 * ACL will automatically be put in read-only mode.
248
	 *
249
	 * @param string $principalUri
250
	 * @return array
251
	 */
252
	function getCalendarsForUser($principalUri) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
253
		$principalUriOriginal = $principalUri;
254
		$principalUri = $this->convertPrincipal($principalUri, true);
255
		$fields = array_values($this->propertyMap);
256
		$fields[] = 'id';
257
		$fields[] = 'uri';
258
		$fields[] = 'synctoken';
259
		$fields[] = 'components';
260
		$fields[] = 'principaluri';
261
		$fields[] = 'transparent';
262
263
		// Making fields a comma-delimited list
264
		$query = $this->db->getQueryBuilder();
265
		$query->select($fields)->from('calendars')
266
				->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
267
				->orderBy('calendarorder', 'ASC');
268
		$stmt = $query->execute();
269
270
		$calendars = [];
271
		while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
272
273
			$components = [];
274
			if ($row['components']) {
275
				$components = explode(',',$row['components']);
276
			}
277
278
			$calendar = [
279
				'id' => $row['id'],
280
				'uri' => $row['uri'],
281
				'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
282
				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
283
				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
284
				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
285
				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
286
				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($principalUri, !$this->legacyEndpoint),
287
			];
288
289
			foreach($this->propertyMap as $xmlName=>$dbName) {
290
				$calendar[$xmlName] = $row[$dbName];
291
			}
292
293
			$this->addOwnerPrincipal($calendar);
294
295
			if (!isset($calendars[$calendar['id']])) {
296
				$calendars[$calendar['id']] = $calendar;
297
			}
298
		}
299
300
		$stmt->closeCursor();
301
302
		// query for shared calendars
303
		$principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true);
304
		$principals = array_map(function($principal) {
305
			return urldecode($principal);
306
		}, $principals);
307
		$principals[]= $principalUri;
308
309
		$fields = array_values($this->propertyMap);
310
		$fields[] = 'a.id';
311
		$fields[] = 'a.uri';
312
		$fields[] = 'a.synctoken';
313
		$fields[] = 'a.components';
314
		$fields[] = 'a.principaluri';
315
		$fields[] = 'a.transparent';
316
		$fields[] = 's.access';
317
		$query = $this->db->getQueryBuilder();
318
		$result = $query->select($fields)
319
			->from('dav_shares', 's')
320
			->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
321
			->where($query->expr()->in('s.principaluri', $query->createParameter('principaluri')))
322
			->andWhere($query->expr()->eq('s.type', $query->createParameter('type')))
323
			->setParameter('type', 'calendar')
324
			->setParameter('principaluri', $principals, \Doctrine\DBAL\Connection::PARAM_STR_ARRAY)
325
			->execute();
326
327
		$readOnlyPropertyName = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only';
328
		while($row = $result->fetch()) {
329
			if ($row['principaluri'] === $principalUri) {
330
				continue;
331
			}
332
333
			$readOnly = (int) $row['access'] === Backend::ACCESS_READ;
334
			if (isset($calendars[$row['id']])) {
335
				if ($readOnly) {
336
					// New share can not have more permissions then the old one.
337
					continue;
338
				}
339
				if (isset($calendars[$row['id']][$readOnlyPropertyName]) &&
340
					$calendars[$row['id']][$readOnlyPropertyName] === 0) {
341
					// Old share is already read-write, no more permissions can be gained
342
					continue;
343
				}
344
			}
345
346
			list(, $name) = Uri\split($row['principaluri']);
0 ignored issues
show
Deprecated Code introduced by
The function split() has been deprecated: 5.3.0 Use preg_split() instead ( Ignorable by Annotation )

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

346
			list(, $name) = /** @scrutinizer ignore-deprecated */ Uri\split($row['principaluri']);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
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

346
			list(, $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...
347
			$uri = $row['uri'] . '_shared_by_' . $name;
348
			$row['displayname'] = $row['displayname'] . ' (' . $this->getUserDisplayName($name) . ')';
349
			$components = [];
350
			if ($row['components']) {
351
				$components = explode(',',$row['components']);
352
			}
353
			$calendar = [
354
				'id' => $row['id'],
355
				'uri' => $uri,
356
				'principaluri' => $this->convertPrincipal($principalUri, !$this->legacyEndpoint),
357
				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
358
				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
359
				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
360
				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp('transparent'),
361
				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
362
				$readOnlyPropertyName => $readOnly,
363
			];
364
365
			foreach($this->propertyMap as $xmlName=>$dbName) {
366
				$calendar[$xmlName] = $row[$dbName];
367
			}
368
369
			$this->addOwnerPrincipal($calendar);
370
371
			$calendars[$calendar['id']] = $calendar;
372
		}
373
		$result->closeCursor();
374
375
		return array_values($calendars);
376
	}
377
378
	/**
379
	 * @param $principalUri
380
	 * @return array
381
	 */
382
	public function getUsersOwnCalendars($principalUri) {
383
		$principalUri = $this->convertPrincipal($principalUri, true);
384
		$fields = array_values($this->propertyMap);
385
		$fields[] = 'id';
386
		$fields[] = 'uri';
387
		$fields[] = 'synctoken';
388
		$fields[] = 'components';
389
		$fields[] = 'principaluri';
390
		$fields[] = 'transparent';
391
		// Making fields a comma-delimited list
392
		$query = $this->db->getQueryBuilder();
393
		$query->select($fields)->from('calendars')
394
			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
395
			->orderBy('calendarorder', 'ASC');
396
		$stmt = $query->execute();
397
		$calendars = [];
398
		while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
399
			$components = [];
400
			if ($row['components']) {
401
				$components = explode(',',$row['components']);
402
			}
403
			$calendar = [
404
				'id' => $row['id'],
405
				'uri' => $row['uri'],
406
				'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
407
				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
408
				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
409
				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
410
				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
411
			];
412
			foreach($this->propertyMap as $xmlName=>$dbName) {
413
				$calendar[$xmlName] = $row[$dbName];
414
			}
415
416
			$this->addOwnerPrincipal($calendar);
417
418
			if (!isset($calendars[$calendar['id']])) {
419
				$calendars[$calendar['id']] = $calendar;
420
			}
421
		}
422
		$stmt->closeCursor();
423
		return array_values($calendars);
424
	}
425
426
427
	/**
428
	 * @param $uid
429
	 * @return string
430
	 */
431
	private function getUserDisplayName($uid) {
432
		if (!isset($this->userDisplayNames[$uid])) {
433
			$user = $this->userManager->get($uid);
434
435
			if ($user instanceof IUser) {
436
				$this->userDisplayNames[$uid] = $user->getDisplayName();
437
			} else {
438
				$this->userDisplayNames[$uid] = $uid;
439
			}
440
		}
441
442
		return $this->userDisplayNames[$uid];
443
	}
444
	
445
	/**
446
	 * @return array
447
	 */
448
	public function getPublicCalendars() {
449
		$fields = array_values($this->propertyMap);
450
		$fields[] = 'a.id';
451
		$fields[] = 'a.uri';
452
		$fields[] = 'a.synctoken';
453
		$fields[] = 'a.components';
454
		$fields[] = 'a.principaluri';
455
		$fields[] = 'a.transparent';
456
		$fields[] = 's.access';
457
		$fields[] = 's.publicuri';
458
		$calendars = [];
459
		$query = $this->db->getQueryBuilder();
460
		$result = $query->select($fields)
461
			->from('dav_shares', 's')
462
			->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
463
			->where($query->expr()->in('s.access', $query->createNamedParameter(self::ACCESS_PUBLIC)))
464
			->andWhere($query->expr()->eq('s.type', $query->createNamedParameter('calendar')))
465
			->execute();
466
467
		while($row = $result->fetch()) {
468
			list(, $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

468
			list(, $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...
Deprecated Code introduced by
The function split() has been deprecated: 5.3.0 Use preg_split() instead ( Ignorable by Annotation )

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

468
			list(, $name) = /** @scrutinizer ignore-deprecated */ Uri\split($row['principaluri']);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
469
			$row['displayname'] = $row['displayname'] . "($name)";
470
			$components = [];
471
			if ($row['components']) {
472
				$components = explode(',',$row['components']);
473
			}
474
			$calendar = [
475
				'id' => $row['id'],
476
				'uri' => $row['publicuri'],
477
				'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
478
				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
479
				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
480
				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
481
				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
482
				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], $this->legacyEndpoint),
483
				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ,
484
				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}public' => (int)$row['access'] === self::ACCESS_PUBLIC,
485
			];
486
487
			foreach($this->propertyMap as $xmlName=>$dbName) {
488
				$calendar[$xmlName] = $row[$dbName];
489
			}
490
491
			$this->addOwnerPrincipal($calendar);
492
493
			if (!isset($calendars[$calendar['id']])) {
494
				$calendars[$calendar['id']] = $calendar;
495
			}
496
		}
497
		$result->closeCursor();
498
499
		return array_values($calendars);
500
	}
501
502
	/**
503
	 * @param string $uri
504
	 * @return array
505
	 * @throws NotFound
506
	 */
507
	public function getPublicCalendar($uri) {
508
		$fields = array_values($this->propertyMap);
509
		$fields[] = 'a.id';
510
		$fields[] = 'a.uri';
511
		$fields[] = 'a.synctoken';
512
		$fields[] = 'a.components';
513
		$fields[] = 'a.principaluri';
514
		$fields[] = 'a.transparent';
515
		$fields[] = 's.access';
516
		$fields[] = 's.publicuri';
517
		$query = $this->db->getQueryBuilder();
518
		$result = $query->select($fields)
519
			->from('dav_shares', 's')
520
			->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
521
			->where($query->expr()->in('s.access', $query->createNamedParameter(self::ACCESS_PUBLIC)))
522
			->andWhere($query->expr()->eq('s.type', $query->createNamedParameter('calendar')))
523
			->andWhere($query->expr()->eq('s.publicuri', $query->createNamedParameter($uri)))
524
			->execute();
525
526
		$row = $result->fetch(\PDO::FETCH_ASSOC);
527
528
		$result->closeCursor();
529
530
		if ($row === false) {
531
			throw new NotFound('Node with name \'' . $uri . '\' could not be found');
532
		}
533
534
		list(, $name) = Uri\split($row['principaluri']);
0 ignored issues
show
Deprecated Code introduced by
The function split() has been deprecated: 5.3.0 Use preg_split() instead ( Ignorable by Annotation )

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

534
		list(, $name) = /** @scrutinizer ignore-deprecated */ Uri\split($row['principaluri']);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
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

534
		list(, $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...
535
		$row['displayname'] = $row['displayname'] . ' ' . "($name)";
536
		$components = [];
537
		if ($row['components']) {
538
			$components = explode(',',$row['components']);
539
		}
540
		$calendar = [
541
			'id' => $row['id'],
542
			'uri' => $row['publicuri'],
543
			'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
544
			'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
545
			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
546
			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
547
			'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
548
			'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
549
			'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ,
550
			'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}public' => (int)$row['access'] === self::ACCESS_PUBLIC,
551
		];
552
553
		foreach($this->propertyMap as $xmlName=>$dbName) {
554
			$calendar[$xmlName] = $row[$dbName];
555
		}
556
557
		$this->addOwnerPrincipal($calendar);
558
559
		return $calendar;
560
561
	}
562
563
	/**
564
	 * @param string $principal
565
	 * @param string $uri
566
	 * @return array|null
567
	 */
568
	public function getCalendarByUri($principal, $uri) {
569
		$fields = array_values($this->propertyMap);
570
		$fields[] = 'id';
571
		$fields[] = 'uri';
572
		$fields[] = 'synctoken';
573
		$fields[] = 'components';
574
		$fields[] = 'principaluri';
575
		$fields[] = 'transparent';
576
577
		// Making fields a comma-delimited list
578
		$query = $this->db->getQueryBuilder();
579
		$query->select($fields)->from('calendars')
580
			->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
581
			->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($principal)))
582
			->setMaxResults(1);
583
		$stmt = $query->execute();
584
585
		$row = $stmt->fetch(\PDO::FETCH_ASSOC);
586
		$stmt->closeCursor();
587
		if ($row === false) {
588
			return null;
589
		}
590
591
		$components = [];
592
		if ($row['components']) {
593
			$components = explode(',',$row['components']);
594
		}
595
596
		$calendar = [
597
			'id' => $row['id'],
598
			'uri' => $row['uri'],
599
			'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
600
			'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
601
			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
602
			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
603
			'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
604
		];
605
606
		foreach($this->propertyMap as $xmlName=>$dbName) {
607
			$calendar[$xmlName] = $row[$dbName];
608
		}
609
610
		$this->addOwnerPrincipal($calendar);
611
612
		return $calendar;
613
	}
614
615
	/**
616
	 * @param $calendarId
617
	 * @return array|null
618
	 */
619
	public function getCalendarById($calendarId) {
620
		$fields = array_values($this->propertyMap);
621
		$fields[] = 'id';
622
		$fields[] = 'uri';
623
		$fields[] = 'synctoken';
624
		$fields[] = 'components';
625
		$fields[] = 'principaluri';
626
		$fields[] = 'transparent';
627
628
		// Making fields a comma-delimited list
629
		$query = $this->db->getQueryBuilder();
630
		$query->select($fields)->from('calendars')
631
			->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)))
632
			->setMaxResults(1);
633
		$stmt = $query->execute();
634
635
		$row = $stmt->fetch(\PDO::FETCH_ASSOC);
636
		$stmt->closeCursor();
637
		if ($row === false) {
638
			return null;
639
		}
640
641
		$components = [];
642
		if ($row['components']) {
643
			$components = explode(',',$row['components']);
644
		}
645
646
		$calendar = [
647
			'id' => $row['id'],
648
			'uri' => $row['uri'],
649
			'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
650
			'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
651
			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
652
			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
653
			'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
654
		];
655
656
		foreach($this->propertyMap as $xmlName=>$dbName) {
657
			$calendar[$xmlName] = $row[$dbName];
658
		}
659
660
		$this->addOwnerPrincipal($calendar);
661
662
		return $calendar;
663
	}
664
665
	/**
666
	 * @param $subscriptionId
667
	 */
668
	public function getSubscriptionById($subscriptionId) {
669
		$fields = array_values($this->subscriptionPropertyMap);
670
		$fields[] = 'id';
671
		$fields[] = 'uri';
672
		$fields[] = 'source';
673
		$fields[] = 'synctoken';
674
		$fields[] = 'principaluri';
675
		$fields[] = 'lastmodified';
676
677
		$query = $this->db->getQueryBuilder();
678
		$query->select($fields)
679
			->from('calendarsubscriptions')
680
			->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
681
			->orderBy('calendarorder', 'asc');
682
		$stmt =$query->execute();
683
684
		$row = $stmt->fetch(\PDO::FETCH_ASSOC);
685
		$stmt->closeCursor();
686
		if ($row === false) {
687
			return null;
688
		}
689
690
		$subscription = [
691
			'id'           => $row['id'],
692
			'uri'          => $row['uri'],
693
			'principaluri' => $row['principaluri'],
694
			'source'       => $row['source'],
695
			'lastmodified' => $row['lastmodified'],
696
			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
697
			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
698
		];
699
700
		foreach($this->subscriptionPropertyMap as $xmlName=>$dbName) {
701
			if (!is_null($row[$dbName])) {
702
				$subscription[$xmlName] = $row[$dbName];
703
			}
704
		}
705
706
		return $subscription;
707
	}
708
709
	/**
710
	 * Creates a new calendar for a principal.
711
	 *
712
	 * If the creation was a success, an id must be returned that can be used to reference
713
	 * this calendar in other methods, such as updateCalendar.
714
	 *
715
	 * @param string $principalUri
716
	 * @param string $calendarUri
717
	 * @param array $properties
718
	 * @return int
719
	 * @suppress SqlInjectionChecker
720
	 */
721
	function createCalendar($principalUri, $calendarUri, array $properties) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
722
		$values = [
723
			'principaluri' => $this->convertPrincipal($principalUri, true),
724
			'uri'          => $calendarUri,
725
			'synctoken'    => 1,
726
			'transparent'  => 0,
727
			'components'   => 'VEVENT,VTODO',
728
			'displayname'  => $calendarUri
729
		];
730
731
		// Default value
732
		$sccs = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set';
733
		if (isset($properties[$sccs])) {
734
			if (!($properties[$sccs] instanceof SupportedCalendarComponentSet)) {
735
				throw new DAV\Exception('The ' . $sccs . ' property must be of type: \Sabre\CalDAV\Property\SupportedCalendarComponentSet');
736
			}
737
			$values['components'] = implode(',',$properties[$sccs]->getValue());
738
		}
739
		$transp = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp';
740
		if (isset($properties[$transp])) {
741
			$values['transparent'] = (int) ($properties[$transp]->getValue() === 'transparent');
742
		}
743
744
		foreach($this->propertyMap as $xmlName=>$dbName) {
745
			if (isset($properties[$xmlName])) {
746
				$values[$dbName] = $properties[$xmlName];
747
			}
748
		}
749
750
		$query = $this->db->getQueryBuilder();
751
		$query->insert('calendars');
752
		foreach($values as $column => $value) {
753
			$query->setValue($column, $query->createNamedParameter($value));
754
		}
755
		$query->execute();
756
		$calendarId = $query->getLastInsertId();
757
758
		$this->dispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createCalendar', new GenericEvent(
759
			'\OCA\DAV\CalDAV\CalDavBackend::createCalendar',
760
			[
761
				'calendarId' => $calendarId,
762
				'calendarData' => $this->getCalendarById($calendarId),
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->getCalendarById($calendarId) targeting OCA\DAV\CalDAV\CalDavBackend::getCalendarById() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
763
		]));
764
765
		return $calendarId;
766
	}
767
768
	/**
769
	 * Updates properties for a calendar.
770
	 *
771
	 * The list of mutations is stored in a Sabre\DAV\PropPatch object.
772
	 * To do the actual updates, you must tell this object which properties
773
	 * you're going to process with the handle() method.
774
	 *
775
	 * Calling the handle method is like telling the PropPatch object "I
776
	 * promise I can handle updating this property".
777
	 *
778
	 * Read the PropPatch documentation for more info and examples.
779
	 *
780
	 * @param mixed $calendarId
781
	 * @param PropPatch $propPatch
782
	 * @return void
783
	 */
784
	function updateCalendar($calendarId, PropPatch $propPatch) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
785
		$supportedProperties = array_keys($this->propertyMap);
786
		$supportedProperties[] = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp';
787
788
		/**
789
		 * @suppress SqlInjectionChecker
790
		 */
791
		$propPatch->handle($supportedProperties, function($mutations) use ($calendarId) {
792
			$newValues = [];
793
			foreach ($mutations as $propertyName => $propertyValue) {
794
795
				switch ($propertyName) {
796
					case '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' :
797
						$fieldName = 'transparent';
798
						$newValues[$fieldName] = (int) ($propertyValue->getValue() === 'transparent');
799
						break;
800
					default :
801
						$fieldName = $this->propertyMap[$propertyName];
802
						$newValues[$fieldName] = $propertyValue;
803
						break;
804
				}
805
806
			}
807
			$query = $this->db->getQueryBuilder();
808
			$query->update('calendars');
809
			foreach ($newValues as $fieldName => $value) {
810
				$query->set($fieldName, $query->createNamedParameter($value));
811
			}
812
			$query->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)));
813
			$query->execute();
814
815
			$this->addChange($calendarId, "", 2);
816
817
			$this->dispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateCalendar', new GenericEvent(
818
				'\OCA\DAV\CalDAV\CalDavBackend::updateCalendar',
819
				[
820
					'calendarId' => $calendarId,
821
					'calendarData' => $this->getCalendarById($calendarId),
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->getCalendarById($calendarId) targeting OCA\DAV\CalDAV\CalDavBackend::getCalendarById() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
822
					'shares' => $this->getShares($calendarId),
823
					'propertyMutations' => $mutations,
824
			]));
825
826
			return true;
827
		});
828
	}
829
830
	/**
831
	 * Delete a calendar and all it's objects
832
	 *
833
	 * @param mixed $calendarId
834
	 * @return void
835
	 */
836
	function deleteCalendar($calendarId) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
837
		$this->dispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteCalendar', new GenericEvent(
838
			'\OCA\DAV\CalDAV\CalDavBackend::deleteCalendar',
839
			[
840
				'calendarId' => $calendarId,
841
				'calendarData' => $this->getCalendarById($calendarId),
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->getCalendarById($calendarId) targeting OCA\DAV\CalDAV\CalDavBackend::getCalendarById() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
842
				'shares' => $this->getShares($calendarId),
843
		]));
844
845
		$stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ? AND `calendartype` = ?');
846
		$stmt->execute([$calendarId, self::CALENDAR_TYPE_CALENDAR]);
847
848
		$stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendars` WHERE `id` = ?');
849
		$stmt->execute([$calendarId]);
850
851
		$stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarchanges` WHERE `calendarid` = ? AND `calendartype` = ?');
852
		$stmt->execute([$calendarId, self::CALENDAR_TYPE_CALENDAR]);
853
854
		$this->calendarSharingBackend->deleteAllShares($calendarId);
855
856
		$query = $this->db->getQueryBuilder();
857
		$query->delete($this->dbObjectPropertiesTable)
858
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
859
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)))
860
			->execute();
861
	}
862
863
	/**
864
	 * Delete all of an user's shares
865
	 *
866
	 * @param string $principaluri
867
	 * @return void
868
	 */
869
	function deleteAllSharesByUser($principaluri) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
870
		$this->calendarSharingBackend->deleteAllSharesByUser($principaluri);
871
	}
872
873
	/**
874
	 * Returns all calendar objects within a calendar.
875
	 *
876
	 * Every item contains an array with the following keys:
877
	 *   * calendardata - The iCalendar-compatible calendar data
878
	 *   * uri - a unique key which will be used to construct the uri. This can
879
	 *     be any arbitrary string, but making sure it ends with '.ics' is a
880
	 *     good idea. This is only the basename, or filename, not the full
881
	 *     path.
882
	 *   * lastmodified - a timestamp of the last modification time
883
	 *   * etag - An arbitrary string, surrounded by double-quotes. (e.g.:
884
	 *   '"abcdef"')
885
	 *   * size - The size of the calendar objects, in bytes.
886
	 *   * component - optional, a string containing the type of object, such
887
	 *     as 'vevent' or 'vtodo'. If specified, this will be used to populate
888
	 *     the Content-Type header.
889
	 *
890
	 * Note that the etag is optional, but it's highly encouraged to return for
891
	 * speed reasons.
892
	 *
893
	 * The calendardata is also optional. If it's not returned
894
	 * 'getCalendarObject' will be called later, which *is* expected to return
895
	 * calendardata.
896
	 *
897
	 * If neither etag or size are specified, the calendardata will be
898
	 * used/fetched to determine these numbers. If both are specified the
899
	 * amount of times this is needed is reduced by a great degree.
900
	 *
901
	 * @param mixed $id
902
	 * @param int $calendarType
903
	 * @return array
904
	 */
905
	public function getCalendarObjects($id, $calendarType=self::CALENDAR_TYPE_CALENDAR):array {
906
		$query = $this->db->getQueryBuilder();
907
		$query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'componenttype', 'classification'])
908
			->from('calendarobjects')
909
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($id)))
910
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)));
911
		$stmt = $query->execute();
912
913
		$result = [];
914
		foreach($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) {
915
			$result[] = [
916
				'id'           => $row['id'],
917
				'uri'          => $row['uri'],
918
				'lastmodified' => $row['lastmodified'],
919
				'etag'         => '"' . $row['etag'] . '"',
920
				'calendarid'   => $row['calendarid'],
921
				'size'         => (int)$row['size'],
922
				'component'    => strtolower($row['componenttype']),
923
				'classification'=> (int)$row['classification']
924
			];
925
		}
926
927
		return $result;
928
	}
929
930
	/**
931
	 * Returns information from a single calendar object, based on it's object
932
	 * uri.
933
	 *
934
	 * The object uri is only the basename, or filename and not a full path.
935
	 *
936
	 * The returned array must have the same keys as getCalendarObjects. The
937
	 * 'calendardata' object is required here though, while it's not required
938
	 * for getCalendarObjects.
939
	 *
940
	 * This method must return null if the object did not exist.
941
	 *
942
	 * @param mixed $id
943
	 * @param string $objectUri
944
	 * @param int $calendarType
945
	 * @return array|null
946
	 */
947
	public function getCalendarObject($id, $objectUri, $calendarType=self::CALENDAR_TYPE_CALENDAR) {
948
		$query = $this->db->getQueryBuilder();
949
		$query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification'])
950
			->from('calendarobjects')
951
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($id)))
952
			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
953
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)));
954
		$stmt = $query->execute();
955
		$row = $stmt->fetch(\PDO::FETCH_ASSOC);
956
957
		if(!$row) {
958
			return null;
959
		}
960
961
		return [
962
			'id'            => $row['id'],
963
			'uri'           => $row['uri'],
964
			'lastmodified'  => $row['lastmodified'],
965
			'etag'          => '"' . $row['etag'] . '"',
966
			'calendarid'    => $row['calendarid'],
967
			'size'          => (int)$row['size'],
968
			'calendardata'  => $this->readBlob($row['calendardata']),
969
			'component'     => strtolower($row['componenttype']),
970
			'classification'=> (int)$row['classification']
971
		];
972
	}
973
974
	/**
975
	 * Returns a list of calendar objects.
976
	 *
977
	 * This method should work identical to getCalendarObject, but instead
978
	 * return all the calendar objects in the list as an array.
979
	 *
980
	 * If the backend supports this, it may allow for some speed-ups.
981
	 *
982
	 * @param mixed $calendarId
983
	 * @param string[] $uris
984
	 * @param int $calendarType
985
	 * @return array
986
	 */
987
	public function getMultipleCalendarObjects($id, array $uris, $calendarType=self::CALENDAR_TYPE_CALENDAR):array {
988
		if (empty($uris)) {
989
			return [];
990
		}
991
992
		$chunks = array_chunk($uris, 100);
993
		$objects = [];
994
995
		$query = $this->db->getQueryBuilder();
996
		$query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification'])
997
			->from('calendarobjects')
998
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($id)))
999
			->andWhere($query->expr()->in('uri', $query->createParameter('uri')))
1000
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)));
1001
1002
		foreach ($chunks as $uris) {
1003
			$query->setParameter('uri', $uris, IQueryBuilder::PARAM_STR_ARRAY);
1004
			$result = $query->execute();
1005
1006
			while ($row = $result->fetch()) {
1007
				$objects[] = [
1008
					'id'           => $row['id'],
1009
					'uri'          => $row['uri'],
1010
					'lastmodified' => $row['lastmodified'],
1011
					'etag'         => '"' . $row['etag'] . '"',
1012
					'calendarid'   => $row['calendarid'],
1013
					'size'         => (int)$row['size'],
1014
					'calendardata' => $this->readBlob($row['calendardata']),
1015
					'component'    => strtolower($row['componenttype']),
1016
					'classification' => (int)$row['classification']
1017
				];
1018
			}
1019
			$result->closeCursor();
1020
		}
1021
1022
		return $objects;
1023
	}
1024
1025
	/**
1026
	 * Creates a new calendar object.
1027
	 *
1028
	 * The object uri is only the basename, or filename and not a full path.
1029
	 *
1030
	 * It is possible return an etag from this function, which will be used in
1031
	 * the response to this PUT request. Note that the ETag must be surrounded
1032
	 * by double-quotes.
1033
	 *
1034
	 * However, you should only really return this ETag if you don't mangle the
1035
	 * calendar-data. If the result of a subsequent GET to this object is not
1036
	 * the exact same as this request body, you should omit the ETag.
1037
	 *
1038
	 * @param mixed $calendarId
1039
	 * @param string $objectUri
1040
	 * @param string $calendarData
1041
	 * @param int $calendarType
1042
	 * @return string
1043
	 */
1044
	function createCalendarObject($calendarId, $objectUri, $calendarData, $calendarType=self::CALENDAR_TYPE_CALENDAR) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
1045
		$extraData = $this->getDenormalizedData($calendarData);
1046
1047
		$q = $this->db->getQueryBuilder();
1048
		$q->select($q->func()->count('*'))
1049
			->from('calendarobjects')
1050
			->where($q->expr()->eq('calendarid', $q->createNamedParameter($calendarId)))
1051
			->andWhere($q->expr()->eq('uid', $q->createNamedParameter($extraData['uid'])))
1052
			->andWhere($q->expr()->eq('calendartype', $q->createNamedParameter($calendarType)));
1053
1054
		$result = $q->execute();
1055
		$count = (int) $result->fetchColumn();
1056
		$result->closeCursor();
1057
1058
		if ($count !== 0) {
1059
			throw new \Sabre\DAV\Exception\BadRequest('Calendar object with uid already exists in this calendar collection.');
1060
		}
1061
1062
		$query = $this->db->getQueryBuilder();
1063
		$query->insert('calendarobjects')
1064
			->values([
1065
				'calendarid' => $query->createNamedParameter($calendarId),
1066
				'uri' => $query->createNamedParameter($objectUri),
1067
				'calendardata' => $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB),
1068
				'lastmodified' => $query->createNamedParameter(time()),
1069
				'etag' => $query->createNamedParameter($extraData['etag']),
1070
				'size' => $query->createNamedParameter($extraData['size']),
1071
				'componenttype' => $query->createNamedParameter($extraData['componentType']),
1072
				'firstoccurence' => $query->createNamedParameter($extraData['firstOccurence']),
1073
				'lastoccurence' => $query->createNamedParameter($extraData['lastOccurence']),
1074
				'classification' => $query->createNamedParameter($extraData['classification']),
1075
				'uid' => $query->createNamedParameter($extraData['uid']),
1076
				'calendartype' => $query->createNamedParameter($calendarType),
1077
			])
1078
			->execute();
1079
1080
		$this->updateProperties($calendarId, $objectUri, $calendarData, $calendarType);
1081
1082
		if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
1083
			$this->dispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createCalendarObject', new GenericEvent(
1084
				'\OCA\DAV\CalDAV\CalDavBackend::createCalendarObject',
1085
				[
1086
					'calendarId' => $calendarId,
1087
					'calendarData' => $this->getCalendarById($calendarId),
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->getCalendarById($calendarId) targeting OCA\DAV\CalDAV\CalDavBackend::getCalendarById() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
1088
					'shares' => $this->getShares($calendarId),
1089
					'objectData' => $this->getCalendarObject($calendarId, $objectUri),
1090
				]
1091
			));
1092
		} else {
1093
			$this->dispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createCachedCalendarObject', new GenericEvent(
1094
				'\OCA\DAV\CalDAV\CalDavBackend::createCachedCalendarObject',
1095
				[
1096
					'subscriptionId' => $calendarId,
1097
					'calendarData' => $this->getCalendarById($calendarId),
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->getCalendarById($calendarId) targeting OCA\DAV\CalDAV\CalDavBackend::getCalendarById() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
1098
					'shares' => $this->getShares($calendarId),
1099
					'objectData' => $this->getCalendarObject($calendarId, $objectUri),
1100
				]
1101
			));
1102
		}
1103
		$this->addChange($calendarId, $objectUri, 1, $calendarType);
1104
1105
		return '"' . $extraData['etag'] . '"';
1106
	}
1107
1108
	/**
1109
	 * Updates an existing calendarobject, based on it's uri.
1110
	 *
1111
	 * The object uri is only the basename, or filename and not a full path.
1112
	 *
1113
	 * It is possible return an etag from this function, which will be used in
1114
	 * the response to this PUT request. Note that the ETag must be surrounded
1115
	 * by double-quotes.
1116
	 *
1117
	 * However, you should only really return this ETag if you don't mangle the
1118
	 * calendar-data. If the result of a subsequent GET to this object is not
1119
	 * the exact same as this request body, you should omit the ETag.
1120
	 *
1121
	 * @param mixed $calendarId
1122
	 * @param string $objectUri
1123
	 * @param string $calendarData
1124
	 * @param int $calendarType
1125
	 * @return string
1126
	 */
1127
	function updateCalendarObject($calendarId, $objectUri, $calendarData, $calendarType=self::CALENDAR_TYPE_CALENDAR) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
1128
		$extraData = $this->getDenormalizedData($calendarData);
1129
1130
		$query = $this->db->getQueryBuilder();
1131
		$query->update('calendarobjects')
1132
				->set('calendardata', $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB))
1133
				->set('lastmodified', $query->createNamedParameter(time()))
1134
				->set('etag', $query->createNamedParameter($extraData['etag']))
1135
				->set('size', $query->createNamedParameter($extraData['size']))
1136
				->set('componenttype', $query->createNamedParameter($extraData['componentType']))
1137
				->set('firstoccurence', $query->createNamedParameter($extraData['firstOccurence']))
1138
				->set('lastoccurence', $query->createNamedParameter($extraData['lastOccurence']))
1139
				->set('classification', $query->createNamedParameter($extraData['classification']))
1140
				->set('uid', $query->createNamedParameter($extraData['uid']))
1141
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
1142
			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
1143
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)))
1144
			->execute();
1145
1146
		$this->updateProperties($calendarId, $objectUri, $calendarData, $calendarType);
1147
1148
		$data = $this->getCalendarObject($calendarId, $objectUri);
1149
		if (is_array($data)) {
1150
			if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
1151
				$this->dispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateCalendarObject', new GenericEvent(
1152
					'\OCA\DAV\CalDAV\CalDavBackend::updateCalendarObject',
1153
					[
1154
						'calendarId' => $calendarId,
1155
						'calendarData' => $this->getCalendarById($calendarId),
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->getCalendarById($calendarId) targeting OCA\DAV\CalDAV\CalDavBackend::getCalendarById() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
1156
						'shares' => $this->getShares($calendarId),
1157
						'objectData' => $data,
1158
					]
1159
				));
1160
			} else {
1161
				$this->dispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateCachedCalendarObject', new GenericEvent(
1162
					'\OCA\DAV\CalDAV\CalDavBackend::updateCachedCalendarObject',
1163
					[
1164
						'subscriptionId' => $calendarId,
1165
						'calendarData' => $this->getCalendarById($calendarId),
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->getCalendarById($calendarId) targeting OCA\DAV\CalDAV\CalDavBackend::getCalendarById() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
1166
						'shares' => $this->getShares($calendarId),
1167
						'objectData' => $data,
1168
					]
1169
				));
1170
			}
1171
		}
1172
		$this->addChange($calendarId, $objectUri, 2, $calendarType);
1173
1174
		return '"' . $extraData['etag'] . '"';
1175
	}
1176
1177
	/**
1178
	 * @param int $calendarObjectId
1179
	 * @param int $classification
1180
	 */
1181
	public function setClassification($calendarObjectId, $classification) {
1182
		if (!in_array($classification, [
1183
			self::CLASSIFICATION_PUBLIC, self::CLASSIFICATION_PRIVATE, self::CLASSIFICATION_CONFIDENTIAL
1184
		])) {
1185
			throw new \InvalidArgumentException();
1186
		}
1187
		$query = $this->db->getQueryBuilder();
1188
		$query->update('calendarobjects')
1189
			->set('classification', $query->createNamedParameter($classification))
1190
			->where($query->expr()->eq('id', $query->createNamedParameter($calendarObjectId)))
1191
			->execute();
1192
	}
1193
1194
	/**
1195
	 * Deletes an existing calendar object.
1196
	 *
1197
	 * The object uri is only the basename, or filename and not a full path.
1198
	 *
1199
	 * @param mixed $calendarId
1200
	 * @param string $objectUri
1201
	 * @param int $calendarType
1202
	 * @return void
1203
	 */
1204
	function deleteCalendarObject($calendarId, $objectUri, $calendarType=self::CALENDAR_TYPE_CALENDAR) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
1205
		$data = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
1206
		if (is_array($data)) {
1207
			if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
1208
				$this->dispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteCalendarObject', new GenericEvent(
1209
					'\OCA\DAV\CalDAV\CalDavBackend::deleteCalendarObject',
1210
					[
1211
						'calendarId' => $calendarId,
1212
						'calendarData' => $this->getCalendarById($calendarId),
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->getCalendarById($calendarId) targeting OCA\DAV\CalDAV\CalDavBackend::getCalendarById() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
1213
						'shares' => $this->getShares($calendarId),
1214
						'objectData' => $data,
1215
					]
1216
				));
1217
			} else {
1218
				$this->dispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteCachedCalendarObject', new GenericEvent(
1219
					'\OCA\DAV\CalDAV\CalDavBackend::deleteCachedCalendarObject',
1220
					[
1221
						'subscriptionId' => $calendarId,
1222
						'calendarData' => $this->getCalendarById($calendarId),
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->getCalendarById($calendarId) targeting OCA\DAV\CalDAV\CalDavBackend::getCalendarById() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
1223
						'shares' => $this->getShares($calendarId),
1224
						'objectData' => $data,
1225
					]
1226
				));
1227
			}
1228
		}
1229
1230
		$stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ? AND `uri` = ? AND `calendartype` = ?');
1231
		$stmt->execute([$calendarId, $objectUri, $calendarType]);
1232
1233
		$this->purgeProperties($calendarId, $data['id'], $calendarType);
0 ignored issues
show
Unused Code introduced by
The call to OCA\DAV\CalDAV\CalDavBackend::purgeProperties() has too many arguments starting with $calendarType. ( Ignorable by Annotation )

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

1233
		$this->/** @scrutinizer ignore-call */ 
1234
         purgeProperties($calendarId, $data['id'], $calendarType);

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...
1234
1235
		$this->addChange($calendarId, $objectUri, 3, $calendarType);
1236
	}
1237
1238
	/**
1239
	 * Performs a calendar-query on the contents of this calendar.
1240
	 *
1241
	 * The calendar-query is defined in RFC4791 : CalDAV. Using the
1242
	 * calendar-query it is possible for a client to request a specific set of
1243
	 * object, based on contents of iCalendar properties, date-ranges and
1244
	 * iCalendar component types (VTODO, VEVENT).
1245
	 *
1246
	 * This method should just return a list of (relative) urls that match this
1247
	 * query.
1248
	 *
1249
	 * The list of filters are specified as an array. The exact array is
1250
	 * documented by Sabre\CalDAV\CalendarQueryParser.
1251
	 *
1252
	 * Note that it is extremely likely that getCalendarObject for every path
1253
	 * returned from this method will be called almost immediately after. You
1254
	 * may want to anticipate this to speed up these requests.
1255
	 *
1256
	 * This method provides a default implementation, which parses *all* the
1257
	 * iCalendar objects in the specified calendar.
1258
	 *
1259
	 * This default may well be good enough for personal use, and calendars
1260
	 * that aren't very large. But if you anticipate high usage, big calendars
1261
	 * or high loads, you are strongly advised to optimize certain paths.
1262
	 *
1263
	 * The best way to do so is override this method and to optimize
1264
	 * specifically for 'common filters'.
1265
	 *
1266
	 * Requests that are extremely common are:
1267
	 *   * requests for just VEVENTS
1268
	 *   * requests for just VTODO
1269
	 *   * requests with a time-range-filter on either VEVENT or VTODO.
1270
	 *
1271
	 * ..and combinations of these requests. It may not be worth it to try to
1272
	 * handle every possible situation and just rely on the (relatively
1273
	 * easy to use) CalendarQueryValidator to handle the rest.
1274
	 *
1275
	 * Note that especially time-range-filters may be difficult to parse. A
1276
	 * time-range filter specified on a VEVENT must for instance also handle
1277
	 * recurrence rules correctly.
1278
	 * A good example of how to interprete all these filters can also simply
1279
	 * be found in Sabre\CalDAV\CalendarQueryFilter. This class is as correct
1280
	 * as possible, so it gives you a good idea on what type of stuff you need
1281
	 * to think of.
1282
	 *
1283
	 * @param mixed $id
1284
	 * @param array $filters
1285
	 * @param int $calendarType
1286
	 * @return array
1287
	 */
1288
	public function calendarQuery($id, array $filters, $calendarType=self::CALENDAR_TYPE_CALENDAR):array {
1289
		$componentType = null;
1290
		$requirePostFilter = true;
1291
		$timeRange = null;
1292
1293
		// if no filters were specified, we don't need to filter after a query
1294
		if (!$filters['prop-filters'] && !$filters['comp-filters']) {
1295
			$requirePostFilter = false;
1296
		}
1297
1298
		// Figuring out if there's a component filter
1299
		if (count($filters['comp-filters']) > 0 && !$filters['comp-filters'][0]['is-not-defined']) {
1300
			$componentType = $filters['comp-filters'][0]['name'];
1301
1302
			// Checking if we need post-filters
1303
			if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['time-range'] && !$filters['comp-filters'][0]['prop-filters']) {
1304
				$requirePostFilter = false;
1305
			}
1306
			// There was a time-range filter
1307
			if ($componentType === 'VEVENT' && isset($filters['comp-filters'][0]['time-range'])) {
1308
				$timeRange = $filters['comp-filters'][0]['time-range'];
1309
1310
				// If start time OR the end time is not specified, we can do a
1311
				// 100% accurate mysql query.
1312
				if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['prop-filters'] && (!$timeRange['start'] || !$timeRange['end'])) {
1313
					$requirePostFilter = false;
1314
				}
1315
			}
1316
1317
		}
1318
		$columns = ['uri'];
1319
		if ($requirePostFilter) {
1320
			$columns = ['uri', 'calendardata'];
1321
		}
1322
		$query = $this->db->getQueryBuilder();
1323
		$query->select($columns)
1324
			->from('calendarobjects')
1325
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($id)))
1326
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)));
1327
1328
		if ($componentType) {
1329
			$query->andWhere($query->expr()->eq('componenttype', $query->createNamedParameter($componentType)));
1330
		}
1331
1332
		if ($timeRange && $timeRange['start']) {
1333
			$query->andWhere($query->expr()->gt('lastoccurence', $query->createNamedParameter($timeRange['start']->getTimeStamp())));
1334
		}
1335
		if ($timeRange && $timeRange['end']) {
1336
			$query->andWhere($query->expr()->lt('firstoccurence', $query->createNamedParameter($timeRange['end']->getTimeStamp())));
1337
		}
1338
1339
		$stmt = $query->execute();
1340
1341
		$result = [];
1342
		while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
1343
			if ($requirePostFilter) {
1344
				// validateFilterForObject will parse the calendar data
1345
				// catch parsing errors
1346
				try {
1347
					$matches = $this->validateFilterForObject($row, $filters);
1348
				} catch(ParseException $ex) {
1349
					$this->logger->logException($ex, [
1350
						'app' => 'dav',
1351
						'message' => 'Caught parsing exception for calendar data. This usually indicates invalid calendar data. calendar-id:'.$id.' uri:'.$row['uri']
1352
					]);
1353
					continue;
1354
				} catch (InvalidDataException $ex) {
1355
					$this->logger->logException($ex, [
1356
						'app' => 'dav',
1357
						'message' => 'Caught invalid data exception for calendar data. This usually indicates invalid calendar data. calendar-id:'.$id.' uri:'.$row['uri']
1358
					]);
1359
					continue;
1360
				}
1361
1362
				if (!$matches) {
1363
					continue;
1364
				}
1365
			}
1366
			$result[] = $row['uri'];
1367
		}
1368
1369
		return $result;
1370
	}
1371
1372
	/**
1373
	 * custom Nextcloud search extension for CalDAV
1374
	 *
1375
	 * TODO - this should optionally cover cached calendar objects as well
1376
	 *
1377
	 * @param string $principalUri
1378
	 * @param array $filters
1379
	 * @param integer|null $limit
1380
	 * @param integer|null $offset
1381
	 * @return array
1382
	 */
1383
	public function calendarSearch($principalUri, array $filters, $limit=null, $offset=null) {
1384
		$calendars = $this->getCalendarsForUser($principalUri);
1385
		$ownCalendars = [];
1386
		$sharedCalendars = [];
1387
1388
		$uriMapper = [];
1389
1390
		foreach($calendars as $calendar) {
1391
			if ($calendar['{http://owncloud.org/ns}owner-principal'] === $principalUri) {
1392
				$ownCalendars[] = $calendar['id'];
1393
			} else {
1394
				$sharedCalendars[] = $calendar['id'];
1395
			}
1396
			$uriMapper[$calendar['id']] = $calendar['uri'];
1397
		}
1398
		if (count($ownCalendars) === 0 && count($sharedCalendars) === 0) {
1399
			return [];
1400
		}
1401
1402
		$query = $this->db->getQueryBuilder();
1403
		// Calendar id expressions
1404
		$calendarExpressions = [];
1405
		foreach($ownCalendars as $id) {
1406
			$calendarExpressions[] = $query->expr()->andX(
1407
				$query->expr()->eq('c.calendarid',
1408
					$query->createNamedParameter($id)),
1409
				$query->expr()->eq('c.calendartype',
1410
						$query->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
1411
		}
1412
		foreach($sharedCalendars as $id) {
1413
			$calendarExpressions[] = $query->expr()->andX(
1414
				$query->expr()->eq('c.calendarid',
1415
					$query->createNamedParameter($id)),
1416
				$query->expr()->eq('c.classification',
1417
					$query->createNamedParameter(self::CLASSIFICATION_PUBLIC)),
1418
				$query->expr()->eq('c.calendartype',
1419
					$query->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
1420
		}
1421
1422
		if (count($calendarExpressions) === 1) {
1423
			$calExpr = $calendarExpressions[0];
1424
		} else {
1425
			$calExpr = call_user_func_array([$query->expr(), 'orX'], $calendarExpressions);
1426
		}
1427
1428
		// Component expressions
1429
		$compExpressions = [];
1430
		foreach($filters['comps'] as $comp) {
1431
			$compExpressions[] = $query->expr()
1432
				->eq('c.componenttype', $query->createNamedParameter($comp));
1433
		}
1434
1435
		if (count($compExpressions) === 1) {
1436
			$compExpr = $compExpressions[0];
1437
		} else {
1438
			$compExpr = call_user_func_array([$query->expr(), 'orX'], $compExpressions);
1439
		}
1440
1441
		if (!isset($filters['props'])) {
1442
			$filters['props'] = [];
1443
		}
1444
		if (!isset($filters['params'])) {
1445
			$filters['params'] = [];
1446
		}
1447
1448
		$propParamExpressions = [];
1449
		foreach($filters['props'] as $prop) {
1450
			$propParamExpressions[] = $query->expr()->andX(
1451
				$query->expr()->eq('i.name', $query->createNamedParameter($prop)),
1452
				$query->expr()->isNull('i.parameter')
1453
			);
1454
		}
1455
		foreach($filters['params'] as $param) {
1456
			$propParamExpressions[] = $query->expr()->andX(
1457
				$query->expr()->eq('i.name', $query->createNamedParameter($param['property'])),
1458
				$query->expr()->eq('i.parameter', $query->createNamedParameter($param['parameter']))
1459
			);
1460
		}
1461
1462
		if (count($propParamExpressions) === 1) {
1463
			$propParamExpr = $propParamExpressions[0];
1464
		} else {
1465
			$propParamExpr = call_user_func_array([$query->expr(), 'orX'], $propParamExpressions);
1466
		}
1467
1468
		$query->select(['c.calendarid', 'c.uri'])
1469
			->from($this->dbObjectPropertiesTable, 'i')
1470
			->join('i', 'calendarobjects', 'c', $query->expr()->eq('i.objectid', 'c.id'))
1471
			->where($calExpr)
1472
			->andWhere($compExpr)
1473
			->andWhere($propParamExpr)
1474
			->andWhere($query->expr()->iLike('i.value',
1475
				$query->createNamedParameter('%'.$this->db->escapeLikeParameter($filters['search-term']).'%')));
1476
1477
		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...
1478
			$query->setFirstResult($offset);
1479
		}
1480
		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...
1481
			$query->setMaxResults($limit);
1482
		}
1483
1484
		$stmt = $query->execute();
1485
1486
		$result = [];
1487
		while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
1488
			$path = $uriMapper[$row['calendarid']] . '/' . $row['uri'];
1489
			if (!in_array($path, $result)) {
1490
				$result[] = $path;
1491
			}
1492
		}
1493
1494
		return $result;
1495
	}
1496
1497
	/**
1498
	 * used for Nextcloud's calendar API
1499
	 *
1500
	 * @param array $calendarInfo
1501
	 * @param string $pattern
1502
	 * @param array $searchProperties
1503
	 * @param array $options
1504
	 * @param integer|null $limit
1505
	 * @param integer|null $offset
1506
	 *
1507
	 * @return array
1508
	 */
1509
	public function search(array $calendarInfo, $pattern, array $searchProperties,
1510
						   array $options, $limit, $offset) {
1511
		$outerQuery = $this->db->getQueryBuilder();
1512
		$innerQuery = $this->db->getQueryBuilder();
1513
1514
		$innerQuery->selectDistinct('op.objectid')
1515
			->from($this->dbObjectPropertiesTable, 'op')
1516
			->andWhere($innerQuery->expr()->eq('op.calendarid',
1517
				$outerQuery->createNamedParameter($calendarInfo['id'])))
1518
			->andWhere($innerQuery->expr()->eq('op.calendartype',
1519
				$outerQuery->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
1520
1521
		// only return public items for shared calendars for now
1522
		if ($calendarInfo['principaluri'] !== $calendarInfo['{http://owncloud.org/ns}owner-principal']) {
1523
			$innerQuery->andWhere($innerQuery->expr()->eq('c.classification',
1524
				$outerQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
1525
		}
1526
1527
		$or = $innerQuery->expr()->orX();
1528
		foreach($searchProperties as $searchProperty) {
1529
			$or->add($innerQuery->expr()->eq('op.name',
1530
				$outerQuery->createNamedParameter($searchProperty)));
1531
		}
1532
		$innerQuery->andWhere($or);
1533
1534
		if ($pattern !== '') {
1535
			$innerQuery->andWhere($innerQuery->expr()->iLike('op.value',
1536
				$outerQuery->createNamedParameter('%' .
1537
					$this->db->escapeLikeParameter($pattern) . '%')));
1538
		}
1539
1540
		$outerQuery->select('c.id', 'c.calendardata', 'c.componenttype', 'c.uid', 'c.uri')
1541
			->from('calendarobjects', 'c');
1542
1543
		if (isset($options['timerange'])) {
1544
			if (isset($options['timerange']['start'])) {
1545
				$outerQuery->andWhere($outerQuery->expr()->gt('lastoccurence',
1546
					$outerQuery->createNamedParameter($options['timerange']['start']->getTimeStamp)));
1547
1548
			}
1549
			if (isset($options['timerange']['end'])) {
1550
				$outerQuery->andWhere($outerQuery->expr()->lt('firstoccurence',
1551
					$outerQuery->createNamedParameter($options['timerange']['end']->getTimeStamp)));
1552
			}
1553
		}
1554
1555
		if (isset($options['types'])) {
1556
			$or = $outerQuery->expr()->orX();
1557
			foreach($options['types'] as $type) {
1558
				$or->add($outerQuery->expr()->eq('componenttype',
1559
					$outerQuery->createNamedParameter($type)));
1560
			}
1561
			$outerQuery->andWhere($or);
1562
		}
1563
1564
		$outerQuery->andWhere($outerQuery->expr()->in('c.id',
1565
			$outerQuery->createFunction($innerQuery->getSQL())));
1566
1567
		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...
1568
			$outerQuery->setFirstResult($offset);
1569
		}
1570
		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...
1571
			$outerQuery->setMaxResults($limit);
1572
		}
1573
1574
		$result = $outerQuery->execute();
1575
		$calendarObjects = $result->fetchAll();
1576
1577
		return array_map(function($o) {
1578
			$calendarData = Reader::read($o['calendardata']);
1579
			$comps = $calendarData->getComponents();
1580
			$objects = [];
1581
			$timezones = [];
1582
			foreach($comps as $comp) {
1583
				if ($comp instanceof VTimeZone) {
1584
					$timezones[] = $comp;
1585
				} else {
1586
					$objects[] = $comp;
1587
				}
1588
			}
1589
1590
			return [
1591
				'id' => $o['id'],
1592
				'type' => $o['componenttype'],
1593
				'uid' => $o['uid'],
1594
				'uri' => $o['uri'],
1595
				'objects' => array_map(function($c) {
1596
					return $this->transformSearchData($c);
1597
				}, $objects),
1598
				'timezones' => array_map(function($c) {
1599
					return $this->transformSearchData($c);
1600
				}, $timezones),
1601
			];
1602
		}, $calendarObjects);
1603
	}
1604
1605
	/**
1606
	 * @param Component $comp
1607
	 * @return array
1608
	 */
1609
	private function transformSearchData(Component $comp) {
1610
		$data = [];
1611
		/** @var Component[] $subComponents */
1612
		$subComponents = $comp->getComponents();
1613
		/** @var Property[] $properties */
1614
		$properties = array_filter($comp->children(), function($c) {
1615
			return $c instanceof Property;
1616
		});
1617
		$validationRules = $comp->getValidationRules();
1618
1619
		foreach($subComponents as $subComponent) {
1620
			$name = $subComponent->name;
1621
			if (!isset($data[$name])) {
1622
				$data[$name] = [];
1623
			}
1624
			$data[$name][] = $this->transformSearchData($subComponent);
1625
		}
1626
1627
		foreach($properties as $property) {
1628
			$name = $property->name;
1629
			if (!isset($validationRules[$name])) {
1630
				$validationRules[$name] = '*';
1631
			}
1632
1633
			$rule = $validationRules[$property->name];
1634
			if ($rule === '+' || $rule === '*') { // multiple
1635
				if (!isset($data[$name])) {
1636
					$data[$name] = [];
1637
				}
1638
1639
				$data[$name][] = $this->transformSearchProperty($property);
1640
			} else { // once
1641
				$data[$name] = $this->transformSearchProperty($property);
1642
			}
1643
		}
1644
1645
		return $data;
1646
	}
1647
1648
	/**
1649
	 * @param Property $prop
1650
	 * @return array
1651
	 */
1652
	private function transformSearchProperty(Property $prop) {
1653
		// No need to check Date, as it extends DateTime
1654
		if ($prop instanceof Property\ICalendar\DateTime) {
1655
			$value = $prop->getDateTime();
1656
		} else {
1657
			$value = $prop->getValue();
1658
		}
1659
1660
		return [
1661
			$value,
1662
			$prop->parameters()
1663
		];
1664
	}
1665
1666
	/**
1667
	 * Searches through all of a users calendars and calendar objects to find
1668
	 * an object with a specific UID.
1669
	 *
1670
	 * This method should return the path to this object, relative to the
1671
	 * calendar home, so this path usually only contains two parts:
1672
	 *
1673
	 * calendarpath/objectpath.ics
1674
	 *
1675
	 * If the uid is not found, return null.
1676
	 *
1677
	 * This method should only consider * objects that the principal owns, so
1678
	 * any calendars owned by other principals that also appear in this
1679
	 * collection should be ignored.
1680
	 *
1681
	 * @param string $principalUri
1682
	 * @param string $uid
1683
	 * @return string|null
1684
	 */
1685
	function getCalendarObjectByUID($principalUri, $uid) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
1686
1687
		$query = $this->db->getQueryBuilder();
1688
		$query->selectAlias('c.uri', 'calendaruri')->selectAlias('co.uri', 'objecturi')
1689
			->from('calendarobjects', 'co')
1690
			->leftJoin('co', 'calendars', 'c', $query->expr()->eq('co.calendarid', 'c.id'))
1691
			->where($query->expr()->eq('c.principaluri', $query->createNamedParameter($principalUri)))
1692
			->andWhere($query->expr()->eq('co.uid', $query->createNamedParameter($uid)))
1693
			->andWhere($query->expr()->eq('co.uid', $query->createNamedParameter($uid)));
1694
1695
		$stmt = $query->execute();
1696
1697
		if ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
1698
			return $row['calendaruri'] . '/' . $row['objecturi'];
1699
		}
1700
1701
		return null;
1702
	}
1703
1704
	/**
1705
	 * The getChanges method returns all the changes that have happened, since
1706
	 * the specified syncToken in the specified calendar.
1707
	 *
1708
	 * This function should return an array, such as the following:
1709
	 *
1710
	 * [
1711
	 *   'syncToken' => 'The current synctoken',
1712
	 *   'added'   => [
1713
	 *      'new.txt',
1714
	 *   ],
1715
	 *   'modified'   => [
1716
	 *      'modified.txt',
1717
	 *   ],
1718
	 *   'deleted' => [
1719
	 *      'foo.php.bak',
1720
	 *      'old.txt'
1721
	 *   ]
1722
	 * );
1723
	 *
1724
	 * The returned syncToken property should reflect the *current* syncToken
1725
	 * of the calendar, as reported in the {http://sabredav.org/ns}sync-token
1726
	 * property This is * needed here too, to ensure the operation is atomic.
1727
	 *
1728
	 * If the $syncToken argument is specified as null, this is an initial
1729
	 * sync, and all members should be reported.
1730
	 *
1731
	 * The modified property is an array of nodenames that have changed since
1732
	 * the last token.
1733
	 *
1734
	 * The deleted property is an array with nodenames, that have been deleted
1735
	 * from collection.
1736
	 *
1737
	 * The $syncLevel argument is basically the 'depth' of the report. If it's
1738
	 * 1, you only have to report changes that happened only directly in
1739
	 * immediate descendants. If it's 2, it should also include changes from
1740
	 * the nodes below the child collections. (grandchildren)
1741
	 *
1742
	 * The $limit argument allows a client to specify how many results should
1743
	 * be returned at most. If the limit is not specified, it should be treated
1744
	 * as infinite.
1745
	 *
1746
	 * If the limit (infinite or not) is higher than you're willing to return,
1747
	 * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
1748
	 *
1749
	 * If the syncToken is expired (due to data cleanup) or unknown, you must
1750
	 * return null.
1751
	 *
1752
	 * The limit is 'suggestive'. You are free to ignore it.
1753
	 *
1754
	 * @param string $calendarId
1755
	 * @param string $syncToken
1756
	 * @param int $syncLevel
1757
	 * @param int $limit
1758
	 * @param int $calendarType
1759
	 * @return array
1760
	 */
1761
	function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit = null, $calendarType=self::CALENDAR_TYPE_CALENDAR) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
1762
		// Current synctoken
1763
		$stmt = $this->db->prepare('SELECT `synctoken` FROM `*PREFIX*calendars` WHERE `id` = ?');
1764
		$stmt->execute([ $calendarId ]);
1765
		$currentToken = $stmt->fetchColumn(0);
1766
1767
		if (is_null($currentToken)) {
0 ignored issues
show
introduced by
The condition is_null($currentToken) is always false.
Loading history...
1768
			return null;
1769
		}
1770
1771
		$result = [
1772
			'syncToken' => $currentToken,
1773
			'added'     => [],
1774
			'modified'  => [],
1775
			'deleted'   => [],
1776
		];
1777
1778
		if ($syncToken) {
1779
1780
			$query = "SELECT `uri`, `operation` FROM `*PREFIX*calendarchanges` WHERE `synctoken` >= ? AND `synctoken` < ? AND `calendarid` = ? AND `calendartype` = ? ORDER BY `synctoken`";
1781
			if ($limit>0) {
1782
				$query.= " LIMIT " . (int)$limit;
1783
			}
1784
1785
			// Fetching all changes
1786
			$stmt = $this->db->prepare($query);
1787
			$stmt->execute([$syncToken, $currentToken, $calendarId, $calendarType]);
1788
1789
			$changes = [];
1790
1791
			// This loop ensures that any duplicates are overwritten, only the
1792
			// last change on a node is relevant.
1793
			while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
1794
1795
				$changes[$row['uri']] = $row['operation'];
1796
1797
			}
1798
1799
			foreach($changes as $uri => $operation) {
1800
1801
				switch($operation) {
1802
					case 1 :
1803
						$result['added'][] = $uri;
1804
						break;
1805
					case 2 :
1806
						$result['modified'][] = $uri;
1807
						break;
1808
					case 3 :
1809
						$result['deleted'][] = $uri;
1810
						break;
1811
				}
1812
1813
			}
1814
		} else {
1815
			// No synctoken supplied, this is the initial sync.
1816
			$query = "SELECT `uri` FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ? AND `calendartype` = ?";
1817
			$stmt = $this->db->prepare($query);
1818
			$stmt->execute([$calendarId, $calendarType]);
1819
1820
			$result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
1821
		}
1822
		return $result;
1823
1824
	}
1825
1826
	/**
1827
	 * Returns a list of subscriptions for a principal.
1828
	 *
1829
	 * Every subscription is an array with the following keys:
1830
	 *  * id, a unique id that will be used by other functions to modify the
1831
	 *    subscription. This can be the same as the uri or a database key.
1832
	 *  * uri. This is just the 'base uri' or 'filename' of the subscription.
1833
	 *  * principaluri. The owner of the subscription. Almost always the same as
1834
	 *    principalUri passed to this method.
1835
	 *
1836
	 * Furthermore, all the subscription info must be returned too:
1837
	 *
1838
	 * 1. {DAV:}displayname
1839
	 * 2. {http://apple.com/ns/ical/}refreshrate
1840
	 * 3. {http://calendarserver.org/ns/}subscribed-strip-todos (omit if todos
1841
	 *    should not be stripped).
1842
	 * 4. {http://calendarserver.org/ns/}subscribed-strip-alarms (omit if alarms
1843
	 *    should not be stripped).
1844
	 * 5. {http://calendarserver.org/ns/}subscribed-strip-attachments (omit if
1845
	 *    attachments should not be stripped).
1846
	 * 6. {http://calendarserver.org/ns/}source (Must be a
1847
	 *     Sabre\DAV\Property\Href).
1848
	 * 7. {http://apple.com/ns/ical/}calendar-color
1849
	 * 8. {http://apple.com/ns/ical/}calendar-order
1850
	 * 9. {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
1851
	 *    (should just be an instance of
1852
	 *    Sabre\CalDAV\Property\SupportedCalendarComponentSet, with a bunch of
1853
	 *    default components).
1854
	 *
1855
	 * @param string $principalUri
1856
	 * @return array
1857
	 */
1858
	function getSubscriptionsForUser($principalUri) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
1859
		$fields = array_values($this->subscriptionPropertyMap);
1860
		$fields[] = 'id';
1861
		$fields[] = 'uri';
1862
		$fields[] = 'source';
1863
		$fields[] = 'principaluri';
1864
		$fields[] = 'lastmodified';
1865
		$fields[] = 'synctoken';
1866
1867
		$query = $this->db->getQueryBuilder();
1868
		$query->select($fields)
1869
			->from('calendarsubscriptions')
1870
			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
1871
			->orderBy('calendarorder', 'asc');
1872
		$stmt =$query->execute();
1873
1874
		$subscriptions = [];
1875
		while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
1876
1877
			$subscription = [
1878
				'id'           => $row['id'],
1879
				'uri'          => $row['uri'],
1880
				'principaluri' => $row['principaluri'],
1881
				'source'       => $row['source'],
1882
				'lastmodified' => $row['lastmodified'],
1883
1884
				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
1885
				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
1886
			];
1887
1888
			foreach($this->subscriptionPropertyMap as $xmlName=>$dbName) {
1889
				if (!is_null($row[$dbName])) {
1890
					$subscription[$xmlName] = $row[$dbName];
1891
				}
1892
			}
1893
1894
			$subscriptions[] = $subscription;
1895
1896
		}
1897
1898
		return $subscriptions;
1899
	}
1900
1901
	/**
1902
	 * Creates a new subscription for a principal.
1903
	 *
1904
	 * If the creation was a success, an id must be returned that can be used to reference
1905
	 * this subscription in other methods, such as updateSubscription.
1906
	 *
1907
	 * @param string $principalUri
1908
	 * @param string $uri
1909
	 * @param array $properties
1910
	 * @return mixed
1911
	 */
1912
	function createSubscription($principalUri, $uri, array $properties) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
1913
1914
		if (!isset($properties['{http://calendarserver.org/ns/}source'])) {
1915
			throw new Forbidden('The {http://calendarserver.org/ns/}source property is required when creating subscriptions');
1916
		}
1917
1918
		$values = [
1919
			'principaluri' => $principalUri,
1920
			'uri'          => $uri,
1921
			'source'       => $properties['{http://calendarserver.org/ns/}source']->getHref(),
1922
			'lastmodified' => time(),
1923
		];
1924
1925
		$propertiesBoolean = ['striptodos', 'stripalarms', 'stripattachments'];
1926
1927
		foreach($this->subscriptionPropertyMap as $xmlName=>$dbName) {
1928
			if (array_key_exists($xmlName, $properties)) {
1929
					$values[$dbName] = $properties[$xmlName];
1930
					if (in_array($dbName, $propertiesBoolean)) {
1931
						$values[$dbName] = true;
1932
				}
1933
			}
1934
		}
1935
1936
		$valuesToInsert = array();
1937
1938
		$query = $this->db->getQueryBuilder();
1939
1940
		foreach (array_keys($values) as $name) {
1941
			$valuesToInsert[$name] = $query->createNamedParameter($values[$name]);
1942
		}
1943
1944
		$query->insert('calendarsubscriptions')
1945
			->values($valuesToInsert)
1946
			->execute();
1947
1948
		$subscriptionId = $this->db->lastInsertId('*PREFIX*calendarsubscriptions');
1949
1950
		$this->dispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createSubscription', new GenericEvent(
1951
			'\OCA\DAV\CalDAV\CalDavBackend::createSubscription',
1952
			[
1953
				'subscriptionId' => $subscriptionId,
1954
				'subscriptionData' => $this->getSubscriptionById($subscriptionId),
1955
			]));
1956
1957
		return $subscriptionId;
1958
	}
1959
1960
	/**
1961
	 * Updates a subscription
1962
	 *
1963
	 * The list of mutations is stored in a Sabre\DAV\PropPatch object.
1964
	 * To do the actual updates, you must tell this object which properties
1965
	 * you're going to process with the handle() method.
1966
	 *
1967
	 * Calling the handle method is like telling the PropPatch object "I
1968
	 * promise I can handle updating this property".
1969
	 *
1970
	 * Read the PropPatch documentation for more info and examples.
1971
	 *
1972
	 * @param mixed $subscriptionId
1973
	 * @param PropPatch $propPatch
1974
	 * @return void
1975
	 */
1976
	function updateSubscription($subscriptionId, PropPatch $propPatch) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
1977
		$supportedProperties = array_keys($this->subscriptionPropertyMap);
1978
		$supportedProperties[] = '{http://calendarserver.org/ns/}source';
1979
1980
		/**
1981
		 * @suppress SqlInjectionChecker
1982
		 */
1983
		$propPatch->handle($supportedProperties, function($mutations) use ($subscriptionId) {
1984
1985
			$newValues = [];
1986
1987
			foreach($mutations as $propertyName=>$propertyValue) {
1988
				if ($propertyName === '{http://calendarserver.org/ns/}source') {
1989
					$newValues['source'] = $propertyValue->getHref();
1990
				} else {
1991
					$fieldName = $this->subscriptionPropertyMap[$propertyName];
1992
					$newValues[$fieldName] = $propertyValue;
1993
				}
1994
			}
1995
1996
			$query = $this->db->getQueryBuilder();
1997
			$query->update('calendarsubscriptions')
1998
				->set('lastmodified', $query->createNamedParameter(time()));
1999
			foreach($newValues as $fieldName=>$value) {
2000
				$query->set($fieldName, $query->createNamedParameter($value));
2001
			}
2002
			$query->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
2003
				->execute();
2004
2005
			$this->dispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateSubscription', new GenericEvent(
2006
				'\OCA\DAV\CalDAV\CalDavBackend::updateSubscription',
2007
				[
2008
					'subscriptionId' => $subscriptionId,
2009
					'subscriptionData' => $this->getSubscriptionById($subscriptionId),
2010
					'propertyMutations' => $mutations,
2011
				]));
2012
2013
			return true;
2014
2015
		});
2016
	}
2017
2018
	/**
2019
	 * Deletes a subscription.
2020
	 *
2021
	 * @param mixed $subscriptionId
2022
	 * @return void
2023
	 */
2024
	function deleteSubscription($subscriptionId) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
2025
		$this->dispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteSubscription', new GenericEvent(
2026
			'\OCA\DAV\CalDAV\CalDavBackend::deleteSubscription',
2027
			[
2028
				'subscriptionId' => $subscriptionId,
2029
				'subscriptionData' => $this->getSubscriptionById($subscriptionId),
2030
			]));
2031
2032
		$query = $this->db->getQueryBuilder();
2033
		$query->delete('calendarsubscriptions')
2034
			->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
2035
			->execute();
2036
2037
		$query = $this->db->getQueryBuilder();
2038
		$query->delete('calendarobjects')
2039
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2040
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2041
			->execute();
2042
2043
		$query->delete('calendarchanges')
2044
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2045
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2046
			->execute();
2047
2048
		$query->delete($this->dbObjectPropertiesTable)
2049
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2050
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2051
			->execute();
2052
	}
2053
2054
	/**
2055
	 * Returns a single scheduling object for the inbox collection.
2056
	 *
2057
	 * The returned array should contain the following elements:
2058
	 *   * uri - A unique basename for the object. This will be used to
2059
	 *           construct a full uri.
2060
	 *   * calendardata - The iCalendar object
2061
	 *   * lastmodified - The last modification date. Can be an int for a unix
2062
	 *                    timestamp, or a PHP DateTime object.
2063
	 *   * etag - A unique token that must change if the object changed.
2064
	 *   * size - The size of the object, in bytes.
2065
	 *
2066
	 * @param string $principalUri
2067
	 * @param string $objectUri
2068
	 * @return array
2069
	 */
2070
	function getSchedulingObject($principalUri, $objectUri) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
2071
		$query = $this->db->getQueryBuilder();
2072
		$stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size'])
2073
			->from('schedulingobjects')
2074
			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
2075
			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
2076
			->execute();
2077
2078
		$row = $stmt->fetch(\PDO::FETCH_ASSOC);
2079
2080
		if(!$row) {
2081
			return null;
2082
		}
2083
2084
		return [
2085
				'uri'          => $row['uri'],
2086
				'calendardata' => $row['calendardata'],
2087
				'lastmodified' => $row['lastmodified'],
2088
				'etag'         => '"' . $row['etag'] . '"',
2089
				'size'         => (int)$row['size'],
2090
		];
2091
	}
2092
2093
	/**
2094
	 * Returns all scheduling objects for the inbox collection.
2095
	 *
2096
	 * These objects should be returned as an array. Every item in the array
2097
	 * should follow the same structure as returned from getSchedulingObject.
2098
	 *
2099
	 * The main difference is that 'calendardata' is optional.
2100
	 *
2101
	 * @param string $principalUri
2102
	 * @return array
2103
	 */
2104
	function getSchedulingObjects($principalUri) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
2105
		$query = $this->db->getQueryBuilder();
2106
		$stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size'])
2107
				->from('schedulingobjects')
2108
				->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
2109
				->execute();
2110
2111
		$result = [];
2112
		foreach($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) {
2113
			$result[] = [
2114
					'calendardata' => $row['calendardata'],
2115
					'uri'          => $row['uri'],
2116
					'lastmodified' => $row['lastmodified'],
2117
					'etag'         => '"' . $row['etag'] . '"',
2118
					'size'         => (int)$row['size'],
2119
			];
2120
		}
2121
2122
		return $result;
2123
	}
2124
2125
	/**
2126
	 * Deletes a scheduling object from the inbox collection.
2127
	 *
2128
	 * @param string $principalUri
2129
	 * @param string $objectUri
2130
	 * @return void
2131
	 */
2132
	function deleteSchedulingObject($principalUri, $objectUri) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
2133
		$query = $this->db->getQueryBuilder();
2134
		$query->delete('schedulingobjects')
2135
				->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
2136
				->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
2137
				->execute();
2138
	}
2139
2140
	/**
2141
	 * Creates a new scheduling object. This should land in a users' inbox.
2142
	 *
2143
	 * @param string $principalUri
2144
	 * @param string $objectUri
2145
	 * @param string $objectData
2146
	 * @return void
2147
	 */
2148
	function createSchedulingObject($principalUri, $objectUri, $objectData) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
2149
		$query = $this->db->getQueryBuilder();
2150
		$query->insert('schedulingobjects')
2151
			->values([
2152
				'principaluri' => $query->createNamedParameter($principalUri),
2153
				'calendardata' => $query->createNamedParameter($objectData, IQueryBuilder::PARAM_LOB),
2154
				'uri' => $query->createNamedParameter($objectUri),
2155
				'lastmodified' => $query->createNamedParameter(time()),
2156
				'etag' => $query->createNamedParameter(md5($objectData)),
2157
				'size' => $query->createNamedParameter(strlen($objectData))
2158
			])
2159
			->execute();
2160
	}
2161
2162
	/**
2163
	 * Adds a change record to the calendarchanges table.
2164
	 *
2165
	 * @param mixed $calendarId
2166
	 * @param string $objectUri
2167
	 * @param int $operation 1 = add, 2 = modify, 3 = delete.
2168
	 * @param int $calendarType
2169
	 * @return void
2170
	 */
2171
	protected function addChange($calendarId, $objectUri, $operation, $calendarType=self::CALENDAR_TYPE_CALENDAR) {
2172
		$table = $calendarType === self::CALENDAR_TYPE_CALENDAR ? 'calendars': 'calendarsubscriptions';
2173
2174
		$query = $this->db->getQueryBuilder();
2175
		$query->select('synctoken')
2176
			->from($table)
2177
			->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)));
2178
		$syncToken = (int)$query->execute()->fetchColumn();
2179
2180
		$query = $this->db->getQueryBuilder();
2181
		$query->insert('calendarchanges')
2182
			->values([
2183
				'uri' => $query->createNamedParameter($objectUri),
2184
				'synctoken' => $query->createNamedParameter($syncToken),
2185
				'calendarid' => $query->createNamedParameter($calendarId),
2186
				'operation' => $query->createNamedParameter($operation),
2187
				'calendartype' => $query->createNamedParameter($calendarType),
2188
			])
2189
			->execute();
2190
2191
		$stmt = $this->db->prepare("UPDATE `*PREFIX*$table` SET `synctoken` = `synctoken` + 1 WHERE `id` = ?");
2192
		$stmt->execute([
2193
			$calendarId
2194
		]);
2195
2196
	}
2197
2198
	/**
2199
	 * Parses some information from calendar objects, used for optimized
2200
	 * calendar-queries.
2201
	 *
2202
	 * Returns an array with the following keys:
2203
	 *   * etag - An md5 checksum of the object without the quotes.
2204
	 *   * size - Size of the object in bytes
2205
	 *   * componentType - VEVENT, VTODO or VJOURNAL
2206
	 *   * firstOccurence
2207
	 *   * lastOccurence
2208
	 *   * uid - value of the UID property
2209
	 *
2210
	 * @param string $calendarData
2211
	 * @return array
2212
	 */
2213
	public function getDenormalizedData($calendarData) {
2214
2215
		$vObject = Reader::read($calendarData);
2216
		$componentType = null;
2217
		$component = null;
2218
		$firstOccurrence = null;
2219
		$lastOccurrence = null;
2220
		$uid = null;
2221
		$classification = self::CLASSIFICATION_PUBLIC;
2222
		foreach($vObject->getComponents() as $component) {
2223
			if ($component->name!=='VTIMEZONE') {
2224
				$componentType = $component->name;
2225
				$uid = (string)$component->UID;
2226
				break;
2227
			}
2228
		}
2229
		if (!$componentType) {
2230
			throw new \Sabre\DAV\Exception\BadRequest('Calendar objects must have a VJOURNAL, VEVENT or VTODO component');
2231
		}
2232
		if ($componentType === 'VEVENT' && $component->DTSTART) {
2233
			$firstOccurrence = $component->DTSTART->getDateTime()->getTimeStamp();
2234
			// Finding the last occurrence is a bit harder
2235
			if (!isset($component->RRULE)) {
2236
				if (isset($component->DTEND)) {
2237
					$lastOccurrence = $component->DTEND->getDateTime()->getTimeStamp();
2238
				} elseif (isset($component->DURATION)) {
2239
					$endDate = clone $component->DTSTART->getDateTime();
2240
					$endDate->add(DateTimeParser::parse($component->DURATION->getValue()));
2241
					$lastOccurrence = $endDate->getTimeStamp();
2242
				} elseif (!$component->DTSTART->hasTime()) {
2243
					$endDate = clone $component->DTSTART->getDateTime();
2244
					$endDate->modify('+1 day');
2245
					$lastOccurrence = $endDate->getTimeStamp();
2246
				} else {
2247
					$lastOccurrence = $firstOccurrence;
2248
				}
2249
			} else {
2250
				$it = new EventIterator($vObject, (string)$component->UID);
2251
				$maxDate = new \DateTime(self::MAX_DATE);
2252
				if ($it->isInfinite()) {
2253
					$lastOccurrence = $maxDate->getTimestamp();
2254
				} else {
2255
					$end = $it->getDtEnd();
2256
					while($it->valid() && $end < $maxDate) {
2257
						$end = $it->getDtEnd();
2258
						$it->next();
2259
2260
					}
2261
					$lastOccurrence = $end->getTimestamp();
2262
				}
2263
2264
			}
2265
		}
2266
2267
		if ($component->CLASS) {
2268
			$classification = CalDavBackend::CLASSIFICATION_PRIVATE;
2269
			switch ($component->CLASS->getValue()) {
2270
				case 'PUBLIC':
2271
					$classification = CalDavBackend::CLASSIFICATION_PUBLIC;
2272
					break;
2273
				case 'CONFIDENTIAL':
2274
					$classification = CalDavBackend::CLASSIFICATION_CONFIDENTIAL;
2275
					break;
2276
			}
2277
		}
2278
		return [
2279
			'etag' => md5($calendarData),
2280
			'size' => strlen($calendarData),
2281
			'componentType' => $componentType,
2282
			'firstOccurence' => is_null($firstOccurrence) ? null : max(0, $firstOccurrence),
2283
			'lastOccurence'  => $lastOccurrence,
2284
			'uid' => $uid,
2285
			'classification' => $classification
2286
		];
2287
2288
	}
2289
2290
	/**
2291
	 * @param $cardData
2292
	 * @return bool|string
2293
	 */
2294
	private function readBlob($cardData) {
2295
		if (is_resource($cardData)) {
2296
			return stream_get_contents($cardData);
2297
		}
2298
2299
		return $cardData;
2300
	}
2301
2302
	/**
2303
	 * @param IShareable $shareable
2304
	 * @param array $add
2305
	 * @param array $remove
2306
	 */
2307
	public function updateShares($shareable, $add, $remove) {
2308
		$calendarId = $shareable->getResourceId();
2309
		$this->dispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateShares', new GenericEvent(
2310
			'\OCA\DAV\CalDAV\CalDavBackend::updateShares',
2311
			[
2312
				'calendarId' => $calendarId,
2313
				'calendarData' => $this->getCalendarById($calendarId),
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->getCalendarById($calendarId) targeting OCA\DAV\CalDAV\CalDavBackend::getCalendarById() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
2314
				'shares' => $this->getShares($calendarId),
2315
				'add' => $add,
2316
				'remove' => $remove,
2317
			]));
2318
		$this->calendarSharingBackend->updateShares($shareable, $add, $remove);
2319
	}
2320
2321
	/**
2322
	 * @param int $resourceId
2323
	 * @param int $calendarType
2324
	 * @return array
2325
	 */
2326
	public function getShares($resourceId, $calendarType=self::CALENDAR_TYPE_CALENDAR) {
0 ignored issues
show
Unused Code introduced by
The parameter $calendarType is not used and could be removed. ( Ignorable by Annotation )

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

2326
	public function getShares($resourceId, /** @scrutinizer ignore-unused */ $calendarType=self::CALENDAR_TYPE_CALENDAR) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
2327
		return $this->calendarSharingBackend->getShares($resourceId);
2328
	}
2329
2330
	/**
2331
	 * @param boolean $value
2332
	 * @param \OCA\DAV\CalDAV\Calendar $calendar
2333
	 * @return string|null
2334
	 */
2335
	public function setPublishStatus($value, $calendar) {
2336
2337
		$calendarId = $calendar->getResourceId();
2338
		$this->dispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::publishCalendar', new GenericEvent(
2339
			'\OCA\DAV\CalDAV\CalDavBackend::updateShares',
2340
			[
2341
				'calendarId' => $calendarId,
2342
				'calendarData' => $this->getCalendarById($calendarId),
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->getCalendarById($calendarId) targeting OCA\DAV\CalDAV\CalDavBackend::getCalendarById() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
2343
				'public' => $value,
2344
			]));
2345
2346
		$query = $this->db->getQueryBuilder();
2347
		if ($value) {
2348
			$publicUri = $this->random->generate(16, ISecureRandom::CHAR_HUMAN_READABLE);
2349
			$query->insert('dav_shares')
2350
				->values([
2351
					'principaluri' => $query->createNamedParameter($calendar->getPrincipalURI()),
2352
					'type' => $query->createNamedParameter('calendar'),
2353
					'access' => $query->createNamedParameter(self::ACCESS_PUBLIC),
2354
					'resourceid' => $query->createNamedParameter($calendar->getResourceId()),
2355
					'publicuri' => $query->createNamedParameter($publicUri)
2356
				]);
2357
			$query->execute();
2358
			return $publicUri;
2359
		}
2360
		$query->delete('dav_shares')
2361
			->where($query->expr()->eq('resourceid', $query->createNamedParameter($calendar->getResourceId())))
2362
			->andWhere($query->expr()->eq('access', $query->createNamedParameter(self::ACCESS_PUBLIC)));
2363
		$query->execute();
2364
		return null;
2365
	}
2366
2367
	/**
2368
	 * @param \OCA\DAV\CalDAV\Calendar $calendar
2369
	 * @return mixed
2370
	 */
2371
	public function getPublishStatus($calendar) {
2372
		$query = $this->db->getQueryBuilder();
2373
		$result = $query->select('publicuri')
2374
			->from('dav_shares')
2375
			->where($query->expr()->eq('resourceid', $query->createNamedParameter($calendar->getResourceId())))
2376
			->andWhere($query->expr()->eq('access', $query->createNamedParameter(self::ACCESS_PUBLIC)))
2377
			->execute();
2378
2379
		$row = $result->fetch();
2380
		$result->closeCursor();
2381
		return $row ? reset($row) : false;
2382
	}
2383
2384
	/**
2385
	 * @param int $resourceId
2386
	 * @param array $acl
2387
	 * @return array
2388
	 */
2389
	public function applyShareAcl($resourceId, $acl) {
2390
		return $this->calendarSharingBackend->applyShareAcl($resourceId, $acl);
2391
	}
2392
2393
2394
2395
	/**
2396
	 * update properties table
2397
	 *
2398
	 * @param int $calendarId
2399
	 * @param string $objectUri
2400
	 * @param string $calendarData
2401
	 * @param int $calendarType
2402
	 */
2403
	public function updateProperties($calendarId, $objectUri, $calendarData, $calendarType=self::CALENDAR_TYPE_CALENDAR) {
2404
		$objectId = $this->getCalendarObjectId($calendarId, $objectUri, $calendarType);
2405
2406
		try {
2407
			$vCalendar = $this->readCalendarData($calendarData);
2408
		} catch (\Exception $ex) {
2409
			return;
2410
		}
2411
2412
		$this->purgeProperties($calendarId, $objectId);
2413
2414
		$query = $this->db->getQueryBuilder();
2415
		$query->insert($this->dbObjectPropertiesTable)
2416
			->values(
2417
				[
2418
					'calendarid' => $query->createNamedParameter($calendarId),
2419
					'calendartype' => $query->createNamedParameter($calendarType),
2420
					'objectid' => $query->createNamedParameter($objectId),
2421
					'name' => $query->createParameter('name'),
2422
					'parameter' => $query->createParameter('parameter'),
2423
					'value' => $query->createParameter('value'),
2424
				]
2425
			);
2426
2427
		$indexComponents = ['VEVENT', 'VJOURNAL', 'VTODO'];
2428
		foreach ($vCalendar->getComponents() as $component) {
2429
			if (!in_array($component->name, $indexComponents)) {
2430
				continue;
2431
			}
2432
2433
			foreach ($component->children() as $property) {
2434
				if (in_array($property->name, self::$indexProperties)) {
2435
					$value = $property->getValue();
2436
					// is this a shitty db?
2437
					if (!$this->db->supports4ByteText()) {
2438
						$value = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $value);
2439
					}
2440
					$value = mb_substr($value, 0, 254);
2441
2442
					$query->setParameter('name', $property->name);
2443
					$query->setParameter('parameter', null);
2444
					$query->setParameter('value', $value);
2445
					$query->execute();
2446
				}
2447
2448
				if (array_key_exists($property->name, self::$indexParameters)) {
2449
					$parameters = $property->parameters();
2450
					$indexedParametersForProperty = self::$indexParameters[$property->name];
2451
2452
					foreach ($parameters as $key => $value) {
2453
						if (in_array($key, $indexedParametersForProperty)) {
2454
							// is this a shitty db?
2455
							if ($this->db->supports4ByteText()) {
2456
								$value = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $value);
2457
							}
2458
							$value = mb_substr($value, 0, 254);
2459
2460
							$query->setParameter('name', $property->name);
2461
							$query->setParameter('parameter', substr($key, 0, 254));
2462
							$query->setParameter('value', substr($value, 0, 254));
2463
							$query->execute();
2464
						}
2465
					}
2466
				}
2467
			}
2468
		}
2469
	}
2470
2471
	/**
2472
	 * deletes all birthday calendars
2473
	 */
2474
	public function deleteAllBirthdayCalendars() {
2475
		$query = $this->db->getQueryBuilder();
2476
		$result = $query->select(['id'])->from('calendars')
2477
			->where($query->expr()->eq('uri', $query->createNamedParameter(BirthdayService::BIRTHDAY_CALENDAR_URI)))
2478
			->execute();
2479
2480
		$ids = $result->fetchAll();
2481
		foreach($ids as $id) {
2482
			$this->deleteCalendar($id['id']);
2483
		}
2484
	}
2485
2486
	/**
2487
	 * @param $subscriptionId
2488
	 */
2489
	public function purgeAllCachedEventsForSubscription($subscriptionId) {
2490
		$query = $this->db->getQueryBuilder();
2491
		$query->select('uri')
2492
			->from('calendarobjects')
2493
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2494
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)));
2495
		$stmt = $query->execute();
2496
2497
		$uris = [];
2498
		foreach($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) {
2499
			$uris[] = $row['uri'];
2500
		}
2501
		$stmt->closeCursor();
2502
2503
		$query = $this->db->getQueryBuilder();
2504
		$query->delete('calendarobjects')
2505
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2506
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2507
			->execute();
2508
2509
		$query->delete('calendarchanges')
2510
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2511
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2512
			->execute();
2513
2514
		$query->delete($this->dbObjectPropertiesTable)
2515
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2516
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2517
			->execute();
2518
2519
		foreach($uris as $uri) {
2520
			$this->addChange($subscriptionId, $uri, 3, self::CALENDAR_TYPE_SUBSCRIPTION);
2521
		}
2522
	}
2523
2524
	/**
2525
	 * Move a calendar from one user to another
2526
	 *
2527
	 * @param string $uriName
2528
	 * @param string $uriOrigin
2529
	 * @param string $uriDestination
2530
	 */
2531
	public function moveCalendar($uriName, $uriOrigin, $uriDestination)
2532
	{
2533
		$query = $this->db->getQueryBuilder();
2534
		$query->update('calendars')
2535
			->set('principaluri', $query->createNamedParameter($uriDestination))
2536
			->where($query->expr()->eq('principaluri', $query->createNamedParameter($uriOrigin)))
2537
			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($uriName)))
2538
			->execute();
2539
	}
2540
2541
	/**
2542
	 * read VCalendar data into a VCalendar object
2543
	 *
2544
	 * @param string $objectData
2545
	 * @return VCalendar
2546
	 */
2547
	protected function readCalendarData($objectData) {
2548
		return Reader::read($objectData);
2549
	}
2550
2551
	/**
2552
	 * delete all properties from a given calendar object
2553
	 *
2554
	 * @param int $calendarId
2555
	 * @param int $objectId
2556
	 */
2557
	protected function purgeProperties($calendarId, $objectId) {
2558
		$query = $this->db->getQueryBuilder();
2559
		$query->delete($this->dbObjectPropertiesTable)
2560
			->where($query->expr()->eq('objectid', $query->createNamedParameter($objectId)))
2561
			->andWhere($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)));
2562
		$query->execute();
2563
	}
2564
2565
	/**
2566
	 * get ID from a given calendar object
2567
	 *
2568
	 * @param int $calendarId
2569
	 * @param string $uri
2570
	 * @param int $calendarType
2571
	 * @return int
2572
	 */
2573
	protected function getCalendarObjectId($calendarId, $uri, $calendarType):int {
2574
		$query = $this->db->getQueryBuilder();
2575
		$query->select('id')
2576
			->from('calendarobjects')
2577
			->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
2578
			->andWhere($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
2579
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)));
2580
2581
		$result = $query->execute();
2582
		$objectIds = $result->fetch();
2583
		$result->closeCursor();
2584
2585
		if (!isset($objectIds['id'])) {
2586
			throw new \InvalidArgumentException('Calendarobject does not exists: ' . $uri);
2587
		}
2588
2589
		return (int)$objectIds['id'];
2590
	}
2591
2592
	/**
2593
	 * return legacy endpoint principal name to new principal name
2594
	 *
2595
	 * @param $principalUri
2596
	 * @param $toV2
2597
	 * @return string
2598
	 */
2599
	private function convertPrincipal($principalUri, $toV2) {
2600
		if ($this->principalBackend->getPrincipalPrefix() === 'principals') {
2601
			list(, $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

2601
			list(, $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...
Deprecated Code introduced by
The function split() has been deprecated: 5.3.0 Use preg_split() instead ( Ignorable by Annotation )

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

2601
			list(, $name) = /** @scrutinizer ignore-deprecated */ Uri\split($principalUri);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
2602
			if ($toV2 === true) {
2603
				return "principals/users/$name";
2604
			}
2605
			return "principals/$name";
2606
		}
2607
		return $principalUri;
2608
	}
2609
2610
	/**
2611
	 * adds information about an owner to the calendar data
2612
	 *
2613
	 * @param $calendarInfo
2614
	 */
2615
	private function addOwnerPrincipal(&$calendarInfo) {
2616
		$ownerPrincipalKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal';
2617
		$displaynameKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}owner-displayname';
2618
		if (isset($calendarInfo[$ownerPrincipalKey])) {
2619
			$uri = $calendarInfo[$ownerPrincipalKey];
2620
		} else {
2621
			$uri = $calendarInfo['principaluri'];
2622
		}
2623
2624
		$principalInformation = $this->principalBackend->getPrincipalByPath($uri);
2625
		if (isset($principalInformation['{DAV:}displayname'])) {
2626
			$calendarInfo[$displaynameKey] = $principalInformation['{DAV:}displayname'];
2627
		}
2628
	}
2629
}
2630