Completed
Push — master ( 830834...005b3d )
by Thomas
10:38
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 Sabre\CalDAV\Backend\AbstractBackend;
34
use Sabre\CalDAV\Backend\SchedulingSupport;
35
use Sabre\CalDAV\Backend\SubscriptionSupport;
36
use Sabre\CalDAV\Backend\SyncSupport;
37
use Sabre\CalDAV\Plugin;
38
use Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp;
39
use Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet;
40
use Sabre\DAV;
41
use Sabre\DAV\Exception\Forbidden;
42
use Sabre\DAV\Exception\NotFound;
43
use Sabre\DAV\PropPatch;
44
use Sabre\HTTP\URLUtil;
45
use Sabre\VObject\DateTimeParser;
46
use Sabre\VObject\Reader;
47
use Sabre\VObject\Recur\EventIterator;
48
49
/**
50
 * Class CalDavBackend
51
 *
52
 * Code is heavily inspired by https://github.com/fruux/sabre-dav/blob/master/lib/CalDAV/Backend/PDO.php
53
 *
54
 * @package OCA\DAV\CalDAV
55
 */
56
class CalDavBackend extends AbstractBackend implements SyncSupport, SubscriptionSupport, SchedulingSupport {
57
58
	/**
59
	 * We need to specify a max date, because we need to stop *somewhere*
60
	 *
61
	 * On 32 bit system the maximum for a signed integer is 2147483647, so
62
	 * MAX_DATE cannot be higher than date('Y-m-d', 2147483647) which results
63
	 * in 2038-01-19 to avoid problems when the date is converted
64
	 * to a unix timestamp.
65
	 */
66
	const MAX_DATE = '2038-01-01';
67
68
	const ACCESS_PUBLIC = 4;
69
	const CLASSIFICATION_PUBLIC = 0;
70
	const CLASSIFICATION_PRIVATE = 1;
71
	const CLASSIFICATION_CONFIDENTIAL = 2;
72
73
	/**
74
	 * List of CalDAV properties, and how they map to database field names
75
	 * Add your own properties by simply adding on to this array.
76
	 *
77
	 * Note that only string-based properties are supported here.
78
	 *
79
	 * @var array
80
	 */
81
	public $propertyMap = [
82
		'{DAV:}displayname'                          => 'displayname',
83
		'{urn:ietf:params:xml:ns:caldav}calendar-description' => 'description',
84
		'{urn:ietf:params:xml:ns:caldav}calendar-timezone'    => 'timezone',
85
		'{http://apple.com/ns/ical/}calendar-order'  => 'calendarorder',
86
		'{http://apple.com/ns/ical/}calendar-color'  => 'calendarcolor',
87
	];
88
89
	/**
90
	 * List of subscription properties, and how they map to database field names.
91
	 *
92
	 * @var array
93
	 */
94
	public $subscriptionPropertyMap = [
95
		'{DAV:}displayname'                                           => 'displayname',
96
		'{http://apple.com/ns/ical/}refreshrate'                      => 'refreshrate',
97
		'{http://apple.com/ns/ical/}calendar-order'                   => 'calendarorder',
98
		'{http://apple.com/ns/ical/}calendar-color'                   => 'calendarcolor',
99
		'{http://calendarserver.org/ns/}subscribed-strip-todos'       => 'striptodos',
100
		'{http://calendarserver.org/ns/}subscribed-strip-alarms'      => 'stripalarms',
101
		'{http://calendarserver.org/ns/}subscribed-strip-attachments' => 'stripattachments',
102
	];
103
104
	/** @var IDBConnection */
105
	private $db;
106
107
	/** @var Backend */
108
	private $sharingBackend;
109
110
	/** @var Principal */
111
	private $principalBackend;
112
113
	/** @var IConfig */
114
	private $config;
115
116
	/**
117
	 * CalDavBackend constructor.
118
	 *
119
	 * @param IDBConnection $db
120
	 * @param Principal $principalBackend
121
	 * @param IConfig $config
122
	 */
123 View Code Duplication
	public function __construct(IDBConnection $db, Principal $principalBackend, IConfig $config) {
124
		$this->db = $db;
125
		$this->principalBackend = $principalBackend;
126
		$this->sharingBackend = new Backend($this->db, $principalBackend, 'calendar');
127
		$this->config = $config;
128
	}
129
130
	/**
131
	 * Returns a list of calendars for a principal.
132
	 *
133
	 * Every project is an array with the following keys:
134
	 *  * id, a unique id that will be used by other functions to modify the
135
	 *    calendar. This can be the same as the uri or a database key.
136
	 *  * uri, which the basename of the uri with which the calendar is
137
	 *    accessed.
138
	 *  * principaluri. The owner of the calendar. Almost always the same as
139
	 *    principalUri passed to this method.
140
	 *
141
	 * Furthermore it can contain webdav properties in clark notation. A very
142
	 * common one is '{DAV:}displayname'.
143
	 *
144
	 * Many clients also require:
145
	 * {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
146
	 * For this property, you can just return an instance of
147
	 * Sabre\CalDAV\Property\SupportedCalendarComponentSet.
148
	 *
149
	 * If you return {http://sabredav.org/ns}read-only and set the value to 1,
150
	 * ACL will automatically be put in read-only mode.
151
	 *
152
	 * @param string $principalUri
153
	 * @return array
154
	 */
155
	function getCalendarsForUser($principalUri) {
156
		$principalUriOriginal = $principalUri;
157
		$principalUri = $this->convertPrincipal($principalUri, true);
158
		$fields = array_values($this->propertyMap);
159
		$fields[] = 'id';
160
		$fields[] = 'uri';
161
		$fields[] = 'synctoken';
162
		$fields[] = 'components';
163
		$fields[] = 'principaluri';
164
		$fields[] = 'transparent';
165
166
		// Making fields a comma-delimited list
167
		$query = $this->db->getQueryBuilder();
168
		$query->select($fields)->from('calendars')
169
				->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
170
				->orderBy('calendarorder', 'ASC');
171
		$stmt = $query->execute();
172
173
		$calendars = [];
174
		while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
175
176
			$components = [];
177
			if ($row['components']) {
178
				$components = explode(',',$row['components']);
179
			}
180
181
			$calendar = [
182
				'id' => $row['id'],
183
				'uri' => $row['uri'],
184
				'principaluri' => $this->convertPrincipal($row['principaluri'], false),
185
				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
186
				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
187
				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
188
				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
189
			];
190
191
			foreach($this->propertyMap as $xmlName=>$dbName) {
192
				$calendar[$xmlName] = $row[$dbName];
193
			}
194
195
			if (!isset($calendars[$calendar['id']])) {
196
				$calendars[$calendar['id']] = $calendar;
197
			}
198
		}
199
200
		$stmt->closeCursor();
201
202
		// query for shared calendars
203
		$principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true);
204
		$principals[]= $principalUri;
205
206
		$fields = array_values($this->propertyMap);
207
		$fields[] = 'a.id';
208
		$fields[] = 'a.uri';
209
		$fields[] = 'a.synctoken';
210
		$fields[] = 'a.components';
211
		$fields[] = 'a.principaluri';
212
		$fields[] = 'a.transparent';
213
		$fields[] = 's.access';
214
		$query = $this->db->getQueryBuilder();
215
		$result = $query->select($fields)
216
			->from('dav_shares', 's')
217
			->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
