Passed
Push — master ( 6675f9...995692 )
by Roeland
21:02 queued 09:19
created

CalDavBackend::search()   F

Complexity

Conditions 15
Paths 320

Size

Total Lines 94
Code Lines 65

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
cc 15
eloc 65
nc 320
nop 6
dl 0
loc 94
rs 3.5833
c 2
b 1
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

351
			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

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

473
			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

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

539
		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

539
		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...
540
		$row['displayname'] = $row['displayname'] . ' ' . "($name)";
541
		$components = [];
542
		if ($row['components']) {
543
			$components = explode(',',$row['components']);
544
		}
545
		$calendar = [
546
			'id' => $row['id'],
547
			'uri' => $row['publicuri'],
548
			'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
549
			'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
550
			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
551
			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
552
			'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
553
			'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
554
			'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ,
555
			'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}public' => (int)$row['access'] === self::ACCESS_PUBLIC,
556
		];
557
558
		foreach($this->propertyMap as $xmlName=>$dbName) {
559
			$calendar[$xmlName] = $row[$dbName];
560
		}
561
562
		$this->addOwnerPrincipal($calendar);
563
564
		return $calendar;
565
566
	}
567
568
	/**
569
	 * @param string $principal
570
	 * @param string $uri
571
	 * @return array|null
572
	 */
573
	public function getCalendarByUri($principal, $uri) {
574
		$fields = array_values($this->propertyMap);
575
		$fields[] = 'id';
576
		$fields[] = 'uri';
577
		$fields[] = 'synctoken';
578
		$fields[] = 'components';
579
		$fields[] = 'principaluri';
580
		$fields[] = 'transparent';
581
582
		// Making fields a comma-delimited list
583
		$query = $this->db->getQueryBuilder();
584
		$query->select($fields)->from('calendars')
585
			->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
586
			->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($principal)))
587
			->setMaxResults(1);
588
		$stmt = $query->execute();
589
590
		$row = $stmt->fetch(\PDO::FETCH_ASSOC);
591
		$stmt->closeCursor();
592
		if ($row === false) {
593
			return null;
594
		}
595
596
		$components = [];
597
		if ($row['components']) {
598
			$components = explode(',',$row['components']);
599
		}
600
601
		$calendar = [
602
			'id' => $row['id'],
603
			'uri' => $row['uri'],
604
			'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
605
			'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
606
			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
607
			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
608
			'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
609
		];
610
611
		foreach($this->propertyMap as $xmlName=>$dbName) {
612
			$calendar[$xmlName] = $row[$dbName];
613
		}
614
615
		$this->addOwnerPrincipal($calendar);
616
617
		return $calendar;
618
	}
619
620
	/**
621
	 * @param $calendarId
622
	 * @return array|null
623
	 */
624
	public function getCalendarById($calendarId) {
625
		$fields = array_values($this->propertyMap);
626
		$fields[] = 'id';
627
		$fields[] = 'uri';
628
		$fields[] = 'synctoken';
629
		$fields[] = 'components';
630
		$fields[] = 'principaluri';
631
		$fields[] = 'transparent';
632
633
		// Making fields a comma-delimited list
634
		$query = $this->db->getQueryBuilder();
635
		$query->select($fields)->from('calendars')
636
			->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)))
637
			->setMaxResults(1);
638
		$stmt = $query->execute();
639
640
		$row = $stmt->fetch(\PDO::FETCH_ASSOC);
641
		$stmt->closeCursor();
642
		if ($row === false) {
643
			return null;
644
		}
645
646
		$components = [];
647
		if ($row['components']) {
648
			$components = explode(',',$row['components']);
649
		}
650
651
		$calendar = [
652
			'id' => $row['id'],
653
			'uri' => $row['uri'],
654
			'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
655
			'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
656
			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
657
			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
658
			'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
659
		];
660
661
		foreach($this->propertyMap as $xmlName=>$dbName) {
662
			$calendar[$xmlName] = $row[$dbName];
663
		}
664
665
		$this->addOwnerPrincipal($calendar);
666
667
		return $calendar;
668
	}
669
670
	/**
671
	 * @param $subscriptionId
672
	 */
673
	public function getSubscriptionById($subscriptionId) {
674
		$fields = array_values($this->subscriptionPropertyMap);
675
		$fields[] = 'id';
676
		$fields[] = 'uri';
677
		$fields[] = 'source';
678
		$fields[] = 'synctoken';
679
		$fields[] = 'principaluri';
680
		$fields[] = 'lastmodified';
681
682
		$query = $this->db->getQueryBuilder();
683
		$query->select($fields)
684
			->from('calendarsubscriptions')
685
			->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
686
			->orderBy('calendarorder', 'asc');
687
		$stmt =$query->execute();
688
689
		$row = $stmt->fetch(\PDO::FETCH_ASSOC);
690
		$stmt->closeCursor();
691
		if ($row === false) {
692
			return null;
693
		}
694
695
		$subscription = [
696
			'id'           => $row['id'],
697
			'uri'          => $row['uri'],
698
			'principaluri' => $row['principaluri'],
699
			'source'       => $row['source'],
700
			'lastmodified' => $row['lastmodified'],
701
			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
702
			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
703
		];
704
705
		foreach($this->subscriptionPropertyMap as $xmlName=>$dbName) {
706
			if (!is_null($row[$dbName])) {
707
				$subscription[$xmlName] = $row[$dbName];
708
			}
709
		}
710
711
		return $subscription;
712
	}
713
714
	/**
715
	 * Creates a new calendar for a principal.
716
	 *
717
	 * If the creation was a success, an id must be returned that can be used to reference
718
	 * this calendar in other methods, such as updateCalendar.
719
	 *
720
	 * @param string $principalUri
721
	 * @param string $calendarUri
722
	 * @param array $properties
723
	 * @return int
724
	 * @suppress SqlInjectionChecker
725
	 */
726
	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...
727
		$values = [
728
			'principaluri' => $this->convertPrincipal($principalUri, true),
729
			'uri'          => $calendarUri,
730
			'synctoken'    => 1,
731
			'transparent'  => 0,
732
			'components'   => 'VEVENT,VTODO',
733
			'displayname'  => $calendarUri
734
		];
735
736
		// Default value
737
		$sccs = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set';
738
		if (isset($properties[$sccs])) {
739
			if (!($properties[$sccs] instanceof SupportedCalendarComponentSet)) {
740
				throw new DAV\Exception('The ' . $sccs . ' property must be of type: \Sabre\CalDAV\Property\SupportedCalendarComponentSet');
741
			}
742
			$values['components'] = implode(',',$properties[$sccs]->getValue());
743
		} else if (isset($properties['components'])) {
744
			// Allow to provide components internally without having
745
			// to create a SupportedCalendarComponentSet object
746
			$values['components'] = $properties['components'];
747
		}
748
749
		$transp = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp';
750
		if (isset($properties[$transp])) {
751
			$values['transparent'] = (int) ($properties[$transp]->getValue() === 'transparent');
752
		}
753
754
		foreach($this->propertyMap as $xmlName=>$dbName) {
755
			if (isset($properties[$xmlName])) {
756
				$values[$dbName] = $properties[$xmlName];
757
			}
758
		}
759
760
		$query = $this->db->getQueryBuilder();
761
		$query->insert('calendars');
762
		foreach($values as $column => $value) {
763
			$query->setValue($column, $query->createNamedParameter($value));
764
		}
765
		$query->execute();
766
		$calendarId = $query->getLastInsertId();
767
768
		$this->dispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createCalendar', new GenericEvent(
0 ignored issues
show
Bug introduced by
'\OCA\DAV\CalDAV\CalDavBackend::createCalendar' of type string is incompatible with the type object expected by parameter $event of Symfony\Contracts\EventD...erInterface::dispatch(). ( Ignorable by Annotation )

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

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

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

768
		$this->dispatcher->/** @scrutinizer ignore-call */ 
769
                     dispatch('\OCA\DAV\CalDAV\CalDavBackend::createCalendar', new GenericEvent(

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
769
			'\OCA\DAV\CalDAV\CalDavBackend::createCalendar',
770
			[
771
				'calendarId' => $calendarId,
772
				'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...
773
		]));
774
775
		return $calendarId;
776
	}
777
778
	/**
779
	 * Updates properties for a calendar.
780
	 *
781
	 * The list of mutations is stored in a Sabre\DAV\PropPatch object.
782
	 * To do the actual updates, you must tell this object which properties
783
	 * you're going to process with the handle() method.
784
	 *
785
	 * Calling the handle method is like telling the PropPatch object "I
786
	 * promise I can handle updating this property".
787
	 *
788
	 * Read the PropPatch documentation for more info and examples.
789
	 *
790
	 * @param mixed $calendarId
791
	 * @param PropPatch $propPatch
792
	 * @return void
793
	 */
794
	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...
795
		$supportedProperties = array_keys($this->propertyMap);
796
		$supportedProperties[] = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp';
797
798
		/**
799
		 * @suppress SqlInjectionChecker
800
		 */
801
		$propPatch->handle($supportedProperties, function($mutations) use ($calendarId) {
802
			$newValues = [];
803
			foreach ($mutations as $propertyName => $propertyValue) {
804
805
				switch ($propertyName) {
806
					case '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' :
807
						$fieldName = 'transparent';
808
						$newValues[$fieldName] = (int) ($propertyValue->getValue() === 'transparent');
809
						break;
810
					default :
811
						$fieldName = $this->propertyMap[$propertyName];
812
						$newValues[$fieldName] = $propertyValue;
813
						break;
814
				}
815
816
			}
817
			$query = $this->db->getQueryBuilder();
818
			$query->update('calendars');
819
			foreach ($newValues as $fieldName => $value) {
820
				$query->set($fieldName, $query->createNamedParameter($value));
821
			}
822
			$query->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)));
