Completed
Push — master ( 46e8fe...37177d )
by Thomas
11:43
created

CalDavBackend::deleteSubscription()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 5
nc 1
nop 1
dl 0
loc 6
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/**
3
 * @author Joas Schilling <[email protected]>
4
 * @author Stefan Weil <[email protected]>
5
 * @author Thomas Citharel <[email protected]>
6
 * @author Thomas Müller <[email protected]>
7
 *
8
 * @copyright Copyright (c) 2016, ownCloud GmbH.
9
 * @license AGPL-3.0
10
 *
11
 * This code is free software: you can redistribute it and/or modify
12
 * it under the terms of the GNU Affero General Public License, version 3,
13
 * as published by the Free Software Foundation.
14
 *
15
 * This program is distributed in the hope that it will be useful,
16
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18
 * GNU Affero General Public License for more details.
19
 *
20
 * You should have received a copy of the GNU Affero General Public License, version 3,
21
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
22
 *
23
 */
24
25
namespace OCA\DAV\CalDAV;
26
27
use OCA\DAV\DAV\Sharing\IShareable;
28
use OCP\DB\QueryBuilder\IQueryBuilder;
29
use OCA\DAV\Connector\Sabre\Principal;
30
use OCA\DAV\DAV\Sharing\Backend;
31
use OCP\IConfig;
32
use OCP\IDBConnection;
33
use OCP\Security\ISecureRandom;
34
use Sabre\CalDAV\Backend\AbstractBackend;
35
use Sabre\CalDAV\Backend\SchedulingSupport;
36
use Sabre\CalDAV\Backend\SubscriptionSupport;
37
use Sabre\CalDAV\Backend\SyncSupport;
38
use Sabre\CalDAV\Plugin;
39
use Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp;
40
use Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet;
41
use Sabre\DAV;
42
use Sabre\DAV\Exception\Forbidden;
43
use Sabre\DAV\Exception\NotFound;
44
use Sabre\DAV\PropPatch;
45
use Sabre\HTTP\URLUtil;
46
use Sabre\VObject\DateTimeParser;
47
use Sabre\VObject\Reader;
48
use Sabre\VObject\Recur\EventIterator;
49
50
/**
51
 * Class CalDavBackend
52
 *
53
 * Code is heavily inspired by https://github.com/fruux/sabre-dav/blob/master/lib/CalDAV/Backend/PDO.php
54
 *
55
 * @package OCA\DAV\CalDAV
56
 */
57
class CalDavBackend extends AbstractBackend implements SyncSupport, SubscriptionSupport, SchedulingSupport {
58
59
	/**
60
	 * We need to specify a max date, because we need to stop *somewhere*
61
	 *
62
	 * On 32 bit system the maximum for a signed integer is 2147483647, so
63
	 * MAX_DATE cannot be higher than date('Y-m-d', 2147483647) which results
64
	 * in 2038-01-19 to avoid problems when the date is converted
65
	 * to a unix timestamp.
66
	 */
67
	const MAX_DATE = '2038-01-01';
68
69
	const ACCESS_PUBLIC = 4;
70
	const CLASSIFICATION_PUBLIC = 0;
71
	const CLASSIFICATION_PRIVATE = 1;
72
	const CLASSIFICATION_CONFIDENTIAL = 2;
73
74
	/**
75
	 * List of CalDAV properties, and how they map to database field names
76
	 * Add your own properties by simply adding on to this array.
77
	 *
78
	 * Note that only string-based properties are supported here.
79
	 *
80
	 * @var array
81
	 */
82
	public $propertyMap = [
83
		'{DAV:}displayname'                          => 'displayname',
84
		'{urn:ietf:params:xml:ns:caldav}calendar-description' => 'description',
85
		'{urn:ietf:params:xml:ns:caldav}calendar-timezone'    => 'timezone',
86
		'{http://apple.com/ns/ical/}calendar-order'  => 'calendarorder',
87
		'{http://apple.com/ns/ical/}calendar-color'  => 'calendarcolor',
88
	];
89
90
	/**
91
	 * List of subscription properties, and how they map to database field names.
92
	 *
93
	 * @var array
94
	 */
95
	public $subscriptionPropertyMap = [
96
		'{DAV:}displayname'                                           => 'displayname',
97
		'{http://apple.com/ns/ical/}refreshrate'                      => 'refreshrate',
98
		'{http://apple.com/ns/ical/}calendar-order'                   => 'calendarorder',
99
		'{http://apple.com/ns/ical/}calendar-color'                   => 'calendarcolor',
100
		'{http://calendarserver.org/ns/}subscribed-strip-todos'       => 'striptodos',
101
		'{http://calendarserver.org/ns/}subscribed-strip-alarms'      => 'stripalarms',
102
		'{http://calendarserver.org/ns/}subscribed-strip-attachments' => 'stripattachments',
103
	];
104
105
	/** @var IDBConnection */
106
	private $db;
107
108
	/** @var Backend */
109
	private $sharingBackend;
110
111
	/** @var Principal */
112
	private $principalBackend;
113
114
	/** @var IConfig */
115
	private $config;
116
117
	/** @var ISecureRandom */
118
	private $random;
119
120
	/**
121
	 * CalDavBackend constructor.
122
	 *
123
	 * @param IDBConnection $db
124
	 * @param Principal $principalBackend
125
	 * @param IConfig $config
126
	 */
127
	public function __construct(IDBConnection $db,
128
								Principal $principalBackend,
129
								IConfig $config,
130
								ISecureRandom $random) {
131
		$this->db = $db;
132
		$this->principalBackend = $principalBackend;
133
		$this->sharingBackend = new Backend($this->db, $principalBackend, 'calendar');
134
		$this->config = $config;
135
		$this->random = $random;
136
	}
137
138
	/**
139
	 * Returns a list of calendars for a principal.
140
	 *
141
	 * Every project is an array with the following keys:
142
	 *  * id, a unique id that will be used by other functions to modify the
143
	 *    calendar. This can be the same as the uri or a database key.
144
	 *  * uri, which the basename of the uri with which the calendar is
145
	 *    accessed.
146
	 *  * principaluri. The owner of the calendar. Almost always the same as
147
	 *    principalUri passed to this method.
148
	 *
149
	 * Furthermore it can contain webdav properties in clark notation. A very
150
	 * common one is '{DAV:}displayname'.
151
	 *
152
	 * Many clients also require:
153
	 * {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
154
	 * For this property, you can just return an instance of
155
	 * Sabre\CalDAV\Property\SupportedCalendarComponentSet.
156
	 *
157
	 * If you return {http://sabredav.org/ns}read-only and set the value to 1,
158
	 * ACL will automatically be put in read-only mode.
159
	 *
160
	 * @param string $principalUri
161
	 * @return array
162
	 */
163
	function getCalendarsForUser($principalUri) {
164
		$principalUriOriginal = $principalUri;
165
		$principalUri = $this->convertPrincipal($principalUri, true);
166
		$fields = array_values($this->propertyMap);
167
		$fields[] = 'id';
168
		$fields[] = 'uri';
169
		$fields[] = 'synctoken';
170
		$fields[] = 'components';
171
		$fields[] = 'principaluri';
172
		$fields[] = 'transparent';
173
174
		// Making fields a comma-delimited list
175
		$query = $this->db->getQueryBuilder();
176
		$query->select($fields)->from('calendars')
177
				->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
178
				->orderBy('calendarorder', 'ASC');
179
		$stmt = $query->execute();
180
181
		$calendars = [];
182 View Code Duplication
		while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
183
184
			$components = [];
185
			if ($row['components']) {
186
				$components = explode(',',$row['components']);
187
			}
188
189
			$calendar = [
190
				'id' => $row['id'],
191
				'uri' => $row['uri'],
192
				'principaluri' => $this->convertPrincipal($row['principaluri'], false),
193
				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
194
				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
195
				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
196
				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
197
			];
198
199
			foreach($this->propertyMap as $xmlName=>$dbName) {
200
				$calendar[$xmlName] = $row[$dbName];
201
			}
202
203
			if (!isset($calendars[$calendar['id']])) {
204
				$calendars[$calendar['id']] = $calendar;
205
			}
206
		}
207
208
		$stmt->closeCursor();
209
210
		// query for shared calendars
211
		$principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true);
