Completed
Push — master ( 44b423...f32fbb )
by Morris
13:09
created

CalDavBackend::getSubscriptionsForUser()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 40
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 27
nc 4
nop 1
dl 0
loc 40
rs 8.5806
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\IUser;
41
use OCP\IUserManager;
42
use OCP\Security\ISecureRandom;
43
use Sabre\CalDAV\Backend\AbstractBackend;
44
use Sabre\CalDAV\Backend\SchedulingSupport;
45
use Sabre\CalDAV\Backend\SubscriptionSupport;
46
use Sabre\CalDAV\Backend\SyncSupport;
47
use Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp;
48
use Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet;
49
use Sabre\DAV;
50
use Sabre\DAV\Exception\Forbidden;
51
use Sabre\DAV\Exception\NotFound;
52
use Sabre\DAV\PropPatch;
53
use Sabre\HTTP\URLUtil;
54
use Sabre\VObject\Component;
55
use Sabre\VObject\Component\VCalendar;
56
use Sabre\VObject\Component\VEvent;
57
use Sabre\VObject\Component\VTimeZone;
58
use Sabre\VObject\DateTimeParser;
59
use Sabre\VObject\Property;
60
use Sabre\VObject\Reader;
61
use Sabre\VObject\Recur\EventIterator;
62
use Sabre\Uri;
63
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
64
use Symfony\Component\EventDispatcher\GenericEvent;
65
66
/**
67
 * Class CalDavBackend
68
 *
69
 * Code is heavily inspired by https://github.com/fruux/sabre-dav/blob/master/lib/CalDAV/Backend/PDO.php
70
 *
71
 * @package OCA\DAV\CalDAV
72
 */
73
class CalDavBackend extends AbstractBackend implements SyncSupport, SubscriptionSupport, SchedulingSupport {
74
75
	const PERSONAL_CALENDAR_URI = 'personal';
76
	const PERSONAL_CALENDAR_NAME = 'Personal';
77
78
	/**
79
	 * We need to specify a max date, because we need to stop *somewhere*
80
	 *
81
	 * On 32 bit system the maximum for a signed integer is 2147483647, so
82
	 * MAX_DATE cannot be higher than date('Y-m-d', 2147483647) which results
83
	 * in 2038-01-19 to avoid problems when the date is converted
84
	 * to a unix timestamp.
85
	 */
86
	const MAX_DATE = '2038-01-01';
87
88
	const ACCESS_PUBLIC = 4;
89
	const CLASSIFICATION_PUBLIC = 0;
90
	const CLASSIFICATION_PRIVATE = 1;
91
	const CLASSIFICATION_CONFIDENTIAL = 2;
92
93
	/**
94
	 * List of CalDAV properties, and how they map to database field names
95
	 * Add your own properties by simply adding on to this array.
96
	 *
97
	 * Note that only string-based properties are supported here.
98
	 *
99
	 * @var array
100
	 */
101
	public $propertyMap = [
102
		'{DAV:}displayname'                          => 'displayname',
103
		'{urn:ietf:params:xml:ns:caldav}calendar-description' => 'description',
104
		'{urn:ietf:params:xml:ns:caldav}calendar-timezone'    => 'timezone',
105
		'{http://apple.com/ns/ical/}calendar-order'  => 'calendarorder',
106
		'{http://apple.com/ns/ical/}calendar-color'  => 'calendarcolor',
107
	];
108
109
	/**
110
	 * List of subscription properties, and how they map to database field names.
111
	 *
112
	 * @var array
113
	 */
114
	public $subscriptionPropertyMap = [
115
		'{DAV:}displayname'                                           => 'displayname',
116
		'{http://apple.com/ns/ical/}refreshrate'                      => 'refreshrate',
117
		'{http://apple.com/ns/ical/}calendar-order'                   => 'calendarorder',
118
		'{http://apple.com/ns/ical/}calendar-color'                   => 'calendarcolor',
119
		'{http://calendarserver.org/ns/}subscribed-strip-todos'       => 'striptodos',
120
		'{http://calendarserver.org/ns/}subscribed-strip-alarms'      => 'stripalarms',
121
		'{http://calendarserver.org/ns/}subscribed-strip-attachments' => 'stripattachments',
122
	];
123
124
	/** @var array properties to index */
125
	public static $indexProperties = ['CATEGORIES', 'COMMENT', 'DESCRIPTION',
126
		'LOCATION', 'RESOURCES', 'STATUS', 'SUMMARY', 'ATTENDEE', 'CONTACT',
127
		'ORGANIZER'];
128
129
	/** @var array parameters to index */
130
	public static $indexParameters = [
131
		'ATTENDEE' => ['CN'],
132
		'ORGANIZER' => ['CN'],
133
	];
134
135
	/**
136
	 * @var string[] Map of uid => display name
137
	 */
138
	protected $userDisplayNames;
139
140
	/** @var IDBConnection */
141
	private $db;
142
143
	/** @var Backend */
144
	private $sharingBackend;
145
146
	/** @var Principal */
147
	private $principalBackend;
148
149
	/** @var IUserManager */
150
	private $userManager;
151
152
	/** @var ISecureRandom */
153
	private $random;
154
155
	/** @var EventDispatcherInterface */
156
	private $dispatcher;
157
158
	/** @var bool */
159
	private $legacyEndpoint;
160
161
	/** @var string */
162
	private $dbObjectPropertiesTable = 'calendarobjects_props';
163
164
	/**
165
	 * CalDavBackend constructor.
166
	 *
167
	 * @param IDBConnection $db
168
	 * @param Principal $principalBackend
169
	 * @param IUserManager $userManager
170
	 * @param IGroupManager $groupManager
171
	 * @param ISecureRandom $random
172
	 * @param EventDispatcherInterface $dispatcher
173
	 * @param bool $legacyEndpoint
174
	 */
175
	public function __construct(IDBConnection $db,
176
								Principal $principalBackend,
177
								IUserManager $userManager,
178
								IGroupManager $groupManager,
179
								ISecureRandom $random,
180
								EventDispatcherInterface $dispatcher,
181
								$legacyEndpoint = false) {
182
		$this->db = $db;
183
		$this->principalBackend = $principalBackend;
184
		$this->userManager = $userManager;
185
		$this->sharingBackend = new Backend($this->db, $this->userManager, $groupManager, $principalBackend, 'calendar');
186
		$this->random = $random;
187
		$this->dispatcher = $dispatcher;
188
		$this->legacyEndpoint = $legacyEndpoint;
189
	}
190
191
	/**
192
	 * Return the number of calendars for a principal
193
	 *
194
	 * By default this excludes the automatically generated birthday calendar
195
	 *
196
	 * @param $principalUri
197
	 * @param bool $excludeBirthday
198
	 * @return int
199
	 */
200 View Code Duplication
	public function getCalendarsForUserCount($principalUri, $excludeBirthday = true) {
201
		$principalUri = $this->convertPrincipal($principalUri, true);
202
		$query = $this->db->getQueryBuilder();
203
		$query->select($query->createFunction('COUNT(*)'))
204
			->from('calendars')
205
			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
206
207
		if ($excludeBirthday) {
208
			$query->andWhere($query->expr()->neq('uri', $query->createNamedParameter(BirthdayService::BIRTHDAY_CALENDAR_URI)));
209
		}
210
211
		return (int)$query->execute()->fetchColumn();
212
	}
213
214
	/**
215
	 * Returns a list of calendars for a principal.
216
	 *
217
	 * Every project is an array with the following keys:
218
	 *  * id, a unique id that will be used by other functions to modify the
219
	 *    calendar. This can be the same as the uri or a database key.
220
	 *  * uri, which the basename of the uri with which the calendar is
221
	 *    accessed.
222
	 *  * principaluri. The owner of the calendar. Almost always the same as
223
	 *    principalUri passed to this method.
224
	 *
225
	 * Furthermore it can contain webdav properties in clark notation. A very
226
	 * common one is '{DAV:}displayname'.
227
	 *
228
	 * Many clients also require:
229
	 * {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
230
	 * For this property, you can just return an instance of
231
	 * Sabre\CalDAV\Property\SupportedCalendarComponentSet.
232
	 *
233
	 * If you return {http://sabredav.org/ns}read-only and set the value to 1,
234
	 * ACL will automatically be put in read-only mode.
235
	 *
236
	 * @param string $principalUri
237
	 * @return array
238
	 */
239
	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...
240
		$principalUriOriginal = $principalUri;
241
		$principalUri = $this->convertPrincipal($principalUri, true);
242
		$fields = array_values($this->propertyMap);
243
		$fields[] = 'id';
244
		$fields[] = 'uri';
245
		$fields[] = 'synctoken';
246
		$fields[] = 'components';
247
		$fields[] = 'principaluri';
248
		$fields[] = 'transparent';
249
250
		// Making fields a comma-delimited list
251
		$query = $this->db->getQueryBuilder();
252
		$query->select($fields)->from('calendars')
253
				->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
254
				->orderBy('calendarorder', 'ASC');
255
		$stmt = $query->execute();
256
257
		$calendars = [];
258
		while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
259
260
			$components = [];
261
			if ($row['components']) {
262
				$components = explode(',',$row['components']);
263
			}
264
265
			$calendar = [
266
				'id' => $row['id'],
267
				'uri' => $row['uri'],
268
				'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
269
				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
270
				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
271
				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
272
				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
273
				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($principalUri, !$this->legacyEndpoint),
274
			];
275
276
			foreach($this->propertyMap as $xmlName=>$dbName) {
277
				$calendar[$xmlName] = $row[$dbName];
278
			}
279
280
			$this->addOwnerPrincipal($calendar);
281
282
			if (!isset($calendars[$calendar['id']])) {
283
				$calendars[$calendar['id']] = $calendar;
284
			}
285
		}
286
287
		$stmt->closeCursor();
288
289
		// query for shared calendars
290
		$principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true);
291
		$principals = array_map(function($principal) {
292
			return urldecode($principal);
293
		}, $principals);
294
		$principals[]= $principalUri;
295
296
		$fields = array_values($this->propertyMap);
297
		$fields[] = 'a.id';
298
		$fields[] = 'a.uri';
299
		$fields[] = 'a.synctoken';
300
		$fields[] = 'a.components';
301
		$fields[] = 'a.principaluri';
302
		$fields[] = 'a.transparent';
303
		$fields[] = 's.access';
304
		$query = $this->db->getQueryBuilder();
305
		$result = $query->select($fields)
306
			->from('dav_shares', 's')
307
			->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
308
			->where($query->expr()->in('s.principaluri', $query->createParameter('principaluri')))
309
			->andWhere($query->expr()->eq('s.type', $query->createParameter('type')))