823
			$query->execute();
824
825
			$this->addChange($calendarId, "", 2);
826
827
			$this->dispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateCalendar', new GenericEvent(
0 ignored issues
show
Bug introduced by
'\OCA\DAV\CalDAV\CalDavBackend::updateCalendar' of type string is incompatible with the type object expected by parameter $event of Symfony\Contracts\EventD...erInterface::dispatch(). ( Ignorable by Annotation )

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

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

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

827
			$this->dispatcher->/** @scrutinizer ignore-call */ 
828
                      dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateCalendar', new GenericEvent(

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
828
				'\OCA\DAV\CalDAV\CalDavBackend::updateCalendar',
829
				[
830
					'calendarId' => $calendarId,
831
					'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...
832
					'shares' => $this->getShares($calendarId),
833
					'propertyMutations' => $mutations,
834
			]));
835
836
			return true;
837
		});
838
	}
839
840
	/**
841
	 * Delete a calendar and all it's objects
842
	 *
843
	 * @param mixed $calendarId
844
	 * @return void
845
	 */
846
	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...
847
		$this->dispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteCalendar', new GenericEvent(
0 ignored issues
show
Bug introduced by
'\OCA\DAV\CalDAV\CalDavBackend::deleteCalendar' of type string is incompatible with the type object expected by parameter $event of Symfony\Contracts\EventD...erInterface::dispatch(). ( Ignorable by Annotation )

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

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

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

847
		$this->dispatcher->/** @scrutinizer ignore-call */ 
848
                     dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteCalendar', new GenericEvent(

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
848
			'\OCA\DAV\CalDAV\CalDavBackend::deleteCalendar',
849
			[
850
				'calendarId' => $calendarId,
851
				'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...
852
				'shares' => $this->getShares($calendarId),
853
		]));
854
855
		$stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ? AND `calendartype` = ?');
856
		$stmt->execute([$calendarId, self::CALENDAR_TYPE_CALENDAR]);
857
858
		$stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendars` WHERE `id` = ?');
859
		$stmt->execute([$calendarId]);
860
861
		$stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarchanges` WHERE `calendarid` = ? AND `calendartype` = ?');
862
		$stmt->execute([$calendarId, self::CALENDAR_TYPE_CALENDAR]);
863
864
		$this->calendarSharingBackend->deleteAllShares($calendarId);
865
866
		$query = $this->db->getQueryBuilder();
867
		$query->delete($this->dbObjectPropertiesTable)
868
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
869
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)))
870
			->execute();
871
	}
872
873
	/**
874
	 * Delete all of an user's shares
875
	 *
876
	 * @param string $principaluri
877
	 * @return void
878
	 */
879
	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...
880
		$this->calendarSharingBackend->deleteAllSharesByUser($principaluri);
881
	}
882
883
	/**
884
	 * Returns all calendar objects within a calendar.
885
	 *
886
	 * Every item contains an array with the following keys:
887
	 *   * calendardata - The iCalendar-compatible calendar data
888
	 *   * uri - a unique key which will be used to construct the uri. This can
889
	 *     be any arbitrary string, but making sure it ends with '.ics' is a
890
	 *     good idea. This is only the basename, or filename, not the full
891
	 *     path.
892
	 *   * lastmodified - a timestamp of the last modification time
893
	 *   * etag - An arbitrary string, surrounded by double-quotes. (e.g.:
894
	 *   '"abcdef"')
895
	 *   * size - The size of the calendar objects, in bytes.
896
	 *   * component - optional, a string containing the type of object, such
897
	 *     as 'vevent' or 'vtodo'. If specified, this will be used to populate
898
	 *     the Content-Type header.
899
	 *
900
	 * Note that the etag is optional, but it's highly encouraged to return for
901
	 * speed reasons.
902
	 *
903
	 * The calendardata is also optional. If it's not returned
904
	 * 'getCalendarObject' will be called later, which *is* expected to return
905
	 * calendardata.
906
	 *
907
	 * If neither etag or size are specified, the calendardata will be
908
	 * used/fetched to determine these numbers. If both are specified the
909
	 * amount of times this is needed is reduced by a great degree.
910
	 *
911
	 * @param mixed $id
912
	 * @param int $calendarType
913
	 * @return array
914
	 */
915
	public function getCalendarObjects($id, $calendarType=self::CALENDAR_TYPE_CALENDAR):array {
916
		$query = $this->db->getQueryBuilder();
917
		$query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'componenttype', 'classification'])
918
			->from('calendarobjects')
919
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($id)))
920
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)));
921
		$stmt = $query->execute();
922
923
		$result = [];
924
		foreach($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) {
925
			$result[] = [
926
				'id'           => $row['id'],
927
				'uri'          => $row['uri'],
928
				'lastmodified' => $row['lastmodified'],
929
				'etag'         => '"' . $row['etag'] . '"',
930
				'calendarid'   => $row['calendarid'],
931
				'size'         => (int)$row['size'],
932
				'component'    => strtolower($row['componenttype']),
933
				'classification'=> (int)$row['classification']
934
			];
935
		}
936
937
		return $result;
938
	}
939
940
	/**
941
	 * Returns information from a single calendar object, based on it's object
942
	 * uri.
943
	 *
944
	 * The object uri is only the basename, or filename and not a full path.
945
	 *
946
	 * The returned array must have the same keys as getCalendarObjects. The
947
	 * 'calendardata' object is required here though, while it's not required
948
	 * for getCalendarObjects.
949
	 *
950
	 * This method must return null if the object did not exist.
951
	 *
952
	 * @param mixed $id
953
	 * @param string $objectUri
954
	 * @param int $calendarType
955
	 * @return array|null
956
	 */
957
	public function getCalendarObject($id, $objectUri, $calendarType=self::CALENDAR_TYPE_CALENDAR) {
958
		$query = $this->db->getQueryBuilder();
959
		$query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification'])
960
			->from('calendarobjects')
961
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($id)))
962
			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
963
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)));
964
		$stmt = $query->execute();
965
		$row = $stmt->fetch(\PDO::FETCH_ASSOC);
966
967
		if(!$row) {
968
			return null;
969
		}
970
971
		return [
972
			'id'            => $row['id'],
973
			'uri'           => $row['uri'],
974
			'lastmodified'  => $row['lastmodified'],
975
			'etag'          => '"' . $row['etag'] . '"',
976
			'calendarid'    => $row['calendarid'],
977
			'size'          => (int)$row['size'],
978
			'calendardata'  => $this->readBlob($row['calendardata']),
979
			'component'     => strtolower($row['componenttype']),
980
			'classification'=> (int)$row['classification']
981
		];
982
	}
983
984
	/**
985
	 * Returns a list of calendar objects.
986
	 *
987
	 * This method should work identical to getCalendarObject, but instead
988
	 * return all the calendar objects in the list as an array.
989
	 *
990
	 * If the backend supports this, it may allow for some speed-ups.
991
	 *
992
	 * @param mixed $calendarId
993
	 * @param string[] $uris
994
	 * @param int $calendarType
995
	 * @return array
996
	 */
997
	public function getMultipleCalendarObjects($id, array $uris, $calendarType=self::CALENDAR_TYPE_CALENDAR):array {
998
		if (empty($uris)) {
999
			return [];
1000
		}
1001
1002
		$chunks = array_chunk($uris, 100);
1003
		$objects = [];
1004
1005
		$query = $this->db->getQueryBuilder();
1006
		$query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification'])
1007
			->from('calendarobjects')
1008
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($id)))
1009
			->andWhere($query->expr()->in('uri', $query->createParameter('uri')))
1010
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)));
1011
1012
		foreach ($chunks as $uris) {
1013
			$query->setParameter('uri', $uris, IQueryBuilder::PARAM_STR_ARRAY);
1014
			$result = $query->execute();
1015
1016
			while ($row = $result->fetch()) {
1017
				$objects[] = [
1018
					'id'           => $row['id'],
1019
					'uri'          => $row['uri'],
1020
					'lastmodified' => $row['lastmodified'],
1021
					'etag'         => '"' . $row['etag'] . '"',
1022
					'calendarid'   => $row['calendarid'],
1023
					'size'         => (int)$row['size'],
1024
					'calendardata' => $this->readBlob($row['calendardata']),
1025
					'component'    => strtolower($row['componenttype']),
1026
					'classification' => (int)$row['classification']
1027
				];
1028
			}
1029
			$result->closeCursor();
1030
		}
1031
1032
		return $objects;
1033
	}
1034
1035
	/**
1036
	 * Creates a new calendar object.
1037
	 *
1038
	 * The object uri is only the basename, or filename and not a full path.
1039
	 *
1040
	 * It is possible return an etag from this function, which will be used in
1041
	 * the response to this PUT request. Note that the ETag must be surrounded
1042
	 * by double-quotes.
1043
	 *
1044
	 * However, you should only really return this ETag if you don't mangle the
1045
	 * calendar-data. If the result of a subsequent GET to this object is not
1046
	 * the exact same as this request body, you should omit the ETag.
1047
	 *
1048
	 * @param mixed $calendarId
1049
	 * @param string $objectUri
1050
	 * @param string $calendarData
1051
	 * @param int $calendarType
1052
	 * @return string
1053
	 */
1054
	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...
