Completed
Push — master ( e1740c...d98dea )
by Morris
14:21
created

CalDavBackend::deleteAllBirthdayCalendars()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 9
nc 2
nop 0
dl 0
loc 12
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 * @copyright Copyright (c) 2017 Georg Ehrke
5
 *
6
 * @author Georg Ehrke <[email protected]>
7
 * @author Joas Schilling <[email protected]>
8
 * @author Lukas Reschke <[email protected]>
9
 * @author nhirokinet <[email protected]>
10
 * @author Robin Appelman <[email protected]>
11
 * @author Roeland Jago Douma <[email protected]>
12
 * @author Stefan Weil <[email protected]>
13
 * @author Thomas Citharel <[email protected]>
14
 * @author Thomas Müller <[email protected]>
15
 *
16
 * @license AGPL-3.0
17
 *
18
 * This code is free software: you can redistribute it and/or modify
19
 * it under the terms of the GNU Affero General Public License, version 3,
20
 * as published by the Free Software Foundation.
21
 *
22
 * This program is distributed in the hope that it will be useful,
23
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
24
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25
 * GNU Affero General Public License for more details.
26
 *
27
 * You should have received a copy of the GNU Affero General Public License, version 3,
28
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
29
 *
30
 */
31
32
namespace OCA\DAV\CalDAV;
33
34
use OCA\DAV\DAV\Sharing\IShareable;
35
use OCP\DB\QueryBuilder\IQueryBuilder;
36
use OCA\DAV\Connector\Sabre\Principal;
37
use OCA\DAV\DAV\Sharing\Backend;
38
use OCP\IDBConnection;
39
use OCP\IGroupManager;
40
use OCP\ILogger;
41
use OCP\IUser;
42
use OCP\IUserManager;
43
use OCP\Security\ISecureRandom;
44
use Sabre\CalDAV\Backend\AbstractBackend;
45
use Sabre\CalDAV\Backend\SchedulingSupport;
46
use Sabre\CalDAV\Backend\SubscriptionSupport;
47
use Sabre\CalDAV\Backend\SyncSupport;
48
use Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp;
49
use Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet;
50
use Sabre\DAV;
51
use Sabre\DAV\Exception\Forbidden;
52
use Sabre\DAV\Exception\NotFound;
53
use Sabre\DAV\PropPatch;
54
use Sabre\HTTP\URLUtil;
55
use Sabre\VObject\Component;
56
use Sabre\VObject\Component\VCalendar;
57
use Sabre\VObject\Component\VEvent;
58
use Sabre\VObject\Component\VTimeZone;
59
use Sabre\VObject\DateTimeParser;
60
use Sabre\VObject\InvalidDataException;
61
use Sabre\VObject\ParseException;
62
use Sabre\VObject\Property;
63
use Sabre\VObject\Reader;
64
use Sabre\VObject\Recur\EventIterator;
65
use Sabre\Uri;
66
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
67
use Symfony\Component\EventDispatcher\GenericEvent;
68
69
/**
70
 * Class CalDavBackend
71
 *
72
 * Code is heavily inspired by https://github.com/fruux/sabre-dav/blob/master/lib/CalDAV/Backend/PDO.php
73
 *
74
 * @package OCA\DAV\CalDAV
75
 */
76
class CalDavBackend extends AbstractBackend implements SyncSupport, SubscriptionSupport, SchedulingSupport {
77
78
	const PERSONAL_CALENDAR_URI = 'personal';
79
	const PERSONAL_CALENDAR_NAME = 'Personal';
80
81
	/**
82
	 * We need to specify a max date, because we need to stop *somewhere*
83
	 *
84
	 * On 32 bit system the maximum for a signed integer is 2147483647, so
85
	 * MAX_DATE cannot be higher than date('Y-m-d', 2147483647) which results
86
	 * in 2038-01-19 to avoid problems when the date is converted
87
	 * to a unix timestamp.
88
	 */
89
	const MAX_DATE = '2038-01-01';
90
91
	const ACCESS_PUBLIC = 4;
92
	const CLASSIFICATION_PUBLIC = 0;
93
	const CLASSIFICATION_PRIVATE = 1;
94
	const CLASSIFICATION_CONFIDENTIAL = 2;
95
96
	/**
97
	 * List of CalDAV properties, and how they map to database field names
98
	 * Add your own properties by simply adding on to this array.
99
	 *
100
	 * Note that only string-based properties are supported here.
101
	 *
102
	 * @var array
103
	 */
104
	public $propertyMap = [
105
		'{DAV:}displayname'                          => 'displayname',
106
		'{urn:ietf:params:xml:ns:caldav}calendar-description' => 'description',
107
		'{urn:ietf:params:xml:ns:caldav}calendar-timezone'    => 'timezone',
108
		'{http://apple.com/ns/ical/}calendar-order'  => 'calendarorder',
109
		'{http://apple.com/ns/ical/}calendar-color'  => 'calendarcolor',
110
	];
111
112
	/**
113
	 * List of subscription properties, and how they map to database field names.
114
	 *
115
	 * @var array
116
	 */
117
	public $subscriptionPropertyMap = [
118
		'{DAV:}displayname'                                           => 'displayname',
119
		'{http://apple.com/ns/ical/}refreshrate'                      => 'refreshrate',
120
		'{http://apple.com/ns/ical/}calendar-order'                   => 'calendarorder',
121
		'{http://apple.com/ns/ical/}calendar-color'                   => 'calendarcolor',
122
		'{http://calendarserver.org/ns/}subscribed-strip-todos'       => 'striptodos',
123
		'{http://calendarserver.org/ns/}subscribed-strip-alarms'      => 'stripalarms',
124
		'{http://calendarserver.org/ns/}subscribed-strip-attachments' => 'stripattachments',
125
	];
126
127
	/** @var array properties to index */
128
	public static $indexProperties = ['CATEGORIES', 'COMMENT', 'DESCRIPTION',
129
		'LOCATION', 'RESOURCES', 'STATUS', 'SUMMARY', 'ATTENDEE', 'CONTACT',
130
		'ORGANIZER'];
131
132
	/** @var array parameters to index */
133
	public static $indexParameters = [
134
		'ATTENDEE' => ['CN'],
135
		'ORGANIZER' => ['CN'],
136
	];
137
138
	/**
139
	 * @var string[] Map of uid => display name
140
	 */
141
	protected $userDisplayNames;
142
143
	/** @var IDBConnection */
144
	private $db;
145
146
	/** @var Backend */
147
	private $sharingBackend;
148
149
	/** @var Principal */
150
	private $principalBackend;
151
152
	/** @var IUserManager */
153
	private $userManager;
154
155
	/** @var ISecureRandom */
156
	private $random;
157
158
	/** @var ILogger */
159
	private $logger;
160
161
	/** @var EventDispatcherInterface */
162
	private $dispatcher;
163
164
	/** @var bool */
165
	private $legacyEndpoint;
166
167
	/** @var string */
168
	private $dbObjectPropertiesTable = 'calendarobjects_props';
169
170
	/**
171
	 * CalDavBackend constructor.
172
	 *
173
	 * @param IDBConnection $db
174
	 * @param Principal $principalBackend
175
	 * @param IUserManager $userManager
176
	 * @param IGroupManager $groupManager
177
	 * @param ISecureRandom $random
178
	 * @param ILogger $logger
179
	 * @param EventDispatcherInterface $dispatcher
180
	 * @param bool $legacyEndpoint
181
	 */
182
	public function __construct(IDBConnection $db,
183
								Principal $principalBackend,
184
								IUserManager $userManager,
185
								IGroupManager $groupManager,
186
								ISecureRandom $random,
187
								ILogger $logger,
188
								EventDispatcherInterface $dispatcher,
189
								$legacyEndpoint = false) {
190
		$this->db = $db;
191
		$this->principalBackend = $principalBackend;
192
		$this->userManager = $userManager;
193
		$this->sharingBackend = new Backend($this->db, $this->userManager, $groupManager, $principalBackend, 'calendar');
194
		$this->random = $random;
195
		$this->logger = $logger;
196
		$this->dispatcher = $dispatcher;
197
		$this->legacyEndpoint = $legacyEndpoint;
198
	}
199
200
	/**
201
	 * Return the number of calendars for a principal
202
	 *
203
	 * By default this excludes the automatically generated birthday calendar
204
	 *
205
	 * @param $principalUri
206
	 * @param bool $excludeBirthday
207
	 * @return int
208
	 */
209 View Code Duplication
	public function getCalendarsForUserCount($principalUri, $excludeBirthday = true) {
210
		$principalUri = $this->convertPrincipal($principalUri, true);
211
		$query = $this->db->getQueryBuilder();
212
		$query->select($query->createFunction('COUNT(*)'))
213
			->from('calendars')
214
			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
215
216
		if ($excludeBirthday) {
217
			$query->andWhere($query->expr()->neq('uri', $query->createNamedParameter(BirthdayService::BIRTHDAY_CALENDAR_URI)));
218
		}
219
220
		return (int)$query->execute()->fetchColumn();
221
	}
222
223
	/**
224
	 * Returns a list of calendars for a principal.
225
	 *
226
	 * Every project is an array with the following keys:
227
	 *  * id, a unique id that will be used by other functions to modify the
228
	 *    calendar. This can be the same as the uri or a database key.
229
	 *  * uri, which the basename of the uri with which the calendar is
230
	 *    accessed.
231
	 *  * principaluri. The owner of the calendar. Almost always the same as
232
	 *    principalUri passed to this method.
233
	 *
234
	 * Furthermore it can contain webdav properties in clark notation. A very
235
	 * common one is '{DAV:}displayname'.
236
	 *
237
	 * Many clients also require:
238
	 * {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
239
	 * For this property, you can just return an instance of
240
	 * Sabre\CalDAV\Property\SupportedCalendarComponentSet.
241
	 *
242
	 * If you return {http://sabredav.org/ns}read-only and set the value to 1,
243
	 * ACL will automatically be put in read-only mode.
244
	 *
245
	 * @param string $principalUri
246
	 * @return array
247
	 */
248
	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...
249
		$principalUriOriginal = $principalUri;
250
		$principalUri = $this->convertPrincipal($principalUri, true);
251
		$fields = array_values($this->propertyMap);
252
		$fields[] = 'id';
253
		$fields[] = 'uri';
254
		$fields[] = 'synctoken';
255
		$fields[] = 'components';
256
		$fields[] = 'principaluri';
257
		$fields[] = 'transparent';
258
259
		// Making fields a comma-delimited list
260
		$query = $this->db->getQueryBuilder();
261
		$query->select($fields)->from('calendars')
262
				->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
263
				->orderBy('calendarorder', 'ASC');
264
		$stmt = $query->execute();
265
266
		$calendars = [];
267
		while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
268
269
			$components = [];
270
			if ($row['components']) {
271
				$components = explode(',',$row['components']);
272
			}
273
274
			$calendar = [
275
				'id' => $row['id'],
276
				'uri' => $row['uri'],
277
				'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
278
				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
279
				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
280
				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
281
				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
282
				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($principalUri, !$this->legacyEndpoint),
283
			];
284
285
			foreach($this->propertyMap as $xmlName=>$dbName) {
286
				$calendar[$xmlName] = $row[$dbName];
287
			}
288
289
			$this->addOwnerPrincipal($calendar);
290
291
			if (!isset($calendars[$calendar['id']])) {
292
				$calendars[$calendar['id']] = $calendar;
293
			}
294
		}