310
			->setParameter('type', 'calendar')
311
			->setParameter('principaluri', $principals, \Doctrine\DBAL\Connection::PARAM_STR_ARRAY)
312
			->execute();
313
314
		$readOnlyPropertyName = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only';
315
		while($row = $result->fetch()) {
316
			if ($row['principaluri'] === $principalUri) {
317
				continue;
318
			}
319
320
			$readOnly = (int) $row['access'] === Backend::ACCESS_READ;
321 View Code Duplication
			if (isset($calendars[$row['id']])) {
322
				if ($readOnly) {
323
					// New share can not have more permissions then the old one.
324
					continue;
325
				}
326
				if (isset($calendars[$row['id']][$readOnlyPropertyName]) &&
327
					$calendars[$row['id']][$readOnlyPropertyName] === 0) {
328
					// Old share is already read-write, no more permissions can be gained
329
					continue;
330
				}
331
			}
332
333
			list(, $name) = Uri\split($row['principaluri']);
334
			$uri = $row['uri'] . '_shared_by_' . $name;
335
			$row['displayname'] = $row['displayname'] . ' (' . $this->getUserDisplayName($name) . ')';
336
			$components = [];
337
			if ($row['components']) {
338
				$components = explode(',',$row['components']);
339
			}
340
			$calendar = [
341
				'id' => $row['id'],
342
				'uri' => $uri,
343
				'principaluri' => $this->convertPrincipal($principalUri, !$this->legacyEndpoint),
344
				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
345
				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
346
				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
347
				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
348
				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
349
				$readOnlyPropertyName => $readOnly,
350
			];
351
352
			foreach($this->propertyMap as $xmlName=>$dbName) {
353
				$calendar[$xmlName] = $row[$dbName];
354
			}
355
356
			$this->addOwnerPrincipal($calendar);
357
358
			$calendars[$calendar['id']] = $calendar;
359
		}
360
		$result->closeCursor();
361
362
		return array_values($calendars);
363
	}
364
365
	public function getUsersOwnCalendars($principalUri) {
366
		$principalUri = $this->convertPrincipal($principalUri, true);
367
		$fields = array_values($this->propertyMap);
368
		$fields[] = 'id';
369
		$fields[] = 'uri';
370
		$fields[] = 'synctoken';
371
		$fields[] = 'components';
372
		$fields[] = 'principaluri';
373
		$fields[] = 'transparent';
374
		// Making fields a comma-delimited list
375
		$query = $this->db->getQueryBuilder();
376
		$query->select($fields)->from('calendars')
377
			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
378
			->orderBy('calendarorder', 'ASC');
379
		$stmt = $query->execute();
380
		$calendars = [];
381
		while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
382
			$components = [];
383
			if ($row['components']) {
384
				$components = explode(',',$row['components']);
385
			}
386
			$calendar = [
387
				'id' => $row['id'],
388
				'uri' => $row['uri'],
389
				'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
390
				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
391
				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
392
				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
393
				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
394
			];
395
			foreach($this->propertyMap as $xmlName=>$dbName) {
396
				$calendar[$xmlName] = $row[$dbName];
397
			}
398
399
			$this->addOwnerPrincipal($calendar);
400
401
			if (!isset($calendars[$calendar['id']])) {
402
				$calendars[$calendar['id']] = $calendar;
403
			}
404
		}
405
		$stmt->closeCursor();
406
		return array_values($calendars);
407
	}
408
409
410 View Code Duplication
	private function getUserDisplayName($uid) {
411
		if (!isset($this->userDisplayNames[$uid])) {
412
			$user = $this->userManager->get($uid);
413
414
			if ($user instanceof IUser) {
415
				$this->userDisplayNames[$uid] = $user->getDisplayName();
416
			} else {
417
				$this->userDisplayNames[$uid] = $uid;
418
			}
419
		}
420
421
		return $this->userDisplayNames[$uid];
422
	}
423
	
424
	/**
425
	 * @return array
426
	 */
427
	public function getPublicCalendars() {
428
		$fields = array_values($this->propertyMap);
429
		$fields[] = 'a.id';
430
		$fields[] = 'a.uri';
431
		$fields[] = 'a.synctoken';
432
		$fields[] = 'a.components';
433
		$fields[] = 'a.principaluri';
434
		$fields[] = 'a.transparent';
435
		$fields[] = 's.access';
436
		$fields[] = 's.publicuri';
437
		$calendars = [];
438
		$query = $this->db->getQueryBuilder();
439
		$result = $query->select($fields)
440
			->from('dav_shares', 's')
441
			->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
442
			->where($query->expr()->in('s.access', $query->createNamedParameter(self::ACCESS_PUBLIC)))
443
			->andWhere($query->expr()->eq('s.type', $query->createNamedParameter('calendar')))
444
			->execute();
445
446
		while($row = $result->fetch()) {
447
			list(, $name) = Uri\split($row['principaluri']);
448
			$row['displayname'] = $row['displayname'] . "($name)";
449
			$components = [];
450
			if ($row['components']) {
451
				$components = explode(',',$row['components']);
452
			}
453
			$calendar = [
454
				'id' => $row['id'],
455
				'uri' => $row['publicuri'],
456
				'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
457
				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
458
				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
459
				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
460
				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
461
				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], $this->legacyEndpoint),
462
				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ,
463
				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}public' => (int)$row['access'] === self::ACCESS_PUBLIC,
464
			];
465
466
			foreach($this->propertyMap as $xmlName=>$dbName) {
467
				$calendar[$xmlName] = $row[$dbName];
468
			}
469
470
			$this->addOwnerPrincipal($calendar);
471
472
			if (!isset($calendars[$calendar['id']])) {
473
				$calendars[$calendar['id']] = $calendar;
474
			}
475
		}
476
		$result->closeCursor();
477
478
		return array_values($calendars);
479
	}
480
481
	/**
482
	 * @param string $uri
483
	 * @return array
484
	 * @throws NotFound
485
	 */
486
	public function getPublicCalendar($uri) {
487
		$fields = array_values($this->propertyMap);
488
		$fields[] = 'a.id';
489
		$fields[] = 'a.uri';
490
		$fields[] = 'a.synctoken';
491
		$fields[] = 'a.components';
492
		$fields[] = 'a.principaluri';
493
		$fields[] = 'a.transparent';
494
		$fields[] = 's.access';
495
		$fields[] = 's.publicuri';
496
		$query = $this->db->getQueryBuilder();
497
		$result = $query->select($fields)
498
			->from('dav_shares', 's')
499
			->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
500
			->where($query->expr()->in('s.access', $query->createNamedParameter(self::ACCESS_PUBLIC)))
501
			->andWhere($query->expr()->eq('s.type', $query->createNamedParameter('calendar')))
502
			->andWhere($query->expr()->eq('s.publicuri', $query->createNamedParameter($uri)))
503
			->execute();
504
505
		$row = $result->fetch(\PDO::FETCH_ASSOC);
506
507
		$result->closeCursor();
508
509
		if ($row === false) {
510
			throw new NotFound('Node with name \'' . $uri . '\' could not be found');
511
		}
512
513
		list(, $name) = Uri\split($row['principaluri']);
514
		$row['displayname'] = $row['displayname'] . ' ' . "($name)";
515
		$components = [];
516
		if ($row['components']) {
517
			$components = explode(',',$row['components']);
518
		}
519
		$calendar = [
520
			'id' => $row['id'],
521
			'uri' => $row['publicuri'],
522
			'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
523
			'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
524
			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
525
			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
526
			'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
527
			'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
528
			'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ,
529
			'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}public' => (int)$row['access'] === self::ACCESS_PUBLIC,
530
		];
531
532
		foreach($this->propertyMap as $xmlName=>$dbName) {
533
			$calendar[$xmlName] = $row[$dbName];
534
		}
535
536
		$this->addOwnerPrincipal($calendar);
537
538
		return $calendar;
539
540
	}
541
542
	/**
543
	 * @param string $principal
544
	 * @param string $uri
545
	 * @return array|null
546
	 */
547
	public function getCalendarByUri($principal, $uri) {
548
		$fields = array_values($this->propertyMap);
549
		$fields[] = 'id';
550
		$fields[] = 'uri';
551
		$fields[] = 'synctoken';
552
		$fields[] = 'components';
553
		$fields[] = 'principaluri';
554
		$fields[] = 'transparent';
555
556
		// Making fields a comma-delimited list
557
		$query = $this->db->getQueryBuilder();
558
		$query->select($fields)->from('calendars')
559
			->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
560
			->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($principal)))
561
			->setMaxResults(1);
562
		$stmt = $query->execute();
563
564
		$row = $stmt->fetch(\PDO::FETCH_ASSOC);
565
		$stmt->closeCursor();
566
		if ($row === false) {
567
			return null;
568
		}
569
570
		$components = [];
571
		if ($row['components']) {
572
			$components = explode(',',$row['components']);
573
		}
574
575
		$calendar = [
576
			'id' => $row['id'],
577
			'uri' => $row['uri'],
578
			'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
579
			'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
580
			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
581
			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
582
			'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
583
		];
584
585
		foreach($this->propertyMap as $xmlName=>$dbName) {
586
			$calendar[$xmlName] = $row[$dbName];
587
		}
588
589
		$this->addOwnerPrincipal($calendar);
590
591
		return $calendar;
592
	}
593
594
	public function getCalendarById($calendarId) {
595
		$fields = array_values($this->propertyMap);
596
		$fields[] = 'id';
597
		$fields[] = 'uri';
598
		$fields[] = 'synctoken';
599
		$fields[] = 'components';
600
		$fields[] = 'principaluri';
601
		$fields[] = 'transparent';
602
603
		// Making fields a comma-delimited list
604
		$query = $this->db->getQueryBuilder();
605
		$query->select($fields)->from('calendars')
606
			->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)))
607
			->setMaxResults(1);
608
		$stmt = $query->execute();
609
610
		$row = $stmt->fetch(\PDO::FETCH_ASSOC);
611
		$stmt->closeCursor();
612
		if ($row === false) {
613
			return null;
614
		}
615
616
		$components = [];
617
		if ($row['components']) {
618
			$components = explode(',',$row['components']);
619
		}
620
621
		$calendar = [
622
			'id' => $row['id'],
623
			'uri' => $row['uri'],
624
			'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
625
			'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
626
			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
627
			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
628
			'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
629
		];
630
631
		foreach($this->propertyMap as $xmlName=>$dbName) {
632
			$calendar[$xmlName] = $row[$dbName];
633
		}
634
635
		$this->addOwnerPrincipal($calendar);
636
637
		return $calendar;
638
	}