1055
		$extraData = $this->getDenormalizedData($calendarData);
1056
1057
		$q = $this->db->getQueryBuilder();
1058
		$q->select($q->func()->count('*'))
1059
			->from('calendarobjects')
1060
			->where($q->expr()->eq('calendarid', $q->createNamedParameter($calendarId)))
1061
			->andWhere($q->expr()->eq('uid', $q->createNamedParameter($extraData['uid'])))
1062
			->andWhere($q->expr()->eq('calendartype', $q->createNamedParameter($calendarType)));
1063
1064
		$result = $q->execute();
1065
		$count = (int) $result->fetchColumn();
1066
		$result->closeCursor();
1067
1068
		if ($count !== 0) {
1069
			throw new \Sabre\DAV\Exception\BadRequest('Calendar object with uid already exists in this calendar collection.');
1070
		}
1071
1072
		$query = $this->db->getQueryBuilder();
1073
		$query->insert('calendarobjects')
1074
			->values([
1075
				'calendarid' => $query->createNamedParameter($calendarId),
1076
				'uri' => $query->createNamedParameter($objectUri),
1077
				'calendardata' => $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB),
1078
				'lastmodified' => $query->createNamedParameter(time()),
1079
				'etag' => $query->createNamedParameter($extraData['etag']),
1080
				'size' => $query->createNamedParameter($extraData['size']),
1081
				'componenttype' => $query->createNamedParameter($extraData['componentType']),
1082
				'firstoccurence' => $query->createNamedParameter($extraData['firstOccurence']),
1083
				'lastoccurence' => $query->createNamedParameter($extraData['lastOccurence']),
1084
				'classification' => $query->createNamedParameter($extraData['classification']),
1085
				'uid' => $query->createNamedParameter($extraData['uid']),
1086
				'calendartype' => $query->createNamedParameter($calendarType),
1087
			])
1088
			->execute();
1089
1090
		$this->updateProperties($calendarId, $objectUri, $calendarData, $calendarType);
1091
1092
		if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
1093
			$this->dispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createCalendarObject', new GenericEvent(
0 ignored issues
show
Bug introduced by
'\OCA\DAV\CalDAV\CalDavB...::createCalendarObject' of type string is incompatible with the type object expected by parameter $event of Symfony\Contracts\EventD...erInterface::dispatch(). ( Ignorable by Annotation )

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

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

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

1093
			$this->dispatcher->/** @scrutinizer ignore-call */ 
1094
                      dispatch('\OCA\DAV\CalDAV\CalDavBackend::createCalendarObject', new GenericEvent(

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
1094
				'\OCA\DAV\CalDAV\CalDavBackend::createCalendarObject',
1095
				[
1096
					'calendarId' => $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
		} else {
1103
			$this->dispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createCachedCalendarObject', new GenericEvent(
1104
				'\OCA\DAV\CalDAV\CalDavBackend::createCachedCalendarObject',
1105
				[
1106
					'subscriptionId' => $calendarId,
1107
					'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...
1108
					'shares' => $this->getShares($calendarId),
1109
					'objectData' => $this->getCalendarObject($calendarId, $objectUri),
1110
				]
1111
			));
1112
		}
1113
		$this->addChange($calendarId, $objectUri, 1, $calendarType);
1114
1115
		return '"' . $extraData['etag'] . '"';
1116
	}
1117
1118
	/**
1119
	 * Updates an existing calendarobject, based on it's uri.
1120
	 *
1121
	 * The object uri is only the basename, or filename and not a full path.
1122
	 *
1123
	 * It is possible return an etag from this function, which will be used in
1124
	 * the response to this PUT request. Note that the ETag must be surrounded
1125
	 * by double-quotes.
1126
	 *
1127
	 * However, you should only really return this ETag if you don't mangle the
1128
	 * calendar-data. If the result of a subsequent GET to this object is not
1129
	 * the exact same as this request body, you should omit the ETag.
1130
	 *
1131
	 * @param mixed $calendarId
1132
	 * @param string $objectUri
1133
	 * @param string $calendarData
1134
	 * @param int $calendarType
1135
	 * @return string
1136
	 */
1137
	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...
1138
		$extraData = $this->getDenormalizedData($calendarData);
1139
		$query = $this->db->getQueryBuilder();
1140
		$query->update('calendarobjects')
1141
				->set('calendardata', $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB))
1142
				->set('lastmodified', $query->createNamedParameter(time()))
1143
				->set('etag', $query->createNamedParameter($extraData['etag']))
1144
				->set('size', $query->createNamedParameter($extraData['size']))
1145
				->set('componenttype', $query->createNamedParameter($extraData['componentType']))
1146
				->set('firstoccurence', $query->createNamedParameter($extraData['firstOccurence']))
1147
				->set('lastoccurence', $query->createNamedParameter($extraData['lastOccurence']))
1148
				->set('classification', $query->createNamedParameter($extraData['classification']))
1149
				->set('uid', $query->createNamedParameter($extraData['uid']))
1150
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
1151
			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
1152
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)))
1153
			->execute();
1154
1155
		$this->updateProperties($calendarId, $objectUri, $calendarData, $calendarType);
1156
1157
		$data = $this->getCalendarObject($calendarId, $objectUri);
1158
		if (is_array($data)) {
1159
			if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
1160
				$this->dispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateCalendarObject', new GenericEvent(
0 ignored issues
show
Bug introduced by
'\OCA\DAV\CalDAV\CalDavB...::updateCalendarObject' of type string is incompatible with the type object expected by parameter $event of Symfony\Contracts\EventD...erInterface::dispatch(). ( Ignorable by Annotation )

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

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

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

1160
				$this->dispatcher->/** @scrutinizer ignore-call */ 
1161
                       dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateCalendarObject', new GenericEvent(

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
1161
					'\OCA\DAV\CalDAV\CalDavBackend::updateCalendarObject',
1162
					[
1163
						'calendarId' => $calendarId,
1164
						'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...
1165
						'shares' => $this->getShares($calendarId),
1166
						'objectData' => $data,
1167
					]
1168
				));
1169
			} else {
1170
				$this->dispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateCachedCalendarObject', new GenericEvent(
1171
					'\OCA\DAV\CalDAV\CalDavBackend::updateCachedCalendarObject',
1172
					[
1173
						'subscriptionId' => $calendarId,
1174
						'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...
1175
						'shares' => $this->getShares($calendarId),
1176
						'objectData' => $data,
1177
					]
1178
				));
1179
			}
1180
		}
1181
		$this->addChange($calendarId, $objectUri, 2, $calendarType);
1182
1183
		return '"' . $extraData['etag'] . '"';
1184
	}
1185
1186
	/**
1187
	 * @param int $calendarObjectId
1188
	 * @param int $classification
1189
	 */
1190
	public function setClassification($calendarObjectId, $classification) {
1191
		if (!in_array($classification, [
1192
			self::CLASSIFICATION_PUBLIC, self::CLASSIFICATION_PRIVATE, self::CLASSIFICATION_CONFIDENTIAL
1193
		])) {
1194
			throw new \InvalidArgumentException();
1195
		}
1196
		$query = $this->db->getQueryBuilder();
1197
		$query->update('calendarobjects')
1198
			->set('classification', $query->createNamedParameter($classification))
1199
			->where($query->expr()->eq('id', $query->createNamedParameter($calendarObjectId)))
1200
			->execute();
1201
	}
1202
1203
	/**
1204
	 * Deletes an existing calendar object.
1205
	 *
1206
	 * The object uri is only the basename, or filename and not a full path.
1207
	 *
1208
	 * @param mixed $calendarId
1209
	 * @param string $objectUri
1210
	 * @param int $calendarType
1211
	 * @return void
1212
	 */
1213
	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...
1214
		$data = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
1215
		if (is_array($data)) {
1216
			if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
1217
				$this->dispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteCalendarObject', new GenericEvent(
0 ignored issues
show
Unused Code introduced by
The call to Symfony\Contracts\EventD...erInterface::dispatch() has too many arguments starting with new Symfony\Component\Ev...'objectData' => $data)). ( Ignorable by Annotation )

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

1217
				$this->dispatcher->/** @scrutinizer ignore-call */ 
1218
                       dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteCalendarObject', new GenericEvent(

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
Bug introduced by
'\OCA\DAV\CalDAV\CalDavB...::deleteCalendarObject' of type string is incompatible with the type object expected by parameter $event of Symfony\Contracts\EventD...erInterface::dispatch(). ( Ignorable by Annotation )

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

1217
				$this->dispatcher->dispatch(/** @scrutinizer ignore-type */ '\OCA\DAV\CalDAV\CalDavBackend::deleteCalendarObject', new GenericEvent(
Loading history...
1218
					'\OCA\DAV\CalDAV\CalDavBackend::deleteCalendarObject',
1219
					[
1220
						'calendarId' => $calendarId,
1221
						'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...
1222
						'shares' => $this->getShares($calendarId),
1223
						'objectData' => $data,
1224
					]
1225
				));
1226
			} else {
1227
				$this->dispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteCachedCalendarObject', new GenericEvent(
1228
					'\OCA\DAV\CalDAV\CalDavBackend::deleteCachedCalendarObject',
1229
					[
1230
						'subscriptionId' => $calendarId,
1231
						'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...
1232
						'shares' => $this->getShares($calendarId),
1233
						'objectData' => $data,
1234
					]
1235
				));
1236
			}
1237
		}
1238
1239
		$stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ? AND `uri` = ? AND `calendartype` = ?');
1240
		$stmt->execute([$calendarId, $objectUri, $calendarType]);