295
296
		$stmt->closeCursor();
297
298
		// query for shared calendars
299
		$principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true);
300
		$principals = array_map(function($principal) {
301
			return urldecode($principal);
302
		}, $principals);
303
		$principals[]= $principalUri;
304
305
		$fields = array_values($this->propertyMap);
306
		$fields[] = 'a.id';
307
		$fields[] = 'a.uri';
308
		$fields[] = 'a.synctoken';
309
		$fields[] = 'a.components';
310
		$fields[] = 'a.principaluri';
311
		$fields[] = 'a.transparent';
312
		$fields[] = 's.access';
313
		$query = $this->db->getQueryBuilder();
314
		$result = $query->select($fields)
315
			->from('dav_shares', 's')
316
			->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
317
			->where($query->expr()->in('s.principaluri', $query->createParameter('principaluri')))
318
			->andWhere($query->expr()->eq('s.type', $query->createParameter('type')))
319
			->setParameter('type', 'calendar')
320
			->setParameter('principaluri', $principals, \Doctrine\DBAL\Connection::PARAM_STR_ARRAY)
321
			->execute();
322
323
		$readOnlyPropertyName = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only';
324
		while($row = $result->fetch()) {
325
			if ($row['principaluri'] === $principalUri) {
326
				continue;
327
			}
328
329
			$readOnly = (int) $row['access'] === Backend::ACCESS_READ;
330 View Code Duplication
			if (isset($calendars[$row['id']])) {
331
				if ($readOnly) {
332
					// New share can not have more permissions then the old one.
333
					continue;
334
				}
335
				if (isset($calendars[$row['id']][$readOnlyPropertyName]) &&
336
					$calendars[$row['id']][$readOnlyPropertyName] === 0) {
337
					// Old share is already read-write, no more permissions can be gained
338
					continue;
339
				}
340
			}
341
342
			list(, $name) = Uri\split($row['principaluri']);
343
			$uri = $row['uri'] . '_shared_by_' . $name;
344
			$row['displayname'] = $row['displayname'] . ' (' . $this->getUserDisplayName($name) . ')';
345
			$components = [];
346
			if ($row['components']) {
347
				$components = explode(',',$row['components']);
348
			}
349
			$calendar = [
350
				'id' => $row['id'],
351
				'uri' => $uri,
352
				'principaluri' => $this->convertPrincipal($principalUri, !$this->legacyEndpoint),
353
				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
354
				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
355
				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
356
				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp('transparent'),
357
				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
358
				$readOnlyPropertyName => $readOnly,
359
			];
360
361
			foreach($this->propertyMap as $xmlName=>$dbName) {
362
				$calendar[$xmlName] = $row[$dbName];
363
			}
364
365
			$this->addOwnerPrincipal($calendar);
366
367
			$calendars[$calendar['id']] = $calendar;
368
		}
369
		$result->closeCursor();
370
371
		return array_values($calendars);
372
	}
373
374
	public function getUsersOwnCalendars($principalUri) {
375
		$principalUri = $this->convertPrincipal($principalUri, true);
376
		$fields = array_values($this->propertyMap);
377
		$fields[] = 'id';
378
		$fields[] = 'uri';
379
		$fields[] = 'synctoken';
380
		$fields[] = 'components';
381
		$fields[] = 'principaluri';
382
		$fields[] = 'transparent';
383
		// Making fields a comma-delimited list
384
		$query = $this->db->getQueryBuilder();
385
		$query->select($fields)->from('calendars')
386
			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
387
			->orderBy('calendarorder', 'ASC');
388
		$stmt = $query->execute();
389
		$calendars = [];
390
		while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
391
			$components = [];
392
			if ($row['components']) {
393
				$components = explode(',',$row['components']);
394
			}
395
			$calendar = [
396
				'id' => $row['id'],
397
				'uri' => $row['uri'],
398
				'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
399
				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
400
				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
401
				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
402
				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
403
			];
404
			foreach($this->propertyMap as $xmlName=>$dbName) {
405
				$calendar[$xmlName] = $row[$dbName];
406
			}
407
408
			$this->addOwnerPrincipal($calendar);
409
410
			if (!isset($calendars[$calendar['id']])) {
411
				$calendars[$calendar['id']] = $calendar;
412
			}
413
		}
414
		$stmt->closeCursor();
415
		return array_values($calendars);
416
	}
417
418
419 View Code Duplication
	private function getUserDisplayName($uid) {
420
		if (!isset($this->userDisplayNames[$uid])) {
421
			$user = $this->userManager->get($uid);
422
423
			if ($user instanceof IUser) {
424
				$this->userDisplayNames[$uid] = $user->getDisplayName();
425
			} else {
426
				$this->userDisplayNames[$uid] = $uid;
427
			}
428
		}
429
430
		return $this->userDisplayNames[$uid];
431
	}
432
	
433
	/**
434
	 * @return array
435
	 */
436
	public function getPublicCalendars() {
437
		$fields = array_values($this->propertyMap);
438
		$fields[] = 'a.id';
439
		$fields[] = 'a.uri';
440
		$fields[] = 'a.synctoken';
441
		$fields[] = 'a.components';
442
		$fields[] = 'a.principaluri';
443
		$fields[] = 'a.transparent';
444
		$fields[] = 's.access';
445
		$fields[] = 's.publicuri';
446
		$calendars = [];
447
		$query = $this->db->getQueryBuilder();
448
		$result = $query->select($fields)
449
			->from('dav_shares', 's')
450
			->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
451
			->where($query->expr()->in('s.access', $query->createNamedParameter(self::ACCESS_PUBLIC)))
452
			->andWhere($query->expr()->eq('s.type', $query->createNamedParameter('calendar')))
453
			->execute();
454
455
		while($row = $result->fetch()) {
456
			list(, $name) = Uri\split($row['principaluri']);
457
			$row['displayname'] = $row['displayname'] . "($name)";
458
			$components = [];
459
			if ($row['components']) {
460
				$components = explode(',',$row['components']);
461
			}
462
			$calendar = [
463
				'id' => $row['id'],
464
				'uri' => $row['publicuri'],
465
				'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
466
				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
467
				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
468
				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
469
				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
470
				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], $this->legacyEndpoint),
471
				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ,
472
				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}public' => (int)$row['access'] === self::ACCESS_PUBLIC,
473
			];
474
475
			foreach($this->propertyMap as $xmlName=>$dbName) {
476
				$calendar[$xmlName] = $row[$dbName];
477
			}
478
479
			$this->addOwnerPrincipal($calendar);
480
481
			if (!isset($calendars[$calendar['id']])) {
482
				$calendars[$calendar['id']] = $calendar;
483
			}
484
		}
485
		$result->closeCursor();
486
487
		return array_values($calendars);
488
	}
489
490
	/**
491
	 * @param string $uri
492
	 * @return array
493
	 * @throws NotFound
494
	 */
495
	public function getPublicCalendar($uri) {
496
		$fields = array_values($this->propertyMap);
497
		$fields[] = 'a.id';
498
		$fields[] = 'a.uri';
499
		$fields[] = 'a.synctoken';
500
		$fields[] = 'a.components';
501
		$fields[] = 'a.principaluri';
502
		$fields[] = 'a.transparent';
503
		$fields[] = 's.access';
504
		$fields[] = 's.publicuri';
505
		$query = $this->db->getQueryBuilder();
506
		$result = $query->select($fields)
507
			->from('dav_shares', 's')
508
			->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
509
			->where($query->expr()->in('s.access', $query->createNamedParameter(self::ACCESS_PUBLIC)))
510
			->andWhere($query->expr()->eq('s.type', $query->createNamedParameter('calendar')))
511
			->andWhere($query->expr()->eq('s.publicuri', $query->createNamedParameter($uri)))
512
			->execute();
513
514
		$row = $result->fetch(\PDO::FETCH_ASSOC);
515
516
		$result->closeCursor();
517
518
		if ($row === false) {
519
			throw new NotFound('Node with name \'' . $uri . '\' could not be found');
520
		}
521
522
		list(, $name) = Uri\split($row['principaluri']);
523
		$row['displayname'] = $row['displayname'] . ' ' . "($name)";
524
		$components = [];
525
		if ($row['components']) {
526
			$components = explode(',',$row['components']);
527
		}
528
		$calendar = [
529
			'id' => $row['id'],
530
			'uri' => $row['publicuri'],
531
			'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
532
			'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
533
			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
534
			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
535
			'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
536
			'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
537
			'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ,
538
			'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}public' => (int)$row['access'] === self::ACCESS_PUBLIC,
539
		];
540
541
		foreach($this->propertyMap as $xmlName=>$dbName) {
542
			$calendar[$xmlName] = $row[$dbName];
543
		}
544
545
		$this->addOwnerPrincipal($calendar);
546
547
		return $calendar;
548
549
	}
550
551
	/**
552
	 * @param string $principal
553
	 * @param string $uri
554
	 * @return array|null
555
	 */
556
	public function getCalendarByUri($principal, $uri) {
557
		$fields = array_values($this->propertyMap);
558
		$fields[] = 'id';
559
		$fields[] = 'uri';
560
		$fields[] = 'synctoken';
561
		$fields[] = 'components';
562
		$fields[] = 'principaluri';
563
		$fields[] = 'transparent';
564
565
		// Making fields a comma-delimited list
566
		$query = $this->db->getQueryBuilder();
567
		$query->select($fields)->from('calendars')
568
			->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
569
			->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($principal)))
570
			->setMaxResults(1);
571
		$stmt = $query->execute();
572
573
		$row = $stmt->fetch(\PDO::FETCH_ASSOC);
574
		$stmt->closeCursor();
575
		if ($row === false) {
576
			return null;
577
		}
578
579
		$components = [];
580
		if ($row['components']) {
581
			$components = explode(',',$row['components']);
582
		}
583
584
		$calendar = [
585
			'id' => $row['id'],
586
			'uri' => $row['uri'],
587
			'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
588
			'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
589
			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
590
			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
591
			'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
592
		];
593
594
		foreach($this->propertyMap as $xmlName=>$dbName) {
595
			$calendar[$xmlName] = $row[$dbName];
596
		}
597
598
		$this->addOwnerPrincipal($calendar);
599
600
		return $calendar;
601
	}
602
603
	public function getCalendarById($calendarId) {
604
		$fields = array_values($this->propertyMap);
605
		$fields[] = 'id';
606
		$fields[] = 'uri';
607
		$fields[] = 'synctoken';
608
		$fields[] = 'components';
609
		$fields[] = 'principaluri';
610
		$fields[] = 'transparent';
611
612
		// Making fields a comma-delimited list
613
		$query = $this->db->getQueryBuilder();
614
		$query->select($fields)->from('calendars')
615
			->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)))
616
			->setMaxResults(1);
617
		$stmt = $query->execute();
618
619
		$row = $stmt->fetch(\PDO::FETCH_ASSOC);
620
		$stmt->closeCursor();
621
		if ($row === false) {
622
			return null;
623
		}
624
625
		$components = [];
626
		if ($row['components']) {
627
			$components = explode(',',$row['components']);
628
		}
629
630
		$calendar = [
631
			'id' => $row['id'],
632
			'uri' => $row['uri'],
633
			'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
634
			'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
635
			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
636
			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
637
			'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
638
		];