639
640
	/**
641
	 * Creates a new calendar for a principal.
642
	 *
643
	 * If the creation was a success, an id must be returned that can be used to reference
644
	 * this calendar in other methods, such as updateCalendar.
645
	 *
646
	 * @param string $principalUri
647
	 * @param string $calendarUri
648
	 * @param array $properties
649
	 * @return int
650
	 * @suppress SqlInjectionChecker
651
	 */
652
	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...
653
		$values = [
654
			'principaluri' => $this->convertPrincipal($principalUri, true),
655
			'uri'          => $calendarUri,
656
			'synctoken'    => 1,
657
			'transparent'  => 0,
658
			'components'   => 'VEVENT,VTODO',
659
			'displayname'  => $calendarUri
660
		];
661
662
		// Default value
663
		$sccs = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set';
664
		if (isset($properties[$sccs])) {
665
			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...
666
				throw new DAV\Exception('The ' . $sccs . ' property must be of type: \Sabre\CalDAV\Property\SupportedCalendarComponentSet');
667
			}
668
			$values['components'] = implode(',',$properties[$sccs]->getValue());
669
		}
670
		$transp = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp';
671
		if (isset($properties[$transp])) {
672
			$values['transparent'] = (int) ($properties[$transp]->getValue() === 'transparent');
673
		}
674
675
		foreach($this->propertyMap as $xmlName=>$dbName) {
676
			if (isset($properties[$xmlName])) {
677
				$values[$dbName] = $properties[$xmlName];
678
			}
679
		}
680
681
		$query = $this->db->getQueryBuilder();
682
		$query->insert('calendars');
683
		foreach($values as $column => $value) {
684
			$query->setValue($column, $query->createNamedParameter($value));
685
		}
686
		$query->execute();
687
		$calendarId = $query->getLastInsertId();
688
689
		$this->dispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createCalendar', new GenericEvent(
690
			'\OCA\DAV\CalDAV\CalDavBackend::createCalendar',
691
			[
692
				'calendarId' => $calendarId,
693
				'calendarData' => $this->getCalendarById($calendarId),
694
		]));
695
696
		return $calendarId;
697
	}
698
699
	/**
700
	 * Updates properties for a calendar.
701
	 *
702
	 * The list of mutations is stored in a Sabre\DAV\PropPatch object.
703
	 * To do the actual updates, you must tell this object which properties
704
	 * you're going to process with the handle() method.
705
	 *
706
	 * Calling the handle method is like telling the PropPatch object "I
707
	 * promise I can handle updating this property".
708
	 *
709
	 * Read the PropPatch documentation for more info and examples.
710
	 *
711
	 * @param mixed $calendarId
712
	 * @param PropPatch $propPatch
713
	 * @return void
714
	 */
715
	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...
716
		$supportedProperties = array_keys($this->propertyMap);
717
		$supportedProperties[] = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp';
718
719
		/**
720
		 * @suppress SqlInjectionChecker
721
		 */
722
		$propPatch->handle($supportedProperties, function($mutations) use ($calendarId) {
723
			$newValues = [];
724
			foreach ($mutations as $propertyName => $propertyValue) {
725
726
				switch ($propertyName) {
727
					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...
728
						$fieldName = 'transparent';
729
						$newValues[$fieldName] = (int) ($propertyValue->getValue() === 'transparent');
730
						break;
731
					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...
732
						$fieldName = $this->propertyMap[$propertyName];
733
						$newValues[$fieldName] = $propertyValue;
734
						break;
735
				}
736
737
			}
738
			$query = $this->db->getQueryBuilder();
739
			$query->update('calendars');
740
			foreach ($newValues as $fieldName => $value) {
741
				$query->set($fieldName, $query->createNamedParameter($value));
742
			}
743
			$query->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)));
744
			$query->execute();
745
746
			$this->addChange($calendarId, "", 2);
747
748
			$this->dispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateCalendar', new GenericEvent(
749
				'\OCA\DAV\CalDAV\CalDavBackend::updateCalendar',
750
				[
751
					'calendarId' => $calendarId,
752
					'calendarData' => $this->getCalendarById($calendarId),
753
					'shares' => $this->getShares($calendarId),
754
					'propertyMutations' => $mutations,
755
			]));
756
757
			return true;
758
		});
759
	}
760
761
	/**
762
	 * Delete a calendar and all it's objects
763
	 *
764
	 * @param mixed $calendarId
765
	 * @return void
766
	 */
767
	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...
768
		$this->dispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteCalendar', new GenericEvent(
769
			'\OCA\DAV\CalDAV\CalDavBackend::deleteCalendar',
770
			[
771
				'calendarId' => $calendarId,
772
				'calendarData' => $this->getCalendarById($calendarId),
773
				'shares' => $this->getShares($calendarId),
774
		]));
775
776
		$stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ?');
777
		$stmt->execute([$calendarId]);
778
779
		$stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendars` WHERE `id` = ?');
780
		$stmt->execute([$calendarId]);
781
782
		$stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarchanges` WHERE `calendarid` = ?');
783
		$stmt->execute([$calendarId]);
784
785
		$this->sharingBackend->deleteAllShares($calendarId);
786
787
		$query = $this->db->getQueryBuilder();
788
		$query->delete($this->dbObjectPropertiesTable)
789
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
790
			->execute();
791
	}
792
793
	/**
794
	 * Delete all of an user's shares
795
	 *
796
	 * @param string $principaluri
797
	 * @return void
798
	 */
799
	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...
800
		$this->sharingBackend->deleteAllSharesByUser($principaluri);
801
	}
802
803
	/**
804
	 * Returns all calendar objects within a calendar.
805
	 *
806
	 * Every item contains an array with the following keys:
807
	 *   * calendardata - The iCalendar-compatible calendar data
808
	 *   * uri - a unique key which will be used to construct the uri. This can
809
	 *     be any arbitrary string, but making sure it ends with '.ics' is a
810
	 *     good idea. This is only the basename, or filename, not the full
811
	 *     path.
812
	 *   * lastmodified - a timestamp of the last modification time
813
	 *   * etag - An arbitrary string, surrounded by double-quotes. (e.g.:
814
	 *   '"abcdef"')
815
	 *   * size - The size of the calendar objects, in bytes.
816
	 *   * component - optional, a string containing the type of object, such
817
	 *     as 'vevent' or 'vtodo'. If specified, this will be used to populate
818
	 *     the Content-Type header.
819
	 *
820
	 * Note that the etag is optional, but it's highly encouraged to return for
821
	 * speed reasons.
822
	 *
823
	 * The calendardata is also optional. If it's not returned
824
	 * 'getCalendarObject' will be called later, which *is* expected to return
825
	 * calendardata.
826
	 *
827
	 * If neither etag or size are specified, the calendardata will be
828
	 * used/fetched to determine these numbers. If both are specified the
829
	 * amount of times this is needed is reduced by a great degree.
830
	 *
831
	 * @param mixed $calendarId
832
	 * @return array
833
	 */
834
	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...
835
		$query = $this->db->getQueryBuilder();
836
		$query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'componenttype', 'classification'])
837
			->from('calendarobjects')
838
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)));
839
		$stmt = $query->execute();
840
841
		$result = [];
842
		foreach($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) {
843
			$result[] = [
844
					'id'           => $row['id'],
845
					'uri'          => $row['uri'],
846
					'lastmodified' => $row['lastmodified'],
847
					'etag'         => '"' . $row['etag'] . '"',
848
					'calendarid'   => $row['calendarid'],
849
					'size'         => (int)$row['size'],
850
					'component'    => strtolower($row['componenttype']),
851
					'classification'=> (int)$row['classification']
852
			];
853
		}
854
855
		return $result;
856
	}
857
858
	/**
859
	 * Returns information from a single calendar object, based on it's object
860
	 * uri.
861
	 *
862
	 * The object uri is only the basename, or filename and not a full path.
863
	 *
864
	 * The returned array must have the same keys as getCalendarObjects. The
865
	 * 'calendardata' object is required here though, while it's not required
866
	 * for getCalendarObjects.
867
	 *
868
	 * This method must return null if the object did not exist.
869
	 *
870
	 * @param mixed $calendarId
871
	 * @param string $objectUri
872
	 * @return array|null
873
	 */
874
	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...
875
876
		$query = $this->db->getQueryBuilder();
877
		$query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification'])
878
				->from('calendarobjects')
879
				->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
880
				->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)));
881
		$stmt = $query->execute();
882
		$row = $stmt->fetch(\PDO::FETCH_ASSOC);
883
884
		if(!$row) return null;
885
886
		return [
887
				'id'            => $row['id'],
888
				'uri'           => $row['uri'],
889
				'lastmodified'  => $row['lastmodified'],
890
				'etag'          => '"' . $row['etag'] . '"',
891
				'calendarid'    => $row['calendarid'],
892
				'size'          => (int)$row['size'],
893
				'calendardata'  => $this->readBlob($row['calendardata']),
894
				'component'     => strtolower($row['componenttype']),
895
				'classification'=> (int)$row['classification']
896
		];
897
	}
898
899
	/**
900
	 * Returns a list of calendar objects.
901
	 *
902
	 * This method should work identical to getCalendarObject, but instead
903
	 * return all the calendar objects in the list as an array.
904
	 *
905
	 * If the backend supports this, it may allow for some speed-ups.
906
	 *
907
	 * @param mixed $calendarId
908
	 * @param string[] $uris
909
	 * @return array
910
	 */
911
	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...
912
		if (empty($uris)) {
913
			return [];
914
		}
915
916
		$chunks = array_chunk($uris, 100);
917
		$objects = [];
918
919
		$query = $this->db->getQueryBuilder();
920
		$query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification'])
921
			->from('calendarobjects')
922
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
923
			->andWhere($query->expr()->in('uri', $query->createParameter('uri')));
924
925
		foreach ($chunks as $uris) {
926
			$query->setParameter('uri', $uris, IQueryBuilder::PARAM_STR_ARRAY);
927
			$result = $query->execute();
928
929
			while ($row = $result->fetch()) {
930
				$objects[] = [
931
					'id'           => $row['id'],
932
					'uri'          => $row['uri'],
933
					'lastmodified' => $row['lastmodified'],
934
					'etag'         => '"' . $row['etag'] . '"',
935
					'calendarid'   => $row['calendarid'],
936
					'size'         => (int)$row['size'],
937
					'calendardata' => $this->readBlob($row['calendardata']),
938
					'component'    => strtolower($row['componenttype']),
939
					'classification' => (int)$row['classification']
940
				];
941
			}
942
			$result->closeCursor();
943
		}
944
		return $objects;
945
	}