1241
1242
		if (is_array($data)) {
1243
			$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

1243
			$this->/** @scrutinizer ignore-call */ 
1244
          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...
1244
		}
1245
1246
		$this->addChange($calendarId, $objectUri, 3, $calendarType);
1247
	}
1248
1249
	/**
1250
	 * Performs a calendar-query on the contents of this calendar.
1251
	 *
1252
	 * The calendar-query is defined in RFC4791 : CalDAV. Using the
1253
	 * calendar-query it is possible for a client to request a specific set of
1254
	 * object, based on contents of iCalendar properties, date-ranges and
1255
	 * iCalendar component types (VTODO, VEVENT).
1256
	 *
1257
	 * This method should just return a list of (relative) urls that match this
1258
	 * query.
1259
	 *
1260
	 * The list of filters are specified as an array. The exact array is
1261
	 * documented by Sabre\CalDAV\CalendarQueryParser.
1262
	 *
1263
	 * Note that it is extremely likely that getCalendarObject for every path
1264
	 * returned from this method will be called almost immediately after. You
1265
	 * may want to anticipate this to speed up these requests.
1266
	 *
1267
	 * This method provides a default implementation, which parses *all* the
1268
	 * iCalendar objects in the specified calendar.
1269
	 *
1270
	 * This default may well be good enough for personal use, and calendars
1271
	 * that aren't very large. But if you anticipate high usage, big calendars
1272
	 * or high loads, you are strongly advised to optimize certain paths.
1273
	 *
1274
	 * The best way to do so is override this method and to optimize
1275
	 * specifically for 'common filters'.
1276
	 *
1277
	 * Requests that are extremely common are:
1278
	 *   * requests for just VEVENTS
1279
	 *   * requests for just VTODO
1280
	 *   * requests with a time-range-filter on either VEVENT or VTODO.
1281
	 *
1282
	 * ..and combinations of these requests. It may not be worth it to try to
1283
	 * handle every possible situation and just rely on the (relatively
1284
	 * easy to use) CalendarQueryValidator to handle the rest.
1285
	 *
1286
	 * Note that especially time-range-filters may be difficult to parse. A
1287
	 * time-range filter specified on a VEVENT must for instance also handle
1288
	 * recurrence rules correctly.
1289
	 * A good example of how to interprete all these filters can also simply
1290
	 * be found in Sabre\CalDAV\CalendarQueryFilter. This class is as correct
1291
	 * as possible, so it gives you a good idea on what type of stuff you need
1292
	 * to think of.
1293
	 *
1294
	 * @param mixed $id
1295
	 * @param array $filters
1296
	 * @param int $calendarType
1297
	 * @return array
1298
	 */
1299
	public function calendarQuery($id, array $filters, $calendarType=self::CALENDAR_TYPE_CALENDAR):array {
1300
		$componentType = null;
1301
		$requirePostFilter = true;
1302
		$timeRange = null;
1303
1304
		// if no filters were specified, we don't need to filter after a query
1305
		if (!$filters['prop-filters'] && !$filters['comp-filters']) {
1306
			$requirePostFilter = false;
1307
		}
1308
1309
		// Figuring out if there's a component filter
1310
		if (count($filters['comp-filters']) > 0 && !$filters['comp-filters'][0]['is-not-defined']) {
1311
			$componentType = $filters['comp-filters'][0]['name'];
1312
1313
			// Checking if we need post-filters
1314
			if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['time-range'] && !$filters['comp-filters'][0]['prop-filters']) {
1315
				$requirePostFilter = false;
1316
			}
1317
			// There was a time-range filter
1318
			if ($componentType === 'VEVENT' && isset($filters['comp-filters'][0]['time-range'])) {
1319
				$timeRange = $filters['comp-filters'][0]['time-range'];
1320
1321
				// If start time OR the end time is not specified, we can do a
1322
				// 100% accurate mysql query.
1323
				if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['prop-filters'] && (!$timeRange['start'] || !$timeRange['end'])) {
1324
					$requirePostFilter = false;
1325
				}
1326
			}
1327
1328
		}
1329
		$columns = ['uri'];
1330
		if ($requirePostFilter) {
1331
			$columns = ['uri', 'calendardata'];
1332
		}
1333
		$query = $this->db->getQueryBuilder();
1334
		$query->select($columns)
1335
			->from('calendarobjects')
1336
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($id)))
1337
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)));
1338
1339
		if ($componentType) {
1340
			$query->andWhere($query->expr()->eq('componenttype', $query->createNamedParameter($componentType)));
1341
		}
1342
1343
		if ($timeRange && $timeRange['start']) {
1344
			$query->andWhere($query->expr()->gt('lastoccurence', $query->createNamedParameter($timeRange['start']->getTimeStamp())));
1345
		}
1346
		if ($timeRange && $timeRange['end']) {
1347
			$query->andWhere($query->expr()->lt('firstoccurence', $query->createNamedParameter($timeRange['end']->getTimeStamp())));
1348
		}
1349
1350
		$stmt = $query->execute();
1351
1352
		$result = [];
1353
		while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
1354
			if ($requirePostFilter) {
1355
				// validateFilterForObject will parse the calendar data
1356
				// catch parsing errors
1357
				try {
1358
					$matches = $this->validateFilterForObject($row, $filters);
1359
				} catch(ParseException $ex) {
1360
					$this->logger->logException($ex, [
1361
						'app' => 'dav',
1362
						'message' => 'Caught parsing exception for calendar data. This usually indicates invalid calendar data. calendar-id:'.$id.' uri:'.$row['uri']
1363
					]);
1364
					continue;
1365
				} catch (InvalidDataException $ex) {
1366
					$this->logger->logException($ex, [
1367
						'app' => 'dav',
1368
						'message' => 'Caught invalid data exception for calendar data. This usually indicates invalid calendar data. calendar-id:'.$id.' uri:'.$row['uri']
1369
					]);
1370
					continue;
1371
				}
1372
1373
				if (!$matches) {
1374
					continue;
1375
				}
1376
			}
1377
			$result[] = $row['uri'];
1378
		}
1379
1380
		return $result;
1381
	}
1382
1383
	/**
1384
	 * custom Nextcloud search extension for CalDAV
1385
	 *
1386
	 * TODO - this should optionally cover cached calendar objects as well
1387
	 *
1388
	 * @param string $principalUri
1389
	 * @param array $filters
1390
	 * @param integer|null $limit
1391
	 * @param integer|null $offset
1392
	 * @return array
1393
	 */