212
		$principals[]= $principalUri;
213
214
		$fields = array_values($this->propertyMap);
215
		$fields[] = 'a.id';
216
		$fields[] = 'a.uri';
217
		$fields[] = 'a.synctoken';
218
		$fields[] = 'a.components';
219
		$fields[] = 'a.principaluri';
220
		$fields[] = 'a.transparent';
221
		$fields[] = 's.access';
222
		$query = $this->db->getQueryBuilder();
223
		$result = $query->select($fields)
224
			->from('dav_shares', 's')
225
			->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
226
			->where($query->expr()->in('s.principaluri', $query->createParameter('principaluri')))
227
			->andWhere($query->expr()->eq('s.type', $query->createParameter('type')))
228
			->setParameter('type', 'calendar')
229
			->setParameter('principaluri', $principals, \Doctrine\DBAL\Connection::PARAM_STR_ARRAY)
230
			->execute();
231
232
		while($row = $result->fetch()) {
233
			list(, $name) = URLUtil::splitPath($row['principaluri']);
234
			$uri = $row['uri'] . '_shared_by_' . $name;
235
			$row['displayname'] = $row['displayname'] . "($name)";
236
			$components = [];
237
			if ($row['components']) {
238
				$components = explode(',',$row['components']);
239
			}
240
			$calendar = [
241
				'id' => $row['id'],
242
				'uri' => $uri,
243
				'principaluri' => $principalUri,
244
				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
245
				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
246
				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
247
				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
248
				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $row['principaluri'],
249
				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ,
250
			];
251
252
			foreach($this->propertyMap as $xmlName=>$dbName) {
253
				$calendar[$xmlName] = $row[$dbName];
254
			}
255
256
			if (!isset($calendars[$calendar['id']])) {
257
				$calendars[$calendar['id']] = $calendar;
258
			}
259
		}
260
		$result->closeCursor();
261
262
		return array_values($calendars);
263
	}
264
265
	public function getUsersOwnCalendars($principalUri) {
266
		$principalUri = $this->convertPrincipal($principalUri, true);
267
		$fields = array_values($this->propertyMap);
268
		$fields[] = 'id';
269
		$fields[] = 'uri';
270
		$fields[] = 'synctoken';
271
		$fields[] = 'components';
272
		$fields[] = 'principaluri';
273
		$fields[] = 'transparent';
274
275
		// Making fields a comma-delimited list
276
		$query = $this->db->getQueryBuilder();
277
		$query->select($fields)->from('calendars')
278
			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
279
			->orderBy('calendarorder', 'ASC');
280
		$stmt = $query->execute();
281
282
		$calendars = [];
283 View Code Duplication
		while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
284
285
			$components = [];
286
			if ($row['components']) {
287
				$components = explode(',',$row['components']);
288
			}
289
290
			$calendar = [
291
				'id' => $row['id'],
292
				'uri' => $row['uri'],
293
				'principaluri' => $this->convertPrincipal($row['principaluri'], false),
294
				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
295
				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
296
				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
297
				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
298
			];
299
300
			foreach($this->propertyMap as $xmlName=>$dbName) {
301
				$calendar[$xmlName] = $row[$dbName];
302
			}
303
304
			if (!isset($calendars[$calendar['id']])) {
305
				$calendars[$calendar['id']] = $calendar;
306
			}
307
		}
308
309
		$stmt->closeCursor();
310
311
		return array_values($calendars);
312
	}
313
314
	/**
315
	 * @return array
316
	 */
317
	public function getPublicCalendars() {
318
		$fields = array_values($this->propertyMap);
319
		$fields[] = 'a.id';
320
		$fields[] = 'a.uri';
321
		$fields[] = 'a.synctoken';
322
		$fields[] = 'a.components';
323
		$fields[] = 'a.principaluri';
324
		$fields[] = 'a.transparent';
325
		$fields[] = 's.access';
326
		$fields[] = 's.publicuri';
327
		$calendars = [];
328
		$query = $this->db->getQueryBuilder();
329
		$result = $query->select($fields)
330
			->from('dav_shares', 's')
331
			->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
332
			->where($query->expr()->in('s.access', $query->createNamedParameter(self::ACCESS_PUBLIC)))
333
			->andWhere($query->expr()->eq('s.type', $query->createNamedParameter('calendar')))
334
			->execute();
335
336
		while($row = $result->fetch()) {
337
			list(, $name) = URLUtil::splitPath($row['principaluri']);
338
			$row['displayname'] = $row['displayname'] . "($name)";
339
			$components = [];
340
			if ($row['components']) {
341
				$components = explode(',',$row['components']);
342
			}
343
			$calendar = [
344
				'id' => $row['id'],
345
				'uri' => $row['publicuri'],
346
				'principaluri' => $row['principaluri'],
347
				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
348
				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
349
				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
350
				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
351
				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $row['principaluri'],
352
				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ,
353
				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}public' => (int)$row['access'] === self::ACCESS_PUBLIC,
354
			];
355
356
			foreach($this->propertyMap as $xmlName=>$dbName) {
357
				$calendar[$xmlName] = $row[$dbName];
358
			}
359
360
			if (!isset($calendars[$calendar['id']])) {
361
				$calendars[$calendar['id']] = $calendar;
362
			}
363
		}
364
		$result->closeCursor();
365
366
		return array_values($calendars);
367
	}
368
369
	/**
370
	 * @param string $uri
371
	 * @return array
372
	 * @throws NotFound
373
	 */
374
	public function getPublicCalendar($uri) {
375
		$fields = array_values($this->propertyMap);
376
		$fields[] = 'a.id';
377
		$fields[] = 'a.uri';
378
		$fields[] = 'a.synctoken';
379
		$fields[] = 'a.components';
380
		$fields[] = 'a.principaluri';
381
		$fields[] = 'a.transparent';
382
		$fields[] = 's.access';
383
		$fields[] = 's.publicuri';
384
		$query = $this->db->getQueryBuilder();
385
		$result = $query->select($fields)
386
			->from('dav_shares', 's')
387
			->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
388
			->where($query->expr()->in('s.access', $query->createNamedParameter(self::ACCESS_PUBLIC)))
389
			->andWhere($query->expr()->eq('s.type', $query->createNamedParameter('calendar')))
390
			->andWhere($query->expr()->eq('s.publicuri', $query->createNamedParameter($uri)))
391
			->execute();
392
393
		$row = $result->fetch(\PDO::FETCH_ASSOC);
394
395
		$result->closeCursor();
396
397
		if ($row === false) {
398
			throw new NotFound('Node with name \'' . $uri . '\' could not be found');
399
		}
400
401
		list(, $name) = URLUtil::splitPath($row['principaluri']);
402
		$row['displayname'] = $row['displayname'] . ' ' . "($name)";
403
		$components = [];
404
		if ($row['components']) {
405
			$components = explode(',',$row['components']);
406
		}
407
		$calendar = [
408
			'id' => $row['id'],
409
			'uri' => $row['publicuri'],
410
			'principaluri' => $row['principaluri'],
411
			'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
412
			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
413
			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
414
			'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
415
			'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $row['principaluri'],
416
			'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ,
417
			'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}public' => (int)$row['access'] === self::ACCESS_PUBLIC,
418
		];
419
420
		foreach($this->propertyMap as $xmlName=>$dbName) {
421
			$calendar[$xmlName] = $row[$dbName];
422
		}
423
424
		return $calendar;
425
426
	}
427
428
	/**
429
	 * @param string $principal
430
	 * @param string $uri
431
	 * @return array|null
432
	 */