218
			->where($query->expr()->in('s.principaluri', $query->createParameter('principaluri')))
219
			->andWhere($query->expr()->eq('s.type', $query->createParameter('type')))
220
			->setParameter('type', 'calendar')
221
			->setParameter('principaluri', $principals, \Doctrine\DBAL\Connection::PARAM_STR_ARRAY)
222
			->execute();
223
224
		while($row = $result->fetch()) {
225
			list(, $name) = URLUtil::splitPath($row['principaluri']);
226
			$uri = $row['uri'] . '_shared_by_' . $name;
227
			$row['displayname'] = $row['displayname'] . "($name)";
228
			$components = [];
229
			if ($row['components']) {
230
				$components = explode(',',$row['components']);
231
			}
232
			$calendar = [
233
				'id' => $row['id'],
234
				'uri' => $uri,
235
				'principaluri' => $principalUri,
236
				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
237
				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
238
				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
239
				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
240
				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $row['principaluri'],
241
				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ,
242
			];
243
244
			foreach($this->propertyMap as $xmlName=>$dbName) {
245
				$calendar[$xmlName] = $row[$dbName];
246
			}
247
248
			if (!isset($calendars[$calendar['id']])) {
249
				$calendars[$calendar['id']] = $calendar;
250
			}
251
		}
252
		$result->closeCursor();
253
254
		return array_values($calendars);
255
	}
256
257
	/**
258
	 * @return array
259
	 */
260
	public function getPublicCalendars() {
261
		$fields = array_values($this->propertyMap);
262
		$fields[] = 'a.id';
263
		$fields[] = 'a.uri';
264
		$fields[] = 'a.synctoken';
265
		$fields[] = 'a.components';
266
		$fields[] = 'a.principaluri';
267
		$fields[] = 'a.transparent';
268
		$fields[] = 's.access';
269
		$fields[] = 's.publicuri';
270
		$calendars = [];
271
		$query = $this->db->getQueryBuilder();
272
		$result = $query->select($fields)
273
			->from('dav_shares', 's')
274
			->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
275
			->where($query->expr()->in('s.access', $query->createNamedParameter(self::ACCESS_PUBLIC)))
276
			->andWhere($query->expr()->eq('s.type', $query->createNamedParameter('calendar')))
277
			->execute();
278
279
		while($row = $result->fetch()) {
280
			list(, $name) = URLUtil::splitPath($row['principaluri']);
281
			$row['displayname'] = $row['displayname'] . "($name)";
282
			$components = [];
283
			if ($row['components']) {
284
				$components = explode(',',$row['components']);
285
			}
286
			$calendar = [
287
				'id' => $row['id'],
288
				'uri' => $row['publicuri'],
289
				'principaluri' => $row['principaluri'],
290
				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
291
				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
292
				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
293
				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
294
				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $row['principaluri'],
295
				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ,
296
				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}public' => (int)$row['access'] === self::ACCESS_PUBLIC,
297
			];
298
299
			foreach($this->propertyMap as $xmlName=>$dbName) {
300
				$calendar[$xmlName] = $row[$dbName];
301
			}
302
303
			if (!isset($calendars[$calendar['id']])) {
304
				$calendars[$calendar['id']] = $calendar;
305
			}
306
		}
307
		$result->closeCursor();
308
309
		return array_values($calendars);
310
	}
311
312
	/**
313
	 * @param string $uri
314
	 * @return array
315
	 * @throws NotFound
316
	 */
317
	public function getPublicCalendar($uri) {
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
		$query = $this->db->getQueryBuilder();
328
		$result = $query->select($fields)
329
			->from('dav_shares', 's')
330
			->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
331
			->where($query->expr()->in('s.access', $query->createNamedParameter(self::ACCESS_PUBLIC)))
332
			->andWhere($query->expr()->eq('s.type', $query->createNamedParameter('calendar')))
333
			->andWhere($query->expr()->eq('s.publicuri', $query->createNamedParameter($uri)))
334
			->execute();
335
336
		$row = $result->fetch(\PDO::FETCH_ASSOC);
337
338
		$result->closeCursor();
339
340
		if ($row === false) {
341
			throw new NotFound('Node with name \'' . $uri . '\' could not be found');
342
		}
343
344
		list(, $name) = URLUtil::splitPath($row['principaluri']);
345
		$row['displayname'] = $row['displayname'] . ' ' . "($name)";
346
		$components = [];
347
		if ($row['components']) {
348
			$components = explode(',',$row['components']);
349
		}
350
		$uri = md5($this->config->getSystemValue('secret', '') . $row['id']);
351
		$calendar = [
352
			'id' => $row['id'],
353
			'uri' => $uri,
354
			'principaluri' => $row['principaluri'],
355
			'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
356
			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
357
			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
358
			'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
359
			'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $row['principaluri'],
360
			'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ,
361
			'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}public' => (int)$row['access'] === self::ACCESS_PUBLIC,
362
		];
363
364
		foreach($this->propertyMap as $xmlName=>$dbName) {
365
			$calendar[$xmlName] = $row[$dbName];
366
		}
367
368
		return $calendar;
369
370
	}
371
372
	/**
373
	 * @param string $principal
374
	 * @param string $uri
375
	 * @return array|null
376
	 */
377
	public function getCalendarByUri($principal, $uri) {
378
		$fields = array_values($this->propertyMap);
379
		$fields[] = 'id';
380
		$fields[] = 'uri';
381
		$fields[] = 'synctoken';
382
		$fields[] = 'components';
383
		$fields[] = 'principaluri';
384
		$fields[] = 'transparent';
385
386
		// Making fields a comma-delimited list
387
		$query = $this->db->getQueryBuilder();
388
		$query->select($fields)->from('calendars')
389
			->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
390
			->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($principal)))
391
			->setMaxResults(1);
392
		$stmt = $query->execute();
393
394
		$row = $stmt->fetch(\PDO::FETCH_ASSOC);
395
		$stmt->closeCursor();
396
		if ($row === false) {
397
			return null;
398
		}
399
400
		$components = [];
401
		if ($row['components']) {
402
			$components = explode(',',$row['components']);
403
		}
404
405
		$calendar = [
406
			'id' => $row['id'],
407
			'uri' => $row['uri'],
408
			'principaluri' => $row['principaluri'],
409
			'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
410
			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
411
			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
412
			'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
413
		];
414
415
		foreach($this->propertyMap as $xmlName=>$dbName) {
416
			$calendar[$xmlName] = $row[$dbName];
417
		}
418
419
		return $calendar;
420
	}
421
422
	public function getCalendarById($calendarId) {
423
		$fields = array_values($this->propertyMap);
424
		$fields[] = 'id';
425
		$fields[] = 'uri';
426
		$fields[] = 'synctoken';
427
		$fields[] = 'components';
428
		$fields[] = 'principaluri';
429
		$fields[] = 'transparent';
430
431
		// Making fields a comma-delimited list
432
		$query = $this->db->getQueryBuilder();
433
		$query->select($fields)->from('calendars')
434
			->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)))
435
			->setMaxResults(1);
436
		$stmt = $query->execute();
437
438
		$row = $stmt->fetch(\PDO::FETCH_ASSOC);
439
		$stmt->closeCursor();
440
		if ($row === false) {
441
			return null;
442
		}
443
444
		$components = [];
445
		if ($row['components']) {
446
			$components = explode(',',$row['components']);
447
		}