1394
	public function calendarSearch($principalUri, array $filters, $limit=null, $offset=null) {
1395
		$calendars = $this->getCalendarsForUser($principalUri);
1396
		$ownCalendars = [];
1397
		$sharedCalendars = [];
1398
1399
		$uriMapper = [];
1400
1401
		foreach($calendars as $calendar) {
1402
			if ($calendar['{http://owncloud.org/ns}owner-principal'] === $principalUri) {
1403
				$ownCalendars[] = $calendar['id'];
1404
			} else {
1405
				$sharedCalendars[] = $calendar['id'];
1406
			}
1407
			$uriMapper[$calendar['id']] = $calendar['uri'];
1408
		}
1409
		if (count($ownCalendars) === 0 && count($sharedCalendars) === 0) {
1410
			return [];
1411
		}
1412
1413
		$query = $this->db->getQueryBuilder();
1414
		// Calendar id expressions
1415
		$calendarExpressions = [];
1416
		foreach($ownCalendars as $id) {
1417
			$calendarExpressions[] = $query->expr()->andX(
1418
				$query->expr()->eq('c.calendarid',
1419
					$query->createNamedParameter($id)),
1420
				$query->expr()->eq('c.calendartype',
1421
						$query->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
1422
		}
1423
		foreach($sharedCalendars as $id) {
1424
			$calendarExpressions[] = $query->expr()->andX(
1425
				$query->expr()->eq('c.calendarid',
1426
					$query->createNamedParameter($id)),
1427
				$query->expr()->eq('c.classification',
1428
					$query->createNamedParameter(self::CLASSIFICATION_PUBLIC)),
1429
				$query->expr()->eq('c.calendartype',
1430
					$query->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
1431
		}
1432
1433
		if (count($calendarExpressions) === 1) {
1434
			$calExpr = $calendarExpressions[0];
1435
		} else {
1436
			$calExpr = call_user_func_array([$query->expr(), 'orX'], $calendarExpressions);
1437
		}
1438
1439
		// Component expressions
1440
		$compExpressions = [];
1441
		foreach($filters['comps'] as $comp) {
1442
			$compExpressions[] = $query->expr()
1443
				->eq('c.componenttype', $query->createNamedParameter($comp));
1444
		}
1445
1446
		if (count($compExpressions) === 1) {
1447
			$compExpr = $compExpressions[0];
1448
		} else {
1449
			$compExpr = call_user_func_array([$query->expr(), 'orX'], $compExpressions);
1450
		}
1451
1452
		if (!isset($filters['props'])) {
1453
			$filters['props'] = [];
1454
		}
1455
		if (!isset($filters['params'])) {
1456
			$filters['params'] = [];
1457
		}
1458
1459
		$propParamExpressions = [];
1460
		foreach($filters['props'] as $prop) {
1461
			$propParamExpressions[] = $query->expr()->andX(
1462
				$query->expr()->eq('i.name', $query->createNamedParameter($prop)),
1463
				$query->expr()->isNull('i.parameter')
1464
			);
1465
		}
1466
		foreach($filters['params'] as $param) {
1467
			$propParamExpressions[] = $query->expr()->andX(
1468
				$query->expr()->eq('i.name', $query->createNamedParameter($param['property'])),
1469
				$query->expr()->eq('i.parameter', $query->createNamedParameter($param['parameter']))
1470
			);
1471
		}
1472
1473
		if (count($propParamExpressions) === 1) {
1474
			$propParamExpr = $propParamExpressions[0];
1475
		} else {
1476
			$propParamExpr = call_user_func_array([$query->expr(), 'orX'], $propParamExpressions);
1477
		}
1478
1479
		$query->select(['c.calendarid', 'c.uri'])
1480
			->from($this->dbObjectPropertiesTable, 'i')
1481
			->join('i', 'calendarobjects', 'c', $query->expr()->eq('i.objectid', 'c.id'))
1482
			->where($calExpr)
1483
			->andWhere($compExpr)
1484
			->andWhere($propParamExpr)
1485
			->andWhere($query->expr()->iLike('i.value',
1486
				$query->createNamedParameter('%'.$this->db->escapeLikeParameter($filters['search-term']).'%')));
1487
1488
		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...
1489
			$query->setFirstResult($offset);
1490
		}
1491
		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...
1492
			$query->setMaxResults($limit);
1493
		}
1494
1495
		$stmt = $query->execute();
1496
1497
		$result = [];
1498
		while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
1499
			$path = $uriMapper[$row['calendarid']] . '/' . $row['uri'];
1500
			if (!in_array($path, $result)) {
1501
				$result[] = $path;
1502
			}
1503
		}
1504
1505
		return $result;
1506
	}
1507
1508
	/**
1509
	 * used for Nextcloud's calendar API
1510
	 *
1511
	 * @param array $calendarInfo
1512
	 * @param string $pattern
1513
	 * @param array $searchProperties
1514
	 * @param array $options
1515
	 * @param integer|null $limit
1516
	 * @param integer|null $offset
1517
	 *
1518
	 * @return array
1519
	 */
1520
	public function search(array $calendarInfo, $pattern, array $searchProperties,
1521
						   array $options, $limit, $offset) {
1522
		$outerQuery = $this->db->getQueryBuilder();
1523
		$innerQuery = $this->db->getQueryBuilder();
1524
1525
		$innerQuery->selectDistinct('op.objectid')
1526
			->from($this->dbObjectPropertiesTable, 'op')
1527
			->andWhere($innerQuery->expr()->eq('op.calendarid',
1528
				$outerQuery->createNamedParameter($calendarInfo['id'])))
1529
			->andWhere($innerQuery->expr()->eq('op.calendartype',
1530
				$outerQuery->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
1531
1532
		// only return public items for shared calendars for now
1533
		if ($calendarInfo['principaluri'] !== $calendarInfo['{http://owncloud.org/ns}owner-principal']) {
1534
			$innerQuery->andWhere($innerQuery->expr()->eq('c.classification',
1535
				$outerQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
1536
		}
1537
1538
		$or = $innerQuery->expr()->orX();
1539
		foreach($searchProperties as $searchProperty) {
1540
			$or->add($innerQuery->expr()->eq('op.name',
1541
				$outerQuery->createNamedParameter($searchProperty)));
1542
		}
1543
		$innerQuery->andWhere($or);
1544
1545
		if ($pattern !== '') {
1546
			$innerQuery->andWhere($innerQuery->expr()->iLike('op.value',
1547
				$outerQuery->createNamedParameter('%' .
1548
					$this->db->escapeLikeParameter($pattern) . '%')));
1549
		}
1550
1551
		$outerQuery->select('c.id', 'c.calendardata', 'c.componenttype', 'c.uid', 'c.uri')
1552
			->from('calendarobjects', 'c');
1553
1554
		if (isset($options['timerange'])) {
1555
			if (isset($options['timerange']['start']) && $options['timerange']['start'] instanceof DateTime) {
1556
				$outerQuery->andWhere($outerQuery->expr()->gt('lastoccurence',
1557
					$outerQuery->createNamedParameter($options['timerange']['start']->getTimeStamp())));
1558
1559
			}
1560
			if (isset($options['timerange']['end']) && $options['timerange']['end'] instanceof DateTime) {
1561
				$outerQuery->andWhere($outerQuery->expr()->lt('firstoccurence',
1562
					$outerQuery->createNamedParameter($options['timerange']['end']->getTimeStamp())));
1563
			}
1564
		}
1565
1566
		if (isset($options['types'])) {
1567
			$or = $outerQuery->expr()->orX();
1568
			foreach($options['types'] as $type) {
1569
				$or->add($outerQuery->expr()->eq('componenttype',
1570
					$outerQuery->createNamedParameter($type)));
1571
			}
1572
			$outerQuery->andWhere($or);
1573
		}
1574
1575
		$outerQuery->andWhere($outerQuery->expr()->in('c.id',
1576
			$outerQuery->createFunction($innerQuery->getSQL())));
1577
1578
		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...
1579
			$outerQuery->setFirstResult($offset);
1580
		}
1581
		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...
1582
			$outerQuery->setMaxResults($limit);
1583
		}
1584
1585
		$result = $outerQuery->execute();
1586
		$calendarObjects = $result->fetchAll();
1587
1588
		return array_map(function($o) {
1589
			$calendarData = Reader::read($o['calendardata']);
1590
			$comps = $calendarData->getComponents();
1591
			$objects = [];
1592
			$timezones = [];
1593
			foreach($comps as $comp) {
1594
				if ($comp instanceof VTimeZone) {
1595
					$timezones[] = $comp;
1596
				} else {
1597
					$objects[] = $comp;
1598
				}
1599
			}
1600
1601
			return [
1602
				'id' => $o['id'],
1603
				'type' => $o['componenttype'],
1604
				'uid' => $o['uid'],
1605
				'uri' => $o['uri'],
1606
				'objects' => array_map(function($c) {
1607
					return $this->transformSearchData($c);
1608
				}, $objects),
1609
				'timezones' => array_map(function($c) {
1610
					return $this->transformSearchData($c);
1611
				}, $timezones),
1612
			];
1613
		}, $calendarObjects);
1614
	}
1615
1616
	/**
1617
	 * @param Component $comp
1618
	 * @return array
1619
	 */
1620
	private function transformSearchData(Component $comp) {
1621
		$data = [];
1622
		/** @var Component[] $subComponents */
1623
		$subComponents = $comp->getComponents();
1624
		/** @var Property[] $properties */
1625
		$properties = array_filter($comp->children(), function($c) {
1626
			return $c instanceof Property;
1627
		});
1628
		$validationRules = $comp->getValidationRules();
1629
1630
		foreach($subComponents as $subComponent) {
1631
			$name = $subComponent->name;
1632
			if (!isset($data[$name])) {
1633
				$data[$name] = [];
1634
			}
1635
			$data[$name][] = $this->transformSearchData($subComponent);
1636
		}
1637
1638
		foreach($properties as $property) {
1639
			$name = $property->name;
1640
			if (!isset($validationRules[$name])) {
1641
				$validationRules[$name] = '*';
1642
			}
1643
1644
			$rule = $validationRules[$property->name];
1645
			if ($rule === '+' || $rule === '*') { // multiple
1646
				if (!isset($data[$name])) {
1647
					$data[$name] = [];
1648
				}
1649
1650
				$data[$name][] = $this->transformSearchProperty($property);
1651
			} else { // once
1652
				$data[$name] = $this->transformSearchProperty($property);
1653
			}
1654
		}
1655
1656
		return $data;
1657
	}
1658
1659
	/**
1660
	 * @param Property $prop
1661
	 * @return array
1662
	 */
1663
	private function transformSearchProperty(Property $prop) {
1664
		// No need to check Date, as it extends DateTime
1665
		if ($prop instanceof Property\ICalendar\DateTime) {
1666
			$value = $prop->getDateTime();
1667
		} else {
1668
			$value = $prop->getValue();
1669
		}
1670
1671
		return [
1672
			$value,
1673
			$prop->parameters()
1674
		];
1675
	}
1676
1677
	/**
1678
	 * Searches through all of a users calendars and calendar objects to find
1679
	 * an object with a specific UID.
1680
	 *
1681
	 * This method should return the path to this object, relative to the
1682
	 * calendar home, so this path usually only contains two parts:
1683
	 *
1684
	 * calendarpath/objectpath.ics
1685
	 *
1686
	 * If the uid is not found, return null.
1687
	 *
1688
	 * This method should only consider * objects that the principal owns, so
1689
	 * any calendars owned by other principals that also appear in this
1690
	 * collection should be ignored.
1691
	 *
1692
	 * @param string $principalUri
1693
	 * @param string $uid
1694
	 * @return string|null
1695
	 */
1696
	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...
1697
1698
		$query = $this->db->getQueryBuilder();
1699
		$query->selectAlias('c.uri', 'calendaruri')->selectAlias('co.uri', 'objecturi')
1700
			->from('calendarobjects', 'co')
1701
			->leftJoin('co', 'calendars', 'c', $query->expr()->eq('co.calendarid', 'c.id'))
1702
			->where($query->expr()->eq('c.principaluri', $query->createNamedParameter($principalUri)))
1703
			->andWhere($query->expr()->eq('co.uid', $query->createNamedParameter($uid)))
1704
			->andWhere($query->expr()->eq('co.uid', $query->createNamedParameter($uid)));
1705
1706
		$stmt = $query->execute();
1707
1708
		if ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
1709
			return $row['calendaruri'] . '/' . $row['objecturi'];
1710
		}
1711
1712
		return null;
1713
	}
1714
1715
	/**
1716
	 * The getChanges method returns all the changes that have happened, since
1717
	 * the specified syncToken in the specified calendar.
1718
	 *
1719
	 * This function should return an array, such as the following:
1720
	 *
1721
	 * [
1722
	 *   'syncToken' => 'The current synctoken',
1723
	 *   'added'   => [
1724
	 *      'new.txt',
1725
	 *   ],
1726
	 *   'modified'   => [
1727
	 *      'modified.txt',
1728
	 *   ],
1729
	 *   'deleted' => [
1730
	 *      'foo.php.bak',
1731
	 *      'old.txt'
1732
	 *   ]
1733
	 * );
1734
	 *
1735
	 * The returned syncToken property should reflect the *current* syncToken
1736
	 * of the calendar, as reported in the {http://sabredav.org/ns}sync-token
1737
	 * property This is * needed here too, to ensure the operation is atomic.
1738
	 *
1739
	 * If the $syncToken argument is specified as null, this is an initial
1740
	 * sync, and all members should be reported.
1741
	 *
1742
	 * The modified property is an array of nodenames that have changed since
1743
	 * the last token.
1744
	 *
1745
	 * The deleted property is an array with nodenames, that have been deleted
1746
	 * from collection.
1747
	 *
1748
	 * The $syncLevel argument is basically the 'depth' of the report. If it's
1749
	 * 1, you only have to report changes that happened only directly in
1750
	 * immediate descendants. If it's 2, it should also include changes from
1751
	 * the nodes below the child collections. (grandchildren)
1752
	 *
1753
	 * The $limit argument allows a client to specify how many results should
1754
	 * be returned at most. If the limit is not specified, it should be treated
1755
	 * as infinite.
1756
	 *
1757
	 * If the limit (infinite or not) is higher than you're willing to return,
1758
	 * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
1759
	 *
1760
	 * If the syncToken is expired (due to data cleanup) or unknown, you must
1761
	 * return null.
1762
	 *
1763
	 * The limit is 'suggestive'. You are free to ignore it.
1764
	 *
1765
	 * @param string $calendarId
1766
	 * @param string $syncToken
1767
	 * @param int $syncLevel
1768
	 * @param int $limit
1769
	 * @param int $calendarType
1770
	 * @return array
1771
	 */
1772
	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...
1773
		// Current synctoken
1774
		$stmt = $this->db->prepare('SELECT `synctoken` FROM `*PREFIX*calendars` WHERE `id` = ?');
1775
		$stmt->execute([ $calendarId ]);
1776
		$currentToken = $stmt->fetchColumn(0);
1777
1778
		if (is_null($currentToken)) {
1779
			return null;
1780
		}
1781
1782
		$result = [
1783
			'syncToken' => $currentToken,
1784
			'added'     => [],
1785
			'modified'  => [],
1786
			'deleted'   => [],
1787
		];
1788
1789
		if ($syncToken) {
1790
1791
			$query = "SELECT `uri`, `operation` FROM `*PREFIX*calendarchanges` WHERE `synctoken` >= ? AND `synctoken` < ? AND `calendarid` = ? AND `calendartype` = ? ORDER BY `synctoken`";
1792
			if ($limit>0) {
1793
				$query.= " LIMIT " . (int)$limit;
1794
			}
1795
1796
			// Fetching all changes
1797
			$stmt = $this->db->prepare($query);
1798
			$stmt->execute([$syncToken, $currentToken, $calendarId, $calendarType]);
1799
1800
			$changes = [];
1801
1802
			// This loop ensures that any duplicates are overwritten, only the
1803
			// last change on a node is relevant.
1804
			while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
1805
1806
				$changes[$row['uri']] = $row['operation'];
1807
1808
			}
1809
1810
			foreach($changes as $uri => $operation) {
1811
1812
				switch($operation) {
1813
					case 1 :
1814
						$result['added'][] = $uri;
1815
						break;
1816
					case 2 :
1817
						$result['modified'][] = $uri;
1818
						break;
1819
					case 3 :
1820
						$result['deleted'][] = $uri;
1821
						break;
1822
				}
1823
1824
			}
1825
		} else {
1826
			// No synctoken supplied, this is the initial sync.
1827
			$query = "SELECT `uri` FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ? AND `calendartype` = ?";
1828
			$stmt = $this->db->prepare($query);
1829
			$stmt->execute([$calendarId, $calendarType]);
1830
1831
			$result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
1832
		}