433
	public function getCalendarByUri($principal, $uri) {
434
		$fields = array_values($this->propertyMap);
435
		$fields[] = 'id';
436
		$fields[] = 'uri';
437
		$fields[] = 'synctoken';
438
		$fields[] = 'components';
439
		$fields[] = 'principaluri';
440
		$fields[] = 'transparent';
441
442
		// Making fields a comma-delimited list
443
		$query = $this->db->getQueryBuilder();
444
		$query->select($fields)->from('calendars')
445
			->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
446
			->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($principal)))
447
			->setMaxResults(1);
448
		$stmt = $query->execute();
449
450
		$row = $stmt->fetch(\PDO::FETCH_ASSOC);
451
		$stmt->closeCursor();
452
		if ($row === false) {
453
			return null;
454
		}
455
456
		$components = [];
457
		if ($row['components']) {
458
			$components = explode(',',$row['components']);
459
		}
460
461
		$calendar = [
462
			'id' => $row['id'],
463
			'uri' => $row['uri'],
464
			'principaluri' => $row['principaluri'],
465
			'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
466
			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
467
			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
468
			'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
469
		];
470
471
		foreach($this->propertyMap as $xmlName=>$dbName) {
472
			$calendar[$xmlName] = $row[$dbName];
473
		}
474
475
		return $calendar;
476
	}
477
478
	public function getCalendarById($calendarId) {
479
		$fields = array_values($this->propertyMap);
480
		$fields[] = 'id';
481
		$fields[] = 'uri';
482
		$fields[] = 'synctoken';
483
		$fields[] = 'components';
484
		$fields[] = 'principaluri';
485
		$fields[] = 'transparent';
486
487
		// Making fields a comma-delimited list
488
		$query = $this->db->getQueryBuilder();
489
		$query->select($fields)->from('calendars')
490
			->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)))
491
			->setMaxResults(1);
492
		$stmt = $query->execute();
493
494
		$row = $stmt->fetch(\PDO::FETCH_ASSOC);
495
		$stmt->closeCursor();
496
		if ($row === false) {
497
			return null;
498
		}
499
500
		$components = [];
501
		if ($row['components']) {
502
			$components = explode(',',$row['components']);
503
		}
504
505
		$calendar = [
506
			'id' => $row['id'],
507
			'uri' => $row['uri'],
508
			'principaluri' => $row['principaluri'],
509
			'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
510
			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
511
			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
512
			'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
513
		];
514
515
		foreach($this->propertyMap as $xmlName=>$dbName) {
516
			$calendar[$xmlName] = $row[$dbName];
517
		}
518
519
		return $calendar;
520
	}
521
522
	/**
523
	 * Creates a new calendar for a principal.
524
	 *
525
	 * If the creation was a success, an id must be returned that can be used to reference
526
	 * this calendar in other methods, such as updateCalendar.
527
	 *
528
	 * @param string $principalUri
529
	 * @param string $calendarUri
530
	 * @param array $properties
531
	 * @return int
532
	 */
533
	function createCalendar($principalUri, $calendarUri, array $properties) {
534
		$values = [
535
			'principaluri' => $principalUri,
536
			'uri'          => $calendarUri,
537
			'synctoken'    => 1,
538
			'transparent'  => 0,
539
			'components'   => 'VEVENT,VTODO',
540
			'displayname'  => $calendarUri
541
		];
542
543
		// Default value
544
		$sccs = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set';
545
		if (isset($properties[$sccs])) {
546
			if (!($properties[$sccs] instanceof SupportedCalendarComponentSet)) {
547
				throw new DAV\Exception('The ' . $sccs . ' property must be of type: \Sabre\CalDAV\Property\SupportedCalendarComponentSet');
548
			}
549
			$values['components'] = implode(',',$properties[$sccs]->getValue());
550
		}
551
		$transp = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp';
552
		if (isset($properties[$transp])) {
553
			$values['transparent'] = $properties[$transp]->getValue()==='transparent';
554
		}
555
556
		foreach($this->propertyMap as $xmlName=>$dbName) {
557
			if (isset($properties[$xmlName])) {
558
				$values[$dbName] = $properties[$xmlName];
559
			}
560
		}
561
562
		$query = $this->db->getQueryBuilder();
563
		$query->insert('calendars');
564
		foreach($values as $column => $value) {
565
			$query->setValue($column, $query->createNamedParameter($value));
566
		}
567
		$query->execute();
568
		return $query->getLastInsertId();
569
	}
570
571
	/**
572
	 * Updates properties for a calendar.
573
	 *
574
	 * The list of mutations is stored in a Sabre\DAV\PropPatch object.
575
	 * To do the actual updates, you must tell this object which properties
576
	 * you're going to process with the handle() method.
577
	 *
578
	 * Calling the handle method is like telling the PropPatch object "I
579
	 * promise I can handle updating this property".
580
	 *
581
	 * Read the PropPatch documentation for more info and examples.
582
	 *
583
	 * @param PropPatch $propPatch
584
	 * @return void
585
	 */
586
	function updateCalendar($calendarId, PropPatch $propPatch) {
587
		$supportedProperties = array_keys($this->propertyMap);
588
		$supportedProperties[] = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp';
589
590
		$propPatch->handle($supportedProperties, function($mutations) use ($calendarId) {
591
			$newValues = [];
592
			foreach ($mutations as $propertyName => $propertyValue) {
593
594
				switch ($propertyName) {
595
					case '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' :
596
						$fieldName = 'transparent';
597
						$newValues[$fieldName] = $propertyValue->getValue() === 'transparent';
598
						break;
599
					default :
600
						$fieldName = $this->propertyMap[$propertyName];
601
						$newValues[$fieldName] = $propertyValue;
602
						break;
603
				}
604
605
			}
606
			$query = $this->db->getQueryBuilder();
607
			$query->update('calendars');
608
			foreach ($newValues as $fieldName => $value) {
609
				$query->set($fieldName, $query->createNamedParameter($value));
610
			}
611
			$query->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)));
612
			$query->execute();
613
614
			$this->addChange($calendarId, "", 2);
615
616
			return true;
617
		});
618
	}
619
620
	/**
621
	 * Delete a calendar and all it's objects
622
	 *
623
	 * @param mixed $calendarId
624
	 * @return void
625
	 */
626
	function deleteCalendar($calendarId) {
627
		$stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ?');
628
		$stmt->execute([$calendarId]);
629
630
		$stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendars` WHERE `id` = ?');
631
		$stmt->execute([$calendarId]);
632
633
		$stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarchanges` WHERE `calendarid` = ?');
634
		$stmt->execute([$calendarId]);
635
636
		$this->sharingBackend->deleteAllShares($calendarId);
637
	}
638
639
	/**
640
	 * Delete all of an user's shares
641
	 *
642
	 * @param string $principaluri
643
	 * @return void
644
	 */
645
	function deleteAllSharesForUser($principaluri) {
646
		$this->sharingBackend->deleteAllSharesByUser($principaluri);
647
	}
648
649
	/**
650
	 * Returns all calendar objects within a calendar.
651
	 *
652
	 * Every item contains an array with the following keys:
653
	 *   * calendardata - The iCalendar-compatible calendar data
654
	 *   * uri - a unique key which will be used to construct the uri. This can
655
	 *     be any arbitrary string, but making sure it ends with '.ics' is a
656
	 *     good idea. This is only the basename, or filename, not the full
657
	 *     path.
658
	 *   * lastmodified - a timestamp of the last modification time
659
	 *   * etag - An arbitrary string, surrounded by double-quotes. (e.g.:
660
	 *   '"abcdef"')
661
	 *   * size - The size of the calendar objects, in bytes.
662
	 *   * component - optional, a string containing the type of object, such
663
	 *     as 'vevent' or 'vtodo'. If specified, this will be used to populate
664
	 *     the Content-Type header.
665
	 *
666
	 * Note that the etag is optional, but it's highly encouraged to return for
667
	 * speed reasons.
668
	 *
669
	 * The calendardata is also optional. If it's not returned
670
	 * 'getCalendarObject' will be called later, which *is* expected to return
671
	 * calendardata.
672
	 *
673
	 * If neither etag or size are specified, the calendardata will be
674
	 * used/fetched to determine these numbers. If both are specified the
675
	 * amount of times this is needed is reduced by a great degree.
676
	 *
677
	 * @param mixed $calendarId
678
	 * @return array
679
	 */