448
449
		$calendar = [
450
			'id' => $row['id'],
451
			'uri' => $row['uri'],
452
			'principaluri' => $row['principaluri'],
453
			'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
454
			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
455
			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
456
			'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
457
		];
458
459
		foreach($this->propertyMap as $xmlName=>$dbName) {
460
			$calendar[$xmlName] = $row[$dbName];
461
		}
462
463
		return $calendar;
464
	}
465
466
	/**
467
	 * Creates a new calendar for a principal.
468
	 *
469
	 * If the creation was a success, an id must be returned that can be used to reference
470
	 * this calendar in other methods, such as updateCalendar.
471
	 *
472
	 * @param string $principalUri
473
	 * @param string $calendarUri
474
	 * @param array $properties
475
	 * @return int
476
	 */
477
	function createCalendar($principalUri, $calendarUri, array $properties) {
478
		$values = [
479
			'principaluri' => $principalUri,
480
			'uri'          => $calendarUri,
481
			'synctoken'    => 1,
482
			'transparent'  => 0,
483
			'components'   => 'VEVENT,VTODO',
484
			'displayname'  => $calendarUri
485
		];
486
487
		// Default value
488
		$sccs = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set';
489
		if (isset($properties[$sccs])) {
490
			if (!($properties[$sccs] instanceof SupportedCalendarComponentSet)) {
491
				throw new DAV\Exception('The ' . $sccs . ' property must be of type: \Sabre\CalDAV\Property\SupportedCalendarComponentSet');
492
			}
493
			$values['components'] = implode(',',$properties[$sccs]->getValue());
494
		}
495
		$transp = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp';
496
		if (isset($properties[$transp])) {
497
			$values['transparent'] = $properties[$transp]->getValue()==='transparent';
498
		}
499
500
		foreach($this->propertyMap as $xmlName=>$dbName) {
501
			if (isset($properties[$xmlName])) {
502
				$values[$dbName] = $properties[$xmlName];
503
			}
504
		}
505
506
		$query = $this->db->getQueryBuilder();
507
		$query->insert('calendars');
508
		foreach($values as $column => $value) {
509
			$query->setValue($column, $query->createNamedParameter($value));
510
		}
511
		$query->execute();
512
		return $query->getLastInsertId();
513
	}
514
515
	/**
516
	 * Updates properties for a calendar.
517
	 *
518
	 * The list of mutations is stored in a Sabre\DAV\PropPatch object.
519
	 * To do the actual updates, you must tell this object which properties
520
	 * you're going to process with the handle() method.
521
	 *
522
	 * Calling the handle method is like telling the PropPatch object "I
523
	 * promise I can handle updating this property".
524
	 *
525
	 * Read the PropPatch documentation for more info and examples.
526
	 *
527
	 * @param PropPatch $propPatch
528
	 * @return void
529
	 */
530
	function updateCalendar($calendarId, PropPatch $propPatch) {
531
		$supportedProperties = array_keys($this->propertyMap);
532
		$supportedProperties[] = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp';
533
534
		$propPatch->handle($supportedProperties, function($mutations) use ($calendarId) {
535
			$newValues = [];
536
			foreach ($mutations as $propertyName => $propertyValue) {
537
538
				switch ($propertyName) {
539
					case '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' :
540
						$fieldName = 'transparent';
541
						$newValues[$fieldName] = $propertyValue->getValue() === 'transparent';
542
						break;
543
					default :
544
						$fieldName = $this->propertyMap[$propertyName];
545
						$newValues[$fieldName] = $propertyValue;
546
						break;
547
				}
548
549
			}
550
			$query = $this->db->getQueryBuilder();
551
			$query->update('calendars');
552
			foreach ($newValues as $fieldName => $value) {
553
				$query->set($fieldName, $query->createNamedParameter($value));
554
			}
555
			$query->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)));
556
			$query->execute();
557
558
			$this->addChange($calendarId, "", 2);
559
560
			return true;
561
		});
562
	}
563
564
	/**
565
	 * Delete a calendar and all it's objects
566
	 *
567
	 * @param mixed $calendarId
568
	 * @return void
569
	 */
570
	function deleteCalendar($calendarId) {
571
		$stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ?');
572
		$stmt->execute([$calendarId]);
573
574
		$stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendars` WHERE `id` = ?');
575
		$stmt->execute([$calendarId]);
576
577
		$stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarchanges` WHERE `calendarid` = ?');
578
		$stmt->execute([$calendarId]);
579
580
		$this->sharingBackend->deleteAllShares($calendarId);
581
	}
582
583
	/**
584
	 * Returns all calendar objects within a calendar.
585
	 *
586
	 * Every item contains an array with the following keys:
587
	 *   * calendardata - The iCalendar-compatible calendar data
588
	 *   * uri - a unique key which will be used to construct the uri. This can
589
	 *     be any arbitrary string, but making sure it ends with '.ics' is a
590
	 *     good idea. This is only the basename, or filename, not the full
591
	 *     path.
592
	 *   * lastmodified - a timestamp of the last modification time
593
	 *   * etag - An arbitrary string, surrounded by double-quotes. (e.g.:
594
	 *   '"abcdef"')
595
	 *   * size - The size of the calendar objects, in bytes.
596
	 *   * component - optional, a string containing the type of object, such
597
	 *     as 'vevent' or 'vtodo'. If specified, this will be used to populate
598
	 *     the Content-Type header.
599
	 *
600
	 * Note that the etag is optional, but it's highly encouraged to return for
601
	 * speed reasons.
602
	 *
603
	 * The calendardata is also optional. If it's not returned
604
	 * 'getCalendarObject' will be called later, which *is* expected to return
605
	 * calendardata.
606
	 *
607
	 * If neither etag or size are specified, the calendardata will be
608
	 * used/fetched to determine these numbers. If both are specified the
609
	 * amount of times this is needed is reduced by a great degree.
610
	 *
611
	 * @param mixed $calendarId
612
	 * @return array
613
	 */
614
	function getCalendarObjects($calendarId) {
615
		$query = $this->db->getQueryBuilder();
616
		$query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'componenttype', 'classification'])
617
			->from('calendarobjects')
618
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)));
619
		$stmt = $query->execute();
620
621
		$result = [];
622
		foreach($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) {
623
			$result[] = [
624
					'id'           => $row['id'],
625
					'uri'          => $row['uri'],
626
					'lastmodified' => $row['lastmodified'],
627
					'etag'         => '"' . $row['etag'] . '"',
628
					'calendarid'   => $row['calendarid'],
629
					'size'         => (int)$row['size'],
630
					'component'    => strtolower($row['componenttype']),
631
					'classification'=> (int)$row['classification']
632
			];
633
		}
634
635
		return $result;
636
	}
637
638
	/**
639
	 * Returns information from a single calendar object, based on it's object
640
	 * uri.
641
	 *
642
	 * The object uri is only the basename, or filename and not a full path.
643
	 *
644
	 * The returned array must have the same keys as getCalendarObjects. The
645
	 * 'calendardata' object is required here though, while it's not required
646
	 * for getCalendarObjects.
647
	 *
648
	 * This method must return null if the object did not exist.
649
	 *
650
	 * @param mixed $calendarId
651
	 * @param string $objectUri
652
	 * @return array|null
653
	 */