1833
		return $result;
1834
1835
	}
1836
1837
	/**
1838
	 * Returns a list of subscriptions for a principal.
1839
	 *
1840
	 * Every subscription is an array with the following keys:
1841
	 *  * id, a unique id that will be used by other functions to modify the
1842
	 *    subscription. This can be the same as the uri or a database key.
1843
	 *  * uri. This is just the 'base uri' or 'filename' of the subscription.
1844
	 *  * principaluri. The owner of the subscription. Almost always the same as
1845
	 *    principalUri passed to this method.
1846
	 *
1847
	 * Furthermore, all the subscription info must be returned too:
1848
	 *
1849
	 * 1. {DAV:}displayname
1850
	 * 2. {http://apple.com/ns/ical/}refreshrate
1851
	 * 3. {http://calendarserver.org/ns/}subscribed-strip-todos (omit if todos
1852
	 *    should not be stripped).
1853
	 * 4. {http://calendarserver.org/ns/}subscribed-strip-alarms (omit if alarms
1854
	 *    should not be stripped).
1855
	 * 5. {http://calendarserver.org/ns/}subscribed-strip-attachments (omit if
1856
	 *    attachments should not be stripped).
1857
	 * 6. {http://calendarserver.org/ns/}source (Must be a
1858
	 *     Sabre\DAV\Property\Href).
1859
	 * 7. {http://apple.com/ns/ical/}calendar-color
1860
	 * 8. {http://apple.com/ns/ical/}calendar-order
1861
	 * 9. {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
1862
	 *    (should just be an instance of
1863
	 *    Sabre\CalDAV\Property\SupportedCalendarComponentSet, with a bunch of
1864
	 *    default components).
1865
	 *
1866
	 * @param string $principalUri
1867
	 * @return array
1868
	 */
1869
	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...
1870
		$fields = array_values($this->subscriptionPropertyMap);
1871
		$fields[] = 'id';
1872
		$fields[] = 'uri';
1873
		$fields[] = 'source';
1874
		$fields[] = 'principaluri';
1875
		$fields[] = 'lastmodified';
1876
		$fields[] = 'synctoken';
1877
1878
		$query = $this->db->getQueryBuilder();
1879
		$query->select($fields)
1880
			->from('calendarsubscriptions')
1881
			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
1882
			->orderBy('calendarorder', 'asc');
1883
		$stmt =$query->execute();
1884
1885
		$subscriptions = [];
1886
		while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
1887
1888
			$subscription = [
1889
				'id'           => $row['id'],
1890
				'uri'          => $row['uri'],
1891
				'principaluri' => $row['principaluri'],
1892
				'source'       => $row['source'],
1893
				'lastmodified' => $row['lastmodified'],
1894
1895
				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
1896
				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
1897
			];
1898
1899
			foreach($this->subscriptionPropertyMap as $xmlName=>$dbName) {
1900
				if (!is_null($row[$dbName])) {
1901
					$subscription[$xmlName] = $row[$dbName];
1902
				}
1903
			}
1904
1905
			$subscriptions[] = $subscription;
1906
1907
		}
1908
1909
		return $subscriptions;
1910
	}
1911
1912
	/**
1913
	 * Creates a new subscription for a principal.
1914
	 *
1915
	 * If the creation was a success, an id must be returned that can be used to reference
1916
	 * this subscription in other methods, such as updateSubscription.
1917
	 *
1918
	 * @param string $principalUri
1919
	 * @param string $uri
1920
	 * @param array $properties
1921
	 * @return mixed
1922
	 */
1923
	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...
1924
1925
		if (!isset($properties['{http://calendarserver.org/ns/}source'])) {
1926
			throw new Forbidden('The {http://calendarserver.org/ns/}source property is required when creating subscriptions');
1927
		}
1928
1929
		$values = [
1930
			'principaluri' => $principalUri,
1931
			'uri'          => $uri,
1932
			'source'       => $properties['{http://calendarserver.org/ns/}source']->getHref(),
1933
			'lastmodified' => time(),
1934
		];
1935
1936
		$propertiesBoolean = ['striptodos', 'stripalarms', 'stripattachments'];
1937
1938
		foreach($this->subscriptionPropertyMap as $xmlName=>$dbName) {
1939
			if (array_key_exists($xmlName, $properties)) {
1940
					$values[$dbName] = $properties[$xmlName];
1941
					if (in_array($dbName, $propertiesBoolean)) {
1942
						$values[$dbName] = true;
1943
				}
1944
			}
1945
		}
1946
1947
		$valuesToInsert = array();
1948
1949
		$query = $this->db->getQueryBuilder();
1950
1951
		foreach (array_keys($values) as $name) {
1952
			$valuesToInsert[$name] = $query->createNamedParameter($values[$name]);
1953
		}
1954
1955
		$query->insert('calendarsubscriptions')
1956
			->values($valuesToInsert)
1957
			->execute();
1958
1959
		$subscriptionId = $this->db->lastInsertId('*PREFIX*calendarsubscriptions');
1960
1961
		$this->dispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createSubscription', new GenericEvent(