639
640
		foreach($this->propertyMap as $xmlName=>$dbName) {
641
			$calendar[$xmlName] = $row[$dbName];
642
		}
643
644
		$this->addOwnerPrincipal($calendar);
645
646
		return $calendar;
647
	}
648
649
	/**
650
	 * Creates a new calendar for a principal.
651
	 *
652
	 * If the creation was a success, an id must be returned that can be used to reference
653
	 * this calendar in other methods, such as updateCalendar.
654
	 *
655
	 * @param string $principalUri
656
	 * @param string $calendarUri
657
	 * @param array $properties
658
	 * @return int
659
	 * @suppress SqlInjectionChecker
660
	 */
661
	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...
662
		$values = [
663
			'principaluri' => $this->convertPrincipal($principalUri, true),
664
			'uri'          => $calendarUri,
665
			'synctoken'    => 1,
666
			'transparent'  => 0,
667
			'components'   => 'VEVENT,VTODO',
668
			'displayname'  => $calendarUri
669
		];
670
671
		// Default value
672
		$sccs = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set';
673
		if (isset($properties[$sccs])) {
674
			if (!($properties[$sccs] instanceof SupportedCalendarComponentSet)) {
0 ignored issues
show
Bug introduced by
The class Sabre\CalDAV\Xml\Propert...tedCalendarComponentSet does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
675
				throw new DAV\Exception('The ' . $sccs . ' property must be of type: \Sabre\CalDAV\Property\SupportedCalendarComponentSet');
676
			}
677
			$values['components'] = implode(',',$properties[$sccs]->getValue());
678
		}
679
		$transp = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp';
680
		if (isset($properties[$transp])) {
681
			$values['transparent'] = (int) ($properties[$transp]->getValue() === 'transparent');
682
		}
683
684
		foreach($this->propertyMap as $xmlName=>$dbName) {
685
			if (isset($properties[$xmlName])) {
686
				$values[$dbName] = $properties[$xmlName];
687
			}
688
		}
689
690
		$query = $this->db->getQueryBuilder();
691
		$query->insert('calendars');
692
		foreach($values as $column => $value) {
693
			$query->setValue($column, $query->createNamedParameter($value));
694
		}
695
		$query->execute();
696
		$calendarId = $query->getLastInsertId();
697
698
		$this->dispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createCalendar', new GenericEvent(
699
			'\OCA\DAV\CalDAV\CalDavBackend::createCalendar',
700
			[
701
				'calendarId' => $calendarId,
702
				'calendarData' => $this->getCalendarById($calendarId),
703
		]));
704
705
		return $calendarId;
706
	}
707
708
	/**
709
	 * Updates properties for a calendar.
710
	 *
711
	 * The list of mutations is stored in a Sabre\DAV\PropPatch object.
712
	 * To do the actual updates, you must tell this object which properties
713
	 * you're going to process with the handle() method.
714
	 *
715
	 * Calling the handle method is like telling the PropPatch object "I
716
	 * promise I can handle updating this property".
717
	 *
718
	 * Read the PropPatch documentation for more info and examples.
719
	 *
720
	 * @param mixed $calendarId
721
	 * @param PropPatch $propPatch
722
	 * @return void
723
	 */
724
	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...
725
		$supportedProperties = array_keys($this->propertyMap);
726
		$supportedProperties[] = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp';
727
728
		/**
729
		 * @suppress SqlInjectionChecker
730
		 */
731
		$propPatch->handle($supportedProperties, function($mutations) use ($calendarId) {
732
			$newValues = [];
733
			foreach ($mutations as $propertyName => $propertyValue) {
734
735
				switch ($propertyName) {
736
					case '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' :
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a CASE statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in case statements.

switch ($selector) {
    case "A": //right
        doSomething();
        break;
    case "B" : //wrong
        doSomethingElse();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
737
						$fieldName = 'transparent';
738
						$newValues[$fieldName] = (int) ($propertyValue->getValue() === 'transparent');
739
						break;
740
					default :
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a DEFAULT statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in the default statement.

switch ($expr) {
    default : //wrong
        doSomething();
        break;
}

switch ($expr) {
    default: //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
741
						$fieldName = $this->propertyMap[$propertyName];
742
						$newValues[$fieldName] = $propertyValue;
743
						break;
744
				}
745
746
			}
747
			$query = $this->db->getQueryBuilder();
748
			$query->update('calendars');
749
			foreach ($newValues as $fieldName => $value) {
750
				$query->set($fieldName, $query->createNamedParameter($value));
751
			}
752
			$query->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)));
753
			$query->execute();
754
755
			$this->addChange($calendarId, "", 2);
756
757
			$this->dispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateCalendar', new GenericEvent(
758
				'\OCA\DAV\CalDAV\CalDavBackend::updateCalendar',
759
				[
760
					'calendarId' => $calendarId,
761
					'calendarData' => $this->getCalendarById($calendarId),
762
					'shares' => $this->getShares($calendarId),
763
					'propertyMutations' => $mutations,
764
			]));
765
766
			return true;
767
		});
768
	}
769
770
	/**
771
	 * Delete a calendar and all it's objects
772
	 *
773
	 * @param mixed $calendarId
774
	 * @return void
775
	 */
776
	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...
777
		$this->dispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteCalendar', new GenericEvent(
778
			'\OCA\DAV\CalDAV\CalDavBackend::deleteCalendar',
779
			[
780
				'calendarId' => $calendarId,
781
				'calendarData' => $this->getCalendarById($calendarId),
782
				'shares' => $this->getShares($calendarId),
783
		]));
784
785
		$stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ?');
786
		$stmt->execute([$calendarId]);
787
788
		$stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendars` WHERE `id` = ?');
789
		$stmt->execute([$calendarId]);
790
791
		$stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarchanges` WHERE `calendarid` = ?');
792
		$stmt->execute([$calendarId]);
793
794
		$this->sharingBackend->deleteAllShares($calendarId);
795
796
		$query = $this->db->getQueryBuilder();
797
		$query->delete($this->dbObjectPropertiesTable)
798
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
799
			->execute();
800
	}
801
802
	/**
803
	 * Delete all of an user's shares
804
	 *
805
	 * @param string $principaluri
806
	 * @return void
807
	 */
808
	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...
809
		$this->sharingBackend->deleteAllSharesByUser($principaluri);
810
	}
811
812
	/**
813
	 * Returns all calendar objects within a calendar.
814
	 *
815
	 * Every item contains an array with the following keys:
816
	 *   * calendardata - The iCalendar-compatible calendar data
817
	 *   * uri - a unique key which will be used to construct the uri. This can
818
	 *     be any arbitrary string, but making sure it ends with '.ics' is a
819
	 *     good idea. This is only the basename, or filename, not the full
820
	 *     path.
821
	 *   * lastmodified - a timestamp of the last modification time
822
	 *   * etag - An arbitrary string, surrounded by double-quotes. (e.g.:
823
	 *   '"abcdef"')
824
	 *   * size - The size of the calendar objects, in bytes.
825
	 *   * component - optional, a string containing the type of object, such
826
	 *     as 'vevent' or 'vtodo'. If specified, this will be used to populate
827
	 *     the Content-Type header.
828
	 *
829
	 * Note that the etag is optional, but it's highly encouraged to return for
830
	 * speed reasons.
831
	 *
832
	 * The calendardata is also optional. If it's not returned
833
	 * 'getCalendarObject' will be called later, which *is* expected to return
834
	 * calendardata.
835
	 *
836
	 * If neither etag or size are specified, the calendardata will be
837
	 * used/fetched to determine these numbers. If both are specified the
838
	 * amount of times this is needed is reduced by a great degree.
839
	 *
840
	 * @param mixed $calendarId
841
	 * @return array
842
	 */
843
	function getCalendarObjects($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...
844
		$query = $this->db->getQueryBuilder();
845
		$query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'componenttype', 'classification'])
846
			->from('calendarobjects')
847
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)));
848
		$stmt = $query->execute();
849
850
		$result = [];
851
		foreach($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) {
852
			$result[] = [
853
					'id'           => $row['id'],
854
					'uri'          => $row['uri'],
855
					'lastmodified' => $row['lastmodified'],
856
					'etag'         => '"' . $row['etag'] . '"',
857
					'calendarid'   => $row['calendarid'],
858
					'size'         => (int)$row['size'],
859
					'component'    => strtolower($row['componenttype']),
860
					'classification'=> (int)$row['classification']
861
			];
862
		}
863
864
		return $result;
865
	}
866
867
	/**
868
	 * Returns information from a single calendar object, based on it's object
869
	 * uri.
870
	 *
871
	 * The object uri is only the basename, or filename and not a full path.
872
	 *
873
	 * The returned array must have the same keys as getCalendarObjects. The
874
	 * 'calendardata' object is required here though, while it's not required
875
	 * for getCalendarObjects.
876
	 *
877
	 * This method must return null if the object did not exist.
878
	 *
879
	 * @param mixed $calendarId
880
	 * @param string $objectUri
881
	 * @return array|null
882
	 */
883
	function getCalendarObject($calendarId, $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...
884
885
		$query = $this->db->getQueryBuilder();
886
		$query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification'])
887
				->from('calendarobjects')
888
				->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
889
				->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)));
890
		$stmt = $query->execute();
891
		$row = $stmt->fetch(\PDO::FETCH_ASSOC);
892
893
		if(!$row) return null;
894
895
		return [
896
				'id'            => $row['id'],
897
				'uri'           => $row['uri'],
898
				'lastmodified'  => $row['lastmodified'],
899
				'etag'          => '"' . $row['etag'] . '"',
900
				'calendarid'    => $row['calendarid'],
901
				'size'          => (int)$row['size'],
902
				'calendardata'  => $this->readBlob($row['calendardata']),
903
				'component'     => strtolower($row['componenttype']),
904
				'classification'=> (int)$row['classification']
905
		];
906
	}
907
908
	/**
909
	 * Returns a list of calendar objects.
910
	 *
911
	 * This method should work identical to getCalendarObject, but instead
912
	 * return all the calendar objects in the list as an array.
913
	 *
914
	 * If the backend supports this, it may allow for some speed-ups.
915
	 *
916
	 * @param mixed $calendarId
917
	 * @param string[] $uris
918
	 * @return array
919
	 */
920
	function getMultipleCalendarObjects($calendarId, array $uris) {
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...
921
		if (empty($uris)) {
922
			return [];
923
		}
924
925
		$chunks = array_chunk($uris, 100);
926
		$objects = [];
927
928
		$query = $this->db->getQueryBuilder();
929
		$query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification'])
930
			->from('calendarobjects')
931
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
932
			->andWhere($query->expr()->in('uri', $query->createParameter('uri')));
933
934
		foreach ($chunks as $uris) {
935
			$query->setParameter('uri', $uris, IQueryBuilder::PARAM_STR_ARRAY);
936
			$result = $query->execute();
937
938
			while ($row = $result->fetch()) {
939
				$objects[] = [
940
					'id'           => $row['id'],
941
					'uri'          => $row['uri'],
942
					'lastmodified' => $row['lastmodified'],
943
					'etag'         => '"' . $row['etag'] . '"',
944
					'calendarid'   => $row['calendarid'],
945
					'size'         => (int)$row['size'],
946
					'calendardata' => $this->readBlob($row['calendardata']),
947
					'component'    => strtolower($row['componenttype']),
948
					'classification' => (int)$row['classification']
949
				];
950
			}
951
			$result->closeCursor();
952
		}
953
		return $objects;
954
	}