946
947
	/**
948
	 * Creates a new calendar object.
949
	 *
950
	 * The object uri is only the basename, or filename and not a full path.
951
	 *
952
	 * It is possible return an etag from this function, which will be used in
953
	 * the response to this PUT request. Note that the ETag must be surrounded
954
	 * by double-quotes.
955
	 *
956
	 * However, you should only really return this ETag if you don't mangle the
957
	 * calendar-data. If the result of a subsequent GET to this object is not
958
	 * the exact same as this request body, you should omit the ETag.
959
	 *
960
	 * @param mixed $calendarId
961
	 * @param string $objectUri
962
	 * @param string $calendarData
963
	 * @return string
964
	 */
965
	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...
966
		$extraData = $this->getDenormalizedData($calendarData);
967
968
		$q = $this->db->getQueryBuilder();
969
		$q->select($q->createFunction('COUNT(*)'))
970
			->from('calendarobjects')
971
			->where($q->expr()->eq('calendarid', $q->createNamedParameter($calendarId)))
972
			->andWhere($q->expr()->eq('uid', $q->createNamedParameter($extraData['uid'])));
973
974
		$result = $q->execute();
975
		$count = (int) $result->fetchColumn();
976
		$result->closeCursor();
977
978
		if ($count !== 0) {
979
			throw new \Sabre\DAV\Exception\BadRequest('Calendar object with uid already exists in this calendar collection.');
980
		}
981
982
		$query = $this->db->getQueryBuilder();
983
		$query->insert('calendarobjects')
984
			->values([
985
				'calendarid' => $query->createNamedParameter($calendarId),
986
				'uri' => $query->createNamedParameter($objectUri),
987
				'calendardata' => $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB),
988
				'lastmodified' => $query->createNamedParameter(time()),
989
				'etag' => $query->createNamedParameter($extraData['etag']),
990
				'size' => $query->createNamedParameter($extraData['size']),
991
				'componenttype' => $query->createNamedParameter($extraData['componentType']),
992
				'firstoccurence' => $query->createNamedParameter($extraData['firstOccurence']),
993
				'lastoccurence' => $query->createNamedParameter($extraData['lastOccurence']),
994
				'classification' => $query->createNamedParameter($extraData['classification']),
995
				'uid' => $query->createNamedParameter($extraData['uid']),
996
			])
997
			->execute();
998
999
		$this->updateProperties($calendarId, $objectUri, $calendarData);
1000
1001
		$this->dispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createCalendarObject', new GenericEvent(
1002
			'\OCA\DAV\CalDAV\CalDavBackend::createCalendarObject',
1003
			[
1004
				'calendarId' => $calendarId,
1005
				'calendarData' => $this->getCalendarById($calendarId),
1006
				'shares' => $this->getShares($calendarId),
1007
				'objectData' => $this->getCalendarObject($calendarId, $objectUri),
1008
			]
1009
		));
1010
		$this->addChange($calendarId, $objectUri, 1);
1011
1012
		return '"' . $extraData['etag'] . '"';
1013
	}
1014
1015
	/**
1016
	 * Updates an existing calendarobject, based on it's uri.
1017
	 *
1018
	 * The object uri is only the basename, or filename and not a full path.
1019
	 *
1020
	 * It is possible return an etag from this function, which will be used in
1021
	 * the response to this PUT request. Note that the ETag must be surrounded
1022
	 * by double-quotes.
1023
	 *
1024
	 * However, you should only really return this ETag if you don't mangle the
1025
	 * calendar-data. If the result of a subsequent GET to this object is not
1026
	 * the exact same as this request body, you should omit the ETag.
1027
	 *
1028
	 * @param mixed $calendarId
1029
	 * @param string $objectUri
1030
	 * @param string $calendarData
1031
	 * @return string
1032
	 */
1033
	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...
1034
		$extraData = $this->getDenormalizedData($calendarData);
1035
1036
		$query = $this->db->getQueryBuilder();
1037
		$query->update('calendarobjects')
1038
				->set('calendardata', $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB))
1039
				->set('lastmodified', $query->createNamedParameter(time()))
1040
				->set('etag', $query->createNamedParameter($extraData['etag']))
1041
				->set('size', $query->createNamedParameter($extraData['size']))
1042
				->set('componenttype', $query->createNamedParameter($extraData['componentType']))
1043
				->set('firstoccurence', $query->createNamedParameter($extraData['firstOccurence']))
1044
				->set('lastoccurence', $query->createNamedParameter($extraData['lastOccurence']))
1045
				->set('classification', $query->createNamedParameter($extraData['classification']))
1046
				->set('uid', $query->createNamedParameter($extraData['uid']))
1047
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
1048
			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
1049
			->execute();
1050
1051
		$this->updateProperties($calendarId, $objectUri, $calendarData);
1052
1053
		$data = $this->getCalendarObject($calendarId, $objectUri);
1054 View Code Duplication
		if (is_array($data)) {
1055
			$this->dispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateCalendarObject', new GenericEvent(
1056
				'\OCA\DAV\CalDAV\CalDavBackend::updateCalendarObject',
1057
				[
1058
					'calendarId' => $calendarId,
1059
					'calendarData' => $this->getCalendarById($calendarId),
1060
					'shares' => $this->getShares($calendarId),
1061
					'objectData' => $data,
1062
				]
1063
			));
1064
		}
1065
		$this->addChange($calendarId, $objectUri, 2);
1066
1067
		return '"' . $extraData['etag'] . '"';
1068
	}
1069
1070
	/**
1071
	 * @param int $calendarObjectId
1072
	 * @param int $classification
1073
	 */
1074
	public function setClassification($calendarObjectId, $classification) {
1075
		if (!in_array($classification, [
1076
			self::CLASSIFICATION_PUBLIC, self::CLASSIFICATION_PRIVATE, self::CLASSIFICATION_CONFIDENTIAL
1077
		])) {
1078
			throw new \InvalidArgumentException();
1079
		}
1080
		$query = $this->db->getQueryBuilder();
1081
		$query->update('calendarobjects')
1082
			->set('classification', $query->createNamedParameter($classification))
1083
			->where($query->expr()->eq('id', $query->createNamedParameter($calendarObjectId)))
1084
			->execute();
1085
	}
1086
1087
	/**
1088
	 * Deletes an existing calendar object.
1089
	 *
1090
	 * The object uri is only the basename, or filename and not a full path.
1091
	 *
1092
	 * @param mixed $calendarId
1093
	 * @param string $objectUri
1094
	 * @return void
1095
	 */
1096
	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...
1097
		$data = $this->getCalendarObject($calendarId, $objectUri);
1098 View Code Duplication
		if (is_array($data)) {
1099
			$this->dispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteCalendarObject', new GenericEvent(
1100
				'\OCA\DAV\CalDAV\CalDavBackend::deleteCalendarObject',
1101
				[
1102
					'calendarId' => $calendarId,
1103
					'calendarData' => $this->getCalendarById($calendarId),
1104
					'shares' => $this->getShares($calendarId),
1105
					'objectData' => $data,
1106
				]
1107
			));
1108
		}
1109
1110
		$stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ? AND `uri` = ?');
1111
		$stmt->execute([$calendarId, $objectUri]);
1112
1113
		$this->purgeProperties($calendarId, $data['id']);
1114
1115
		$this->addChange($calendarId, $objectUri, 3);
1116
	}
1117
1118
	/**
1119
	 * Performs a calendar-query on the contents of this calendar.
1120
	 *
1121
	 * The calendar-query is defined in RFC4791 : CalDAV. Using the
1122
	 * calendar-query it is possible for a client to request a specific set of
1123
	 * object, based on contents of iCalendar properties, date-ranges and
1124
	 * iCalendar component types (VTODO, VEVENT).
1125
	 *
1126
	 * This method should just return a list of (relative) urls that match this
1127
	 * query.
1128
	 *
1129
	 * The list of filters are specified as an array. The exact array is
1130
	 * documented by Sabre\CalDAV\CalendarQueryParser.
1131
	 *
1132
	 * Note that it is extremely likely that getCalendarObject for every path
1133
	 * returned from this method will be called almost immediately after. You
1134
	 * may want to anticipate this to speed up these requests.
1135
	 *
1136
	 * This method provides a default implementation, which parses *all* the
1137
	 * iCalendar objects in the specified calendar.
1138
	 *
1139
	 * This default may well be good enough for personal use, and calendars
1140
	 * that aren't very large. But if you anticipate high usage, big calendars
1141
	 * or high loads, you are strongly advised to optimize certain paths.
1142
	 *
1143
	 * The best way to do so is override this method and to optimize
1144
	 * specifically for 'common filters'.
1145
	 *
1146
	 * Requests that are extremely common are:
1147
	 *   * requests for just VEVENTS
1148
	 *   * requests for just VTODO
1149
	 *   * requests with a time-range-filter on either VEVENT or VTODO.
1150
	 *
1151
	 * ..and combinations of these requests. It may not be worth it to try to
1152
	 * handle every possible situation and just rely on the (relatively
1153
	 * easy to use) CalendarQueryValidator to handle the rest.
1154
	 *
1155
	 * Note that especially time-range-filters may be difficult to parse. A
1156
	 * time-range filter specified on a VEVENT must for instance also handle
1157
	 * recurrence rules correctly.
1158
	 * A good example of how to interprete all these filters can also simply
1159
	 * be found in Sabre\CalDAV\CalendarQueryFilter. This class is as correct
1160
	 * as possible, so it gives you a good idea on what type of stuff you need
1161
	 * to think of.
1162
	 *
1163
	 * @param mixed $calendarId
1164
	 * @param array $filters
1165
	 * @return array
1166
	 */
1167
	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...
1168
		$componentType = null;
1169
		$requirePostFilter = true;
1170
		$timeRange = null;
1171
1172
		// if no filters were specified, we don't need to filter after a query
1173
		if (!$filters['prop-filters'] && !$filters['comp-filters']) {
1174
			$requirePostFilter = false;
1175
		}
1176
1177
		// Figuring out if there's a component filter
1178
		if (count($filters['comp-filters']) > 0 && !$filters['comp-filters'][0]['is-not-defined']) {
1179
			$componentType = $filters['comp-filters'][0]['name'];
1180
1181
			// Checking if we need post-filters
1182 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']) {
1183
				$requirePostFilter = false;
1184
			}
1185
			// There was a time-range filter
1186
			if ($componentType === 'VEVENT' && isset($filters['comp-filters'][0]['time-range'])) {
1187
				$timeRange = $filters['comp-filters'][0]['time-range'];
1188
1189
				// If start time OR the end time is not specified, we can do a
1190
				// 100% accurate mysql query.
1191 View Code Duplication
				if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['prop-filters'] && (!$timeRange['start'] || !$timeRange['end'])) {
1192
					$requirePostFilter = false;
1193
				}