0 ignored issues
show
Unused Code introduced by
The call to Symfony\Contracts\EventD...erInterface::dispatch() has too many arguments starting with new Symfony\Component\Ev...ById($subscriptionId))). ( Ignorable by Annotation )

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

1961
		$this->dispatcher->/** @scrutinizer ignore-call */ 
1962
                     dispatch('\OCA\DAV\CalDAV\CalDavBackend::createSubscription', new GenericEvent(

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
Bug introduced by
'\OCA\DAV\CalDAV\CalDavB...nd::createSubscription' of type string is incompatible with the type object expected by parameter $event of Symfony\Contracts\EventD...erInterface::dispatch(). ( Ignorable by Annotation )

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

1961
		$this->dispatcher->dispatch(/** @scrutinizer ignore-type */ '\OCA\DAV\CalDAV\CalDavBackend::createSubscription', new GenericEvent(
Loading history...
1962
			'\OCA\DAV\CalDAV\CalDavBackend::createSubscription',
1963
			[
1964
				'subscriptionId' => $subscriptionId,
1965
				'subscriptionData' => $this->getSubscriptionById($subscriptionId),
1966
			]));
1967
1968
		return $subscriptionId;
1969
	}
1970
1971
	/**
1972
	 * Updates a subscription
1973
	 *
1974
	 * The list of mutations is stored in a Sabre\DAV\PropPatch object.
1975
	 * To do the actual updates, you must tell this object which properties
1976
	 * you're going to process with the handle() method.
1977
	 *
1978
	 * Calling the handle method is like telling the PropPatch object "I
1979
	 * promise I can handle updating this property".
1980
	 *
1981
	 * Read the PropPatch documentation for more info and examples.
1982
	 *
1983
	 * @param mixed $subscriptionId
1984
	 * @param PropPatch $propPatch
1985
	 * @return void
1986
	 */
1987
	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...
1988
		$supportedProperties = array_keys($this->subscriptionPropertyMap);
1989
		$supportedProperties[] = '{http://calendarserver.org/ns/}source';
1990
1991
		/**
1992
		 * @suppress SqlInjectionChecker
1993
		 */
1994
		$propPatch->handle($supportedProperties, function($mutations) use ($subscriptionId) {
1995
1996
			$newValues = [];
1997
1998
			foreach($mutations as $propertyName=>$propertyValue) {
1999
				if ($propertyName === '{http://calendarserver.org/ns/}source') {
2000
					$newValues['source'] = $propertyValue->getHref();
2001
				} else {
2002
					$fieldName = $this->subscriptionPropertyMap[$propertyName];
2003
					$newValues[$fieldName] = $propertyValue;
2004
				}
2005
			}
2006
2007
			$query = $this->db->getQueryBuilder();
2008
			$query->update('calendarsubscriptions')
2009
				->set('lastmodified', $query->createNamedParameter(time()));
2010
			foreach($newValues as $fieldName=>$value) {
2011
				$query->set($fieldName, $query->createNamedParameter($value));
2012
			}
2013
			$query->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
2014
				->execute();
2015
2016
			$this->dispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateSubscription', new GenericEvent(
0 ignored issues
show
Unused Code introduced by
The call to Symfony\Contracts\EventD...erInterface::dispatch() has too many arguments starting with new Symfony\Component\Ev...ations' => $mutations)). ( Ignorable by Annotation )

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

2016
			$this->dispatcher->/** @scrutinizer ignore-call */ 
2017
                      dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateSubscription', new GenericEvent(

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
Bug introduced by
'\OCA\DAV\CalDAV\CalDavB...nd::updateSubscription' of type string is incompatible with the type object expected by parameter $event of Symfony\Contracts\EventD...erInterface::dispatch(). ( Ignorable by Annotation )

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

2016
			$this->dispatcher->dispatch(/** @scrutinizer ignore-type */ '\OCA\DAV\CalDAV\CalDavBackend::updateSubscription', new GenericEvent(
Loading history...
2017
				'\OCA\DAV\CalDAV\CalDavBackend::updateSubscription',
2018
				[
2019
					'subscriptionId' => $subscriptionId,
2020
					'subscriptionData' => $this->getSubscriptionById($subscriptionId),
2021
					'propertyMutations' => $mutations,
2022
				]));
2023
2024
			return true;
2025
2026
		});
2027
	}
2028
2029
	/**
2030
	 * Deletes a subscription.
2031
	 *
2032
	 * @param mixed $subscriptionId
2033
	 * @return void
2034
	 */
2035
	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...
2036
		$this->dispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteSubscription', new GenericEvent(
0 ignored issues
show
Unused Code introduced by
The call to Symfony\Contracts\EventD...erInterface::dispatch() has too many arguments starting with new Symfony\Component\Ev...ById($subscriptionId))). ( Ignorable by Annotation )

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

2036
		$this->dispatcher->/** @scrutinizer ignore-call */ 
2037
                     dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteSubscription', new GenericEvent(

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
Bug introduced by
'\OCA\DAV\CalDAV\CalDavB...nd::deleteSubscription' of type string is incompatible with the type object expected by parameter $event of Symfony\Contracts\EventD...erInterface::dispatch(). ( Ignorable by Annotation )

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

2036
		$this->dispatcher->dispatch(/** @scrutinizer ignore-type */ '\OCA\DAV\CalDAV\CalDavBackend::deleteSubscription', new GenericEvent(
Loading history...
2037
			'\OCA\DAV\CalDAV\CalDavBackend::deleteSubscription',
2038
			[
2039
				'subscriptionId' => $subscriptionId,
2040
				'subscriptionData' => $this->getSubscriptionById($subscriptionId),
2041
			]));
2042
2043
		$query = $this->db->getQueryBuilder();
2044
		$query->delete('calendarsubscriptions')
2045
			->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
2046
			->execute();
2047
2048
		$query = $this->db->getQueryBuilder();
2049
		$query->delete('calendarobjects')
2050
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2051
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2052
			->execute();
2053
2054
		$query->delete('calendarchanges')
2055
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2056
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2057
			->execute();
2058
2059
		$query->delete($this->dbObjectPropertiesTable)
2060
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
2061
			->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
2062
			->execute();
2063
	}
2064
2065
	/**
2066
	 * Returns a single scheduling object for the inbox collection.
2067
	 *
2068
	 * The returned array should contain the following elements:
2069
	 *   * uri - A unique basename for the object. This will be used to
2070
	 *           construct a full uri.
2071
	 *   * calendardata - The iCalendar object
2072
	 *   * lastmodified - The last modification date. Can be an int for a unix
2073
	 *                    timestamp, or a PHP DateTime object.
2074
	 *   * etag - A unique token that must change if the object changed.
2075
	 *   * size - The size of the object, in bytes.
2076
	 *
2077
	 * @param string $principalUri
2078
	 * @param string $objectUri
2079
	 * @return array
2080
	 */
2081
	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...
2082
		$query = $this->db->getQueryBuilder();
2083
		$stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size'])
2084
			->from('schedulingobjects')
2085
			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
2086
			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
2087
			->execute();
2088
2089
		$row = $stmt->fetch(\PDO::FETCH_ASSOC);
2090
2091
		if(!$row) {
2092
			return null;
2093
		}
2094
2095
		return [
2096
				'uri'          => $row['uri'],
2097
				'calendardata' => $row['calendardata'],
2098
				'lastmodified' => $row['lastmodified'],
2099
				'etag'         => '"' . $row['etag'] . '"',
2100
				'size'         => (int)$row['size'],
2101
		];
2102
	}
2103
2104
	/**
2105
	 * Returns all scheduling objects for the inbox collection.
2106
	 *
2107
	 * These objects should be returned as an array. Every item in the array
2108
	 * should follow the same structure as returned from getSchedulingObject.
2109
	 *
2110
	 * The main difference is that 'calendardata' is optional.
2111
	 *
2112
	 * @param string $principalUri
2113
	 * @return array
2114
	 */
2115
	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...
2116
		$query = $this->db->getQueryBuilder();
2117
		$stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size'])
2118
				->from('schedulingobjects')
2119
				->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
2120
				->execute();
2121
2122
		$result = [];
2123
		foreach($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) {
2124
			$result[] = [
2125
					'calendardata' => $row['calendardata'],
2126
					'uri'          => $row['uri'],
2127
					'lastmodified' => $row['lastmodified'],
2128
					'etag'         => '"' . $row['etag'] . '"',
2129
					'size'         => (int)$row['size'],
2130
			];
2131
		}
2132
2133
		return $result;
2134
	}
2135
2136
	/**
2137
	 * Deletes a scheduling object from the inbox collection.
2138
	 *
2139
	 * @param string $principalUri
2140
	 * @param string $objectUri
2141
	 * @return void
2142
	 */
2143
	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...
2144
		$query = $this->db->getQueryBuilder();
2145
		$query->delete('schedulingobjects')
2146
				->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
2147
				->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
2148
				->execute();
2149
	}
2150
2151
	/**
2152
	 * Creates a new scheduling object. This should land in a users' inbox.
2153
	 *
2154
	 * @param string $principalUri
2155
	 * @param string $objectUri
2156
	 * @param string $objectData
2157
	 * @return void
2158
	 */
2159
	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...
2160
		$query = $this->db->getQueryBuilder();
2161
		$query->insert('schedulingobjects')
2162
			->values([
2163
				'principaluri' => $query->createNamedParameter($principalUri),
2164
				'calendardata' => $query->createNamedParameter($objectData, IQueryBuilder::PARAM_LOB),
2165
				'uri' => $query->createNamedParameter($objectUri),
2166
				'lastmodified' => $query->createNamedParameter(time()),
2167
				'etag' => $query->createNamedParameter(md5($objectData)),
2168
				'size' => $query->createNamedParameter(strlen($objectData))
2169
			])