955
956
	/**
957
	 * Creates a new calendar object.
958
	 *
959
	 * The object uri is only the basename, or filename and not a full path.
960
	 *
961
	 * It is possible return an etag from this function, which will be used in
962
	 * the response to this PUT request. Note that the ETag must be surrounded
963
	 * by double-quotes.
964
	 *
965
	 * However, you should only really return this ETag if you don't mangle the
966
	 * calendar-data. If the result of a subsequent GET to this object is not
967
	 * the exact same as this request body, you should omit the ETag.
968
	 *
969
	 * @param mixed $calendarId
970
	 * @param string $objectUri
971
	 * @param string $calendarData
972
	 * @return string
973
	 */
974
	function createCalendarObject($calendarId, $objectUri, $calendarData) {
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...
975
		$extraData = $this->getDenormalizedData($calendarData);
976
977
		$q = $this->db->getQueryBuilder();
978
		$q->select($q->createFunction('COUNT(*)'))
979
			->from('calendarobjects')
980
			->where($q->expr()->eq('calendarid', $q->createNamedParameter($calendarId)))
981
			->andWhere($q->expr()->eq('uid', $q->createNamedParameter($extraData['uid'])));
982
983
		$result = $q->execute();
984
		$count = (int) $result->fetchColumn();
985
		$result->closeCursor();
986
987
		if ($count !== 0) {
988
			throw new \Sabre\DAV\Exception\BadRequest('Calendar object with uid already exists in this calendar collection.');
989
		}
990
991
		$query = $this->db->getQueryBuilder();
992
		$query->insert('calendarobjects')
993
			->values([
994
				'calendarid' => $query->createNamedParameter($calendarId),
995
				'uri' => $query->createNamedParameter($objectUri),
996
				'calendardata' => $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB),
997
				'lastmodified' => $query->createNamedParameter(time()),
998
				'etag' => $query->createNamedParameter($extraData['etag']),
999
				'size' => $query->createNamedParameter($extraData['size']),
1000
				'componenttype' => $query->createNamedParameter($extraData['componentType']),
1001
				'firstoccurence' => $query->createNamedParameter($extraData['firstOccurence']),
1002
				'lastoccurence' => $query->createNamedParameter($extraData['lastOccurence']),
1003
				'classification' => $query->createNamedParameter($extraData['classification']),
1004
				'uid' => $query->createNamedParameter($extraData['uid']),
1005
			])
1006
			->execute();
1007
1008
		$this->updateProperties($calendarId, $objectUri, $calendarData);
1009
1010
		$this->dispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createCalendarObject', new GenericEvent(
1011
			'\OCA\DAV\CalDAV\CalDavBackend::createCalendarObject',
1012
			[
1013
				'calendarId' => $calendarId,
1014
				'calendarData' => $this->getCalendarById($calendarId),
1015
				'shares' => $this->getShares($calendarId),
1016
				'objectData' => $this->getCalendarObject($calendarId, $objectUri),
1017
			]
1018
		));
1019
		$this->addChange($calendarId, $objectUri, 1);
1020
1021
		return '"' . $extraData['etag'] . '"';
1022
	}
1023
1024
	/**
1025
	 * Updates an existing calendarobject, based on it's uri.
1026
	 *
1027
	 * The object uri is only the basename, or filename and not a full path.
1028
	 *
1029
	 * It is possible return an etag from this function, which will be used in
1030
	 * the response to this PUT request. Note that the ETag must be surrounded
1031
	 * by double-quotes.
1032
	 *
1033
	 * However, you should only really return this ETag if you don't mangle the
1034
	 * calendar-data. If the result of a subsequent GET to this object is not
1035
	 * the exact same as this request body, you should omit the ETag.
1036
	 *
1037
	 * @param mixed $calendarId
1038
	 * @param string $objectUri
1039
	 * @param string $calendarData
1040
	 * @return string
1041
	 */
1042
	function updateCalendarObject($calendarId, $objectUri, $calendarData) {
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...
1043
		$extraData = $this->getDenormalizedData($calendarData);
1044
1045
		$query = $this->db->getQueryBuilder();
1046
		$query->update('calendarobjects')
1047
				->set('calendardata', $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB))
1048
				->set('lastmodified', $query->createNamedParameter(time()))
1049
				->set('etag', $query->createNamedParameter($extraData['etag']))
1050
				->set('size', $query->createNamedParameter($extraData['size']))
1051
				->set('componenttype', $query->createNamedParameter($extraData['componentType']))
1052
				->set('firstoccurence', $query->createNamedParameter($extraData['firstOccurence']))
1053
				->set('lastoccurence', $query->createNamedParameter($extraData['lastOccurence']))
1054
				->set('classification', $query->createNamedParameter($extraData['classification']))
1055
				->set('uid', $query->createNamedParameter($extraData['uid']))
1056
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
1057
			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
1058
			->execute();
1059
1060
		$this->updateProperties($calendarId, $objectUri, $calendarData);
1061
1062
		$data = $this->getCalendarObject($calendarId, $objectUri);
1063 View Code Duplication
		if (is_array($data)) {
1064
			$this->dispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateCalendarObject', new GenericEvent(
1065
				'\OCA\DAV\CalDAV\CalDavBackend::updateCalendarObject',
1066
				[
1067
					'calendarId' => $calendarId,
1068
					'calendarData' => $this->getCalendarById($calendarId),
1069
					'shares' => $this->getShares($calendarId),
1070
					'objectData' => $data,
1071
				]
1072
			));
1073
		}
1074
		$this->addChange($calendarId, $objectUri, 2);
1075
1076
		return '"' . $extraData['etag'] . '"';
1077
	}
1078
1079
	/**
1080
	 * @param int $calendarObjectId
1081
	 * @param int $classification
1082
	 */
1083
	public function setClassification($calendarObjectId, $classification) {
1084
		if (!in_array($classification, [
1085
			self::CLASSIFICATION_PUBLIC, self::CLASSIFICATION_PRIVATE, self::CLASSIFICATION_CONFIDENTIAL
1086
		])) {
1087
			throw new \InvalidArgumentException();
1088
		}
1089
		$query = $this->db->getQueryBuilder();
1090
		$query->update('calendarobjects')
1091
			->set('classification', $query->createNamedParameter($classification))
1092
			->where($query->expr()->eq('id', $query->createNamedParameter($calendarObjectId)))
1093
			->execute();
1094
	}
1095
1096
	/**
1097
	 * Deletes an existing calendar object.
1098
	 *
1099
	 * The object uri is only the basename, or filename and not a full path.
1100
	 *
1101
	 * @param mixed $calendarId
1102
	 * @param string $objectUri
1103
	 * @return void
1104
	 */
1105
	function deleteCalendarObject($calendarId, $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...
1106
		$data = $this->getCalendarObject($calendarId, $objectUri);
1107 View Code Duplication
		if (is_array($data)) {
1108
			$this->dispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteCalendarObject', new GenericEvent(
1109
				'\OCA\DAV\CalDAV\CalDavBackend::deleteCalendarObject',
1110
				[
1111
					'calendarId' => $calendarId,
1112
					'calendarData' => $this->getCalendarById($calendarId),
1113
					'shares' => $this->getShares($calendarId),
1114
					'objectData' => $data,
1115
				]
1116
			));
1117
		}
1118
1119
		$stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ? AND `uri` = ?');
1120
		$stmt->execute([$calendarId, $objectUri]);
1121
1122
		$this->purgeProperties($calendarId, $data['id']);
1123
1124
		$this->addChange($calendarId, $objectUri, 3);
1125
	}
1126
1127
	/**
1128
	 * Performs a calendar-query on the contents of this calendar.
1129
	 *
1130
	 * The calendar-query is defined in RFC4791 : CalDAV. Using the
1131
	 * calendar-query it is possible for a client to request a specific set of
1132
	 * object, based on contents of iCalendar properties, date-ranges and
1133
	 * iCalendar component types (VTODO, VEVENT).
1134
	 *
1135
	 * This method should just return a list of (relative) urls that match this
1136
	 * query.
1137
	 *
1138
	 * The list of filters are specified as an array. The exact array is
1139
	 * documented by Sabre\CalDAV\CalendarQueryParser.
1140
	 *
1141
	 * Note that it is extremely likely that getCalendarObject for every path
1142
	 * returned from this method will be called almost immediately after. You
1143
	 * may want to anticipate this to speed up these requests.
1144
	 *
1145
	 * This method provides a default implementation, which parses *all* the
1146
	 * iCalendar objects in the specified calendar.
1147
	 *
1148
	 * This default may well be good enough for personal use, and calendars
1149
	 * that aren't very large. But if you anticipate high usage, big calendars
1150
	 * or high loads, you are strongly advised to optimize certain paths.
1151
	 *
1152
	 * The best way to do so is override this method and to optimize
1153
	 * specifically for 'common filters'.
1154
	 *
1155
	 * Requests that are extremely common are:
1156
	 *   * requests for just VEVENTS
1157
	 *   * requests for just VTODO
1158
	 *   * requests with a time-range-filter on either VEVENT or VTODO.
1159
	 *
1160
	 * ..and combinations of these requests. It may not be worth it to try to
1161
	 * handle every possible situation and just rely on the (relatively
1162
	 * easy to use) CalendarQueryValidator to handle the rest.
1163
	 *
1164
	 * Note that especially time-range-filters may be difficult to parse. A
1165
	 * time-range filter specified on a VEVENT must for instance also handle
1166
	 * recurrence rules correctly.
1167
	 * A good example of how to interprete all these filters can also simply
1168
	 * be found in Sabre\CalDAV\CalendarQueryFilter. This class is as correct
1169
	 * as possible, so it gives you a good idea on what type of stuff you need
1170
	 * to think of.
1171
	 *
1172
	 * @param mixed $calendarId
1173
	 * @param array $filters
1174
	 * @return array
1175
	 */
1176
	function calendarQuery($calendarId, array $filters) {
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...
1177
		$componentType = null;
1178
		$requirePostFilter = true;
1179
		$timeRange = null;
1180
1181
		// if no filters were specified, we don't need to filter after a query
1182
		if (!$filters['prop-filters'] && !$filters['comp-filters']) {
1183
			$requirePostFilter = false;
1184
		}
1185
1186
		// Figuring out if there's a component filter
1187
		if (count($filters['comp-filters']) > 0 && !$filters['comp-filters'][0]['is-not-defined']) {
1188
			$componentType = $filters['comp-filters'][0]['name'];
1189
1190
			// Checking if we need post-filters
1191 View Code Duplication
			if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['time-range'] && !$filters['comp-filters'][0]['prop-filters']) {
1192
				$requirePostFilter = false;
1193
			}
1194
			// There was a time-range filter
1195
			if ($componentType === 'VEVENT' && isset($filters['comp-filters'][0]['time-range'])) {
1196
				$timeRange = $filters['comp-filters'][0]['time-range'];
1197
1198
				// If start time OR the end time is not specified, we can do a
1199
				// 100% accurate mysql query.
1200 View Code Duplication
				if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['prop-filters'] && (!$timeRange['start'] || !$timeRange['end'])) {
1201
					$requirePostFilter = false;
1202
				}
1203
			}
1204
1205
		}
1206
		$columns = ['uri'];