680
	function getCalendarObjects($calendarId) {
681
		$query = $this->db->getQueryBuilder();
682
		$query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'componenttype', 'classification'])
683
			->from('calendarobjects')
684
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)));
685
		$stmt = $query->execute();
686
687
		$result = [];
688
		foreach($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) {
689
			$result[] = [
690
					'id'           => $row['id'],
691
					'uri'          => $row['uri'],
692
					'lastmodified' => $row['lastmodified'],
693
					'etag'         => '"' . $row['etag'] . '"',
694
					'calendarid'   => $row['calendarid'],
695
					'size'         => (int)$row['size'],
696
					'component'    => strtolower($row['componenttype']),
697
					'classification'=> (int)$row['classification']
698
			];
699
		}
700
701
		return $result;
702
	}
703
704
	/**
705
	 * Returns information from a single calendar object, based on it's object
706
	 * uri.
707
	 *
708
	 * The object uri is only the basename, or filename and not a full path.
709
	 *
710
	 * The returned array must have the same keys as getCalendarObjects. The
711
	 * 'calendardata' object is required here though, while it's not required
712
	 * for getCalendarObjects.
713
	 *
714
	 * This method must return null if the object did not exist.
715
	 *
716
	 * @param mixed $calendarId
717
	 * @param string $objectUri
718
	 * @return array|null
719
	 */
720
	function getCalendarObject($calendarId, $objectUri) {
721
722
		$query = $this->db->getQueryBuilder();
723
		$query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification'])
724
				->from('calendarobjects')
725
				->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
726
				->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)));
727
		$stmt = $query->execute();
728
		$row = $stmt->fetch(\PDO::FETCH_ASSOC);
729
730
		if(!$row) return null;
731
732
		return [
733
				'id'            => $row['id'],
734
				'uri'           => $row['uri'],
735
				'lastmodified'  => $row['lastmodified'],
736
				'etag'          => '"' . $row['etag'] . '"',
737
				'calendarid'    => $row['calendarid'],
738
				'size'          => (int)$row['size'],
739
				'calendardata'  => $this->readBlob($row['calendardata']),
740
				'component'     => strtolower($row['componenttype']),
741
				'classification'=> (int)$row['classification']
742
		];
743
	}
744
745
	/**
746
	 * Returns a list of calendar objects.
747
	 *
748
	 * This method should work identical to getCalendarObject, but instead
749
	 * return all the calendar objects in the list as an array.
750
	 *
751
	 * If the backend supports this, it may allow for some speed-ups.
752
	 *
753
	 * @param mixed $calendarId
754
	 * @param string[] $uris
755
	 * @return array
756
	 */
757
	function getMultipleCalendarObjects($calendarId, array $uris) {
758
		$query = $this->db->getQueryBuilder();
759
		$query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification'])
760
				->from('calendarobjects')
761
				->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
762
				->andWhere($query->expr()->in('uri', $query->createParameter('uri')))
763
				->setParameter('uri', $uris, IQueryBuilder::PARAM_STR_ARRAY);
764
765
		$stmt = $query->execute();
766
767
		$result = [];
768
		while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
769
770
			$result[] = [
771
					'id'           => $row['id'],
772
					'uri'          => $row['uri'],
773
					'lastmodified' => $row['lastmodified'],
774
					'etag'         => '"' . $row['etag'] . '"',
775
					'calendarid'   => $row['calendarid'],
776
					'size'         => (int)$row['size'],
777
					'calendardata' => $this->readBlob($row['calendardata']),
778
					'component'    => strtolower($row['componenttype']),
779
					'classification' => (int)$row['classification']
780
			];
781
782
		}
783
		return $result;
784
	}
785
786
	/**
787
	 * Creates a new calendar object.
788
	 *
789
	 * The object uri is only the basename, or filename and not a full path.
790
	 *
791
	 * It is possible return an etag from this function, which will be used in
792
	 * the response to this PUT request. Note that the ETag must be surrounded
793
	 * by double-quotes.
794
	 *
795
	 * However, you should only really return this ETag if you don't mangle the
796
	 * calendar-data. If the result of a subsequent GET to this object is not
797
	 * the exact same as this request body, you should omit the ETag.
798
	 *
799
	 * @param mixed $calendarId
800
	 * @param string $objectUri
801
	 * @param string $calendarData
802
	 * @return string
803
	 */
804
	function createCalendarObject($calendarId, $objectUri, $calendarData) {
805
		$extraData = $this->getDenormalizedData($calendarData);
806
807
		$query = $this->db->getQueryBuilder();
808
		$query->insert('calendarobjects')
809
			->values([
810
				'calendarid' => $query->createNamedParameter($calendarId),
811
				'uri' => $query->createNamedParameter($objectUri),
812
				'calendardata' => $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB),
813
				'lastmodified' => $query->createNamedParameter(time()),
814
				'etag' => $query->createNamedParameter($extraData['etag']),
815
				'size' => $query->createNamedParameter($extraData['size']),
816
				'componenttype' => $query->createNamedParameter($extraData['componentType']),
817
				'firstoccurence' => $query->createNamedParameter($extraData['firstOccurence']),
818
				'lastoccurence' => $query->createNamedParameter($extraData['lastOccurence']),
819
				'classification' => $query->createNamedParameter($extraData['classification']),
820
				'uid' => $query->createNamedParameter($extraData['uid']),
821
			])
822
			->execute();
823
824
		$this->addChange($calendarId, $objectUri, 1);
825
826
		return '"' . $extraData['etag'] . '"';
827
	}
828
829
	/**
830
	 * Updates an existing calendarobject, based on it's uri.
831
	 *
832
	 * The object uri is only the basename, or filename and not a full path.
833
	 *
834
	 * It is possible return an etag from this function, which will be used in
835
	 * the response to this PUT request. Note that the ETag must be surrounded
836
	 * by double-quotes.
837
	 *
838
	 * However, you should only really return this ETag if you don't mangle the
839
	 * calendar-data. If the result of a subsequent GET to this object is not
840
	 * the exact same as this request body, you should omit the ETag.
841
	 *
842
	 * @param mixed $calendarId
843
	 * @param string $objectUri
844
	 * @param string $calendarData
845
	 * @return string
846
	 */
847
	function updateCalendarObject($calendarId, $objectUri, $calendarData) {
848
		$extraData = $this->getDenormalizedData($calendarData);
849
850
		$query = $this->db->getQueryBuilder();
851
		$query->update('calendarobjects')
852
				->set('calendardata', $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB))
853
				->set('lastmodified', $query->createNamedParameter(time()))
854
				->set('etag', $query->createNamedParameter($extraData['etag']))
855
				->set('size', $query->createNamedParameter($extraData['size']))
856
				->set('componenttype', $query->createNamedParameter($extraData['componentType']))
857
				->set('firstoccurence', $query->createNamedParameter($extraData['firstOccurence']))
858
				->set('lastoccurence', $query->createNamedParameter($extraData['lastOccurence']))
859
				->set('classification', $query->createNamedParameter($extraData['classification']))
860
				->set('uid', $query->createNamedParameter($extraData['uid']))
861
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
862
			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
863
			->execute();
864
865
		$this->addChange($calendarId, $objectUri, 2);
866
867
		return '"' . $extraData['etag'] . '"';
868
	}
869
870
	/**
871
	 * @param int $calendarObjectId
872
	 * @param int $classification
873
	 */