2170
			->execute();
2171
	}
2172
2173
	/**
2174
	 * Adds a change record to the calendarchanges table.
2175
	 *
2176
	 * @param mixed $calendarId
2177
	 * @param string $objectUri
2178
	 * @param int $operation 1 = add, 2 = modify, 3 = delete.
2179
	 * @param int $calendarType
2180
	 * @return void
2181
	 */
2182
	protected function addChange($calendarId, $objectUri, $operation, $calendarType=self::CALENDAR_TYPE_CALENDAR) {
2183
		$table = $calendarType === self::CALENDAR_TYPE_CALENDAR ? 'calendars': 'calendarsubscriptions';
2184
2185
		$query = $this->db->getQueryBuilder();
2186
		$query->select('synctoken')
2187
			->from($table)
2188
			->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)));
2189
		$syncToken = (int)$query->execute()->fetchColumn();
2190
2191
		$query = $this->db->getQueryBuilder();
2192
		$query->insert('calendarchanges')
2193
			->values([
2194
				'uri' => $query->createNamedParameter($objectUri),
2195
				'synctoken' => $query->createNamedParameter($syncToken),
2196
				'calendarid' => $query->createNamedParameter($calendarId),
2197
				'operation' => $query->createNamedParameter($operation),
2198
				'calendartype' => $query->createNamedParameter($calendarType),
2199
			])
2200
			->execute();
2201
2202
		$stmt = $this->db->prepare("UPDATE `*PREFIX*$table` SET `synctoken` = `synctoken` + 1 WHERE `id` = ?");
2203
		$stmt->execute([
2204
			$calendarId
2205
		]);
2206
2207
	}
2208
2209
	/**
2210
	 * Parses some information from calendar objects, used for optimized
2211
	 * calendar-queries.
2212
	 *
2213
	 * Returns an array with the following keys:
2214
	 *   * etag - An md5 checksum of the object without the quotes.
2215
	 *   * size - Size of the object in bytes
2216
	 *   * componentType - VEVENT, VTODO or VJOURNAL
2217
	 *   * firstOccurence
2218
	 *   * lastOccurence
2219
	 *   * uid - value of the UID property
2220
	 *
2221
	 * @param string $calendarData
2222
	 * @return array
2223
	 */
2224
	public function getDenormalizedData($calendarData) {
2225
2226
		$vObject = Reader::read($calendarData);
2227
		$componentType = null;
2228
		$component = null;
2229
		$firstOccurrence = null;
2230
		$lastOccurrence = null;
2231
		$uid = null;
2232
		$classification = self::CLASSIFICATION_PUBLIC;
2233
		foreach($vObject->getComponents() as $component) {
2234
			if ($component->name!=='VTIMEZONE') {
2235
				$componentType = $component->name;
2236
				$uid = (string)$component->UID;
2237
				break;
2238
			}
2239
		}
2240
		if (!$componentType) {
2241
			throw new \Sabre\DAV\Exception\BadRequest('Calendar objects must have a VJOURNAL, VEVENT or VTODO component');
2242
		}
2243
		if ($componentType === 'VEVENT' && $component->DTSTART) {
2244
			$firstOccurrence = $component->DTSTART->getDateTime()->getTimeStamp();
2245
			// Finding the last occurrence is a bit harder
2246
			if (!isset($component->RRULE)) {
2247
				if (isset($component->DTEND)) {
2248
					$lastOccurrence = $component->DTEND->getDateTime()->getTimeStamp();
2249
				} elseif (isset($component->DURATION)) {
2250
					$endDate = clone $component->DTSTART->getDateTime();
2251
					$endDate->add(DateTimeParser::parse($component->DURATION->getValue()));
2252
					$lastOccurrence = $endDate->getTimeStamp();
2253
				} elseif (!$component->DTSTART->hasTime()) {
2254
					$endDate = clone $component->DTSTART->getDateTime();
2255
					$endDate->modify('+1 day');
2256
					$lastOccurrence = $endDate->getTimeStamp();
2257
				} else {
2258
					$lastOccurrence = $firstOccurrence;
2259
				}
2260
			} else {
2261
				$it = new EventIterator($vObject, (string)$component->UID);
2262
				$maxDate = new DateTime(self::MAX_DATE);
2263
				if ($it->isInfinite()) {
2264
					$lastOccurrence = $maxDate->getTimestamp();
2265
				} else {
2266
					$end = $it->getDtEnd();
2267
					while($it->valid() && $end < $maxDate) {
2268
						$end = $it->getDtEnd();
2269
						$it->next();
2270
2271
					}
2272
					$lastOccurrence = $end->getTimestamp();
2273
				}
2274
2275
			}
2276
		}
2277
2278
		if ($component->CLASS) {
2279
			$classification = CalDavBackend::CLASSIFICATION_PRIVATE;
2280
			switch ($component->CLASS->getValue()) {
2281
				case 'PUBLIC':
2282
					$classification = CalDavBackend::CLASSIFICATION_PUBLIC;
2283
					break;
2284
				case 'CONFIDENTIAL':
2285
					$classification = CalDavBackend::CLASSIFICATION_CONFIDENTIAL;
2286
					break;
2287
			}
2288
		}
2289
		return [
2290
			'etag' => md5($calendarData),
2291
			'size' => strlen($calendarData),
2292
			'componentType' => $componentType,
2293
			'firstOccurence' => is_null($firstOccurrence) ? null : max(0, $firstOccurrence),
2294
			'lastOccurence'  => $lastOccurrence,
2295
			'uid' => $uid,
2296
			'classification' => $classification
2297
		];
2298
2299
	}
2300
2301
	/**
2302
	 * @param $cardData
2303
	 * @return bool|string
2304
	 */
2305
	private function readBlob($cardData) {
2306
		if (is_resource($cardData)) {
2307
			return stream_get_contents($cardData);
2308
		}
2309
2310
		return $cardData;
2311
	}
2312
2313
	/**
2314
	 * @param IShareable $shareable
2315
	 * @param array $add
2316
	 * @param array $remove
2317
	 */
2318
	public function updateShares($shareable, $add, $remove) {
2319
		$calendarId = $shareable->getResourceId();
2320
		$this->dispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateShares', new GenericEvent(
0 ignored issues
show
Bug introduced by
'\OCA\DAV\CalDAV\CalDavBackend::updateShares' of type string is incompatible with the type object expected by parameter $event of Symfony\Contracts\EventD...erInterface::dispatch(). ( Ignorable by Annotation )

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

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

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

2320
		$this->dispatcher->/** @scrutinizer ignore-call */ 
2321
                     dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateShares', new GenericEvent(

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
2321
			'\OCA\DAV\CalDAV\CalDavBackend::updateShares',
2322
			[
2323
				'calendarId' => $calendarId,
2324
				'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...
2325
				'shares' => $this->getShares($calendarId),
2326
				'add' => $add,
2327
				'remove' => $remove,
2328
			]));
2329
		$this->calendarSharingBackend->updateShares($shareable, $add, $remove);
2330
	}
2331
2332
	/**
2333
	 * @param int $resourceId
2334
	 * @param int $calendarType
2335
	 * @return array
2336
	 */
2337
	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

2337
	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...
2338
		return $this->calendarSharingBackend->getShares($resourceId);
2339
	}
2340
2341
	/**
2342
	 * @param boolean $value
2343
	 * @param \OCA\DAV\CalDAV\Calendar $calendar
2344
	 * @return string|null
2345
	 */
2346
	public function setPublishStatus($value, $calendar) {
2347
2348
		$calendarId = $calendar->getResourceId();
2349
		$this->dispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::publishCalendar', new GenericEvent(
0 ignored issues
show
Unused Code introduced by
The call to Symfony\Contracts\EventD...erInterface::dispatch() has too many arguments starting with new Symfony\Component\Ev...), 'public' => $value)). ( Ignorable by Annotation )

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

2349
		$this->dispatcher->/** @scrutinizer ignore-call */ 
2350
                     dispatch('\OCA\DAV\CalDAV\CalDavBackend::publishCalendar', new GenericEvent(

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
Bug introduced by
'\OCA\DAV\CalDAV\CalDavBackend::publishCalendar' of type string is incompatible with the type object expected by parameter $event of Symfony\Contracts\EventD...erInterface::dispatch(). ( Ignorable by Annotation )

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

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

2611
			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

2611
			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...
2612
			if ($toV2 === true) {
2613
				return "principals/users/$name";
2614
			}
2615
			return "principals/$name";
2616
		}
2617
		return $principalUri;
2618
	}
2619
2620
	/**
2621
	 * adds information about an owner to the calendar data
2622
	 *
2623
	 * @param $calendarInfo
2624
	 */
2625
	private function addOwnerPrincipal(&$calendarInfo) {
2626
		$ownerPrincipalKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal';
2627
		$displaynameKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}owner-displayname';
2628
		if (isset($calendarInfo[$ownerPrincipalKey])) {
2629
			$uri = $calendarInfo[$ownerPrincipalKey];
2630
		} else {
2631
			$uri = $calendarInfo['principaluri'];
2632
		}
2633
2634
		$principalInformation = $this->principalBackend->getPrincipalByPath($uri);
2635
		if (isset($principalInformation['{DAV:}displayname'])) {
2636
			$calendarInfo[$displaynameKey] = $principalInformation['{DAV:}displayname'];
2637
		}
2638
	}
2639
}
2640