1207
		if ($requirePostFilter) {
1208
			$columns = ['uri', 'calendardata'];
1209
		}
1210
		$query = $this->db->getQueryBuilder();
1211
		$query->select($columns)
1212
			->from('calendarobjects')
1213
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)));
1214
1215
		if ($componentType) {
1216
			$query->andWhere($query->expr()->eq('componenttype', $query->createNamedParameter($componentType)));
1217
		}
1218
1219
		if ($timeRange && $timeRange['start']) {
1220
			$query->andWhere($query->expr()->gt('lastoccurence', $query->createNamedParameter($timeRange['start']->getTimeStamp())));
1221
		}
1222
		if ($timeRange && $timeRange['end']) {
1223
			$query->andWhere($query->expr()->lt('firstoccurence', $query->createNamedParameter($timeRange['end']->getTimeStamp())));
1224
		}
1225
1226
		$stmt = $query->execute();
1227
1228
		$result = [];
1229
		while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
1230
			if ($requirePostFilter) {
1231
				// validateFilterForObject will parse the calendar data
1232
				// catch parsing errors
1233
				try {
1234
					$matches = $this->validateFilterForObject($row, $filters);
1235
				} catch(ParseException $ex) {
0 ignored issues
show
Bug introduced by
The class Sabre\VObject\ParseException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
1236
					$this->logger->logException($ex, [
1237
						'app' => 'dav',
1238
						'message' => 'Caught parsing exception for calendar data. This usually indicates invalid calendar data. calendar-id:'.$calendarId.' uri:'.$row['uri']
1239
					]);
1240
					continue;
1241
				} catch (InvalidDataException $ex) {
0 ignored issues
show
Bug introduced by
The class Sabre\VObject\InvalidDataException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
1242
					$this->logger->logException($ex, [
1243
						'app' => 'dav',
1244
						'message' => 'Caught invalid data exception for calendar data. This usually indicates invalid calendar data. calendar-id:'.$calendarId.' uri:'.$row['uri']
1245
					]);
1246
					continue;
1247
				}
1248
1249
				if (!$matches) {
1250
					continue;
1251
				}
1252
			}
1253
			$result[] = $row['uri'];
1254
		}
1255
1256
		return $result;
1257
	}
1258
1259
	/**
1260
	 * custom Nextcloud search extension for CalDAV
1261
	 *
1262
	 * @param string $principalUri
1263
	 * @param array $filters
1264
	 * @param integer|null $limit
1265
	 * @param integer|null $offset
1266
	 * @return array
1267
	 */
1268
	public function calendarSearch($principalUri, array $filters, $limit=null, $offset=null) {
1269
		$calendars = $this->getCalendarsForUser($principalUri);
1270
		$ownCalendars = [];
1271
		$sharedCalendars = [];
1272
1273
		$uriMapper = [];
1274
1275
		foreach($calendars as $calendar) {
1276
			if ($calendar['{http://owncloud.org/ns}owner-principal'] === $principalUri) {
1277
				$ownCalendars[] = $calendar['id'];
1278
			} else {
1279
				$sharedCalendars[] = $calendar['id'];
1280
			}
1281
			$uriMapper[$calendar['id']] = $calendar['uri'];
1282
		}
1283
		if (count($ownCalendars) === 0 && count($sharedCalendars) === 0) {
1284
			return [];
1285
		}
1286
1287
		$query = $this->db->getQueryBuilder();
1288
		// Calendar id expressions
1289
		$calendarExpressions = [];
1290
		foreach($ownCalendars as $id) {
1291
			$calendarExpressions[] = $query->expr()
1292
				->eq('c.calendarid', $query->createNamedParameter($id));
1293
		}
1294
		foreach($sharedCalendars as $id) {
1295
			$calendarExpressions[] = $query->expr()->andX(
1296
				$query->expr()->eq('c.calendarid',
1297
					$query->createNamedParameter($id)),
1298
				$query->expr()->eq('c.classification',
1299
					$query->createNamedParameter(self::CLASSIFICATION_PUBLIC))
1300
			);
1301
		}
1302
1303
		if (count($calendarExpressions) === 1) {
1304
			$calExpr = $calendarExpressions[0];
1305
		} else {
1306
			$calExpr = call_user_func_array([$query->expr(), 'orX'], $calendarExpressions);
1307
		}
1308
1309
		// Component expressions
1310
		$compExpressions = [];
1311
		foreach($filters['comps'] as $comp) {
1312
			$compExpressions[] = $query->expr()
1313
				->eq('c.componenttype', $query->createNamedParameter($comp));
1314
		}
1315
1316
		if (count($compExpressions) === 1) {
1317
			$compExpr = $compExpressions[0];
1318
		} else {
1319
			$compExpr = call_user_func_array([$query->expr(), 'orX'], $compExpressions);
1320
		}
1321
1322
		if (!isset($filters['props'])) {
1323
			$filters['props'] = [];
1324
		}
1325
		if (!isset($filters['params'])) {
1326
			$filters['params'] = [];
1327
		}
1328
1329
		$propParamExpressions = [];
1330
		foreach($filters['props'] as $prop) {
1331
			$propParamExpressions[] = $query->expr()->andX(
1332
				$query->expr()->eq('i.name', $query->createNamedParameter($prop)),
1333
				$query->expr()->isNull('i.parameter')
1334
			);
1335
		}
1336
		foreach($filters['params'] as $param) {
1337
			$propParamExpressions[] = $query->expr()->andX(
1338
				$query->expr()->eq('i.name', $query->createNamedParameter($param['property'])),
1339
				$query->expr()->eq('i.parameter', $query->createNamedParameter($param['parameter']))
1340
			);
1341
		}
1342
1343
		if (count($propParamExpressions) === 1) {
1344
			$propParamExpr = $propParamExpressions[0];
1345
		} else {
1346
			$propParamExpr = call_user_func_array([$query->expr(), 'orX'], $propParamExpressions);
1347
		}
1348
1349
		$query->select(['c.calendarid', 'c.uri'])
1350
			->from($this->dbObjectPropertiesTable, 'i')
1351
			->join('i', 'calendarobjects', 'c', $query->expr()->eq('i.objectid', 'c.id'))
1352
			->where($calExpr)
1353
			->andWhere($compExpr)
1354
			->andWhere($propParamExpr)
1355
			->andWhere($query->expr()->iLike('i.value',
1356
				$query->createNamedParameter('%'.$this->db->escapeLikeParameter($filters['search-term']).'%')));
1357
1358
		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 zero. 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...
1359
			$query->setFirstResult($offset);
1360
		}
1361
		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 zero. 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...
1362
			$query->setMaxResults($limit);
1363
		}
1364
1365
		$stmt = $query->execute();
1366
1367
		$result = [];
1368
		while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
1369
			$path = $uriMapper[$row['calendarid']] . '/' . $row['uri'];
1370
			if (!in_array($path, $result)) {
1371
				$result[] = $path;
1372
			}
1373
		}
1374
1375
		return $result;
1376
	}
1377
1378
	/**
1379
	 * used for Nextcloud's calendar API
1380
	 *
1381
	 * @param array $calendarInfo
1382
	 * @param string $pattern
1383
	 * @param array $searchProperties
1384
	 * @param array $options
1385
	 * @param integer|null $limit
1386
	 * @param integer|null $offset
1387
	 *
1388
	 * @return array
1389
	 */
1390
	public function search(array $calendarInfo, $pattern, array $searchProperties,
1391
						   array $options, $limit, $offset) {
1392
		$outerQuery = $this->db->getQueryBuilder();
1393
		$innerQuery = $this->db->getQueryBuilder();
1394
1395
		$innerQuery->selectDistinct('op.objectid')
1396
			->from($this->dbObjectPropertiesTable, 'op')
1397
			->andWhere($innerQuery->expr()->eq('op.calendarid',
1398
				$outerQuery->createNamedParameter($calendarInfo['id'])));
1399
1400
		// only return public items for shared calendars for now
1401
		if ($calendarInfo['principaluri'] !== $calendarInfo['{http://owncloud.org/ns}owner-principal']) {
1402
			$innerQuery->andWhere($innerQuery->expr()->eq('c.classification',
1403
				$outerQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
1404
		}
1405
1406
		$or = $innerQuery->expr()->orX();
1407
		foreach($searchProperties as $searchProperty) {
1408
			$or->add($innerQuery->expr()->eq('op.name',
1409
				$outerQuery->createNamedParameter($searchProperty)));
1410
		}
1411
		$innerQuery->andWhere($or);
1412
1413 View Code Duplication
		if ($pattern !== '') {
1414
			$innerQuery->andWhere($innerQuery->expr()->iLike('op.value',
1415
				$outerQuery->createNamedParameter('%' .
1416
					$this->db->escapeLikeParameter($pattern) . '%')));
1417
		}
1418
1419
		$outerQuery->select('c.id', 'c.calendardata', 'c.componenttype', 'c.uid', 'c.uri')
1420
			->from('calendarobjects', 'c');
1421
1422
		if (isset($options['timerange'])) {
1423
			if (isset($options['timerange']['start'])) {
1424
				$outerQuery->andWhere($outerQuery->expr()->gt('lastoccurence',
1425
					$outerQuery->createNamedParameter($options['timerange']['start']->getTimeStamp)));
1426
1427
			}
1428
			if (isset($options['timerange']['end'])) {
1429
				$outerQuery->andWhere($outerQuery->expr()->lt('firstoccurence',
1430
					$outerQuery->createNamedParameter($options['timerange']['end']->getTimeStamp)));
1431
			}
1432
		}
1433
1434
		if (isset($options['types'])) {
1435
			$or = $outerQuery->expr()->orX();
1436
			foreach($options['types'] as $type) {
1437
				$or->add($outerQuery->expr()->eq('componenttype',
1438
					$outerQuery->createNamedParameter($type)));
1439
			}
1440
			$outerQuery->andWhere($or);
1441
		}
1442
1443
		$outerQuery->andWhere($outerQuery->expr()->in('c.id',
1444
			$outerQuery->createFunction($innerQuery->getSQL())));
1445
1446
		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 zero. 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...
1447
			$outerQuery->setFirstResult($offset);
1448
		}
1449
		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 zero. 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...
1450
			$outerQuery->setMaxResults($limit);
1451
		}
1452
1453
		$result = $outerQuery->execute();
1454
		$calendarObjects = $result->fetchAll();
1455
1456
		return array_map(function($o) {
1457
			$calendarData = Reader::read($o['calendardata']);
1458
			$comps = $calendarData->getComponents();
1459
			$objects = [];
1460
			$timezones = [];
1461
			foreach($comps as $comp) {
1462
				if ($comp instanceof VTimeZone) {
0 ignored issues
show
Bug introduced by
The class Sabre\VObject\Component\VTimeZone does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
1463
					$timezones[] = $comp;
1464
				} else {
1465
					$objects[] = $comp;
1466
				}
1467
			}
1468
1469
			return [
1470
				'id' => $o['id'],
1471
				'type' => $o['componenttype'],
1472
				'uid' => $o['uid'],
1473
				'uri' => $o['uri'],
1474
				'objects' => array_map(function($c) {
1475
					return $this->transformSearchData($c);
1476
				}, $objects),
1477
				'timezones' => array_map(function($c) {
1478
					return $this->transformSearchData($c);
1479
				}, $timezones),
1480
			];
1481
		}, $calendarObjects);
1482
	}