874
	public function setClassification($calendarObjectId, $classification) {
875
		if (!in_array($classification, [
876
			self::CLASSIFICATION_PUBLIC, self::CLASSIFICATION_PRIVATE, self::CLASSIFICATION_CONFIDENTIAL
877
		])) {
878
			throw new \InvalidArgumentException();
879
		}
880
		$query = $this->db->getQueryBuilder();
881
		$query->update('calendarobjects')
882
			->set('classification', $query->createNamedParameter($classification))
883
			->where($query->expr()->eq('id', $query->createNamedParameter($calendarObjectId)))
884
			->execute();
885
	}
886
887
	/**
888
	 * Deletes an existing calendar object.
889
	 *
890
	 * The object uri is only the basename, or filename and not a full path.
891
	 *
892
	 * @param mixed $calendarId
893
	 * @param string $objectUri
894
	 * @return void
895
	 */
896
	function deleteCalendarObject($calendarId, $objectUri) {
897
		$stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ? AND `uri` = ?');
898
		$stmt->execute([$calendarId, $objectUri]);
899
900
		$this->addChange($calendarId, $objectUri, 3);
901
	}
902
903
	/**
904
	 * Performs a calendar-query on the contents of this calendar.
905
	 *
906
	 * The calendar-query is defined in RFC4791 : CalDAV. Using the
907
	 * calendar-query it is possible for a client to request a specific set of
908
	 * object, based on contents of iCalendar properties, date-ranges and
909
	 * iCalendar component types (VTODO, VEVENT).
910
	 *
911
	 * This method should just return a list of (relative) urls that match this
912
	 * query.
913
	 *
914
	 * The list of filters are specified as an array. The exact array is
915
	 * documented by Sabre\CalDAV\CalendarQueryParser.
916
	 *
917
	 * Note that it is extremely likely that getCalendarObject for every path
918
	 * returned from this method will be called almost immediately after. You
919
	 * may want to anticipate this to speed up these requests.
920
	 *
921
	 * This method provides a default implementation, which parses *all* the
922
	 * iCalendar objects in the specified calendar.
923
	 *
924
	 * This default may well be good enough for personal use, and calendars
925
	 * that aren't very large. But if you anticipate high usage, big calendars
926
	 * or high loads, you are strongly advised to optimize certain paths.
927
	 *
928
	 * The best way to do so is override this method and to optimize
929
	 * specifically for 'common filters'.
930
	 *
931
	 * Requests that are extremely common are:
932
	 *   * requests for just VEVENTS
933
	 *   * requests for just VTODO
934
	 *   * requests with a time-range-filter on either VEVENT or VTODO.
935
	 *
936
	 * ..and combinations of these requests. It may not be worth it to try to
937
	 * handle every possible situation and just rely on the (relatively
938
	 * easy to use) CalendarQueryValidator to handle the rest.
939
	 *
940
	 * Note that especially time-range-filters may be difficult to parse. A
941
	 * time-range filter specified on a VEVENT must for instance also handle
942
	 * recurrence rules correctly.
943
	 * A good example of how to interprete all these filters can also simply
944
	 * be found in Sabre\CalDAV\CalendarQueryFilter. This class is as correct
945
	 * as possible, so it gives you a good idea on what type of stuff you need
946
	 * to think of.
947
	 *
948
	 * @param mixed $calendarId
949
	 * @param array $filters
950
	 * @return array
951
	 */
952
	function calendarQuery($calendarId, array $filters) {
953
		$componentType = null;
954
		$requirePostFilter = true;
955
		$timeRange = null;
956
957
		// if no filters were specified, we don't need to filter after a query
958
		if (!$filters['prop-filters'] && !$filters['comp-filters']) {
959
			$requirePostFilter = false;
960
		}
961
962
		// Figuring out if there's a component filter
963
		if (count($filters['comp-filters']) > 0 && !$filters['comp-filters'][0]['is-not-defined']) {
964
			$componentType = $filters['comp-filters'][0]['name'];
965
966
			// Checking if we need post-filters
967 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']) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
968
				$requirePostFilter = false;
969
			}
970
			// There was a time-range filter
971
			if ($componentType == 'VEVENT' && isset($filters['comp-filters'][0]['time-range'])) {
972
				$timeRange = $filters['comp-filters'][0]['time-range'];
973
974
				// If start time OR the end time is not specified, we can do a
975
				// 100% accurate mysql query.
976 View Code Duplication
				if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['prop-filters'] && (!$timeRange['start'] || !$timeRange['end'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
977
					$requirePostFilter = false;
978
				}
979
			}
980
981
		}
982
		$columns = ['uri'];
983
		if ($requirePostFilter) {
984
			$columns = ['uri', 'calendardata'];
985
		}
986
		$query = $this->db->getQueryBuilder();
987
		$query->select($columns)
988
			->from('calendarobjects')
989
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)));
990
991
		if ($componentType) {
992
			$query->andWhere($query->expr()->eq('componenttype', $query->createNamedParameter($componentType)));
993
		}
994
995
		if ($timeRange && $timeRange['start']) {
996
			$query->andWhere($query->expr()->gt('lastoccurence', $query->createNamedParameter($timeRange['start']->getTimeStamp())));
997
		}
998
		if ($timeRange && $timeRange['end']) {
999
			$query->andWhere($query->expr()->lt('firstoccurence', $query->createNamedParameter($timeRange['end']->getTimeStamp())));
1000
		}
1001
1002
		$stmt = $query->execute();
1003
1004
		$result = [];
1005
		while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
1006
			if ($requirePostFilter) {
1007
				if (!$this->validateFilterForObject($row, $filters)) {
1008
					continue;
1009
				}
1010
			}
1011
			$result[] = $row['uri'];
1012
		}
1013
1014
		return $result;
1015
	}
1016
1017
	/**
1018
	 * Searches through all of a users calendars and calendar objects to find
1019
	 * an object with a specific UID.
1020
	 *
1021
	 * This method should return the path to this object, relative to the
1022
	 * calendar home, so this path usually only contains two parts:
1023
	 *
1024
	 * calendarpath/objectpath.ics
1025
	 *
1026
	 * If the uid is not found, return null.
1027
	 *
1028
	 * This method should only consider * objects that the principal owns, so
1029
	 * any calendars owned by other principals that also appear in this
1030
	 * collection should be ignored.
1031
	 *
1032
	 * @param string $principalUri
1033
	 * @param string $uid
1034
	 * @return string|null
1035
	 */
1036
	function getCalendarObjectByUID($principalUri, $uid) {
1037
1038
		$query = $this->db->getQueryBuilder();
1039
		$query->selectAlias('c.uri', 'calendaruri')->selectAlias('co.uri', 'objecturi')
1040
			->from('calendarobjects', 'co')
1041
			->leftJoin('co', 'calendars', 'c', $query->expr()->eq('co.calendarid', 'c.id'))
1042
			->where($query->expr()->eq('c.principaluri', $query->createNamedParameter($principalUri)))
1043
			->andWhere($query->expr()->eq('co.uid', $query->createNamedParameter($uid)));
1044
1045
		$stmt = $query->execute();
1046
1047
		if ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
1048
			return $row['calendaruri'] . '/' . $row['objecturi'];
1049
		}
1050
1051
		return null;
1052
	}