654
	function getCalendarObject($calendarId, $objectUri) {
655
656
		$query = $this->db->getQueryBuilder();
657
		$query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification'])
658
				->from('calendarobjects')
659
				->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
660
				->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)));
661
		$stmt = $query->execute();
662
		$row = $stmt->fetch(\PDO::FETCH_ASSOC);
663
664
		if(!$row) return null;
665
666
		return [
667
				'id'            => $row['id'],
668
				'uri'           => $row['uri'],
669
				'lastmodified'  => $row['lastmodified'],
670
				'etag'          => '"' . $row['etag'] . '"',
671
				'calendarid'    => $row['calendarid'],
672
				'size'          => (int)$row['size'],
673
				'calendardata'  => $this->readBlob($row['calendardata']),
674
				'component'     => strtolower($row['componenttype']),
675
				'classification'=> (int)$row['classification']
676
		];
677
	}
678
679
	/**
680
	 * Returns a list of calendar objects.
681
	 *
682
	 * This method should work identical to getCalendarObject, but instead
683
	 * return all the calendar objects in the list as an array.
684
	 *
685
	 * If the backend supports this, it may allow for some speed-ups.
686
	 *
687
	 * @param mixed $calendarId
688
	 * @param string[] $uris
689
	 * @return array
690
	 */
691
	function getMultipleCalendarObjects($calendarId, array $uris) {
692
		$query = $this->db->getQueryBuilder();
693
		$query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification'])
694
				->from('calendarobjects')
695
				->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
696
				->andWhere($query->expr()->in('uri', $query->createParameter('uri')))
697
				->setParameter('uri', $uris, IQueryBuilder::PARAM_STR_ARRAY);
698
699
		$stmt = $query->execute();
700
701
		$result = [];
702
		while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
703
704
			$result[] = [
705
					'id'           => $row['id'],
706
					'uri'          => $row['uri'],
707
					'lastmodified' => $row['lastmodified'],
708
					'etag'         => '"' . $row['etag'] . '"',
709
					'calendarid'   => $row['calendarid'],
710
					'size'         => (int)$row['size'],
711
					'calendardata' => $this->readBlob($row['calendardata']),
712
					'component'    => strtolower($row['componenttype']),
713
					'classification' => (int)$row['classification']
714
			];
715
716
		}
717
		return $result;
718
	}
719
720
	/**
721
	 * Creates a new calendar object.
722
	 *
723
	 * The object uri is only the basename, or filename and not a full path.
724
	 *
725
	 * It is possible return an etag from this function, which will be used in
726
	 * the response to this PUT request. Note that the ETag must be surrounded
727
	 * by double-quotes.
728
	 *
729
	 * However, you should only really return this ETag if you don't mangle the
730
	 * calendar-data. If the result of a subsequent GET to this object is not
731
	 * the exact same as this request body, you should omit the ETag.
732
	 *
733
	 * @param mixed $calendarId
734
	 * @param string $objectUri
735
	 * @param string $calendarData
736
	 * @return string
737
	 */
738
	function createCalendarObject($calendarId, $objectUri, $calendarData) {
739
		$extraData = $this->getDenormalizedData($calendarData);
740
741
		$query = $this->db->getQueryBuilder();
742
		$query->insert('calendarobjects')
743
			->values([
744
				'calendarid' => $query->createNamedParameter($calendarId),
745
				'uri' => $query->createNamedParameter($objectUri),
746
				'calendardata' => $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB),
747
				'lastmodified' => $query->createNamedParameter(time()),
748
				'etag' => $query->createNamedParameter($extraData['etag']),
749
				'size' => $query->createNamedParameter($extraData['size']),
750
				'componenttype' => $query->createNamedParameter($extraData['componentType']),
751
				'firstoccurence' => $query->createNamedParameter($extraData['firstOccurence']),
752
				'lastoccurence' => $query->createNamedParameter($extraData['lastOccurence']),
753
				'classification' => $query->createNamedParameter($extraData['classification']),
754
				'uid' => $query->createNamedParameter($extraData['uid']),
755
			])
756
			->execute();
757
758
		$this->addChange($calendarId, $objectUri, 1);
759
760
		return '"' . $extraData['etag'] . '"';
761
	}
762
763
	/**
764
	 * Updates an existing calendarobject, based on it's uri.
765
	 *
766
	 * The object uri is only the basename, or filename and not a full path.
767
	 *
768
	 * It is possible return an etag from this function, which will be used in
769
	 * the response to this PUT request. Note that the ETag must be surrounded
770
	 * by double-quotes.
771
	 *
772
	 * However, you should only really return this ETag if you don't mangle the
773
	 * calendar-data. If the result of a subsequent GET to this object is not
774
	 * the exact same as this request body, you should omit the ETag.
775
	 *
776
	 * @param mixed $calendarId
777
	 * @param string $objectUri
778
	 * @param string $calendarData
779
	 * @return string
780
	 */
781
	function updateCalendarObject($calendarId, $objectUri, $calendarData) {
782
		$extraData = $this->getDenormalizedData($calendarData);
783
784
		$query = $this->db->getQueryBuilder();
785
		$query->update('calendarobjects')
786
				->set('calendardata', $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB))
787
				->set('lastmodified', $query->createNamedParameter(time()))
788
				->set('etag', $query->createNamedParameter($extraData['etag']))
789
				->set('size', $query->createNamedParameter($extraData['size']))
790
				->set('componenttype', $query->createNamedParameter($extraData['componentType']))
791
				->set('firstoccurence', $query->createNamedParameter($extraData['firstOccurence']))
792
				->set('lastoccurence', $query->createNamedParameter($extraData['lastOccurence']))
793
				->set('classification', $query->createNamedParameter($extraData['classification']))
794
				->set('uid', $query->createNamedParameter($extraData['uid']))
795
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
796
			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
797
			->execute();
798
799
		$this->addChange($calendarId, $objectUri, 2);
800
801
		return '"' . $extraData['etag'] . '"';
802
	}
803
804
	/**
805
	 * @param int $calendarObjectId
806
	 * @param int $classification
807
	 */
808
	public function setClassification($calendarObjectId, $classification) {
809
		if (!in_array($classification, [
810
			self::CLASSIFICATION_PUBLIC, self::CLASSIFICATION_PRIVATE, self::CLASSIFICATION_CONFIDENTIAL
811
		])) {
812
			throw new \InvalidArgumentException();
813
		}
814
		$query = $this->db->getQueryBuilder();
815
		$query->update('calendarobjects')
816
			->set('classification', $query->createNamedParameter($classification))
817
			->where($query->expr()->eq('id', $query->createNamedParameter($calendarObjectId)))
818
			->execute();
819
	}
820
821
	/**
822
	 * Deletes an existing calendar object.
823
	 *
824
	 * The object uri is only the basename, or filename and not a full path.
825
	 *
826
	 * @param mixed $calendarId
827
	 * @param string $objectUri
828
	 * @return void
829
	 */
830
	function deleteCalendarObject($calendarId, $objectUri) {
831
		$stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ? AND `uri` = ?');
832
		$stmt->execute([$calendarId, $objectUri]);
833
834
		$this->addChange($calendarId, $objectUri, 3);
835
	}
