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