1053
1054
	/**
1055
	 * The getChanges method returns all the changes that have happened, since
1056
	 * the specified syncToken in the specified calendar.
1057
	 *
1058
	 * This function should return an array, such as the following:
1059
	 *
1060
	 * [
1061
	 *   'syncToken' => 'The current synctoken',
1062
	 *   'added'   => [
1063
	 *      'new.txt',
1064
	 *   ],
1065
	 *   'modified'   => [
1066
	 *      'modified.txt',
1067
	 *   ],
1068
	 *   'deleted' => [
1069
	 *      'foo.php.bak',
1070
	 *      'old.txt'
1071
	 *   ]
1072
	 * );
1073
	 *
1074
	 * The returned syncToken property should reflect the *current* syncToken
1075
	 * of the calendar, as reported in the {http://sabredav.org/ns}sync-token
1076
	 * property This is * needed here too, to ensure the operation is atomic.
1077
	 *
1078
	 * If the $syncToken argument is specified as null, this is an initial
1079
	 * sync, and all members should be reported.
1080
	 *
1081
	 * The modified property is an array of nodenames that have changed since
1082
	 * the last token.
1083
	 *
1084
	 * The deleted property is an array with nodenames, that have been deleted
1085
	 * from collection.
1086
	 *
1087
	 * The $syncLevel argument is basically the 'depth' of the report. If it's
1088
	 * 1, you only have to report changes that happened only directly in
1089
	 * immediate descendants. If it's 2, it should also include changes from
1090
	 * the nodes below the child collections. (grandchildren)
1091
	 *
1092
	 * The $limit argument allows a client to specify how many results should
1093
	 * be returned at most. If the limit is not specified, it should be treated
1094
	 * as infinite.
1095
	 *
1096
	 * If the limit (infinite or not) is higher than you're willing to return,
1097
	 * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
1098
	 *
1099
	 * If the syncToken is expired (due to data cleanup) or unknown, you must
1100
	 * return null.
1101
	 *
1102
	 * The limit is 'suggestive'. You are free to ignore it.
1103
	 *
1104
	 * @param string $calendarId
1105
	 * @param string $syncToken
1106
	 * @param int $syncLevel
1107
	 * @param int $limit
1108
	 * @return array
1109
	 */
1110 View Code Duplication
	function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit = null) {
1111
		// Current synctoken
1112
		$stmt = $this->db->prepare('SELECT `synctoken` FROM `*PREFIX*calendars` WHERE `id` = ?');
1113
		$stmt->execute([ $calendarId ]);
1114
		$currentToken = $stmt->fetchColumn(0);
1115
1116
		if (is_null($currentToken)) {
1117
			return null;
1118
		}
1119
1120
		$result = [
1121
			'syncToken' => $currentToken,
1122
			'added'     => [],
1123
			'modified'  => [],
1124
			'deleted'   => [],
1125
		];
1126
1127
		if ($syncToken) {
1128
1129
			$query = "SELECT `uri`, `operation` FROM `*PREFIX*calendarchanges` WHERE `synctoken` >= ? AND `synctoken` < ? AND `calendarid` = ? ORDER BY `synctoken`";
1130
			if ($limit>0) {
1131
				$query.= " `LIMIT` " . (int)$limit;
1132
			}
1133
1134
			// Fetching all changes
1135
			$stmt = $this->db->prepare($query);
1136
			$stmt->execute([$syncToken, $currentToken, $calendarId]);
1137
1138
			$changes = [];
1139
1140
			// This loop ensures that any duplicates are overwritten, only the
1141
			// last change on a node is relevant.
1142
			while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
1143
1144
				$changes[$row['uri']] = $row['operation'];
1145
1146
			}
1147
1148
			foreach($changes as $uri => $operation) {
1149
1150
				switch($operation) {
1151
					case 1 :
1152
						$result['added'][] = $uri;
1153
						break;
1154
					case 2 :
1155
						$result['modified'][] = $uri;
1156
						break;
1157
					case 3 :
1158
						$result['deleted'][] = $uri;
1159
						break;
1160
				}
1161
1162
			}
1163
		} else {
1164
			// No synctoken supplied, this is the initial sync.
1165
			$query = "SELECT `uri` FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ?";
1166
			$stmt = $this->db->prepare($query);
1167
			$stmt->execute([$calendarId]);
1168
1169
			$result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
1170
		}
1171
		return $result;
1172
1173
	}
1174
1175
	/**
1176
	 * Returns a list of subscriptions for a principal.
1177
	 *
1178
	 * Every subscription is an array with the following keys:
1179
	 *  * id, a unique id that will be used by other functions to modify the
1180
	 *    subscription. This can be the same as the uri or a database key.
1181
	 *  * uri. This is just the 'base uri' or 'filename' of the subscription.
1182
	 *  * principaluri. The owner of the subscription. Almost always the same as
1183
	 *    principalUri passed to this method.
1184
	 *
1185
	 * Furthermore, all the subscription info must be returned too:
1186
	 *
1187
	 * 1. {DAV:}displayname
1188
	 * 2. {http://apple.com/ns/ical/}refreshrate
1189
	 * 3. {http://calendarserver.org/ns/}subscribed-strip-todos (omit if todos
1190
	 *    should not be stripped).
1191
	 * 4. {http://calendarserver.org/ns/}subscribed-strip-alarms (omit if alarms
1192
	 *    should not be stripped).
1193
	 * 5. {http://calendarserver.org/ns/}subscribed-strip-attachments (omit if
1194
	 *    attachments should not be stripped).
1195
	 * 6. {http://calendarserver.org/ns/}source (Must be a
1196
	 *     Sabre\DAV\Property\Href).
1197
	 * 7. {http://apple.com/ns/ical/}calendar-color
1198
	 * 8. {http://apple.com/ns/ical/}calendar-order
1199
	 * 9. {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
1200
	 *    (should just be an instance of
1201
	 *    Sabre\CalDAV\Property\SupportedCalendarComponentSet, with a bunch of
1202
	 *    default components).
1203
	 *
1204
	 * @param string $principalUri
1205
	 * @return array
1206
	 */
1207
	function getSubscriptionsForUser($principalUri) {
1208
		$fields = array_values($this->subscriptionPropertyMap);
1209
		$fields[] = 'id';
1210
		$fields[] = 'uri';
1211
		$fields[] = 'source';
1212
		$fields[] = 'principaluri';
1213
		$fields[] = 'lastmodified';
1214
1215
		$query = $this->db->getQueryBuilder();
1216
		$query->select($fields)
1217
			->from('calendarsubscriptions')
1218
			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
1219
			->orderBy('calendarorder', 'asc');
1220
		$stmt =$query->execute();
1221
1222
		$subscriptions = [];
1223
		while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
1224
1225
			$subscription = [
1226
				'id'           => $row['id'],
1227
				'uri'          => $row['uri'],
1228
				'principaluri' => $row['principaluri'],
1229
				'source'       => $row['source'],
1230
				'lastmodified' => $row['lastmodified'],
1231
1232
				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
1233
			];
1234
1235
			foreach($this->subscriptionPropertyMap as $xmlName=>$dbName) {
1236
				if (!is_null($row[$dbName])) {
1237
					$subscription[$xmlName] = $row[$dbName];
1238
				}
1239
			}
1240
1241
			$subscriptions[] = $subscription;
1242
1243
		}
1244
1245
		return $subscriptions;
1246
	}
1247
1248
	/**
1249
	 * Creates a new subscription for a principal.
1250
	 *
1251
	 * If the creation was a success, an id must be returned that can be used to reference
1252
	 * this subscription in other methods, such as updateSubscription.
1253
	 *
1254
	 * @param string $principalUri
1255
	 * @param string $uri
1256
	 * @param array $properties
1257
	 * @return mixed
1258
	 */
1259
	function createSubscription($principalUri, $uri, array $properties) {
1260
1261
		if (!isset($properties['{http://calendarserver.org/ns/}source'])) {
1262
			throw new Forbidden('The {http://calendarserver.org/ns/}source property is required when creating subscriptions');
1263
		}
1264
1265
		$values = [
1266
			'principaluri' => $principalUri,
1267
			'uri'          => $uri,
1268
			'source'       => $properties['{http://calendarserver.org/ns/}source']->getHref(),
1269
			'lastmodified' => time(),
1270
		];
1271
1272
		$propertiesBoolean = ['striptodos', 'stripalarms', 'stripattachments'];
1273
1274
		foreach($this->subscriptionPropertyMap as $xmlName=>$dbName) {
1275
			if (array_key_exists($xmlName, $properties)) {
1276
					$values[$dbName] = $properties[$xmlName];
1277
					if (in_array($dbName, $propertiesBoolean)) {
1278
						$values[$dbName] = true;
1279
				}
1280
			}
1281
		}
1282
1283
		$valuesToInsert = [];
1284
1285
		$query = $this->db->getQueryBuilder();
1286
1287
		foreach (array_keys($values) as $name) {
1288
			$valuesToInsert[$name] = $query->createNamedParameter($values[$name]);
1289
		}
1290
1291
		$query->insert('calendarsubscriptions')
1292
			->values($valuesToInsert)
1293
			->execute();
1294
1295
		return $this->db->lastInsertId('*PREFIX*calendarsubscriptions');
1296
	}