836
837
	/**
838
	 * Performs a calendar-query on the contents of this calendar.
839
	 *
840
	 * The calendar-query is defined in RFC4791 : CalDAV. Using the
841
	 * calendar-query it is possible for a client to request a specific set of
842
	 * object, based on contents of iCalendar properties, date-ranges and
843
	 * iCalendar component types (VTODO, VEVENT).
844
	 *
845
	 * This method should just return a list of (relative) urls that match this
846
	 * query.
847
	 *
848
	 * The list of filters are specified as an array. The exact array is
849
	 * documented by Sabre\CalDAV\CalendarQueryParser.
850
	 *
851
	 * Note that it is extremely likely that getCalendarObject for every path
852
	 * returned from this method will be called almost immediately after. You
853
	 * may want to anticipate this to speed up these requests.
854
	 *
855
	 * This method provides a default implementation, which parses *all* the
856
	 * iCalendar objects in the specified calendar.
857
	 *
858
	 * This default may well be good enough for personal use, and calendars
859
	 * that aren't very large. But if you anticipate high usage, big calendars
860
	 * or high loads, you are strongly advised to optimize certain paths.
861
	 *
862
	 * The best way to do so is override this method and to optimize
863
	 * specifically for 'common filters'.
864
	 *
865
	 * Requests that are extremely common are:
866
	 *   * requests for just VEVENTS
867
	 *   * requests for just VTODO
868
	 *   * requests with a time-range-filter on either VEVENT or VTODO.
869
	 *
870
	 * ..and combinations of these requests. It may not be worth it to try to
871
	 * handle every possible situation and just rely on the (relatively
872
	 * easy to use) CalendarQueryValidator to handle the rest.
873
	 *
874
	 * Note that especially time-range-filters may be difficult to parse. A
875
	 * time-range filter specified on a VEVENT must for instance also handle
876
	 * recurrence rules correctly.
877
	 * A good example of how to interprete all these filters can also simply
878
	 * be found in Sabre\CalDAV\CalendarQueryFilter. This class is as correct
879
	 * as possible, so it gives you a good idea on what type of stuff you need
880
	 * to think of.
881
	 *
882
	 * @param mixed $calendarId
883
	 * @param array $filters
884
	 * @return array
885
	 */
886
	function calendarQuery($calendarId, array $filters) {
887
		$componentType = null;
888
		$requirePostFilter = true;
889
		$timeRange = null;
890
891
		// if no filters were specified, we don't need to filter after a query
892
		if (!$filters['prop-filters'] && !$filters['comp-filters']) {
893
			$requirePostFilter = false;
894
		}
895
896
		// Figuring out if there's a component filter
897
		if (count($filters['comp-filters']) > 0 && !$filters['comp-filters'][0]['is-not-defined']) {
898
			$componentType = $filters['comp-filters'][0]['name'];
899
900
			// Checking if we need post-filters
901 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...
902
				$requirePostFilter = false;
903
			}
904
			// There was a time-range filter
905
			if ($componentType == 'VEVENT' && isset($filters['comp-filters'][0]['time-range'])) {
906
				$timeRange = $filters['comp-filters'][0]['time-range'];
907
908
				// If start time OR the end time is not specified, we can do a
909
				// 100% accurate mysql query.
910 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...
911
					$requirePostFilter = false;
912
				}
913
			}
914
915
		}
916
		$columns = ['uri'];
917
		if ($requirePostFilter) {
918
			$columns = ['uri', 'calendardata'];
919
		}
920
		$query = $this->db->getQueryBuilder();
921
		$query->select($columns)
922
			->from('calendarobjects')
923
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)));
924
925
		if ($componentType) {
926
			$query->andWhere($query->expr()->eq('componenttype', $query->createNamedParameter($componentType)));
927
		}
928
929
		if ($timeRange && $timeRange['start']) {
930
			$query->andWhere($query->expr()->gt('lastoccurence', $query->createNamedParameter($timeRange['start']->getTimeStamp())));
931
		}
932
		if ($timeRange && $timeRange['end']) {
933
			$query->andWhere($query->expr()->lt('firstoccurence', $query->createNamedParameter($timeRange['end']->getTimeStamp())));
934
		}
935
936
		$stmt = $query->execute();
937
938
		$result = [];
939
		while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
940
			if ($requirePostFilter) {
941
				if (!$this->validateFilterForObject($row, $filters)) {
942
					continue;
943
				}
944
			}
945
			$result[] = $row['uri'];
946
		}
947
948
		return $result;
949
	}
950
951
	/**
952
	 * Searches through all of a users calendars and calendar objects to find
953
	 * an object with a specific UID.
954
	 *
955
	 * This method should return the path to this object, relative to the
956
	 * calendar home, so this path usually only contains two parts:
957
	 *
958
	 * calendarpath/objectpath.ics
959
	 *
960
	 * If the uid is not found, return null.
961
	 *
962
	 * This method should only consider * objects that the principal owns, so
963
	 * any calendars owned by other principals that also appear in this
964
	 * collection should be ignored.
965
	 *
966
	 * @param string $principalUri
967
	 * @param string $uid
968
	 * @return string|null
969
	 */
970
	function getCalendarObjectByUID($principalUri, $uid) {
971
972
		$query = $this->db->getQueryBuilder();
973
		$query->selectAlias('c.uri', 'calendaruri')->selectAlias('co.uri', 'objecturi')
974
			->from('calendarobjects', 'co')
975
			->leftJoin('co', 'calendars', 'c', $query->expr()->eq('co.calendarid', 'c.id'))
976
			->where($query->expr()->eq('c.principaluri', $query->createNamedParameter($principalUri)))
977
			->andWhere($query->expr()->eq('co.uid', $query->createNamedParameter($uid)));
978
979
		$stmt = $query->execute();
980
981
		if ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
982
			return $row['calendaruri'] . '/' . $row['objecturi'];
983
		}
984
985
		return null;
986
	}
987
988
	/**
989
	 * The getChanges method returns all the changes that have happened, since
990
	 * the specified syncToken in the specified calendar.
991
	 *
992
	 * This function should return an array, such as the following:
993
	 *
994
	 * [
995
	 *   'syncToken' => 'The current synctoken',
996
	 *   'added'   => [
997
	 *      'new.txt',
998
	 *   ],
999
	 *   'modified'   => [
1000
	 *      'modified.txt',
1001
	 *   ],
1002
	 *   'deleted' => [
1003
	 *      'foo.php.bak',
1004
	 *      'old.txt'
1005
	 *   ]
1006
	 * );
1007
	 *
1008
	 * The returned syncToken property should reflect the *current* syncToken
1009
	 * of the calendar, as reported in the {http://sabredav.org/ns}sync-token
1010
	 * property This is * needed here too, to ensure the operation is atomic.
1011
	 *
1012
	 * If the $syncToken argument is specified as null, this is an initial
1013
	 * sync, and all members should be reported.
1014
	 *
1015
	 * The modified property is an array of nodenames that have changed since
1016
	 * the last token.
1017
	 *
1018
	 * The deleted property is an array with nodenames, that have been deleted
1019
	 * from collection.
1020
	 *
1021
	 * The $syncLevel argument is basically the 'depth' of the report. If it's
1022
	 * 1, you only have to report changes that happened only directly in
1023
	 * immediate descendants. If it's 2, it should also include changes from
1024
	 * the nodes below the child collections. (grandchildren)
1025
	 *
1026
	 * The $limit argument allows a client to specify how many results should
1027
	 * be returned at most. If the limit is not specified, it should be treated
1028
	 * as infinite.
1029
	 *
1030
	 * If the limit (infinite or not) is higher than you're willing to return,
1031
	 * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
1032
	 *
1033
	 * If the syncToken is expired (due to data cleanup) or unknown, you must
1034
	 * return null.
1035
	 *
1036
	 * The limit is 'suggestive'. You are free to ignore it.
1037
	 *
1038
	 * @param string $calendarId
1039
	 * @param string $syncToken
1040
	 * @param int $syncLevel
1041
	 * @param int $limit
1042
	 * @return array
1043
	 */