1194
			}
1195
1196
		}
1197
		$columns = ['uri'];
1198
		if ($requirePostFilter) {
1199
			$columns = ['uri', 'calendardata'];
1200
		}
1201
		$query = $this->db->getQueryBuilder();
1202
		$query->select($columns)
1203
			->from('calendarobjects')
1204
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)));
1205
1206
		if ($componentType) {
1207
			$query->andWhere($query->expr()->eq('componenttype', $query->createNamedParameter($componentType)));
1208
		}
1209
1210
		if ($timeRange && $timeRange['start']) {
1211
			$query->andWhere($query->expr()->gt('lastoccurence', $query->createNamedParameter($timeRange['start']->getTimeStamp())));
1212
		}
1213
		if ($timeRange && $timeRange['end']) {
1214
			$query->andWhere($query->expr()->lt('firstoccurence', $query->createNamedParameter($timeRange['end']->getTimeStamp())));
1215
		}
1216
1217
		$stmt = $query->execute();
1218
1219
		$result = [];
1220
		while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
1221
			if ($requirePostFilter) {
1222
				if (!$this->validateFilterForObject($row, $filters)) {
1223
					continue;
1224
				}
1225
			}
1226
			$result[] = $row['uri'];
1227
		}
1228
1229
		return $result;
1230
	}
1231
1232
	/**
1233
	 * custom Nextcloud search extension for CalDAV
1234
	 *
1235
	 * @param string $principalUri
1236
	 * @param array $filters
1237
	 * @param integer|null $limit
1238
	 * @param integer|null $offset
1239
	 * @return array
1240
	 */
1241
	public function calendarSearch($principalUri, array $filters, $limit=null, $offset=null) {
1242
		$calendars = $this->getCalendarsForUser($principalUri);
1243
		$ownCalendars = [];
1244
		$sharedCalendars = [];
1245
1246
		$uriMapper = [];
1247
1248
		foreach($calendars as $calendar) {
1249
			if ($calendar['{http://owncloud.org/ns}owner-principal'] === $principalUri) {
1250
				$ownCalendars[] = $calendar['id'];
1251
			} else {
1252
				$sharedCalendars[] = $calendar['id'];
1253
			}
1254
			$uriMapper[$calendar['id']] = $calendar['uri'];
1255
		}
1256
		if (count($ownCalendars) === 0 && count($sharedCalendars) === 0) {
1257
			return [];
1258
		}
1259
1260
		$query = $this->db->getQueryBuilder();
1261
		// Calendar id expressions
1262
		$calendarExpressions = [];
1263
		foreach($ownCalendars as $id) {
1264
			$calendarExpressions[] = $query->expr()
1265
				->eq('c.calendarid', $query->createNamedParameter($id));
1266
		}
1267
		foreach($sharedCalendars as $id) {
1268
			$calendarExpressions[] = $query->expr()->andX(
1269
				$query->expr()->eq('c.calendarid',
1270
					$query->createNamedParameter($id)),
1271
				$query->expr()->eq('c.classification',
1272
					$query->createNamedParameter(self::CLASSIFICATION_PUBLIC))
1273
			);
1274
		}
1275
1276
		if (count($calendarExpressions) === 1) {
1277
			$calExpr = $calendarExpressions[0];
1278
		} else {
1279
			$calExpr = call_user_func_array([$query->expr(), 'orX'], $calendarExpressions);
1280
		}
1281
1282
		// Component expressions
1283
		$compExpressions = [];
1284
		foreach($filters['comps'] as $comp) {
1285
			$compExpressions[] = $query->expr()
1286
				->eq('c.componenttype', $query->createNamedParameter($comp));
1287
		}
1288
1289
		if (count($compExpressions) === 1) {
1290
			$compExpr = $compExpressions[0];
1291
		} else {
1292
			$compExpr = call_user_func_array([$query->expr(), 'orX'], $compExpressions);
1293
		}
1294
1295
		if (!isset($filters['props'])) {
1296
			$filters['props'] = [];
1297
		}
1298
		if (!isset($filters['params'])) {
1299
			$filters['params'] = [];
1300
		}
1301
1302
		$propParamExpressions = [];
1303
		foreach($filters['props'] as $prop) {
1304
			$propParamExpressions[] = $query->expr()->andX(
1305
				$query->expr()->eq('i.name', $query->createNamedParameter($prop)),
1306
				$query->expr()->isNull('i.parameter')
1307
			);
1308
		}
1309
		foreach($filters['params'] as $param) {
1310
			$propParamExpressions[] = $query->expr()->andX(
1311
				$query->expr()->eq('i.name', $query->createNamedParameter($param['property'])),
1312
				$query->expr()->eq('i.parameter', $query->createNamedParameter($param['parameter']))
1313
			);
1314
		}
1315
1316
		if (count($propParamExpressions) === 1) {
1317
			$propParamExpr = $propParamExpressions[0];
1318
		} else {
1319
			$propParamExpr = call_user_func_array([$query->expr(), 'orX'], $propParamExpressions);
1320
		}
1321
1322
		$query->select(['c.calendarid', 'c.uri'])
1323
			->from($this->dbObjectPropertiesTable, 'i')
1324
			->join('i', 'calendarobjects', 'c', $query->expr()->eq('i.objectid', 'c.id'))
1325
			->where($calExpr)
1326
			->andWhere($compExpr)
1327
			->andWhere($propParamExpr)
1328
			->andWhere($query->expr()->iLike('i.value',
1329
				$query->createNamedParameter('%'.$this->db->escapeLikeParameter($filters['search-term']).'%')));
1330
1331
		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...
1332
			$query->setFirstResult($offset);
1333
		}
1334
		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...
1335
			$query->setMaxResults($limit);
1336
		}
1337
1338
		$stmt = $query->execute();
1339
1340
		$result = [];
1341
		while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
1342
			$path = $uriMapper[$row['calendarid']] . '/' . $row['uri'];
1343
			if (!in_array($path, $result)) {
1344
				$result[] = $path;
1345
			}
1346
		}
1347
1348
		return $result;
1349
	}
1350
1351
	/**
1352
	 * used for Nextcloud's calendar API
1353
	 *
1354
	 * @param array $calendarInfo
1355
	 * @param string $pattern
1356
	 * @param array $searchProperties
1357
	 * @param array $options
1358
	 * @param integer|null $limit
1359
	 * @param integer|null $offset
1360
	 *
1361
	 * @return array
1362
	 */
1363
	public function search(array $calendarInfo, $pattern, array $searchProperties,
1364
						   array $options, $limit, $offset) {
1365
		$outerQuery = $this->db->getQueryBuilder();
1366
		$innerQuery = $this->db->getQueryBuilder();
1367
1368
		$innerQuery->selectDistinct('op.objectid')
1369
			->from($this->dbObjectPropertiesTable, 'op')
1370
			->andWhere($innerQuery->expr()->eq('op.calendarid',
1371
				$outerQuery->createNamedParameter($calendarInfo['id'])));
1372
1373
		// only return public items for shared calendars for now
1374
		if ($calendarInfo['principaluri'] !== $calendarInfo['{http://owncloud.org/ns}owner-principal']) {
1375
			$innerQuery->andWhere($innerQuery->expr()->eq('c.classification',
1376
				$outerQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
1377
		}
1378
1379
		$or = $innerQuery->expr()->orX();
1380
		foreach($searchProperties as $searchProperty) {
1381
			$or->add($innerQuery->expr()->eq('op.name',
1382
				$outerQuery->createNamedParameter($searchProperty)));
1383
		}
1384
		$innerQuery->andWhere($or);
1385
1386 View Code Duplication
		if ($pattern !== '') {
1387
			$innerQuery->andWhere($innerQuery->expr()->iLike('op.value',
1388
				$outerQuery->createNamedParameter('%' .
1389
					$this->db->escapeLikeParameter($pattern) . '%')));
1390
		}
1391
1392
		$outerQuery->select('c.id', 'c.calendardata', 'c.componenttype', 'c.uid', 'c.uri')
1393
			->from('calendarobjects', 'c');
1394
1395
		if (isset($options['timerange'])) {
1396
			if (isset($options['timerange']['start'])) {
1397
				$outerQuery->andWhere($outerQuery->expr()->gt('lastoccurence',
1398
					$outerQuery->createNamedParameter($options['timerange']['start']->getTimeStamp)));
1399
1400
			}
1401
			if (isset($options['timerange']['end'])) {
1402
				$outerQuery->andWhere($outerQuery->expr()->lt('firstoccurence',
1403
					$outerQuery->createNamedParameter($options['timerange']['end']->getTimeStamp)));
1404
			}
1405
		}
1406
1407
		if (isset($options['types'])) {
1408
			$or = $outerQuery->expr()->orX();
1409
			foreach($options['types'] as $type) {
1410
				$or->add($outerQuery->expr()->eq('componenttype',
1411
					$outerQuery->createNamedParameter($type)));
1412
			}
1413
			$outerQuery->andWhere($or);
1414
		}
1415
1416
		$outerQuery->andWhere($outerQuery->expr()->in('c.id',
1417
			$outerQuery->createFunction($innerQuery->getSQL())));
1418
1419
		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...
1420
			$outerQuery->setFirstResult($offset);
1421
		}
1422
		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...
1423
			$outerQuery->setMaxResults($limit);
1424
		}
1425
1426
		$result = $outerQuery->execute();
1427
		$calendarObjects = $result->fetchAll();
1428
1429
		return array_map(function($o) {
1430
			$calendarData = Reader::read($o['calendardata']);
1431
			$comps = $calendarData->getComponents();
1432
			$objects = [];
1433
			$timezones = [];
1434
			foreach($comps as $comp) {
1435
				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...
1436
					$timezones[] = $comp;
1437
				} else {
1438
					$objects[] = $comp;
1439
				}
1440
			}
1441
1442
			return [
1443
				'id' => $o['id'],
1444
				'type' => $o['componenttype'],
1445
				'uid' => $o['uid'],
1446
				'uri' => $o['uri'],
1447
				'objects' => array_map(function($c) {
1448
					return $this->transformSearchData($c);
1449
				}, $objects),
1450
				'timezones' => array_map(function($c) {
1451
					return $this->transformSearchData($c);
1452
				}, $timezones),
1453
			];
1454
		}, $calendarObjects);
1455
	}
1456
1457
	/**
1458
	 * @param Component $comp
1459
	 * @return array
1460
	 */