1483
1484
	/**
1485
	 * @param Component $comp
1486
	 * @return array
1487
	 */
1488
	private function transformSearchData(Component $comp) {
1489
		$data = [];
1490
		/** @var Component[] $subComponents */
1491
		$subComponents = $comp->getComponents();
1492
		/** @var Property[] $properties */
1493
		$properties = array_filter($comp->children(), function($c) {
1494
			return $c instanceof Property;
0 ignored issues
show
Bug introduced by
The class Sabre\VObject\Property does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
1495
		});
1496
		$validationRules = $comp->getValidationRules();
1497
1498
		foreach($subComponents as $subComponent) {
1499
			$name = $subComponent->name;
1500
			if (!isset($data[$name])) {
1501
				$data[$name] = [];
1502
			}
1503
			$data[$name][] = $this->transformSearchData($subComponent);
1504
		}
1505
1506
		foreach($properties as $property) {
1507
			$name = $property->name;
1508
			if (!isset($validationRules[$name])) {
1509
				$validationRules[$name] = '*';
1510
			}
1511
1512
			$rule = $validationRules[$property->name];
1513
			if ($rule === '+' || $rule === '*') { // multiple
1514
				if (!isset($data[$name])) {
1515
					$data[$name] = [];
1516
				}
1517
1518
				$data[$name][] = $this->transformSearchProperty($property);
1519
			} else { // once
1520
				$data[$name] = $this->transformSearchProperty($property);
1521
			}
1522
		}
1523
1524
		return $data;
1525
	}
1526
1527
	/**
1528
	 * @param Property $prop
1529
	 * @return array
1530
	 */
1531
	private function transformSearchProperty(Property $prop) {
1532
		// No need to check Date, as it extends DateTime
1533
		if ($prop instanceof Property\ICalendar\DateTime) {
0 ignored issues
show
Bug introduced by
The class Sabre\VObject\Property\ICalendar\DateTime does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
1534
			$value = $prop->getDateTime();
1535
		} else {
1536
			$value = $prop->getValue();
1537
		}
1538
1539
		return [
1540
			$value,
1541
			$prop->parameters()
1542
		];
1543
	}
1544
1545
	/**
1546
	 * Searches through all of a users calendars and calendar objects to find
1547
	 * an object with a specific UID.
1548
	 *
1549
	 * This method should return the path to this object, relative to the
1550
	 * calendar home, so this path usually only contains two parts:
1551
	 *
1552
	 * calendarpath/objectpath.ics
1553
	 *
1554
	 * If the uid is not found, return null.
1555
	 *
1556
	 * This method should only consider * objects that the principal owns, so
1557
	 * any calendars owned by other principals that also appear in this
1558
	 * collection should be ignored.
1559
	 *
1560
	 * @param string $principalUri
1561
	 * @param string $uid
1562
	 * @return string|null
1563
	 */
1564
	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...
1565
1566
		$query = $this->db->getQueryBuilder();
1567
		$query->selectAlias('c.uri', 'calendaruri')->selectAlias('co.uri', 'objecturi')
1568
			->from('calendarobjects', 'co')
1569
			->leftJoin('co', 'calendars', 'c', $query->expr()->eq('co.calendarid', 'c.id'))
1570
			->where($query->expr()->eq('c.principaluri', $query->createNamedParameter($principalUri)))
1571
			->andWhere($query->expr()->eq('co.uid', $query->createNamedParameter($uid)));
1572
1573
		$stmt = $query->execute();
1574
1575
		if ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
1576
			return $row['calendaruri'] . '/' . $row['objecturi'];
1577
		}
1578
1579
		return null;
1580
	}
1581
1582
	/**
1583
	 * The getChanges method returns all the changes that have happened, since
1584
	 * the specified syncToken in the specified calendar.
1585
	 *
1586
	 * This function should return an array, such as the following:
1587
	 *
1588
	 * [
1589
	 *   'syncToken' => 'The current synctoken',
1590
	 *   'added'   => [
1591
	 *      'new.txt',
1592
	 *   ],
1593
	 *   'modified'   => [
1594
	 *      'modified.txt',
1595
	 *   ],
1596
	 *   'deleted' => [
1597
	 *      'foo.php.bak',
1598
	 *      'old.txt'
1599
	 *   ]
1600
	 * );
1601
	 *
1602
	 * The returned syncToken property should reflect the *current* syncToken
1603
	 * of the calendar, as reported in the {http://sabredav.org/ns}sync-token
1604
	 * property This is * needed here too, to ensure the operation is atomic.
1605
	 *
1606
	 * If the $syncToken argument is specified as null, this is an initial
1607
	 * sync, and all members should be reported.
1608
	 *
1609
	 * The modified property is an array of nodenames that have changed since
1610
	 * the last token.
1611
	 *
1612
	 * The deleted property is an array with nodenames, that have been deleted
1613
	 * from collection.
1614
	 *
1615
	 * The $syncLevel argument is basically the 'depth' of the report. If it's
1616
	 * 1, you only have to report changes that happened only directly in
1617
	 * immediate descendants. If it's 2, it should also include changes from
1618
	 * the nodes below the child collections. (grandchildren)
1619
	 *
1620
	 * The $limit argument allows a client to specify how many results should
1621
	 * be returned at most. If the limit is not specified, it should be treated
1622
	 * as infinite.
1623
	 *
1624
	 * If the limit (infinite or not) is higher than you're willing to return,
1625
	 * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
1626
	 *
1627
	 * If the syncToken is expired (due to data cleanup) or unknown, you must
1628
	 * return null.
1629
	 *
1630
	 * The limit is 'suggestive'. You are free to ignore it.
1631
	 *
1632
	 * @param string $calendarId
1633
	 * @param string $syncToken
1634
	 * @param int $syncLevel
1635
	 * @param int $limit
1636
	 * @return array
1637
	 */
1638 View Code Duplication
	function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit = null) {
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...
1639
		// Current synctoken
1640
		$stmt = $this->db->prepare('SELECT `synctoken` FROM `*PREFIX*calendars` WHERE `id` = ?');
1641
		$stmt->execute([ $calendarId ]);
1642
		$currentToken = $stmt->fetchColumn(0);
1643
1644
		if (is_null($currentToken)) {
1645
			return null;
1646
		}
1647
1648
		$result = [
1649
			'syncToken' => $currentToken,
1650
			'added'     => [],
1651
			'modified'  => [],
1652
			'deleted'   => [],
1653
		];
1654
1655
		if ($syncToken) {
1656
1657
			$query = "SELECT `uri`, `operation` FROM `*PREFIX*calendarchanges` WHERE `synctoken` >= ? AND `synctoken` < ? AND `calendarid` = ? ORDER BY `synctoken`";
1658
			if ($limit>0) {
1659
				$query.= " LIMIT " . (int)$limit;
1660
			}
1661
1662
			// Fetching all changes
1663
			$stmt = $this->db->prepare($query);
1664
			$stmt->execute([$syncToken, $currentToken, $calendarId]);
1665
1666
			$changes = [];
1667
1668
			// This loop ensures that any duplicates are overwritten, only the
1669
			// last change on a node is relevant.
1670
			while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
1671
1672
				$changes[$row['uri']] = $row['operation'];
1673
1674
			}
1675
1676
			foreach($changes as $uri => $operation) {
1677
1678
				switch($operation) {
1679
					case 1 :
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a CASE statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in case statements.

switch ($selector) {
    case "A": //right
        doSomething();
        break;
    case "B" : //wrong
        doSomethingElse();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
1680
						$result['added'][] = $uri;
1681
						break;
1682
					case 2 :
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a CASE statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in case statements.

switch ($selector) {
    case "A": //right
        doSomething();
        break;
    case "B" : //wrong
        doSomethingElse();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
1683
						$result['modified'][] = $uri;
1684
						break;
1685
					case 3 :
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a CASE statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in case statements.

switch ($selector) {
    case "A": //right
        doSomething();
        break;
    case "B" : //wrong
        doSomethingElse();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
1686
						$result['deleted'][] = $uri;
1687
						break;
1688
				}
1689
1690
			}
1691
		} else {
1692
			// No synctoken supplied, this is the initial sync.
1693
			$query = "SELECT `uri` FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ?";
1694
			$stmt = $this->db->prepare($query);
1695
			$stmt->execute([$calendarId]);
1696
1697
			$result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
1698
		}
1699
		return $result;
1700
1701
	}
1702
1703
	/**
1704
	 * Returns a list of subscriptions for a principal.
1705
	 *
1706
	 * Every subscription is an array with the following keys:
1707
	 *  * id, a unique id that will be used by other functions to modify the
1708
	 *    subscription. This can be the same as the uri or a database key.
1709
	 *  * uri. This is just the 'base uri' or 'filename' of the subscription.
1710
	 *  * principaluri. The owner of the subscription. Almost always the same as
1711
	 *    principalUri passed to this method.
1712
	 *
1713
	 * Furthermore, all the subscription info must be returned too:
1714
	 *
1715
	 * 1. {DAV:}displayname
1716
	 * 2. {http://apple.com/ns/ical/}refreshrate
1717
	 * 3. {http://calendarserver.org/ns/}subscribed-strip-todos (omit if todos
1718
	 *    should not be stripped).
1719
	 * 4. {http://calendarserver.org/ns/}subscribed-strip-alarms (omit if alarms
1720
	 *    should not be stripped).
1721
	 * 5. {http://calendarserver.org/ns/}subscribed-strip-attachments (omit if
1722
	 *    attachments should not be stripped).
1723
	 * 6. {http://calendarserver.org/ns/}source (Must be a
1724
	 *     Sabre\DAV\Property\Href).
1725
	 * 7. {http://apple.com/ns/ical/}calendar-color
1726
	 * 8. {http://apple.com/ns/ical/}calendar-order
1727
	 * 9. {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
1728
	 *    (should just be an instance of
1729
	 *    Sabre\CalDAV\Property\SupportedCalendarComponentSet, with a bunch of
1730
	 *    default components).
1731
	 *
1732
	 * @param string $principalUri
1733
	 * @return array
1734
	 */
1735
	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...
1736
		$fields = array_values($this->subscriptionPropertyMap);
1737
		$fields[] = 'id';
1738
		$fields[] = 'uri';
1739
		$fields[] = 'source';
1740
		$fields[] = 'principaluri';
1741
		$fields[] = 'lastmodified';
1742
1743
		$query = $this->db->getQueryBuilder();
1744
		$query->select($fields)
1745
			->from('calendarsubscriptions')
1746
			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
1747
			->orderBy('calendarorder', 'asc');
1748
		$stmt =$query->execute();
1749
1750
		$subscriptions = [];
1751
		while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
1752
1753
			$subscription = [
1754
				'id'           => $row['id'],
1755
				'uri'          => $row['uri'],
1756
				'principaluri' => $row['principaluri'],
1757
				'source'       => $row['source'],
1758
				'lastmodified' => $row['lastmodified'],
1759
1760
				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
1761
			];
1762
1763
			foreach($this->subscriptionPropertyMap as $xmlName=>$dbName) {
1764
				if (!is_null($row[$dbName])) {
1765
					$subscription[$xmlName] = $row[$dbName];
1766
				}
1767
			}
1768
1769
			$subscriptions[] = $subscription;
1770
1771
		}
1772
1773
		return $subscriptions;
1774
	}