1044 View Code Duplication
	function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit = null) {
1045
		// Current synctoken
1046
		$stmt = $this->db->prepare('SELECT `synctoken` FROM `*PREFIX*calendars` WHERE `id` = ?');
1047
		$stmt->execute([ $calendarId ]);
1048
		$currentToken = $stmt->fetchColumn(0);
1049
1050
		if (is_null($currentToken)) {
1051
			return null;
1052
		}
1053
1054
		$result = [
1055
			'syncToken' => $currentToken,
1056
			'added'     => [],
1057
			'modified'  => [],
1058
			'deleted'   => [],
1059
		];
1060
1061
		if ($syncToken) {
1062
1063
			$query = "SELECT `uri`, `operation` FROM `*PREFIX*calendarchanges` WHERE `synctoken` >= ? AND `synctoken` < ? AND `calendarid` = ? ORDER BY `synctoken`";
1064
			if ($limit>0) {
1065
				$query.= " `LIMIT` " . (int)$limit;
1066
			}
1067
1068
			// Fetching all changes
1069
			$stmt = $this->db->prepare($query);
1070
			$stmt->execute([$syncToken, $currentToken, $calendarId]);
1071
1072
			$changes = [];
1073
1074
			// This loop ensures that any duplicates are overwritten, only the
1075
			// last change on a node is relevant.
1076
			while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
1077
1078
				$changes[$row['uri']] = $row['operation'];
1079
1080
			}
1081
1082
			foreach($changes as $uri => $operation) {
1083
1084
				switch($operation) {
1085
					case 1 :
1086
						$result['added'][] = $uri;
1087
						break;
1088
					case 2 :
1089
						$result['modified'][] = $uri;
1090
						break;
1091
					case 3 :
1092
						$result['deleted'][] = $uri;
1093
						break;
1094
				}
1095
1096
			}
1097
		} else {
1098
			// No synctoken supplied, this is the initial sync.
1099
			$query = "SELECT `uri` FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ?";
1100
			$stmt = $this->db->prepare($query);
1101
			$stmt->execute([$calendarId]);
1102
1103
			$result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
1104
		}
1105
		return $result;
1106
1107
	}
1108
1109
	/**
1110
	 * Returns a list of subscriptions for a principal.
1111
	 *
1112
	 * Every subscription is an array with the following keys:
1113
	 *  * id, a unique id that will be used by other functions to modify the
1114
	 *    subscription. This can be the same as the uri or a database key.
1115
	 *  * uri. This is just the 'base uri' or 'filename' of the subscription.
1116
	 *  * principaluri. The owner of the subscription. Almost always the same as
1117
	 *    principalUri passed to this method.
1118
	 *
1119
	 * Furthermore, all the subscription info must be returned too:
1120
	 *
1121
	 * 1. {DAV:}displayname
1122
	 * 2. {http://apple.com/ns/ical/}refreshrate
1123
	 * 3. {http://calendarserver.org/ns/}subscribed-strip-todos (omit if todos
1124
	 *    should not be stripped).
1125
	 * 4. {http://calendarserver.org/ns/}subscribed-strip-alarms (omit if alarms
1126
	 *    should not be stripped).
1127
	 * 5. {http://calendarserver.org/ns/}subscribed-strip-attachments (omit if
1128
	 *    attachments should not be stripped).
1129
	 * 6. {http://calendarserver.org/ns/}source (Must be a
1130
	 *     Sabre\DAV\Property\Href).
1131
	 * 7. {http://apple.com/ns/ical/}calendar-color
1132
	 * 8. {http://apple.com/ns/ical/}calendar-order
1133
	 * 9. {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
1134
	 *    (should just be an instance of
1135
	 *    Sabre\CalDAV\Property\SupportedCalendarComponentSet, with a bunch of
1136
	 *    default components).
1137
	 *
1138
	 * @param string $principalUri
1139
	 * @return array
1140
	 */
1141
	function getSubscriptionsForUser($principalUri) {
1142
		$fields = array_values($this->subscriptionPropertyMap);
1143
		$fields[] = 'id';
1144
		$fields[] = 'uri';
1145
		$fields[] = 'source';
1146
		$fields[] = 'principaluri';
1147
		$fields[] = 'lastmodified';
1148
1149
		$query = $this->db->getQueryBuilder();
1150
		$query->select($fields)
1151
			->from('calendarsubscriptions')
1152
			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
1153
			->orderBy('calendarorder', 'asc');
1154
		$stmt =$query->execute();
1155
1156
		$subscriptions = [];
1157
		while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
1158
1159
			$subscription = [
1160
				'id'           => $row['id'],
1161
				'uri'          => $row['uri'],
1162
				'principaluri' => $row['principaluri'],
1163
				'source'       => $row['source'],
1164
				'lastmodified' => $row['lastmodified'],
1165
1166
				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
1167
			];
1168
1169
			foreach($this->subscriptionPropertyMap as $xmlName=>$dbName) {
1170
				if (!is_null($row[$dbName])) {
1171
					$subscription[$xmlName] = $row[$dbName];
1172
				}
1173
			}
1174
1175
			$subscriptions[] = $subscription;
1176
1177
		}
1178
1179
		return $subscriptions;
1180
	}
1181
1182
	/**
1183
	 * Creates a new subscription for a principal.
1184
	 *
1185
	 * If the creation was a success, an id must be returned that can be used to reference
1186
	 * this subscription in other methods, such as updateSubscription.
1187
	 *
1188
	 * @param string $principalUri
1189
	 * @param string $uri
1190
	 * @param array $properties
1191
	 * @return mixed
1192
	 */
1193
	function createSubscription($principalUri, $uri, array $properties) {
1194
1195
		if (!isset($properties['{http://calendarserver.org/ns/}source'])) {
1196
			throw new Forbidden('The {http://calendarserver.org/ns/}source property is required when creating subscriptions');
1197
		}
1198
1199
		$values = [
1200
			'principaluri' => $principalUri,
1201
			'uri'          => $uri,
1202
			'source'       => $properties['{http://calendarserver.org/ns/}source']->getHref(),
1203
			'lastmodified' => time(),
1204
		];
1205
1206
		$propertiesBoolean = ['striptodos', 'stripalarms', 'stripattachments'];
1207
1208
		foreach($this->subscriptionPropertyMap as $xmlName=>$dbName) {
1209
			if (array_key_exists($xmlName, $properties)) {
1210
					$values[$dbName] = $properties[$xmlName];
1211
					if (in_array($dbName, $propertiesBoolean)) {
1212
						$values[$dbName] = true;
1213
				}
1214
			}
1215
		}
1216
1217
		$valuesToInsert = array();
1218
1219
		$query = $this->db->getQueryBuilder();
1220
1221
		foreach (array_keys($values) as $name) {
1222
			$valuesToInsert[$name] = $query->createNamedParameter($values[$name]);
1223
		}
1224
1225
		$query->insert('calendarsubscriptions')
1226
			->values($valuesToInsert)
1227
			->execute();
1228
1229
		return $this->db->lastInsertId('*PREFIX*calendarsubscriptions');
1230
	}