1461
	private function transformSearchData(Component $comp) {
1462
		$data = [];
1463
		/** @var Component[] $subComponents */
1464
		$subComponents = $comp->getComponents();
1465
		/** @var Property[] $properties */
1466
		$properties = array_filter($comp->children(), function($c) {
1467
			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...
1468
		});
1469
		$validationRules = $comp->getValidationRules();
1470
1471
		foreach($subComponents as $subComponent) {
1472
			$name = $subComponent->name;
1473
			if (!isset($data[$name])) {
1474
				$data[$name] = [];
1475
			}
1476
			$data[$name][] = $this->transformSearchData($subComponent);
1477
		}
1478
1479
		foreach($properties as $property) {
1480
			$name = $property->name;
1481
			if (!isset($validationRules[$name])) {
1482
				$validationRules[$name] = '*';
1483
			}
1484
1485
			$rule = $validationRules[$property->name];
1486
			if ($rule === '+' || $rule === '*') { // multiple
1487
				if (!isset($data[$name])) {
1488
					$data[$name] = [];
1489
				}
1490
1491
				$data[$name][] = $this->transformSearchProperty($property);
1492
			} else { // once
1493
				$data[$name] = $this->transformSearchProperty($property);
1494
			}
1495
		}
1496
1497
		return $data;
1498
	}
1499
1500
	/**
1501
	 * @param Property $prop
1502
	 * @return array
1503
	 */
1504
	private function transformSearchProperty(Property $prop) {
1505
		// No need to check Date, as it extends DateTime
1506
		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...
1507
			$value = $prop->getDateTime();
1508
		} else {
1509
			$value = $prop->getValue();
1510
		}
1511
1512
		return [
1513
			$value,
1514
			$prop->parameters()
1515
		];
1516
	}
1517
1518
	/**
1519
	 * Searches through all of a users calendars and calendar objects to find
1520
	 * an object with a specific UID.
1521
	 *
1522
	 * This method should return the path to this object, relative to the
1523
	 * calendar home, so this path usually only contains two parts:
1524
	 *
1525
	 * calendarpath/objectpath.ics
1526
	 *
1527
	 * If the uid is not found, return null.
1528
	 *
1529
	 * This method should only consider * objects that the principal owns, so
1530
	 * any calendars owned by other principals that also appear in this
1531
	 * collection should be ignored.
1532
	 *
1533
	 * @param string $principalUri
1534
	 * @param string $uid
1535
	 * @return string|null
1536
	 */
1537
	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...
1538
1539
		$query = $this->db->getQueryBuilder();
1540
		$query->selectAlias('c.uri', 'calendaruri')->selectAlias('co.uri', 'objecturi')
1541
			->from('calendarobjects', 'co')
1542
			->leftJoin('co', 'calendars', 'c', $query->expr()->eq('co.calendarid', 'c.id'))
1543
			->where($query->expr()->eq('c.principaluri', $query->createNamedParameter($principalUri)))
1544
			->andWhere($query->expr()->eq('co.uid', $query->createNamedParameter($uid)));
1545
1546
		$stmt = $query->execute();
1547
1548
		if ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
1549
			return $row['calendaruri'] . '/' . $row['objecturi'];
1550
		}
1551
1552
		return null;
1553
	}
1554
1555
	/**
1556
	 * The getChanges method returns all the changes that have happened, since
1557
	 * the specified syncToken in the specified calendar.
1558
	 *
1559
	 * This function should return an array, such as the following:
1560
	 *
1561
	 * [
1562
	 *   'syncToken' => 'The current synctoken',
1563
	 *   'added'   => [
1564
	 *      'new.txt',
1565
	 *   ],
1566
	 *   'modified'   => [
1567
	 *      'modified.txt',
1568
	 *   ],
1569
	 *   'deleted' => [
1570
	 *      'foo.php.bak',
1571
	 *      'old.txt'
1572
	 *   ]
1573
	 * );
1574
	 *
1575
	 * The returned syncToken property should reflect the *current* syncToken
1576
	 * of the calendar, as reported in the {http://sabredav.org/ns}sync-token
1577
	 * property This is * needed here too, to ensure the operation is atomic.
1578
	 *
1579
	 * If the $syncToken argument is specified as null, this is an initial
1580
	 * sync, and all members should be reported.
1581
	 *
1582
	 * The modified property is an array of nodenames that have changed since
1583
	 * the last token.
1584
	 *
1585
	 * The deleted property is an array with nodenames, that have been deleted
1586
	 * from collection.
1587
	 *
1588
	 * The $syncLevel argument is basically the 'depth' of the report. If it's
1589
	 * 1, you only have to report changes that happened only directly in
1590
	 * immediate descendants. If it's 2, it should also include changes from
1591
	 * the nodes below the child collections. (grandchildren)
1592
	 *
1593
	 * The $limit argument allows a client to specify how many results should
1594
	 * be returned at most. If the limit is not specified, it should be treated
1595
	 * as infinite.
1596
	 *
1597
	 * If the limit (infinite or not) is higher than you're willing to return,
1598
	 * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
1599
	 *
1600
	 * If the syncToken is expired (due to data cleanup) or unknown, you must
1601
	 * return null.
1602
	 *
1603
	 * The limit is 'suggestive'. You are free to ignore it.
1604
	 *
1605
	 * @param string $calendarId
1606
	 * @param string $syncToken
1607
	 * @param int $syncLevel
1608
	 * @param int $limit
1609
	 * @return array
1610
	 */
1611 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...
1612
		// Current synctoken
1613
		$stmt = $this->db->prepare('SELECT `synctoken` FROM `*PREFIX*calendars` WHERE `id` = ?');
1614
		$stmt->execute([ $calendarId ]);
1615
		$currentToken = $stmt->fetchColumn(0);
1616
1617
		if (is_null($currentToken)) {
1618
			return null;
1619
		}
1620
1621
		$result = [
1622
			'syncToken' => $currentToken,
1623
			'added'     => [],
1624
			'modified'  => [],
1625
			'deleted'   => [],
1626
		];
1627
1628
		if ($syncToken) {
1629
1630
			$query = "SELECT `uri`, `operation` FROM `*PREFIX*calendarchanges` WHERE `synctoken` >= ? AND `synctoken` < ? AND `calendarid` = ? ORDER BY `synctoken`";
1631
			if ($limit>0) {
1632
				$query.= " LIMIT " . (int)$limit;
1633
			}
1634
1635
			// Fetching all changes
1636
			$stmt = $this->db->prepare($query);
1637
			$stmt->execute([$syncToken, $currentToken, $calendarId]);
1638
1639
			$changes = [];
1640
1641
			// This loop ensures that any duplicates are overwritten, only the
1642
			// last change on a node is relevant.
1643
			while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
1644
1645
				$changes[$row['uri']] = $row['operation'];
1646
1647
			}
1648
1649
			foreach($changes as $uri => $operation) {
1650
1651
				switch($operation) {
1652
					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...
1653
						$result['added'][] = $uri;
1654
						break;
1655
					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...
1656
						$result['modified'][] = $uri;
1657
						break;
1658
					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...
1659
						$result['deleted'][] = $uri;
1660
						break;
1661
				}
1662
1663
			}
1664
		} else {
1665
			// No synctoken supplied, this is the initial sync.
1666
			$query = "SELECT `uri` FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ?";
1667
			$stmt = $this->db->prepare($query);
1668
			$stmt->execute([$calendarId]);
1669
1670
			$result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
1671
		}
1672
		return $result;
1673
1674
	}
1675
1676
	/**
1677
	 * Returns a list of subscriptions for a principal.
1678
	 *
1679
	 * Every subscription is an array with the following keys:
1680
	 *  * id, a unique id that will be used by other functions to modify the
1681
	 *    subscription. This can be the same as the uri or a database key.
1682
	 *  * uri. This is just the 'base uri' or 'filename' of the subscription.
1683
	 *  * principaluri. The owner of the subscription. Almost always the same as
1684
	 *    principalUri passed to this method.
1685
	 *
1686
	 * Furthermore, all the subscription info must be returned too:
1687
	 *
1688
	 * 1. {DAV:}displayname
1689
	 * 2. {http://apple.com/ns/ical/}refreshrate
1690
	 * 3. {http://calendarserver.org/ns/}subscribed-strip-todos (omit if todos
1691
	 *    should not be stripped).
1692
	 * 4. {http://calendarserver.org/ns/}subscribed-strip-alarms (omit if alarms
1693
	 *    should not be stripped).
1694
	 * 5. {http://calendarserver.org/ns/}subscribed-strip-attachments (omit if
1695
	 *    attachments should not be stripped).
1696
	 * 6. {http://calendarserver.org/ns/}source (Must be a
1697
	 *     Sabre\DAV\Property\Href).
1698
	 * 7. {http://apple.com/ns/ical/}calendar-color
1699
	 * 8. {http://apple.com/ns/ical/}calendar-order
1700
	 * 9. {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
1701
	 *    (should just be an instance of
1702
	 *    Sabre\CalDAV\Property\SupportedCalendarComponentSet, with a bunch of
1703
	 *    default components).
1704
	 *
1705
	 * @param string $principalUri
1706
	 * @return array
1707
	 */
1708
	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...
1709
		$fields = array_values($this->subscriptionPropertyMap);
1710
		$fields[] = 'id';
1711
		$fields[] = 'uri';
1712
		$fields[] = 'source';
1713
		$fields[] = 'principaluri';
1714
		$fields[] = 'lastmodified';
1715
1716
		$query = $this->db->getQueryBuilder();
1717
		$query->select($fields)
1718
			->from('calendarsubscriptions')
1719
			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
1720
			->orderBy('calendarorder', 'asc');
1721
		$stmt =$query->execute();
1722
1723
		$subscriptions = [];
1724
		while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
1725
1726
			$subscription = [
1727
				'id'           => $row['id'],
1728
				'uri'          => $row['uri'],
1729
				'principaluri' => $row['principaluri'],
1730
				'source'       => $row['source'],
1731
				'lastmodified' => $row['lastmodified'],
1732
1733
				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
1734
			];
1735
1736
			foreach($this->subscriptionPropertyMap as $xmlName=>$dbName) {
1737
				if (!is_null($row[$dbName])) {
1738
					$subscription[$xmlName] = $row[$dbName];
1739
				}
1740
			}
1741
1742
			$subscriptions[] = $subscription;
1743
1744
		}
1745
1746
		return $subscriptions;
1747
	}
1748
1749
	/**
1750
	 * Creates a new subscription for a principal.
1751
	 *
1752
	 * If the creation was a success, an id must be returned that can be used to reference
1753
	 * this subscription in other methods, such as updateSubscription.
1754
	 *
1755
	 * @param string $principalUri
1756
	 * @param string $uri
1757
	 * @param array $properties
1758
	 * @return mixed
1759
	 */
1760
	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...