1297
1298
	/**
1299
	 * Updates a subscription
1300
	 *
1301
	 * The list of mutations is stored in a Sabre\DAV\PropPatch object.
1302
	 * To do the actual updates, you must tell this object which properties
1303
	 * you're going to process with the handle() method.
1304
	 *
1305
	 * Calling the handle method is like telling the PropPatch object "I
1306
	 * promise I can handle updating this property".
1307
	 *
1308
	 * Read the PropPatch documentation for more info and examples.
1309
	 *
1310
	 * @param mixed $subscriptionId
1311
	 * @param PropPatch $propPatch
1312
	 * @return void
1313
	 */
1314
	function updateSubscription($subscriptionId, PropPatch $propPatch) {
1315
		$supportedProperties = array_keys($this->subscriptionPropertyMap);
1316
		$supportedProperties[] = '{http://calendarserver.org/ns/}source';
1317
1318
		$propPatch->handle($supportedProperties, function($mutations) use ($subscriptionId) {
1319
1320
			$newValues = [];
1321
1322
			foreach($mutations as $propertyName=>$propertyValue) {
1323
				if ($propertyName === '{http://calendarserver.org/ns/}source') {
1324
					$newValues['source'] = $propertyValue->getHref();
1325
				} else {
1326
					$fieldName = $this->subscriptionPropertyMap[$propertyName];
1327
					$newValues[$fieldName] = $propertyValue;
1328
				}
1329
			}
1330
1331
			$query = $this->db->getQueryBuilder();
1332
			$query->update('calendarsubscriptions')
1333
				->set('lastmodified', $query->createNamedParameter(time()));
1334
			foreach($newValues as $fieldName=>$value) {
1335
				$query->set($fieldName, $query->createNamedParameter($value));
1336
			}
1337
			$query->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
1338
				->execute();
1339
1340
			return true;
1341
1342
		});
1343
	}
1344
1345
	/**
1346
	 * Deletes a subscription.
1347
	 *
1348
	 * @param mixed $subscriptionId
1349
	 * @return void
1350
	 */
1351
	function deleteSubscription($subscriptionId) {
1352
		$query = $this->db->getQueryBuilder();
1353
		$query->delete('calendarsubscriptions')
1354
			->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
1355
			->execute();
1356
	}
1357
1358
	/**
1359
	 * Returns a single scheduling object for the inbox collection.
1360
	 *
1361
	 * The returned array should contain the following elements:
1362
	 *   * uri - A unique basename for the object. This will be used to
1363
	 *           construct a full uri.
1364
	 *   * calendardata - The iCalendar object
1365
	 *   * lastmodified - The last modification date. Can be an int for a unix
1366
	 *                    timestamp, or a PHP DateTime object.
1367
	 *   * etag - A unique token that must change if the object changed.
1368
	 *   * size - The size of the object, in bytes.
1369
	 *
1370
	 * @param string $principalUri
1371
	 * @param string $objectUri
1372
	 * @return array
1373
	 */
1374
	function getSchedulingObject($principalUri, $objectUri) {
1375
		$query = $this->db->getQueryBuilder();
1376
		$stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size'])
1377
			->from('schedulingobjects')
1378
			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
1379
			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
1380
			->execute();
1381
1382
		$row = $stmt->fetch(\PDO::FETCH_ASSOC);
1383
1384
		if(!$row) {
1385
			return null;
1386
		}
1387
1388
		return [
1389
				'uri'          => $row['uri'],
1390
				'calendardata' => $row['calendardata'],
1391
				'lastmodified' => $row['lastmodified'],
1392
				'etag'         => '"' . $row['etag'] . '"',
1393
				'size'         => (int)$row['size'],
1394
		];
1395
	}
1396
1397
	/**
1398
	 * Returns all scheduling objects for the inbox collection.
1399
	 *
1400
	 * These objects should be returned as an array. Every item in the array
1401
	 * should follow the same structure as returned from getSchedulingObject.
1402
	 *
1403
	 * The main difference is that 'calendardata' is optional.
1404
	 *
1405
	 * @param string $principalUri
1406
	 * @return array
1407
	 */
1408
	function getSchedulingObjects($principalUri) {
1409
		$query = $this->db->getQueryBuilder();
1410
		$stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size'])
1411
				->from('schedulingobjects')
1412
				->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
1413
				->execute();
1414
1415
		$result = [];
1416
		foreach($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) {
1417
			$result[] = [
1418
					'calendardata' => $row['calendardata'],
1419
					'uri'          => $row['uri'],
1420
					'lastmodified' => $row['lastmodified'],
1421
					'etag'         => '"' . $row['etag'] . '"',
1422
					'size'         => (int)$row['size'],
1423
			];
1424
		}
1425
1426
		return $result;
1427
	}
1428
1429
	/**
1430
	 * Deletes a scheduling object from the inbox collection.
1431
	 *
1432
	 * @param string $principalUri
1433
	 * @param string $objectUri
1434
	 * @return void
1435
	 */
1436
	function deleteSchedulingObject($principalUri, $objectUri) {
1437
		$query = $this->db->getQueryBuilder();
1438
		$query->delete('schedulingobjects')
1439
				->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
1440
				->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
1441
				->execute();
1442
	}
1443
1444
	/**
1445
	 * Creates a new scheduling object. This should land in a users' inbox.
1446
	 *
1447
	 * @param string $principalUri
1448
	 * @param string $objectUri
1449
	 * @param string $objectData
1450
	 * @return void
1451
	 */
1452
	function createSchedulingObject($principalUri, $objectUri, $objectData) {
1453
		$query = $this->db->getQueryBuilder();
1454
		$query->insert('schedulingobjects')
1455
			->values([
1456
				'principaluri' => $query->createNamedParameter($principalUri),
1457
				'calendardata' => $query->createNamedParameter($objectData),
1458
				'uri' => $query->createNamedParameter($objectUri),
1459
				'lastmodified' => $query->createNamedParameter(time()),
1460
				'etag' => $query->createNamedParameter(md5($objectData)),
1461
				'size' => $query->createNamedParameter(strlen($objectData))
1462
			])
1463
			->execute();
1464
	}
1465
1466
	/**
1467
	 * Adds a change record to the calendarchanges table.
1468
	 *
1469
	 * @param mixed $calendarId
1470
	 * @param string $objectUri
1471
	 * @param int $operation 1 = add, 2 = modify, 3 = delete.
1472
	 * @return void
1473
	 */
1474 View Code Duplication
	protected function addChange($calendarId, $objectUri, $operation) {
1475
1476
		$stmt = $this->db->prepare('INSERT INTO `*PREFIX*calendarchanges` (`uri`, `synctoken`, `calendarid`, `operation`) SELECT ?, `synctoken`, ?, ? FROM `*PREFIX*calendars` WHERE `id` = ?');
1477
		$stmt->execute([
1478
			$objectUri,
1479
			$calendarId,
1480
			$operation,
1481
			$calendarId
1482
		]);
1483
		$stmt = $this->db->prepare('UPDATE `*PREFIX*calendars` SET `synctoken` = `synctoken` + 1 WHERE `id` = ?');
1484
		$stmt->execute([
1485
			$calendarId
1486
		]);
1487
1488
	}