1231
1232
	/**
1233
	 * Updates a subscription
1234
	 *
1235
	 * The list of mutations is stored in a Sabre\DAV\PropPatch object.
1236
	 * To do the actual updates, you must tell this object which properties
1237
	 * you're going to process with the handle() method.
1238
	 *
1239
	 * Calling the handle method is like telling the PropPatch object "I
1240
	 * promise I can handle updating this property".
1241
	 *
1242
	 * Read the PropPatch documentation for more info and examples.
1243
	 *
1244
	 * @param mixed $subscriptionId
1245
	 * @param PropPatch $propPatch
1246
	 * @return void
1247
	 */
1248
	function updateSubscription($subscriptionId, PropPatch $propPatch) {
1249
		$supportedProperties = array_keys($this->subscriptionPropertyMap);
1250
		$supportedProperties[] = '{http://calendarserver.org/ns/}source';
1251
1252
		$propPatch->handle($supportedProperties, function($mutations) use ($subscriptionId) {
1253
1254
			$newValues = [];
1255
1256
			foreach($mutations as $propertyName=>$propertyValue) {
1257
				if ($propertyName === '{http://calendarserver.org/ns/}source') {
1258
					$newValues['source'] = $propertyValue->getHref();
1259
				} else {
1260
					$fieldName = $this->subscriptionPropertyMap[$propertyName];
1261
					$newValues[$fieldName] = $propertyValue;
1262
				}
1263
			}
1264
1265
			$query = $this->db->getQueryBuilder();
1266
			$query->update('calendarsubscriptions')
1267
				->set('lastmodified', $query->createNamedParameter(time()));
1268
			foreach($newValues as $fieldName=>$value) {
1269
				$query->set($fieldName, $query->createNamedParameter($value));
1270
			}
1271
			$query->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
1272
				->execute();
1273
1274
			return true;
1275
1276
		});
1277
	}
1278
1279
	/**
1280
	 * Deletes a subscription.
1281
	 *
1282
	 * @param mixed $subscriptionId
1283
	 * @return void
1284
	 */
1285
	function deleteSubscription($subscriptionId) {
1286
		$query = $this->db->getQueryBuilder();
1287
		$query->delete('calendarsubscriptions')
1288
			->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
1289
			->execute();
1290
	}
1291
1292
	/**
1293
	 * Returns a single scheduling object for the inbox collection.
1294
	 *
1295
	 * The returned array should contain the following elements:
1296
	 *   * uri - A unique basename for the object. This will be used to
1297
	 *           construct a full uri.
1298
	 *   * calendardata - The iCalendar object
1299
	 *   * lastmodified - The last modification date. Can be an int for a unix
1300
	 *                    timestamp, or a PHP DateTime object.
1301
	 *   * etag - A unique token that must change if the object changed.
1302
	 *   * size - The size of the object, in bytes.
1303
	 *
1304
	 * @param string $principalUri
1305
	 * @param string $objectUri
1306
	 * @return array
1307
	 */
1308
	function getSchedulingObject($principalUri, $objectUri) {
1309
		$query = $this->db->getQueryBuilder();
1310
		$stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size'])
1311
			->from('schedulingobjects')
1312
			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
1313
			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
1314
			->execute();
1315
1316
		$row = $stmt->fetch(\PDO::FETCH_ASSOC);
1317
1318
		if(!$row) {
1319
			return null;
1320
		}
1321
1322
		return [
1323
				'uri'          => $row['uri'],
1324
				'calendardata' => $row['calendardata'],
1325
				'lastmodified' => $row['lastmodified'],
1326
				'etag'         => '"' . $row['etag'] . '"',
1327
				'size'         => (int)$row['size'],
1328
		];
1329
	}
1330
1331
	/**
1332
	 * Returns all scheduling objects for the inbox collection.
1333
	 *
1334
	 * These objects should be returned as an array. Every item in the array
1335
	 * should follow the same structure as returned from getSchedulingObject.
1336
	 *
1337
	 * The main difference is that 'calendardata' is optional.
1338
	 *
1339
	 * @param string $principalUri
1340
	 * @return array
1341
	 */
1342
	function getSchedulingObjects($principalUri) {
1343
		$query = $this->db->getQueryBuilder();
1344
		$stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size'])
1345
				->from('schedulingobjects')
1346
				->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
1347
				->execute();
1348
1349
		$result = [];
1350
		foreach($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) {
1351
			$result[] = [
1352
					'calendardata' => $row['calendardata'],
1353
					'uri'          => $row['uri'],
1354
					'lastmodified' => $row['lastmodified'],
1355
					'etag'         => '"' . $row['etag'] . '"',
1356
					'size'         => (int)$row['size'],
1357
			];
1358
		}
1359
1360
		return $result;
1361
	}
1362
1363
	/**
1364
	 * Deletes a scheduling object from the inbox collection.
1365
	 *
1366
	 * @param string $principalUri
1367
	 * @param string $objectUri
1368
	 * @return void
1369
	 */
1370
	function deleteSchedulingObject($principalUri, $objectUri) {
1371
		$query = $this->db->getQueryBuilder();
1372
		$query->delete('schedulingobjects')
1373
				->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
1374
				->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
1375
				->execute();
1376
	}
1377
1378
	/**
1379
	 * Creates a new scheduling object. This should land in a users' inbox.
1380
	 *
1381
	 * @param string $principalUri
1382
	 * @param string $objectUri
1383
	 * @param string $objectData
1384
	 * @return void
1385
	 */
1386
	function createSchedulingObject($principalUri, $objectUri, $objectData) {
1387
		$query = $this->db->getQueryBuilder();
1388
		$query->insert('schedulingobjects')
1389
			->values([
1390
				'principaluri' => $query->createNamedParameter($principalUri),
1391
				'calendardata' => $query->createNamedParameter($objectData),
1392
				'uri' => $query->createNamedParameter($objectUri),
1393
				'lastmodified' => $query->createNamedParameter(time()),
1394
				'etag' => $query->createNamedParameter(md5($objectData)),
1395
				'size' => $query->createNamedParameter(strlen($objectData))
1396
			])
1397
			->execute();
1398
	}
1399
1400
	/**
1401
	 * Adds a change record to the calendarchanges table.
1402
	 *
1403
	 * @param mixed $calendarId
1404
	 * @param string $objectUri
1405
	 * @param int $operation 1 = add, 2 = modify, 3 = delete.
1406
	 * @return void
1407
	 */
1408 View Code Duplication
	protected function addChange($calendarId, $objectUri, $operation) {
1409
1410
		$stmt = $this->db->prepare('INSERT INTO `*PREFIX*calendarchanges` (`uri`, `synctoken`, `calendarid`, `operation`) SELECT ?, `synctoken`, ?, ? FROM `*PREFIX*calendars` WHERE `id` = ?');
1411
		$stmt->execute([
1412
			$objectUri,
1413
			$calendarId,
1414
			$operation,
1415
			$calendarId
1416
		]);
1417
		$stmt = $this->db->prepare('UPDATE `*PREFIX*calendars` SET `synctoken` = `synctoken` + 1 WHERE `id` = ?');
1418
		$stmt->execute([
1419
			$calendarId
1420
		]);
1421
1422
	}