1775
1776
	/**
1777
	 * Creates a new subscription for a principal.
1778
	 *
1779
	 * If the creation was a success, an id must be returned that can be used to reference
1780
	 * this subscription in other methods, such as updateSubscription.
1781
	 *
1782
	 * @param string $principalUri
1783
	 * @param string $uri
1784
	 * @param array $properties
1785
	 * @return mixed
1786
	 */
1787
	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...
1788
1789
		if (!isset($properties['{http://calendarserver.org/ns/}source'])) {
1790
			throw new Forbidden('The {http://calendarserver.org/ns/}source property is required when creating subscriptions');
1791
		}
1792
1793
		$values = [
1794
			'principaluri' => $principalUri,
1795
			'uri'          => $uri,
1796
			'source'       => $properties['{http://calendarserver.org/ns/}source']->getHref(),
1797
			'lastmodified' => time(),
1798
		];
1799
1800
		$propertiesBoolean = ['striptodos', 'stripalarms', 'stripattachments'];
1801
1802
		foreach($this->subscriptionPropertyMap as $xmlName=>$dbName) {
1803
			if (array_key_exists($xmlName, $properties)) {
1804
					$values[$dbName] = $properties[$xmlName];
1805
					if (in_array($dbName, $propertiesBoolean)) {
1806
						$values[$dbName] = true;
1807
				}
1808
			}
1809
		}
1810
1811
		$valuesToInsert = array();
1812
1813
		$query = $this->db->getQueryBuilder();
1814
1815
		foreach (array_keys($values) as $name) {
1816
			$valuesToInsert[$name] = $query->createNamedParameter($values[$name]);
1817
		}
1818
1819
		$query->insert('calendarsubscriptions')
1820
			->values($valuesToInsert)
1821
			->execute();
1822
1823
		return $this->db->lastInsertId('*PREFIX*calendarsubscriptions');
1824
	}
1825
1826
	/**
1827
	 * Updates a subscription
1828
	 *
1829
	 * The list of mutations is stored in a Sabre\DAV\PropPatch object.
1830
	 * To do the actual updates, you must tell this object which properties
1831
	 * you're going to process with the handle() method.
1832
	 *
1833
	 * Calling the handle method is like telling the PropPatch object "I
1834
	 * promise I can handle updating this property".
1835
	 *
1836
	 * Read the PropPatch documentation for more info and examples.
1837
	 *
1838
	 * @param mixed $subscriptionId
1839
	 * @param PropPatch $propPatch
1840
	 * @return void
1841
	 */
1842
	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...
1843
		$supportedProperties = array_keys($this->subscriptionPropertyMap);
1844
		$supportedProperties[] = '{http://calendarserver.org/ns/}source';
1845
1846
		/**
1847
		 * @suppress SqlInjectionChecker
1848
		 */
1849
		$propPatch->handle($supportedProperties, function($mutations) use ($subscriptionId) {
1850
1851
			$newValues = [];
1852
1853
			foreach($mutations as $propertyName=>$propertyValue) {
1854
				if ($propertyName === '{http://calendarserver.org/ns/}source') {
1855
					$newValues['source'] = $propertyValue->getHref();
1856
				} else {
1857
					$fieldName = $this->subscriptionPropertyMap[$propertyName];
1858
					$newValues[$fieldName] = $propertyValue;
1859
				}
1860
			}
1861
1862
			$query = $this->db->getQueryBuilder();
1863
			$query->update('calendarsubscriptions')
1864
				->set('lastmodified', $query->createNamedParameter(time()));
1865
			foreach($newValues as $fieldName=>$value) {
1866
				$query->set($fieldName, $query->createNamedParameter($value));
1867
			}
1868
			$query->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
1869
				->execute();
1870
1871
			return true;
1872
1873
		});
1874
	}
1875
1876
	/**
1877
	 * Deletes a subscription.
1878
	 *
1879
	 * @param mixed $subscriptionId
1880
	 * @return void
1881
	 */
1882
	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...
1883
		$query = $this->db->getQueryBuilder();
1884
		$query->delete('calendarsubscriptions')
1885
			->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
1886
			->execute();
1887
	}
1888
1889
	/**
1890
	 * Returns a single scheduling object for the inbox collection.
1891
	 *
1892
	 * The returned array should contain the following elements:
1893
	 *   * uri - A unique basename for the object. This will be used to
1894
	 *           construct a full uri.
1895
	 *   * calendardata - The iCalendar object
1896
	 *   * lastmodified - The last modification date. Can be an int for a unix
1897
	 *                    timestamp, or a PHP DateTime object.
1898
	 *   * etag - A unique token that must change if the object changed.
1899
	 *   * size - The size of the object, in bytes.
1900
	 *
1901
	 * @param string $principalUri
1902
	 * @param string $objectUri
1903
	 * @return array
1904
	 */
1905
	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...
1906
		$query = $this->db->getQueryBuilder();
1907
		$stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size'])
1908
			->from('schedulingobjects')
1909
			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
1910
			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
1911
			->execute();
1912
1913
		$row = $stmt->fetch(\PDO::FETCH_ASSOC);
1914
1915
		if(!$row) {
1916
			return null;
1917
		}
1918
1919
		return [
1920
				'uri'          => $row['uri'],
1921
				'calendardata' => $row['calendardata'],
1922
				'lastmodified' => $row['lastmodified'],
1923
				'etag'         => '"' . $row['etag'] . '"',
1924
				'size'         => (int)$row['size'],
1925
		];
1926
	}
1927
1928
	/**
1929
	 * Returns all scheduling objects for the inbox collection.
1930
	 *
1931
	 * These objects should be returned as an array. Every item in the array
1932
	 * should follow the same structure as returned from getSchedulingObject.
1933
	 *
1934
	 * The main difference is that 'calendardata' is optional.
1935
	 *
1936
	 * @param string $principalUri
1937
	 * @return array
1938
	 */
1939
	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...
1940
		$query = $this->db->getQueryBuilder();
1941
		$stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size'])
1942
				->from('schedulingobjects')
1943
				->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
1944
				->execute();
1945
1946
		$result = [];
1947
		foreach($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) {
1948
			$result[] = [
1949
					'calendardata' => $row['calendardata'],
1950
					'uri'          => $row['uri'],
1951
					'lastmodified' => $row['lastmodified'],
1952
					'etag'         => '"' . $row['etag'] . '"',
1953
					'size'         => (int)$row['size'],
1954
			];
1955
		}
1956
1957
		return $result;
1958
	}
1959
1960
	/**
1961
	 * Deletes a scheduling object from the inbox collection.
1962
	 *
1963
	 * @param string $principalUri
1964
	 * @param string $objectUri
1965
	 * @return void
1966
	 */
1967
	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...
1968
		$query = $this->db->getQueryBuilder();
1969
		$query->delete('schedulingobjects')
1970
				->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
1971
				->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
1972
				->execute();
1973
	}
1974
1975
	/**
1976
	 * Creates a new scheduling object. This should land in a users' inbox.
1977
	 *
1978
	 * @param string $principalUri
1979
	 * @param string $objectUri
1980
	 * @param string $objectData
1981
	 * @return void
1982
	 */
1983
	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...
1984
		$query = $this->db->getQueryBuilder();
1985
		$query->insert('schedulingobjects')
1986
			->values([
1987
				'principaluri' => $query->createNamedParameter($principalUri),
1988
				'calendardata' => $query->createNamedParameter($objectData),
1989
				'uri' => $query->createNamedParameter($objectUri),
1990
				'lastmodified' => $query->createNamedParameter(time()),
1991
				'etag' => $query->createNamedParameter(md5($objectData)),
1992
				'size' => $query->createNamedParameter(strlen($objectData))
1993
			])
1994
			->execute();
1995
	}
1996
1997
	/**
1998
	 * Adds a change record to the calendarchanges table.
1999
	 *
2000
	 * @param mixed $calendarId
2001
	 * @param string $objectUri
2002
	 * @param int $operation 1 = add, 2 = modify, 3 = delete.
2003
	 * @return void
2004
	 */
2005 View Code Duplication
	protected function addChange($calendarId, $objectUri, $operation) {
2006
2007
		$stmt = $this->db->prepare('INSERT INTO `*PREFIX*calendarchanges` (`uri`, `synctoken`, `calendarid`, `operation`) SELECT ?, `synctoken`, ?, ? FROM `*PREFIX*calendars` WHERE `id` = ?');
2008
		$stmt->execute([
2009
			$objectUri,
2010
			$calendarId,
2011
			$operation,
2012
			$calendarId
2013
		]);
2014
		$stmt = $this->db->prepare('UPDATE `*PREFIX*calendars` SET `synctoken` = `synctoken` + 1 WHERE `id` = ?');
2015
		$stmt->execute([
2016
			$calendarId
2017
		]);
2018
2019
	}
2020
2021
	/**
2022
	 * Parses some information from calendar objects, used for optimized
2023
	 * calendar-queries.
2024
	 *
2025
	 * Returns an array with the following keys:
2026
	 *   * etag - An md5 checksum of the object without the quotes.
2027
	 *   * size - Size of the object in bytes
2028
	 *   * componentType - VEVENT, VTODO or VJOURNAL
2029
	 *   * firstOccurence
2030
	 *   * lastOccurence
2031
	 *   * uid - value of the UID property
2032
	 *
2033
	 * @param string $calendarData
2034
	 * @return array
2035
	 */
2036
	public function getDenormalizedData($calendarData) {
2037
2038
		$vObject = Reader::read($calendarData);
2039
		$componentType = null;
2040
		$component = null;
2041
		$firstOccurrence = null;
2042
		$lastOccurrence = null;
2043
		$uid = null;
2044
		$classification = self::CLASSIFICATION_PUBLIC;
2045
		foreach($vObject->getComponents() as $component) {
2046
			if ($component->name!=='VTIMEZONE') {
2047
				$componentType = $component->name;
2048
				$uid = (string)$component->UID;
2049
				break;
2050
			}
2051
		}
2052
		if (!$componentType) {
2053
			throw new \Sabre\DAV\Exception\BadRequest('Calendar objects must have a VJOURNAL, VEVENT or VTODO component');
2054
		}
2055
		if ($componentType === 'VEVENT' && $component->DTSTART) {
2056
			$firstOccurrence = $component->DTSTART->getDateTime()->getTimeStamp();
2057
			// Finding the last occurrence is a bit harder
2058 View Code Duplication
			if (!isset($component->RRULE)) {
2059
				if (isset($component->DTEND)) {
2060
					$lastOccurrence = $component->DTEND->getDateTime()->getTimeStamp();
2061
				} elseif (isset($component->DURATION)) {
2062
					$endDate = clone $component->DTSTART->getDateTime();
2063
					$endDate->add(DateTimeParser::parse($component->DURATION->getValue()));
2064
					$lastOccurrence = $endDate->getTimeStamp();
2065
				} elseif (!$component->DTSTART->hasTime()) {
2066
					$endDate = clone $component->DTSTART->getDateTime();
2067
					$endDate->modify('+1 day');
2068
					$lastOccurrence = $endDate->getTimeStamp();
2069
				} else {
2070
					$lastOccurrence = $firstOccurrence;
2071
				}
2072
			} else {
2073
				$it = new EventIterator($vObject, (string)$component->UID);
2074
				$maxDate = new \DateTime(self::MAX_DATE);
2075
				if ($it->isInfinite()) {
2076
					$lastOccurrence = $maxDate->getTimestamp();
2077
				} else {
2078
					$end = $it->getDtEnd();
2079
					while($it->valid() && $end < $maxDate) {
2080
						$end = $it->getDtEnd();
2081
						$it->next();
2082
2083
					}
2084
					$lastOccurrence = $end->getTimestamp();
2085
				}
2086
2087
			}
2088
		}
2089
2090
		if ($component->CLASS) {
2091
			$classification = CalDavBackend::CLASSIFICATION_PRIVATE;
2092
			switch ($component->CLASS->getValue()) {
2093
				case 'PUBLIC':
2094
					$classification = CalDavBackend::CLASSIFICATION_PUBLIC;
2095
					break;
2096
				case 'CONFIDENTIAL':
2097
					$classification = CalDavBackend::CLASSIFICATION_CONFIDENTIAL;
2098
					break;
2099
			}
2100
		}
2101
		return [
2102
			'etag' => md5($calendarData),
2103
			'size' => strlen($calendarData),
2104
			'componentType' => $componentType,
2105
			'firstOccurence' => is_null($firstOccurrence) ? null : max(0, $firstOccurrence),
2106
			'lastOccurence'  => $lastOccurrence,
2107
			'uid' => $uid,
2108
			'classification' => $classification
2109
		];
2110
2111
	}