1489
1490
	/**
1491
	 * Parses some information from calendar objects, used for optimized
1492
	 * calendar-queries.
1493
	 *
1494
	 * Returns an array with the following keys:
1495
	 *   * etag - An md5 checksum of the object without the quotes.
1496
	 *   * size - Size of the object in bytes
1497
	 *   * componentType - VEVENT, VTODO or VJOURNAL
1498
	 *   * firstOccurence
1499
	 *   * lastOccurence
1500
	 *   * uid - value of the UID property
1501
	 *
1502
	 * @param string $calendarData
1503
	 * @return array
1504
	 */
1505
	public function getDenormalizedData($calendarData) {
1506
1507
		$vObject = Reader::read($calendarData);
1508
		$componentType = null;
1509
		$component = null;
1510
		$firstOccurrence = null;
1511
		$lastOccurrence = null;
1512
		$uid = null;
1513
		$classification = self::CLASSIFICATION_PUBLIC;
1514
		foreach($vObject->getComponents() as $component) {
0 ignored issues
show
Bug introduced by
The method getComponents cannot be called on $vObject (of type array|null).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
1515
			if ($component->name!=='VTIMEZONE') {
1516
				$componentType = $component->name;
1517
				$uid = (string)$component->UID;
1518
				break;
1519
			}
1520
		}
1521
		if (!$componentType) {
1522
			throw new \Sabre\DAV\Exception\BadRequest('Calendar objects must have a VJOURNAL, VEVENT or VTODO component');
1523
		}
1524
		if ($componentType === 'VEVENT' && $component->DTSTART) {
1525
			$firstOccurrence = $component->DTSTART->getDateTime()->getTimeStamp();
1526
			// Finding the last occurrence is a bit harder
1527
			if (!isset($component->RRULE)) {
1528
				if (isset($component->DTEND)) {
1529
					$lastOccurrence = $component->DTEND->getDateTime()->getTimeStamp();
1530
				} elseif (isset($component->DURATION)) {
1531
					$endDate = clone $component->DTSTART->getDateTime();
1532
					$endDate->add(DateTimeParser::parse($component->DURATION->getValue()));
1533
					$lastOccurrence = $endDate->getTimeStamp();
1534
				} elseif (!$component->DTSTART->hasTime()) {
1535
					$endDate = clone $component->DTSTART->getDateTime();
1536
					$endDate->modify('+1 day');
1537
					$lastOccurrence = $endDate->getTimeStamp();
1538
				} else {
1539
					$lastOccurrence = $firstOccurrence;
1540
				}
1541
			} else {
1542
				$it = new EventIterator($vObject, (string)$component->UID);
0 ignored issues
show
Bug introduced by
It seems like $vObject defined by \Sabre\VObject\Reader::read($calendarData) on line 1507 can also be of type null; however, Sabre\VObject\Recur\EventIterator::__construct() does only seem to accept object<Sabre\VObject\Component>|array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1543
				$maxDate = new \DateTime(self::MAX_DATE);
1544
				if ($it->isInfinite()) {
1545
					$lastOccurrence = $maxDate->getTimeStamp();
1546
				} else {
1547
					$end = $it->getDtEnd();
1548
					while($it->valid() && $end < $maxDate) {
1549
						$end = $it->getDtEnd();
1550
						$it->next();
1551
1552
					}
1553
					$lastOccurrence = $end->getTimeStamp();
1554
				}
1555
1556
			}
1557
		}
1558
1559
		if ($component->CLASS) {
1560
			$classification = CalDavBackend::CLASSIFICATION_PRIVATE;
1561
			switch ($component->CLASS->getValue()) {
1562
				case 'PUBLIC':
1563
					$classification = CalDavBackend::CLASSIFICATION_PUBLIC;
1564
					break;
1565
				case 'CONFIDENTIAL':
1566
					$classification = CalDavBackend::CLASSIFICATION_CONFIDENTIAL;
1567
					break;
1568
			}
1569
		}
1570
		return [
1571
			'etag' => md5($calendarData),
1572
			'size' => strlen($calendarData),
1573
			'componentType' => $componentType,
1574
			'firstOccurence' => is_null($firstOccurrence) ? null : max(0, $firstOccurrence),
1575
			'lastOccurence'  => $lastOccurrence,
1576
			'uid' => $uid,
1577
			'classification' => $classification
1578
		];
1579
1580
	}
1581
1582
	private function readBlob($cardData) {
1583
		if (is_resource($cardData)) {
1584
			return stream_get_contents($cardData);
1585
		}
1586
1587
		return $cardData;
1588
	}
1589
1590
	/**
1591
	 * @param IShareable $shareable
1592
	 * @param array $add
1593
	 * @param array $remove
1594
	 */
1595
	public function updateShares($shareable, $add, $remove) {
1596
		$this->sharingBackend->updateShares($shareable, $add, $remove);
1597
	}
1598
1599
	/**
1600
	 * @param int $resourceId
1601
	 * @return array
1602
	 */
1603
	public function getShares($resourceId) {
1604
		return $this->sharingBackend->getShares($resourceId);
1605
	}
1606
1607
	/**
1608
	 * @param boolean $value
1609
	 * @param \OCA\DAV\CalDAV\Calendar $calendar
1610
	 * @return string|null
1611
	 */
1612
	public function setPublishStatus($value, $calendar) {
1613
		$query = $this->db->getQueryBuilder();
1614
		if ($value) {
1615
			$publicUri = $this->random->generate(16, ISecureRandom::CHAR_UPPER.ISecureRandom::CHAR_DIGITS);
1616
			$query->insert('dav_shares')
1617
				->values([
1618
					'principaluri' => $query->createNamedParameter($calendar->getPrincipalURI()),
1619
					'type' => $query->createNamedParameter('calendar'),
1620
					'access' => $query->createNamedParameter(self::ACCESS_PUBLIC),
1621
					'resourceid' => $query->createNamedParameter($calendar->getResourceId()),
1622
					'publicuri' => $query->createNamedParameter($publicUri)
1623
				]);
1624
			$query->execute();
1625
			return $publicUri;
1626
		}
1627
		$query->delete('dav_shares')
1628
			->where($query->expr()->eq('resourceid', $query->createNamedParameter($calendar->getResourceId())))
1629
			->andWhere($query->expr()->eq('access', $query->createNamedParameter(self::ACCESS_PUBLIC)));
1630
		$query->execute();
1631
		return null;
1632
	}
1633
1634
	/**
1635
	 * @param \OCA\DAV\CalDAV\Calendar $calendar
1636
	 * @return mixed
1637
	 */
1638 View Code Duplication
	public function getPublishStatus($calendar) {
1639
		$query = $this->db->getQueryBuilder();
1640
		$result = $query->select('publicuri')
1641
			->from('dav_shares')
1642
			->where($query->expr()->eq('resourceid', $query->createNamedParameter($calendar->getResourceId())))
1643
			->andWhere($query->expr()->eq('access', $query->createNamedParameter(self::ACCESS_PUBLIC)))
1644
			->execute();
1645
1646
		$row = $result->fetch();
1647
		$result->closeCursor();
1648
		return $row ? reset($row) : false;
1649
	}
1650
1651
	/**
1652
	 * @param int $resourceId
1653
	 * @param array $acl
1654
	 * @return array
1655
	 */
1656
	public function applyShareAcl($resourceId, $acl) {
1657
		return $this->sharingBackend->applyShareAcl($resourceId, $acl);
1658
	}
1659
1660 View Code Duplication
	private function convertPrincipal($principalUri, $toV2) {
1661
		if ($this->principalBackend->getPrincipalPrefix() === 'principals') {
1662
			list(, $name) = URLUtil::splitPath($principalUri);
1663
			if ($toV2 === true) {
1664
				return "principals/users/$name";
1665
			}
1666
			return "principals/$name";
1667
		}
1668
		return $principalUri;
1669
	}
1670
}
1671