1761
1762
		if (!isset($properties['{http://calendarserver.org/ns/}source'])) {
1763
			throw new Forbidden('The {http://calendarserver.org/ns/}source property is required when creating subscriptions');
1764
		}
1765
1766
		$values = [
1767
			'principaluri' => $principalUri,
1768
			'uri'          => $uri,
1769
			'source'       => $properties['{http://calendarserver.org/ns/}source']->getHref(),
1770
			'lastmodified' => time(),
1771
		];
1772
1773
		$propertiesBoolean = ['striptodos', 'stripalarms', 'stripattachments'];
1774
1775
		foreach($this->subscriptionPropertyMap as $xmlName=>$dbName) {
1776
			if (array_key_exists($xmlName, $properties)) {
1777
					$values[$dbName] = $properties[$xmlName];
1778
					if (in_array($dbName, $propertiesBoolean)) {
1779
						$values[$dbName] = true;
1780
				}
1781
			}
1782
		}
1783
1784
		$valuesToInsert = array();
1785
1786
		$query = $this->db->getQueryBuilder();
1787
1788
		foreach (array_keys($values) as $name) {
1789
			$valuesToInsert[$name] = $query->createNamedParameter($values[$name]);
1790
		}
1791
1792
		$query->insert('calendarsubscriptions')
1793
			->values($valuesToInsert)
1794
			->execute();
1795
1796
		return $this->db->lastInsertId('*PREFIX*calendarsubscriptions');
1797
	}
1798
1799
	/**
1800
	 * Updates a subscription
1801
	 *
1802
	 * The list of mutations is stored in a Sabre\DAV\PropPatch object.
1803
	 * To do the actual updates, you must tell this object which properties
1804
	 * you're going to process with the handle() method.
1805
	 *
1806
	 * Calling the handle method is like telling the PropPatch object "I
1807
	 * promise I can handle updating this property".
1808
	 *
1809
	 * Read the PropPatch documentation for more info and examples.
1810
	 *
1811
	 * @param mixed $subscriptionId
1812
	 * @param PropPatch $propPatch
1813
	 * @return void
1814
	 */
1815
	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...
1816
		$supportedProperties = array_keys($this->subscriptionPropertyMap);
1817
		$supportedProperties[] = '{http://calendarserver.org/ns/}source';
1818
1819
		/**
1820
		 * @suppress SqlInjectionChecker
1821
		 */
1822
		$propPatch->handle($supportedProperties, function($mutations) use ($subscriptionId) {
1823
1824
			$newValues = [];
1825
1826
			foreach($mutations as $propertyName=>$propertyValue) {
1827
				if ($propertyName === '{http://calendarserver.org/ns/}source') {
1828
					$newValues['source'] = $propertyValue->getHref();
1829
				} else {
1830
					$fieldName = $this->subscriptionPropertyMap[$propertyName];
1831
					$newValues[$fieldName] = $propertyValue;
1832
				}
1833
			}
1834
1835
			$query = $this->db->getQueryBuilder();
1836
			$query->update('calendarsubscriptions')
1837
				->set('lastmodified', $query->createNamedParameter(time()));
1838
			foreach($newValues as $fieldName=>$value) {
1839
				$query->set($fieldName, $query->createNamedParameter($value));
1840
			}
1841
			$query->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
1842
				->execute();
1843
1844
			return true;
1845
1846
		});
1847
	}
1848
1849
	/**
1850
	 * Deletes a subscription.
1851
	 *
1852
	 * @param mixed $subscriptionId
1853
	 * @return void
1854
	 */
1855
	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...
1856
		$query = $this->db->getQueryBuilder();
1857
		$query->delete('calendarsubscriptions')
1858
			->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
1859
			->execute();
1860
	}
1861
1862
	/**
1863
	 * Returns a single scheduling object for the inbox collection.
1864
	 *
1865
	 * The returned array should contain the following elements:
1866
	 *   * uri - A unique basename for the object. This will be used to
1867
	 *           construct a full uri.
1868
	 *   * calendardata - The iCalendar object
1869
	 *   * lastmodified - The last modification date. Can be an int for a unix
1870
	 *                    timestamp, or a PHP DateTime object.
1871
	 *   * etag - A unique token that must change if the object changed.
1872
	 *   * size - The size of the object, in bytes.
1873
	 *
1874
	 * @param string $principalUri
1875
	 * @param string $objectUri
1876
	 * @return array
1877
	 */
1878
	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...
1879
		$query = $this->db->getQueryBuilder();
1880
		$stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size'])
1881
			->from('schedulingobjects')
1882
			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
1883
			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
1884
			->execute();
1885
1886
		$row = $stmt->fetch(\PDO::FETCH_ASSOC);
1887
1888
		if(!$row) {
1889
			return null;
1890
		}
1891
1892
		return [
1893
				'uri'          => $row['uri'],
1894
				'calendardata' => $row['calendardata'],
1895
				'lastmodified' => $row['lastmodified'],
1896
				'etag'         => '"' . $row['etag'] . '"',
1897
				'size'         => (int)$row['size'],
1898
		];
1899
	}
1900
1901
	/**
1902
	 * Returns all scheduling objects for the inbox collection.
1903
	 *
1904
	 * These objects should be returned as an array. Every item in the array
1905
	 * should follow the same structure as returned from getSchedulingObject.
1906
	 *
1907
	 * The main difference is that 'calendardata' is optional.
1908
	 *
1909
	 * @param string $principalUri
1910
	 * @return array
1911
	 */
1912
	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...
1913
		$query = $this->db->getQueryBuilder();
1914
		$stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size'])
1915
				->from('schedulingobjects')
1916
				->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
1917
				->execute();
1918
1919
		$result = [];
1920
		foreach($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) {
1921
			$result[] = [
1922
					'calendardata' => $row['calendardata'],
1923
					'uri'          => $row['uri'],
1924
					'lastmodified' => $row['lastmodified'],
1925
					'etag'         => '"' . $row['etag'] . '"',
1926
					'size'         => (int)$row['size'],
1927
			];
1928
		}
1929
1930
		return $result;
1931
	}
1932
1933
	/**
1934
	 * Deletes a scheduling object from the inbox collection.
1935
	 *
1936
	 * @param string $principalUri
1937
	 * @param string $objectUri
1938
	 * @return void
1939
	 */
1940
	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...
1941
		$query = $this->db->getQueryBuilder();
1942
		$query->delete('schedulingobjects')
1943
				->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
1944
				->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
1945
				->execute();
1946
	}
1947
1948
	/**
1949
	 * Creates a new scheduling object. This should land in a users' inbox.
1950
	 *
1951
	 * @param string $principalUri
1952
	 * @param string $objectUri
1953
	 * @param string $objectData
1954
	 * @return void
1955
	 */
1956
	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...
1957
		$query = $this->db->getQueryBuilder();
1958
		$query->insert('schedulingobjects')
1959
			->values([
1960
				'principaluri' => $query->createNamedParameter($principalUri),
1961
				'calendardata' => $query->createNamedParameter($objectData),
1962
				'uri' => $query->createNamedParameter($objectUri),
1963
				'lastmodified' => $query->createNamedParameter(time()),
1964
				'etag' => $query->createNamedParameter(md5($objectData)),
1965
				'size' => $query->createNamedParameter(strlen($objectData))
1966
			])
1967
			->execute();
1968
	}
1969
1970
	/**
1971
	 * Adds a change record to the calendarchanges table.
1972
	 *
1973
	 * @param mixed $calendarId
1974
	 * @param string $objectUri
1975
	 * @param int $operation 1 = add, 2 = modify, 3 = delete.
1976
	 * @return void
1977
	 */
1978 View Code Duplication
	protected function addChange($calendarId, $objectUri, $operation) {
1979
1980
		$stmt = $this->db->prepare('INSERT INTO `*PREFIX*calendarchanges` (`uri`, `synctoken`, `calendarid`, `operation`) SELECT ?, `synctoken`, ?, ? FROM `*PREFIX*calendars` WHERE `id` = ?');
1981
		$stmt->execute([
1982
			$objectUri,
1983
			$calendarId,
1984
			$operation,
1985
			$calendarId
1986
		]);
1987
		$stmt = $this->db->prepare('UPDATE `*PREFIX*calendars` SET `synctoken` = `synctoken` + 1 WHERE `id` = ?');
1988
		$stmt->execute([
1989
			$calendarId
1990
		]);
1991
1992
	}
1993
1994
	/**
1995
	 * Parses some information from calendar objects, used for optimized
1996
	 * calendar-queries.
1997
	 *
1998
	 * Returns an array with the following keys:
1999
	 *   * etag - An md5 checksum of the object without the quotes.
2000
	 *   * size - Size of the object in bytes
2001
	 *   * componentType - VEVENT, VTODO or VJOURNAL
2002
	 *   * firstOccurence
2003
	 *   * lastOccurence
2004
	 *   * uid - value of the UID property
2005
	 *
2006
	 * @param string $calendarData
2007
	 * @return array
2008
	 */
2009
	public function getDenormalizedData($calendarData) {
2010
2011
		$vObject = Reader::read($calendarData);
2012
		$componentType = null;
2013
		$component = null;
2014
		$firstOccurrence = null;
2015
		$lastOccurrence = null;
2016
		$uid = null;
2017
		$classification = self::CLASSIFICATION_PUBLIC;
2018
		foreach($vObject->getComponents() as $component) {
2019
			if ($component->name!=='VTIMEZONE') {
2020
				$componentType = $component->name;
2021
				$uid = (string)$component->UID;
2022
				break;
2023
			}
2024
		}
2025
		if (!$componentType) {
2026
			throw new \Sabre\DAV\Exception\BadRequest('Calendar objects must have a VJOURNAL, VEVENT or VTODO component');
2027
		}
2028
		if ($componentType === 'VEVENT' && $component->DTSTART) {
2029
			$firstOccurrence = $component->DTSTART->getDateTime()->getTimeStamp();
2030
			// Finding the last occurrence is a bit harder
2031 View Code Duplication
			if (!isset($component->RRULE)) {
2032
				if (isset($component->DTEND)) {
2033
					$lastOccurrence = $component->DTEND->getDateTime()->getTimeStamp();
2034
				} elseif (isset($component->DURATION)) {
2035
					$endDate = clone $component->DTSTART->getDateTime();
2036
					$endDate->add(DateTimeParser::parse($component->DURATION->getValue()));
2037
					$lastOccurrence = $endDate->getTimeStamp();
2038
				} elseif (!$component->DTSTART->hasTime()) {
2039
					$endDate = clone $component->DTSTART->getDateTime();
2040
					$endDate->modify('+1 day');
2041
					$lastOccurrence = $endDate->getTimeStamp();
2042
				} else {
2043
					$lastOccurrence = $firstOccurrence;
2044
				}
2045
			} else {
2046
				$it = new EventIterator($vObject, (string)$component->UID);
2047
				$maxDate = new \DateTime(self::MAX_DATE);
2048
				if ($it->isInfinite()) {
2049
					$lastOccurrence = $maxDate->getTimestamp();
2050
				} else {
2051
					$end = $it->getDtEnd();
2052
					while($it->valid() && $end < $maxDate) {
2053
						$end = $it->getDtEnd();
2054
						$it->next();
2055
2056
					}
2057
					$lastOccurrence = $end->getTimestamp();
2058
				}
2059
2060
			}
2061
		}
2062
2063
		if ($component->CLASS) {
2064
			$classification = CalDavBackend::CLASSIFICATION_PRIVATE;
2065
			switch ($component->CLASS->getValue()) {
2066
				case 'PUBLIC':
2067
					$classification = CalDavBackend::CLASSIFICATION_PUBLIC;
2068
					break;
2069
				case 'CONFIDENTIAL':
2070
					$classification = CalDavBackend::CLASSIFICATION_CONFIDENTIAL;
2071
					break;
2072
			}
2073
		}
2074
		return [
2075
			'etag' => md5($calendarData),
2076
			'size' => strlen($calendarData),
2077
			'componentType' => $componentType,
2078
			'firstOccurence' => is_null($firstOccurrence) ? null : max(0, $firstOccurrence),
2079
			'lastOccurence'  => $lastOccurrence,
2080
			'uid' => $uid,
2081
			'classification' => $classification
2082
		];
2083
2084
	}