2112
2113
	private function readBlob($cardData) {
2114
		if (is_resource($cardData)) {
2115
			return stream_get_contents($cardData);
2116
		}
2117
2118
		return $cardData;
2119
	}
2120
2121
	/**
2122
	 * @param IShareable $shareable
2123
	 * @param array $add
2124
	 * @param array $remove
2125
	 */
2126
	public function updateShares($shareable, $add, $remove) {
2127
		$calendarId = $shareable->getResourceId();
2128
		$this->dispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateShares', new GenericEvent(
2129
			'\OCA\DAV\CalDAV\CalDavBackend::updateShares',
2130
			[
2131
				'calendarId' => $calendarId,
2132
				'calendarData' => $this->getCalendarById($calendarId),
2133
				'shares' => $this->getShares($calendarId),
2134
				'add' => $add,
2135
				'remove' => $remove,
2136
			]));
2137
		$this->sharingBackend->updateShares($shareable, $add, $remove);
2138
	}
2139
2140
	/**
2141
	 * @param int $resourceId
2142
	 * @return array
2143
	 */
2144
	public function getShares($resourceId) {
2145
		return $this->sharingBackend->getShares($resourceId);
2146
	}
2147
2148
	/**
2149
	 * @param boolean $value
2150
	 * @param \OCA\DAV\CalDAV\Calendar $calendar
2151
	 * @return string|null
2152
	 */
2153
	public function setPublishStatus($value, $calendar) {
2154
2155
		$calendarId = $calendar->getResourceId();
2156
		$this->dispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::publishCalendar', new GenericEvent(
2157
			'\OCA\DAV\CalDAV\CalDavBackend::updateShares',
2158
			[
2159
				'calendarId' => $calendarId,
2160
				'calendarData' => $this->getCalendarById($calendarId),
2161
				'public' => $value,
2162
			]));
2163
2164
		$query = $this->db->getQueryBuilder();
2165
		if ($value) {
2166
			$publicUri = $this->random->generate(16, ISecureRandom::CHAR_HUMAN_READABLE);
2167
			$query->insert('dav_shares')
2168
				->values([
2169
					'principaluri' => $query->createNamedParameter($calendar->getPrincipalURI()),
2170
					'type' => $query->createNamedParameter('calendar'),
2171
					'access' => $query->createNamedParameter(self::ACCESS_PUBLIC),
2172
					'resourceid' => $query->createNamedParameter($calendar->getResourceId()),
2173
					'publicuri' => $query->createNamedParameter($publicUri)
2174
				]);
2175
			$query->execute();
2176
			return $publicUri;
2177
		}
2178
		$query->delete('dav_shares')
2179
			->where($query->expr()->eq('resourceid', $query->createNamedParameter($calendar->getResourceId())))
2180
			->andWhere($query->expr()->eq('access', $query->createNamedParameter(self::ACCESS_PUBLIC)));
2181
		$query->execute();
2182
		return null;
2183
	}
2184
2185
	/**
2186
	 * @param \OCA\DAV\CalDAV\Calendar $calendar
2187
	 * @return mixed
2188
	 */
2189 View Code Duplication
	public function getPublishStatus($calendar) {
2190
		$query = $this->db->getQueryBuilder();
2191
		$result = $query->select('publicuri')
2192
			->from('dav_shares')
2193
			->where($query->expr()->eq('resourceid', $query->createNamedParameter($calendar->getResourceId())))
2194
			->andWhere($query->expr()->eq('access', $query->createNamedParameter(self::ACCESS_PUBLIC)))
2195
			->execute();
2196
2197
		$row = $result->fetch();
2198
		$result->closeCursor();
2199
		return $row ? reset($row) : false;
2200
	}
2201
2202
	/**
2203
	 * @param int $resourceId
2204
	 * @param array $acl
2205
	 * @return array
2206
	 */
2207
	public function applyShareAcl($resourceId, $acl) {
2208
		return $this->sharingBackend->applyShareAcl($resourceId, $acl);
2209
	}
2210
2211
2212
2213
	/**
2214
	 * update properties table
2215
	 *
2216
	 * @param int $calendarId
2217
	 * @param string $objectUri
2218
	 * @param string $calendarData
2219
	 */
2220
	public function updateProperties($calendarId, $objectUri, $calendarData) {
2221
		$objectId = $this->getCalendarObjectId($calendarId, $objectUri);
2222
2223
		try {
2224
			$vCalendar = $this->readCalendarData($calendarData);
2225
		} catch (\Exception $ex) {
2226
			return;
2227
		}
2228
2229
		$this->purgeProperties($calendarId, $objectId);
2230
2231
		$query = $this->db->getQueryBuilder();
2232
		$query->insert($this->dbObjectPropertiesTable)
2233
			->values(
2234
				[
2235
					'calendarid' => $query->createNamedParameter($calendarId),
2236
					'objectid' => $query->createNamedParameter($objectId),
2237
					'name' => $query->createParameter('name'),
2238
					'parameter' => $query->createParameter('parameter'),
2239
					'value' => $query->createParameter('value'),
2240
				]
2241
			);
2242
2243
		$indexComponents = ['VEVENT', 'VJOURNAL', 'VTODO'];
2244
		foreach ($vCalendar->getComponents() as $component) {
2245
			if (!in_array($component->name, $indexComponents)) {
2246
				continue;
2247
			}
2248
2249
			foreach ($component->children() as $property) {
2250
				if (in_array($property->name, self::$indexProperties)) {
2251
					$value = $property->getValue();
2252
					// is this a shitty db?
2253
					if (!$this->db->supports4ByteText()) {
2254
						$value = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $value);
2255
					}
2256
					$value = substr($value, 0, 254);
2257
2258
					$query->setParameter('name', $property->name);
2259
					$query->setParameter('parameter', null);
2260
					$query->setParameter('value', $value);
2261
					$query->execute();
2262
				}
2263
2264
				if (in_array($property->name, array_keys(self::$indexParameters))) {
2265
					$parameters = $property->parameters();
2266
					$indexedParametersForProperty = self::$indexParameters[$property->name];
2267
2268
					foreach ($parameters as $key => $value) {
2269
						if (in_array($key, $indexedParametersForProperty)) {
2270
							// is this a shitty db?
2271
							if ($this->db->supports4ByteText()) {
2272
								$value = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $value);
2273
							}
2274
							$value = substr($value, 0, 254);
2275
2276
							$query->setParameter('name', $property->name);
2277
							$query->setParameter('parameter', substr($key, 0, 254));
2278
							$query->setParameter('value', substr($value, 0, 254));
2279
							$query->execute();
2280
						}
2281
					}
2282
				}
2283
			}
2284
		}
2285
	}
2286
2287
	/**
2288
	 * deletes all birthday calendars
2289
	 */
2290
	public function deleteAllBirthdayCalendars() {
2291
		$query = $this->db->getQueryBuilder();
2292
		$result = $query->select(['id'])->from('calendars')
2293
			->where($query->expr()->eq('uri',
2294
				$query->createNamedParameter(BirthdayService::BIRTHDAY_CALENDAR_URI)))
2295
			->execute();
2296
2297
		$ids = $result->fetchAll();
2298
		foreach($ids as $id) {
2299
			$this->deleteCalendar($id['id']);
2300
		}
2301
	}
2302
2303
	/**
2304
	 * read VCalendar data into a VCalendar object
2305
	 *
2306
	 * @param string $objectData
2307
	 * @return VCalendar
2308
	 */
2309
	protected function readCalendarData($objectData) {
2310
		return Reader::read($objectData);
2311
	}
2312
2313
	/**
2314
	 * delete all properties from a given calendar object
2315
	 *
2316
	 * @param int $calendarId
2317
	 * @param int $objectId
2318
	 */
2319 View Code Duplication
	protected function purgeProperties($calendarId, $objectId) {
2320
		$query = $this->db->getQueryBuilder();
2321
		$query->delete($this->dbObjectPropertiesTable)
2322
			->where($query->expr()->eq('objectid', $query->createNamedParameter($objectId)))
2323
			->andWhere($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)));
2324
		$query->execute();
2325
	}
2326
2327
	/**
2328
	 * get ID from a given calendar object
2329
	 *
2330
	 * @param int $calendarId
2331
	 * @param string $uri
2332
	 * @return int
2333
	 */
2334 View Code Duplication
	protected function getCalendarObjectId($calendarId, $uri) {
2335
		$query = $this->db->getQueryBuilder();
2336
		$query->select('id')->from('calendarobjects')
2337
			->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
2338
			->andWhere($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)));
2339
2340
		$result = $query->execute();
2341
		$objectIds = $result->fetch();
2342
		$result->closeCursor();
2343
2344
		if (!isset($objectIds['id'])) {
2345
			throw new \InvalidArgumentException('Calendarobject does not exists: ' . $uri);
2346
		}
2347
2348
		return (int)$objectIds['id'];
2349
	}
2350
2351 View Code Duplication
	private function convertPrincipal($principalUri, $toV2) {
2352
		if ($this->principalBackend->getPrincipalPrefix() === 'principals') {
2353
			list(, $name) = Uri\split($principalUri);
2354
			if ($toV2 === true) {
2355
				return "principals/users/$name";
2356
			}
2357
			return "principals/$name";
2358
		}
2359
		return $principalUri;
2360
	}
2361
2362 View Code Duplication
	private function addOwnerPrincipal(&$calendarInfo) {
2363
		$ownerPrincipalKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal';
2364
		$displaynameKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}owner-displayname';
2365
		if (isset($calendarInfo[$ownerPrincipalKey])) {
2366
			$uri = $calendarInfo[$ownerPrincipalKey];
2367
		} else {
2368
			$uri = $calendarInfo['principaluri'];
2369
		}
2370
2371
		$principalInformation = $this->principalBackend->getPrincipalByPath($uri);
2372
		if (isset($principalInformation['{DAV:}displayname'])) {
2373
			$calendarInfo[$displaynameKey] = $principalInformation['{DAV:}displayname'];
2374
		}
2375
	}
2376
}
2377