1423
1424
	/**
1425
	 * Parses some information from calendar objects, used for optimized
1426
	 * calendar-queries.
1427
	 *
1428
	 * Returns an array with the following keys:
1429
	 *   * etag - An md5 checksum of the object without the quotes.
1430
	 *   * size - Size of the object in bytes
1431
	 *   * componentType - VEVENT, VTODO or VJOURNAL
1432
	 *   * firstOccurence
1433
	 *   * lastOccurence
1434
	 *   * uid - value of the UID property
1435
	 *
1436
	 * @param string $calendarData
1437
	 * @return array
1438
	 */
1439
	public function getDenormalizedData($calendarData) {
1440
1441
		$vObject = Reader::read($calendarData);
1442
		$componentType = null;
1443
		$component = null;
1444
		$firstOccurrence = null;
1445
		$lastOccurrence = null;
1446
		$uid = null;
1447
		$classification = self::CLASSIFICATION_PUBLIC;
1448
		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...
1449
			if ($component->name!=='VTIMEZONE') {
1450
				$componentType = $component->name;
1451
				$uid = (string)$component->UID;
1452
				break;
1453
			}
1454
		}
1455
		if (!$componentType) {
1456
			throw new \Sabre\DAV\Exception\BadRequest('Calendar objects must have a VJOURNAL, VEVENT or VTODO component');
1457
		}
1458
		if ($componentType === 'VEVENT' && $component->DTSTART) {
1459
			$firstOccurrence = $component->DTSTART->getDateTime()->getTimeStamp();
1460
			// Finding the last occurrence is a bit harder
1461
			if (!isset($component->RRULE)) {
1462
				if (isset($component->DTEND)) {
1463
					$lastOccurrence = $component->DTEND->getDateTime()->getTimeStamp();
1464
				} elseif (isset($component->DURATION)) {
1465
					$endDate = clone $component->DTSTART->getDateTime();
1466
					$endDate->add(DateTimeParser::parse($component->DURATION->getValue()));
1467
					$lastOccurrence = $endDate->getTimeStamp();
1468
				} elseif (!$component->DTSTART->hasTime()) {
1469
					$endDate = clone $component->DTSTART->getDateTime();
1470
					$endDate->modify('+1 day');
1471
					$lastOccurrence = $endDate->getTimeStamp();
1472
				} else {
1473
					$lastOccurrence = $firstOccurrence;
1474
				}
1475
			} else {
1476
				$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 1441 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...
1477
				$maxDate = new \DateTime(self::MAX_DATE);
1478
				if ($it->isInfinite()) {
1479
					$lastOccurrence = $maxDate->getTimeStamp();
1480
				} else {
1481
					$end = $it->getDtEnd();
1482
					while($it->valid() && $end < $maxDate) {
1483
						$end = $it->getDtEnd();
1484
						$it->next();
1485
1486
					}
1487
					$lastOccurrence = $end->getTimeStamp();
1488
				}
1489
1490
			}
1491
		}
1492
1493
		if ($component->CLASS) {
1494
			$classification = CalDavBackend::CLASSIFICATION_PRIVATE;
1495
			switch ($component->CLASS->getValue()) {
1496
				case 'PUBLIC':
1497
					$classification = CalDavBackend::CLASSIFICATION_PUBLIC;
1498
					break;
1499
				case 'CONFIDENTIAL':
1500
					$classification = CalDavBackend::CLASSIFICATION_CONFIDENTIAL;
1501
					break;
1502
			}
1503
		}
1504
		return [
1505
			'etag' => md5($calendarData),
1506
			'size' => strlen($calendarData),
1507
			'componentType' => $componentType,
1508
			'firstOccurence' => is_null($firstOccurrence) ? null : max(0, $firstOccurrence),
1509
			'lastOccurence'  => $lastOccurrence,
1510
			'uid' => $uid,
1511
			'classification' => $classification
1512
		];
1513
1514
	}
1515
1516
	private function readBlob($cardData) {
1517
		if (is_resource($cardData)) {
1518
			return stream_get_contents($cardData);
1519
		}
1520
1521
		return $cardData;
1522
	}
1523
1524
	/**
1525
	 * @param IShareable $shareable
1526
	 * @param array $add
1527
	 * @param array $remove
1528
	 */
1529
	public function updateShares($shareable, $add, $remove) {
1530
		$this->sharingBackend->updateShares($shareable, $add, $remove);
1531
	}
1532
1533
	/**
1534
	 * @param int $resourceId
1535
	 * @return array
1536
	 */
1537
	public function getShares($resourceId) {
1538
		return $this->sharingBackend->getShares($resourceId);
1539
	}
1540
1541
	/**
1542
	 * @param boolean $value
1543
	 * @param \OCA\DAV\CalDAV\Calendar $calendar
1544
	 */
1545
	public function setPublishStatus($value, $calendar) {
1546
		$query = $this->db->getQueryBuilder();
1547
		if ($value) {
1548
			$query->insert('dav_shares')
1549
				->values([
1550
					'principaluri' => $query->createNamedParameter($calendar->getPrincipalURI()),
1551
					'type' => $query->createNamedParameter('calendar'),
1552
					'access' => $query->createNamedParameter(self::ACCESS_PUBLIC),
1553
					'resourceid' => $query->createNamedParameter($calendar->getResourceId()),
1554
					'publicuri' => $query->createNamedParameter(md5($this->config->getSystemValue('secret', '') . $calendar->getResourceId()))
1555
				]);
1556
		} else {
1557
			$query->delete('dav_shares')
1558
				->where($query->expr()->eq('resourceid', $query->createNamedParameter($calendar->getResourceId())))
1559
				->andWhere($query->expr()->eq('access', $query->createNamedParameter(self::ACCESS_PUBLIC)));
1560
		}
1561
		$query->execute();
1562
	}
1563
1564
	/**
1565
	 * @param \OCA\DAV\CalDAV\Calendar $calendar
1566
	 * @return boolean
1567
	 */
1568
	public function getPublishStatus($calendar) {
1569
		$query = $this->db->getQueryBuilder();
1570
		$result = $query->select($query->createFunction('COUNT(*)'))
1571
			->from('dav_shares')
1572
			->where($query->expr()->eq('resourceid', $query->createNamedParameter($calendar->getResourceId())))
1573
			->andWhere($query->expr()->eq('access', $query->createNamedParameter(self::ACCESS_PUBLIC)))
1574
			->execute();
1575
1576
		$row = $result->fetch();
1577
		$result->closeCursor();
1578
		return reset($row) > 0;
1579
	}
1580
1581
	/**
1582
	 * @param int $resourceId
1583
	 * @param array $acl
1584
	 * @return array
1585
	 */
1586
	public function applyShareAcl($resourceId, $acl) {
1587
		return $this->sharingBackend->applyShareAcl($resourceId, $acl);
1588
	}
1589
1590 View Code Duplication
	private function convertPrincipal($principalUri, $toV2) {
1591
		if ($this->principalBackend->getPrincipalPrefix() === 'principals') {
1592
			list(, $name) = URLUtil::splitPath($principalUri);
1593
			if ($toV2 === true) {
1594
				return "principals/users/$name";
1595
			}
1596
			return "principals/$name";
1597
		}
1598
		return $principalUri;
1599
	}
1600
}
1601