2085
2086
	private function readBlob($cardData) {
2087
		if (is_resource($cardData)) {
2088
			return stream_get_contents($cardData);
2089
		}
2090
2091
		return $cardData;
2092
	}
2093
2094
	/**
2095
	 * @param IShareable $shareable
2096
	 * @param array $add
2097
	 * @param array $remove
2098
	 */
2099
	public function updateShares($shareable, $add, $remove) {
2100
		$calendarId = $shareable->getResourceId();
2101
		$this->dispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateShares', new GenericEvent(
2102
			'\OCA\DAV\CalDAV\CalDavBackend::updateShares',
2103
			[
2104
				'calendarId' => $calendarId,
2105
				'calendarData' => $this->getCalendarById($calendarId),
2106
				'shares' => $this->getShares($calendarId),
2107
				'add' => $add,
2108
				'remove' => $remove,
2109
			]));
2110
		$this->sharingBackend->updateShares($shareable, $add, $remove);
2111
	}
2112
2113
	/**
2114
	 * @param int $resourceId
2115
	 * @return array
2116
	 */
2117
	public function getShares($resourceId) {
2118
		return $this->sharingBackend->getShares($resourceId);
2119
	}
2120
2121
	/**
2122
	 * @param boolean $value
2123
	 * @param \OCA\DAV\CalDAV\Calendar $calendar
2124
	 * @return string|null
2125
	 */
2126
	public function setPublishStatus($value, $calendar) {
2127
		$query = $this->db->getQueryBuilder();
2128
		if ($value) {
2129
			$publicUri = $this->random->generate(16, ISecureRandom::CHAR_HUMAN_READABLE);
2130
			$query->insert('dav_shares')
2131
				->values([
2132
					'principaluri' => $query->createNamedParameter($calendar->getPrincipalURI()),
2133
					'type' => $query->createNamedParameter('calendar'),
2134
					'access' => $query->createNamedParameter(self::ACCESS_PUBLIC),
2135
					'resourceid' => $query->createNamedParameter($calendar->getResourceId()),
2136
					'publicuri' => $query->createNamedParameter($publicUri)
2137
				]);
2138
			$query->execute();
2139
			return $publicUri;
2140
		}
2141
		$query->delete('dav_shares')
2142
			->where($query->expr()->eq('resourceid', $query->createNamedParameter($calendar->getResourceId())))
2143
			->andWhere($query->expr()->eq('access', $query->createNamedParameter(self::ACCESS_PUBLIC)));
2144
		$query->execute();
2145
		return null;
2146
	}
2147
2148
	/**
2149
	 * @param \OCA\DAV\CalDAV\Calendar $calendar
2150
	 * @return mixed
2151
	 */
2152 View Code Duplication
	public function getPublishStatus($calendar) {
2153
		$query = $this->db->getQueryBuilder();
2154
		$result = $query->select('publicuri')
2155
			->from('dav_shares')
2156
			->where($query->expr()->eq('resourceid', $query->createNamedParameter($calendar->getResourceId())))
2157
			->andWhere($query->expr()->eq('access', $query->createNamedParameter(self::ACCESS_PUBLIC)))
2158
			->execute();
2159
2160
		$row = $result->fetch();
2161
		$result->closeCursor();
2162
		return $row ? reset($row) : false;
2163
	}
2164
2165
	/**
2166
	 * @param int $resourceId
2167
	 * @param array $acl
2168
	 * @return array
2169
	 */
2170
	public function applyShareAcl($resourceId, $acl) {
2171
		return $this->sharingBackend->applyShareAcl($resourceId, $acl);
2172
	}
2173
2174
2175
2176
	/**
2177
	 * update properties table
2178
	 *
2179
	 * @param int $calendarId
2180
	 * @param string $objectUri
2181
	 * @param string $calendarData
2182
	 */
2183
	public function updateProperties($calendarId, $objectUri, $calendarData) {
2184
		$objectId = $this->getCalendarObjectId($calendarId, $objectUri);
2185
2186
		try {
2187
			$vCalendar = $this->readCalendarData($calendarData);
2188
		} catch (\Exception $ex) {
2189
			return;
2190
		}
2191
2192
		$this->purgeProperties($calendarId, $objectId);
2193
2194
		$query = $this->db->getQueryBuilder();
2195
		$query->insert($this->dbObjectPropertiesTable)
2196
			->values(
2197
				[
2198
					'calendarid' => $query->createNamedParameter($calendarId),
2199
					'objectid' => $query->createNamedParameter($objectId),
2200
					'name' => $query->createParameter('name'),
2201
					'parameter' => $query->createParameter('parameter'),
2202
					'value' => $query->createParameter('value'),
2203
				]
2204
			);
2205
2206
		$indexComponents = ['VEVENT', 'VJOURNAL', 'VTODO'];
2207
		foreach ($vCalendar->getComponents() as $component) {
2208
			if (!in_array($component->name, $indexComponents)) {
2209
				continue;
2210
			}
2211
2212
			foreach ($component->children() as $property) {
2213
				if (in_array($property->name, self::$indexProperties)) {
2214
					$value = $property->getValue();
2215
					// is this a shitty db?
2216
					if (!$this->db->supports4ByteText()) {
2217
						$value = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $value);
2218
					}
2219
					$value = substr($value, 0, 254);
2220
2221
					$query->setParameter('name', $property->name);
2222
					$query->setParameter('parameter', null);
2223
					$query->setParameter('value', $value);
2224
					$query->execute();
2225
				}
2226
2227
				if (in_array($property->name, array_keys(self::$indexParameters))) {
2228
					$parameters = $property->parameters();
2229
					$indexedParametersForProperty = self::$indexParameters[$property->name];
2230
2231
					foreach ($parameters as $key => $value) {
2232
						if (in_array($key, $indexedParametersForProperty)) {
2233
							// is this a shitty db?
2234
							if ($this->db->supports4ByteText()) {
2235
								$value = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $value);
2236
							}
2237
							$value = substr($value, 0, 254);
2238
2239
							$query->setParameter('name', $property->name);
2240
							$query->setParameter('parameter', substr($key, 0, 254));
2241
							$query->setParameter('value', substr($value, 0, 254));
2242
							$query->execute();
2243
						}
2244
					}
2245
				}
2246
			}
2247
		}
2248
	}
2249
2250
	/**
2251
	 * read VCalendar data into a VCalendar object
2252
	 *
2253
	 * @param string $objectData
2254
	 * @return VCalendar
2255
	 */
2256
	protected function readCalendarData($objectData) {
2257
		return Reader::read($objectData);
2258
	}
2259
2260
	/**
2261
	 * delete all properties from a given calendar object
2262
	 *
2263
	 * @param int $calendarId
2264
	 * @param int $objectId
2265
	 */
2266 View Code Duplication
	protected function purgeProperties($calendarId, $objectId) {
2267
		$query = $this->db->getQueryBuilder();
2268
		$query->delete($this->dbObjectPropertiesTable)
2269
			->where($query->expr()->eq('objectid', $query->createNamedParameter($objectId)))
2270
			->andWhere($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)));
2271
		$query->execute();
2272
	}
2273
2274
	/**
2275
	 * get ID from a given calendar object
2276
	 *
2277
	 * @param int $calendarId
2278
	 * @param string $uri
2279
	 * @return int
2280
	 */
2281 View Code Duplication
	protected function getCalendarObjectId($calendarId, $uri) {
2282
		$query = $this->db->getQueryBuilder();
2283
		$query->select('id')->from('calendarobjects')
2284
			->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
2285
			->andWhere($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)));
2286
2287
		$result = $query->execute();
2288
		$objectIds = $result->fetch();
2289
		$result->closeCursor();
2290
2291
		if (!isset($objectIds['id'])) {
2292
			throw new \InvalidArgumentException('Calendarobject does not exists: ' . $uri);
2293
		}
2294
2295
		return (int)$objectIds['id'];
2296
	}
2297
2298 View Code Duplication
	private function convertPrincipal($principalUri, $toV2) {
2299
		if ($this->principalBackend->getPrincipalPrefix() === 'principals') {
2300
			list(, $name) = Uri\split($principalUri);
2301
			if ($toV2 === true) {
2302
				return "principals/users/$name";
2303
			}
2304
			return "principals/$name";
2305
		}
2306
		return $principalUri;
2307
	}
2308
2309 View Code Duplication
	private function addOwnerPrincipal(&$calendarInfo) {
2310
		$ownerPrincipalKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal';
2311
		$displaynameKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}owner-displayname';
2312
		if (isset($calendarInfo[$ownerPrincipalKey])) {
2313
			$uri = $calendarInfo[$ownerPrincipalKey];
2314
		} else {
2315
			$uri = $calendarInfo['principaluri'];
2316
		}
2317
2318
		$principalInformation = $this->principalBackend->getPrincipalByPath($uri);
2319
		if (isset($principalInformation['{DAV:}displayname'])) {
2320
			$calendarInfo[$displaynameKey] = $principalInformation['{DAV:}displayname'];
2321
		}
2322
	}
2323
}
2324