diff --git a/apps/dav/appinfo/database.xml b/apps/dav/appinfo/database.xml
index f3fd5079949403d87ec5d283b4196c70e20a1b15..5e2dad097e48458d2072cc7cc0b01d507ed23c4c 100644
--- a/apps/dav/appinfo/database.xml
+++ b/apps/dav/appinfo/database.xml
@@ -183,4 +183,391 @@ CREATE TABLE addressbookchanges (
 		</declaration>
 	</table>
 
+
+<!--
+CREATE TABLE calendarobjects (
+    id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
+    calendardata MEDIUMBLOB,
+    uri VARBINARY(200),
+    calendarid INTEGER UNSIGNED NOT NULL,
+    lastmodified INT(11) UNSIGNED,
+    etag VARBINARY(32),
+    size INT(11) UNSIGNED NOT NULL,
+    componenttype VARBINARY(8),
+    firstoccurence INT(11) UNSIGNED,
+    lastoccurence INT(11) UNSIGNED,
+    uid VARBINARY(200),
+    UNIQUE(calendarid, uri)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+-->
+<table>
+	<name>*dbprefix*calendarobjects</name>
+	<declaration>
+		<field>
+			<name>id</name>
+			<type>integer</type>
+			<default>0</default>
+			<notnull>true</notnull>
+			<autoincrement>1</autoincrement>
+			<unsigned>true</unsigned>
+			<length>11</length>
+		</field>
+		<field>
+			<name>calendardata</name>
+			<type>blob</type>
+		</field>
+		<field>
+			<name>uri</name>
+			<type>text</type>
+		</field>
+		<field>
+			<name>calendarid</name>
+			<type>integer</type>
+			<unsigned>true</unsigned>
+			<notnull>true</notnull>
+		</field>
+		<field>
+			<name>lastmodified</name>
+			<type>integer</type>
+			<unsigned>true</unsigned>
+		</field>
+		<field>
+			<name>etag</name>
+			<type>text</type>
+			<length>32</length>
+		</field>
+		<field>
+			<name>size</name>
+			<type>integer</type>
+			<notnull>true</notnull>
+			<unsigned>true</unsigned>
+			<length>11</length>
+		</field>
+		<field>
+			<name>componenttype</name>
+			<type>text</type>
+		</field>
+		<field>
+			<name>firstoccurence</name>
+			<type>integer</type>
+			<unsigned>true</unsigned>
+		</field>
+		<field>
+			<name>lastoccurence</name>
+			<type>integer</type>
+			<unsigned>true</unsigned>
+		</field>
+		<field>
+			<name>uid</name>
+			<type>text</type>
+		</field>
+		<index>
+			<name>calobjects_index</name>
+			<unique>true</unique>
+			<field>
+				<name>calendarid</name>
+			</field>
+			<field>
+				<name>uri</name>
+			</field>
+		</index>
+	</declaration>
+</table>
+	<!--
+	CREATE TABLE calendars (
+		id INTEGER UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
+		principaluri VARBINARY(100),
+		displayname VARCHAR(100),
+		uri VARBINARY(200),
+		synctoken INTEGER UNSIGNED NOT NULL DEFAULT '1',
+		description TEXT,
+		calendarorder INT(11) UNSIGNED NOT NULL DEFAULT '0',
+		calendarcolor VARBINARY(10),
+		timezone TEXT,
+		components VARBINARY(20),
+		transparent TINYINT(1) NOT NULL DEFAULT '0',
+		UNIQUE(principaluri, uri)
+	) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+	-->
+<table>
+	<name>*dbprefix*calendars</name>
+	<declaration>
+		<field>
+			<name>id</name>
+			<type>integer</type>
+			<default>0</default>
+			<notnull>true</notnull>
+			<autoincrement>1</autoincrement>
+			<unsigned>true</unsigned>
+			<length>11</length>
+		</field>
+		<field>
+			<name>principaluri</name>
+			<type>text</type>
+		</field>
+		<field>
+			<name>displayname</name>
+			<type>text</type>
+		</field>
+		<field>
+			<name>uri</name>
+			<type>text</type>
+		</field>
+		<field>
+			<name>synctoken</name>
+			<type>integer</type>
+			<default>1</default>
+			<notnull>true</notnull>
+			<unsigned>true</unsigned>
+		</field>
+		<field>
+			<name>description</name>
+			<type>text</type>
+		</field>
+		<field>
+			<name>calendarorder</name>
+			<type>integer</type>
+			<default>0</default>
+			<notnull>true</notnull>
+			<unsigned>true</unsigned>
+		</field>
+		<field>
+			<name>calendarcolor</name>
+			<type>text</type>
+		</field>
+		<field>
+			<name>timezone</name>
+			<type>text</type>
+		</field>
+		<field>
+			<name>components</name>
+			<type>text</type>
+		</field>
+		<field>
+			<name>transparent</name>
+			<type>integer</type>
+			<length>1</length>
+			<notnull>true</notnull>
+			<default>0</default>
+		</field>
+		<index>
+			<name>calendars_index</name>
+			<unique>true</unique>
+			<field>
+				<name>principaluri</name>
+			</field>
+			<field>
+				<name>uri</name>
+			</field>
+		</index>
+	</declaration>
+</table>
+	<!--
+	CREATE TABLE calendarchanges (
+		id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
+		uri VARBINARY(200) NOT NULL,
+		synctoken INT(11) UNSIGNED NOT NULL,
+		calendarid INT(11) UNSIGNED NOT NULL,
+		operation TINYINT(1) NOT NULL,
+		INDEX calendarid_synctoken (calendarid, synctoken)
+	) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+	-->
+	<table>
+		<name>*dbprefix*calendarchanges</name>
+		<declaration>
+			<field>
+				<name>id</name>
+				<type>integer</type>
+				<default>0</default>
+				<notnull>true</notnull>
+				<autoincrement>1</autoincrement>
+				<unsigned>true</unsigned>
+				<length>11</length>
+			</field>
+			<field>
+				<name>uri</name>
+				<type>text</type>
+			</field>
+			<field>
+				<name>synctoken</name>
+				<type>integer</type>
+				<default>1</default>
+				<notnull>true</notnull>
+				<unsigned>true</unsigned>
+			</field>
+			<field>
+				<name>calendarid</name>
+				<type>integer</type>
+				<notnull>true</notnull>
+			</field>
+			<field>
+				<name>operation</name>
+				<type>integer</type>
+				<notnull>true</notnull>
+				<length>1</length>
+			</field>
+
+			<index>
+				<name>calendarid_synctoken</name>
+				<field>
+					<name>calendarid</name>
+				</field>
+				<field>
+					<name>synctoken</name>
+				</field>
+			</index>
+
+		</declaration>
+	</table>
+
+	<!--
+	CREATE TABLE calendarsubscriptions (
+		id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
+		uri VARBINARY(200) NOT NULL,
+		principaluri VARBINARY(100) NOT NULL,
+		source TEXT,
+		displayname VARCHAR(100),
+		refreshrate VARCHAR(10),
+		calendarorder INT(11) UNSIGNED NOT NULL DEFAULT '0',
+		calendarcolor VARBINARY(10),
+		striptodos TINYINT(1) NULL,
+		stripalarms TINYINT(1) NULL,
+		stripattachments TINYINT(1) NULL,
+		lastmodified INT(11) UNSIGNED,
+		UNIQUE(principaluri, uri)
+	) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+	-->
+<table>
+	<name>*dbprefix*calendarsubscriptions</name>
+	<declaration>
+		<field>
+			<name>id</name>
+			<type>integer</type>
+			<default>0</default>
+			<notnull>true</notnull>
+			<autoincrement>1</autoincrement>
+			<unsigned>true</unsigned>
+			<length>11</length>
+		</field>
+		<field>
+			<name>uri</name>
+			<type>text</type>
+		</field>
+		<field>
+			<name>principaluri</name>
+			<type>text</type>
+		</field>
+		<field>
+			<name>source</name>
+			<type>text</type>
+		</field>
+		<field>
+			<name>displayname</name>
+			<type>text</type>
+			<length>100</length>
+		</field>
+		<field>
+			<name>refreshrate</name>
+			<type>text</type>
+			<length>10</length>
+		</field>
+		<field>
+			<name>calendarorder</name>
+			<type>integer</type>
+			<default>0</default>
+			<notnull>true</notnull>
+			<unsigned>true</unsigned>
+		</field>
+		<field>
+			<name>calendarcolor</name>
+			<type>text</type>
+		</field>
+		<field>
+			<name>striptodos</name>
+			<type>integer</type>
+			<length>1</length>
+		</field>
+		<field>
+			<name>stripalarms</name>
+			<type>integer</type>
+			<length>1</length>
+		</field>
+		<field>
+			<name>stripattachments</name>
+			<type>integer</type>
+			<length>1</length>
+		</field>
+		<field>
+			<name>lastmodified</name>
+			<type>integer</type>
+			<unsigned>true</unsigned>
+		</field>
+		<index>
+			<name>calsub_index</name>
+			<unique>true</unique>
+			<field>
+				<name>principaluri</name>
+			</field>
+			<field>
+				<name>uri</name>
+			</field>
+		</index>
+	</declaration>
+</table>
+	<!--
+	CREATE TABLE schedulingobjects (
+		id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
+		principaluri VARBINARY(255),
+		calendardata MEDIUMBLOB,
+		uri VARBINARY(200),
+		lastmodified INT(11) UNSIGNED,
+		etag VARBINARY(32),
+		size INT(11) UNSIGNED NOT NULL
+	) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+	-->
+
+	<table>
+		<name>*dbprefix*schedulingobjects</name>
+		<declaration>
+			<field>
+				<name>id</name>
+				<type>integer</type>
+				<default>0</default>
+				<notnull>true</notnull>
+				<autoincrement>1</autoincrement>
+				<unsigned>true</unsigned>
+				<length>11</length>
+			</field>
+			<field>
+				<name>principaluri</name>
+				<type>text</type>
+			</field>
+			<field>
+				<name>calendardata</name>
+				<type>blob</type>
+			</field>
+			<field>
+				<name>uri</name>
+				<type>text</type>
+			</field>
+			<field>
+				<name>lastmodified</name>
+				<type>integer</type>
+				<unsigned>true</unsigned>
+			</field>
+			<field>
+				<name>etag</name>
+				<type>text</type>
+				<length>32</length>
+			</field>
+			<field>
+				<name>size</name>
+				<type>integer</type>
+				<notnull>true</notnull>
+				<unsigned>true</unsigned>
+				<length>11</length>
+			</field>
+
+		</declaration>
+	</table>
 </database>
diff --git a/apps/dav/appinfo/info.xml b/apps/dav/appinfo/info.xml
index 11025115691e2e391e66bc175e6b20869e439f6b..5f681e784fc4fb608ad92bbf5ff7cc5b0fc8988c 100644
--- a/apps/dav/appinfo/info.xml
+++ b/apps/dav/appinfo/info.xml
@@ -5,7 +5,7 @@
 	<description>ownCloud WebDAV endpoint</description>
 	<licence>AGPL</licence>
 	<author>owncloud.org</author>
-	<version>0.1.2</version>
+	<version>0.1.3</version>
 	<requiremin>9.0</requiremin>
 	<shipped>true</shipped>
 	<standalone/>
diff --git a/apps/dav/appinfo/register_command.php b/apps/dav/appinfo/register_command.php
index c996dd44063a694e028ee94a2f112bf62957a0e1..7d57b944fb2772441c2c6e6f4a2a0a92a1183cf6 100644
--- a/apps/dav/appinfo/register_command.php
+++ b/apps/dav/appinfo/register_command.php
@@ -1,8 +1,10 @@
 <?php
 
 use OCA\DAV\Command\CreateAddressBook;
+use OCA\DAV\Command\CreateCalendar;
 
 $dbConnection = \OC::$server->getDatabaseConnection();
 $userManager = OC::$server->getUserManager();
 /** @var Symfony\Component\Console\Application $application */
 $application->add(new CreateAddressBook($userManager, $dbConnection));
+$application->add(new CreateCalendar($userManager, $dbConnection));
diff --git a/apps/dav/command/createcalendar.php b/apps/dav/command/createcalendar.php
new file mode 100644
index 0000000000000000000000000000000000000000..da4f248e51d3d3b49114ef3630022ef05d734022
--- /dev/null
+++ b/apps/dav/command/createcalendar.php
@@ -0,0 +1,52 @@
+<?php
+
+namespace OCA\DAV\Command;
+
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCP\IDBConnection;
+use OCP\IUserManager;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class CreateCalendar extends Command {
+
+	/** @var IUserManager */
+	protected $userManager;
+
+	/** @var \OCP\IDBConnection */
+	protected $dbConnection;
+
+	/**
+	 * @param IUserManager $userManager
+	 * @param IDBConnection $dbConnection
+	 */
+	function __construct(IUserManager $userManager, IDBConnection $dbConnection) {
+		parent::__construct();
+		$this->userManager = $userManager;
+		$this->dbConnection = $dbConnection;
+	}
+
+	protected function configure() {
+		$this
+			->setName('dav:create-calendar')
+			->setDescription('Create a dav calendar')
+			->addArgument('user',
+				InputArgument::REQUIRED,
+				'User for whom the calendar will be created')
+			->addArgument('name',
+				InputArgument::REQUIRED,
+				'Name of the calendar');
+	}
+
+	protected function execute(InputInterface $input, OutputInterface $output) {
+		$user = $input->getArgument('user');
+		if (!$this->userManager->userExists($user)) {
+			throw new \InvalidArgumentException("User <$user> in unknown.");
+		}
+		$name = $input->getArgument('name');
+		$caldav = new CalDavBackend($this->dbConnection);
+		$caldav->createCalendar("principals/$user", $name, []);
+	}
+}
diff --git a/apps/dav/lib/caldav/caldavbackend.php b/apps/dav/lib/caldav/caldavbackend.php
new file mode 100644
index 0000000000000000000000000000000000000000..08a2a70c56d16c4d425124fc29eca8a5e18b5bb0
--- /dev/null
+++ b/apps/dav/lib/caldav/caldavbackend.php
@@ -0,0 +1,1175 @@
+<?php
+/**
+ * @author Thomas Müller <thomas.mueller@tmit.eu>
+ *
+ * @copyright Copyright (c) 2015, ownCloud, Inc.
+ * @license AGPL-3.0
+ *
+ * This code is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License, version 3,
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>
+ *
+ */
+
+namespace OCA\DAV\CalDAV;
+
+use Sabre\CalDAV\Backend\AbstractBackend;
+use Sabre\CalDAV\Backend\SchedulingSupport;
+use Sabre\CalDAV\Backend\SubscriptionSupport;
+use Sabre\CalDAV\Backend\SyncSupport;
+use Sabre\CalDAV\Plugin;
+use Sabre\CalDAV\Property\ScheduleCalendarTransp;
+use Sabre\CalDAV\Property\SupportedCalendarComponentSet;
+use Sabre\DAV;
+use Sabre\DAV\Exception\Forbidden;
+use Sabre\VObject\DateTimeParser;
+use Sabre\VObject\Reader;
+use Sabre\VObject\RecurrenceIterator;
+
+/**
+ * Class CalDavBackend
+ *
+ * Code is heavily inspired by https://github.com/fruux/sabre-dav/blob/master/lib/CalDAV/Backend/PDO.php
+ *
+ * @package OCA\DAV\CalDAV
+ */
+class CalDavBackend extends AbstractBackend implements SyncSupport, SubscriptionSupport, SchedulingSupport {
+
+	/**
+	 * We need to specify a max date, because we need to stop *somewhere*
+	 *
+	 * On 32 bit system the maximum for a signed integer is 2147483647, so
+	 * MAX_DATE cannot be higher than date('Y-m-d', 2147483647) which results
+	 * in 2038-01-19 to avoid problems when the date is converted
+	 * to a unix timestamp.
+	 */
+	const MAX_DATE = '2038-01-01';
+
+	/**
+	 * List of CalDAV properties, and how they map to database fieldnames
+	 * Add your own properties by simply adding on to this array.
+	 *
+	 * Note that only string-based properties are supported here.
+	 *
+	 * @var array
+	 */
+	public $propertyMap = [
+		'{DAV:}displayname'                          => 'displayname',
+		'{urn:ietf:params:xml:ns:caldav}calendar-description' => 'description',
+		'{urn:ietf:params:xml:ns:caldav}calendar-timezone'    => 'timezone',
+		'{http://apple.com/ns/ical/}calendar-order'  => 'calendarorder',
+		'{http://apple.com/ns/ical/}calendar-color'  => 'calendarcolor',
+	];
+
+	/**
+	 * List of subscription properties, and how they map to database fieldnames.
+	 *
+	 * @var array
+	 */
+	public $subscriptionPropertyMap = [
+		'{DAV:}displayname'                                           => 'displayname',
+		'{http://apple.com/ns/ical/}refreshrate'                      => 'refreshrate',
+		'{http://apple.com/ns/ical/}calendar-order'                   => 'calendarorder',
+		'{http://apple.com/ns/ical/}calendar-color'                   => 'calendarcolor',
+		'{http://calendarserver.org/ns/}subscribed-strip-todos'       => 'striptodos',
+		'{http://calendarserver.org/ns/}subscribed-strip-alarms'      => 'stripalarms',
+		'{http://calendarserver.org/ns/}subscribed-strip-attachments' => 'stripattachments',
+	];
+
+	public function __construct(\OCP\IDBConnection $db) {
+		$this->db = $db;
+	}
+
+	/**
+	 * Returns a list of calendars for a principal.
+	 *
+	 * Every project is an array with the following keys:
+	 *  * id, a unique id that will be used by other functions to modify the
+	 *    calendar. This can be the same as the uri or a database key.
+	 *  * uri, which the basename of the uri with which the calendar is
+	 *    accessed.
+	 *  * principaluri. The owner of the calendar. Almost always the same as
+	 *    principalUri passed to this method.
+	 *
+	 * Furthermore it can contain webdav properties in clark notation. A very
+	 * common one is '{DAV:}displayname'.
+	 *
+	 * Many clients also require:
+	 * {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
+	 * For this property, you can just return an instance of
+	 * Sabre\CalDAV\Property\SupportedCalendarComponentSet.
+	 *
+	 * If you return {http://sabredav.org/ns}read-only and set the value to 1,
+	 * ACL will automatically be put in read-only mode.
+	 *
+	 * @param string $principalUri
+	 * @return array
+	 */
+	function getCalendarsForUser($principalUri) {
+		$fields = array_values($this->propertyMap);
+		$fields[] = 'id';
+		$fields[] = 'uri';
+		$fields[] = 'synctoken';
+		$fields[] = 'components';
+		$fields[] = 'principaluri';
+		$fields[] = 'transparent';
+
+		// Making fields a comma-delimited list
+		$query = $this->db->getQueryBuilder();
+		$query->select($fields)->from('calendars')
+				->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
+				->orderBy('calendarorder', 'ASC');
+		$stmt = $query->execute();
+
+		$calendars = [];
+		while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+
+			$components = [];
+			if ($row['components']) {
+				$components = explode(',',$row['components']);
+			}
+
+			$calendar = [
+				'id' => $row['id'],
+				'uri' => $row['uri'],
+				'principaluri' => $row['principaluri'],
+				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
+				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
+				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
+				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
+			];
+
+			foreach($this->propertyMap as $xmlName=>$dbName) {
+				$calendar[$xmlName] = $row[$dbName];
+			}
+
+			$calendars[] = $calendar;
+		}
+
+		return $calendars;
+	}
+
+	/**
+	 * Creates a new calendar for a principal.
+	 *
+	 * If the creation was a success, an id must be returned that can be used to reference
+	 * this calendar in other methods, such as updateCalendar.
+	 *
+	 * @param string $principalUri
+	 * @param string $calendarUri
+	 * @param array $properties
+	 * @return void
+	 */
+	function createCalendar($principalUri, $calendarUri, array $properties) {
+		$values = [
+			'principaluri' => $principalUri,
+			'uri'          => $calendarUri,
+			'synctoken'    => 1,
+			'transparent'  => 0,
+			'components'   => 'VEVENT,VTODO',
+			'displayname'  => $calendarUri
+		];
+
+		// Default value
+		$sccs = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set';
+		if (isset($properties[$sccs])) {
+			if (!($properties[$sccs] instanceof SupportedCalendarComponentSet)) {
+				throw new DAV\Exception('The ' . $sccs . ' property must be of type: \Sabre\CalDAV\Property\SupportedCalendarComponentSet');
+			}
+			$values['components'] = implode(',',$properties[$sccs]->getValue());
+		}
+		$transp = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp';
+		if (isset($properties[$transp])) {
+			$values['transparent'] = $properties[$transp]->getValue()==='transparent';
+		}
+
+		foreach($this->propertyMap as $xmlName=>$dbName) {
+			if (isset($properties[$xmlName])) {
+				$values[$dbName] = $properties[$xmlName];
+			}
+		}
+
+		$query = $this->db->getQueryBuilder();
+		$query->insert('calendars');
+		foreach($values as $column => $value) {
+			$query->setValue($column, $query->createNamedParameter($value));
+		}
+		$query->execute();
+	}
+
+	/**
+	 * Updates properties for a calendar.
+	 *
+	 * The list of mutations is stored in a Sabre\DAV\PropPatch object.
+	 * To do the actual updates, you must tell this object which properties
+	 * you're going to process with the handle() method.
+	 *
+	 * Calling the handle method is like telling the PropPatch object "I
+	 * promise I can handle updating this property".
+	 *
+	 * Read the PropPatch documentation for more info and examples.
+	 *
+	 * @param string $path
+	 * @param \Sabre\DAV\PropPatch $propPatch
+	 * @return void
+	 */
+	function updateCalendar($calendarId, \Sabre\DAV\PropPatch $propPatch) {
+		$supportedProperties = array_keys($this->propertyMap);
+		$supportedProperties[] = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp';
+
+		$propPatch->handle($supportedProperties, function($mutations) use ($calendarId) {
+			$newValues = [];
+			foreach ($mutations as $propertyName => $propertyValue) {
+
+				switch ($propertyName) {
+					case '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' :
+						$fieldName = 'transparent';
+						$newValues[$fieldName] = $propertyValue->getValue() === 'transparent';
+						break;
+					default :
+						$fieldName = $this->propertyMap[$propertyName];
+						$newValues[$fieldName] = $propertyValue;
+						break;
+				}
+
+			}
+			$query = $this->db->getQueryBuilder();
+			$query->update('calendars');
+			foreach ($newValues as $fieldName => $value) {
+				$query->set($fieldName, $query->createNamedParameter($value));
+			}
+			$query->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)));
+			$query->execute();
+
+			$this->addChange($calendarId, "", 2);
+
+			return true;
+		});
+	}
+
+	/**
+	 * Delete a calendar and all it's objects
+	 *
+	 * @param mixed $calendarId
+	 * @return void
+	 */
+	function deleteCalendar($calendarId) {
+		$stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ?');
+		$stmt->execute([$calendarId]);
+
+		$stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendars` WHERE `id` = ?');
+		$stmt->execute([$calendarId]);
+
+		$stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarchanges` WHERE `calendarid` = ?');
+		$stmt->execute([$calendarId]);
+	}
+
+	/**
+	 * Returns all calendar objects within a calendar.
+	 *
+	 * Every item contains an array with the following keys:
+	 *   * calendardata - The iCalendar-compatible calendar data
+	 *   * uri - a unique key which will be used to construct the uri. This can
+	 *     be any arbitrary string, but making sure it ends with '.ics' is a
+	 *     good idea. This is only the basename, or filename, not the full
+	 *     path.
+	 *   * lastmodified - a timestamp of the last modification time
+	 *   * etag - An arbitrary string, surrounded by double-quotes. (e.g.:
+	 *   '"abcdef"')
+	 *   * size - The size of the calendar objects, in bytes.
+	 *   * component - optional, a string containing the type of object, such
+	 *     as 'vevent' or 'vtodo'. If specified, this will be used to populate
+	 *     the Content-Type header.
+	 *
+	 * Note that the etag is optional, but it's highly encouraged to return for
+	 * speed reasons.
+	 *
+	 * The calendardata is also optional. If it's not returned
+	 * 'getCalendarObject' will be called later, which *is* expected to return
+	 * calendardata.
+	 *
+	 * If neither etag or size are specified, the calendardata will be
+	 * used/fetched to determine these numbers. If both are specified the
+	 * amount of times this is needed is reduced by a great degree.
+	 *
+	 * @param mixed $calendarId
+	 * @return array
+	 */
+	function getCalendarObjects($calendarId) {
+		$query = $this->db->getQueryBuilder();
+		$query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'componenttype'])
+			->from('calendarobjects')
+			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)));
+		$stmt = $query->execute();
+
+		$result = [];
+		foreach($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) {
+			$result[] = [
+					'id'           => $row['id'],
+					'uri'          => $row['uri'],
+					'lastmodified' => $row['lastmodified'],
+					'etag'         => '"' . $row['etag'] . '"',
+					'calendarid'   => $row['calendarid'],
+					'size'         => (int)$row['size'],
+					'component'    => strtolower($row['componenttype']),
+			];
+		}
+
+		return $result;
+	}
+
+	/**
+	 * Returns information from a single calendar object, based on it's object
+	 * uri.
+	 *
+	 * The object uri is only the basename, or filename and not a full path.
+	 *
+	 * The returned array must have the same keys as getCalendarObjects. The
+	 * 'calendardata' object is required here though, while it's not required
+	 * for getCalendarObjects.
+	 *
+	 * This method must return null if the object did not exist.
+	 *
+	 * @param mixed $calendarId
+	 * @param string $objectUri
+	 * @return array|null
+	 */
+	function getCalendarObject($calendarId, $objectUri) {
+
+		$query = $this->db->getQueryBuilder();
+		$query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype'])
+				->from('calendarobjects')
+				->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
+				->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)));
+		$stmt = $query->execute();
+		$row = $stmt->fetch(\PDO::FETCH_ASSOC);
+
+		if(!$row) return null;
+
+		return [
+				'id'            => $row['id'],
+				'uri'           => $row['uri'],
+				'lastmodified'  => $row['lastmodified'],
+				'etag'          => '"' . $row['etag'] . '"',
+				'calendarid'    => $row['calendarid'],
+				'size'          => (int)$row['size'],
+				'calendardata'  => $this->readBlob($row['calendardata']),
+				'component'     => strtolower($row['componenttype']),
+		];
+	}
+
+	/**
+	 * Returns a list of calendar objects.
+	 *
+	 * This method should work identical to getCalendarObject, but instead
+	 * return all the calendar objects in the list as an array.
+	 *
+	 * If the backend supports this, it may allow for some speed-ups.
+	 *
+	 * @param mixed $calendarId
+	 * @param array $uris
+	 * @return array
+	 */
+	function getMultipleCalendarObjects($calendarId, array $uris) {
+		$query = $this->db->getQueryBuilder();
+		$query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype'])
+				->from('calendarobjects')
+				->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
+				->andWhere($query->expr()->in('uri', $query->createParameter('uri')))
+				->setParameter('uri', $uris, \Doctrine\DBAL\Connection::PARAM_STR_ARRAY);
+
+		$stmt = $query->execute();
+
+		$result = [];
+		while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+
+			$result[] = [
+					'id'           => $row['id'],
+					'uri'          => $row['uri'],
+					'lastmodified' => $row['lastmodified'],
+					'etag'         => '"' . $row['etag'] . '"',
+					'calendarid'   => $row['calendarid'],
+					'size'         => (int)$row['size'],
+					'calendardata' => $this->readBlob($row['calendardata']),
+					'component'    => strtolower($row['componenttype']),
+			];
+
+		}
+		return $result;
+	}
+
+	/**
+	 * Creates a new calendar object.
+	 *
+	 * The object uri is only the basename, or filename and not a full path.
+	 *
+	 * It is possible return an etag from this function, which will be used in
+	 * the response to this PUT request. Note that the ETag must be surrounded
+	 * by double-quotes.
+	 *
+	 * However, you should only really return this ETag if you don't mangle the
+	 * calendar-data. If the result of a subsequent GET to this object is not
+	 * the exact same as this request body, you should omit the ETag.
+	 *
+	 * @param mixed $calendarId
+	 * @param string $objectUri
+	 * @param string $calendarData
+	 * @return string|null
+	 */
+	function createCalendarObject($calendarId, $objectUri, $calendarData) {
+		$extraData = $this->getDenormalizedData($calendarData);
+
+		$query = $this->db->getQueryBuilder();
+		$query->insert('calendarobjects')
+			->values([
+				'calendarid' => $query->createNamedParameter($calendarId),
+				'uri' => $query->createNamedParameter($objectUri),
+				'calendardata' => $query->createNamedParameter($calendarData, \PDO::PARAM_LOB),
+				'lastmodified' => $query->createNamedParameter(time()),
+				'etag' => $query->createNamedParameter($extraData['etag']),
+				'size' => $query->createNamedParameter($extraData['size']),
+				'componenttype' => $query->createNamedParameter($extraData['componentType']),
+				'firstoccurence' => $query->createNamedParameter($extraData['firstOccurence']),
+				'lastoccurence' => $query->createNamedParameter($extraData['lastOccurence']),
+				'uid' => $query->createNamedParameter($extraData['uid']),
+			])
+			->execute();
+
+		$this->addChange($calendarId, $objectUri, 1);
+
+		return '"' . $extraData['etag'] . '"';
+	}
+
+	/**
+	 * Updates an existing calendarobject, based on it's uri.
+	 *
+	 * The object uri is only the basename, or filename and not a full path.
+	 *
+	 * It is possible return an etag from this function, which will be used in
+	 * the response to this PUT request. Note that the ETag must be surrounded
+	 * by double-quotes.
+	 *
+	 * However, you should only really return this ETag if you don't mangle the
+	 * calendar-data. If the result of a subsequent GET to this object is not
+	 * the exact same as this request body, you should omit the ETag.
+	 *
+	 * @param mixed $calendarId
+	 * @param string $objectUri
+	 * @param string $calendarData
+	 * @return string|null
+	 */
+	function updateCalendarObject($calendarId, $objectUri, $calendarData) {
+		$extraData = $this->getDenormalizedData($calendarData);
+
+		$query = $this->db->getQueryBuilder();
+		$query->update('calendarobjects')
+				->set('calendardata', $query->createNamedParameter($calendarData, \PDO::PARAM_LOB))
+				->set('lastmodified', $query->createNamedParameter(time()))
+				->set('etag', $query->createNamedParameter($extraData['etag']))
+				->set('size', $query->createNamedParameter($extraData['size']))
+				->set('componenttype', $query->createNamedParameter($extraData['componentType']))
+				->set('firstoccurence', $query->createNamedParameter($extraData['firstOccurence']))
+				->set('lastoccurence', $query->createNamedParameter($extraData['lastOccurence']))
+				->set('uid', $query->createNamedParameter($extraData['uid']))
+			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
+			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
+			->execute();
+
+		$this->addChange($calendarId, $objectUri, 2);
+
+		return '"' . $extraData['etag'] . '"';
+	}
+
+	/**
+	 * Deletes an existing calendar object.
+	 *
+	 * The object uri is only the basename, or filename and not a full path.
+	 *
+	 * @param mixed $calendarId
+	 * @param string $objectUri
+	 * @return void
+	 */
+	function deleteCalendarObject($calendarId, $objectUri) {
+		$stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ? AND `uri` = ?');
+		$stmt->execute([$calendarId, $objectUri]);
+
+		$this->addChange($calendarId, $objectUri, 3);
+	}
+
+	/**
+	 * Performs a calendar-query on the contents of this calendar.
+	 *
+	 * The calendar-query is defined in RFC4791 : CalDAV. Using the
+	 * calendar-query it is possible for a client to request a specific set of
+	 * object, based on contents of iCalendar properties, date-ranges and
+	 * iCalendar component types (VTODO, VEVENT).
+	 *
+	 * This method should just return a list of (relative) urls that match this
+	 * query.
+	 *
+	 * The list of filters are specified as an array. The exact array is
+	 * documented by Sabre\CalDAV\CalendarQueryParser.
+	 *
+	 * Note that it is extremely likely that getCalendarObject for every path
+	 * returned from this method will be called almost immediately after. You
+	 * may want to anticipate this to speed up these requests.
+	 *
+	 * This method provides a default implementation, which parses *all* the
+	 * iCalendar objects in the specified calendar.
+	 *
+	 * This default may well be good enough for personal use, and calendars
+	 * that aren't very large. But if you anticipate high usage, big calendars
+	 * or high loads, you are strongly adviced to optimize certain paths.
+	 *
+	 * The best way to do so is override this method and to optimize
+	 * specifically for 'common filters'.
+	 *
+	 * Requests that are extremely common are:
+	 *   * requests for just VEVENTS
+	 *   * requests for just VTODO
+	 *   * requests with a time-range-filter on either VEVENT or VTODO.
+	 *
+	 * ..and combinations of these requests. It may not be worth it to try to
+	 * handle every possible situation and just rely on the (relatively
+	 * easy to use) CalendarQueryValidator to handle the rest.
+	 *
+	 * Note that especially time-range-filters may be difficult to parse. A
+	 * time-range filter specified on a VEVENT must for instance also handle
+	 * recurrence rules correctly.
+	 * A good example of how to interprete all these filters can also simply
+	 * be found in Sabre\CalDAV\CalendarQueryFilter. This class is as correct
+	 * as possible, so it gives you a good idea on what type of stuff you need
+	 * to think of.
+	 *
+	 * @param mixed $calendarId
+	 * @param array $filters
+	 * @return array
+	 */
+	function calendarQuery($calendarId, array $filters) {
+		$componentType = null;
+		$requirePostFilter = true;
+		$timeRange = null;
+
+		// if no filters were specified, we don't need to filter after a query
+		if (!$filters['prop-filters'] && !$filters['comp-filters']) {
+			$requirePostFilter = false;
+		}
+
+		// Figuring out if there's a component filter
+		if (count($filters['comp-filters']) > 0 && !$filters['comp-filters'][0]['is-not-defined']) {
+			$componentType = $filters['comp-filters'][0]['name'];
+
+			// Checking if we need post-filters
+			if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['time-range'] && !$filters['comp-filters'][0]['prop-filters']) {
+				$requirePostFilter = false;
+			}
+			// There was a time-range filter
+			if ($componentType == 'VEVENT' && isset($filters['comp-filters'][0]['time-range'])) {
+				$timeRange = $filters['comp-filters'][0]['time-range'];
+
+				// If start time OR the end time is not specified, we can do a
+				// 100% accurate mysql query.
+				if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['prop-filters'] && (!$timeRange['start'] || !$timeRange['end'])) {
+					$requirePostFilter = false;
+				}
+			}
+
+		}
+		$columns = ['uri'];
+		if ($requirePostFilter) {
+			$columns = ['uri', 'calendardata'];
+		}
+		$query = $this->db->getQueryBuilder();
+		$query->select($columns)
+			->from('calendarobjects')
+			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)));
+
+		if ($componentType) {
+			$query->andWhere($query->expr()->eq('componenttype', $query->createNamedParameter($componentType)));
+		}
+
+		if ($timeRange && $timeRange['start']) {
+			$query->andWhere($query->expr()->gt('lastoccurence', $query->createNamedParameter($timeRange['start']->getTimeStamp())));
+		}
+		if ($timeRange && $timeRange['end']) {
+			$query->andWhere($query->expr()->lt('firstoccurence', $query->createNamedParameter($timeRange['end']->getTimeStamp())));
+		}
+
+		$stmt = $query->execute();
+
+		$result = [];
+		while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+			if ($requirePostFilter) {
+				if (!$this->validateFilterForObject($row, $filters)) {
+					continue;
+				}
+			}
+			$result[] = $row['uri'];
+		}
+
+		return $result;
+	}
+
+	/**
+	 * Searches through all of a users calendars and calendar objects to find
+	 * an object with a specific UID.
+	 *
+	 * This method should return the path to this object, relative to the
+	 * calendar home, so this path usually only contains two parts:
+	 *
+	 * calendarpath/objectpath.ics
+	 *
+	 * If the uid is not found, return null.
+	 *
+	 * This method should only consider * objects that the principal owns, so
+	 * any calendars owned by other principals that also appear in this
+	 * collection should be ignored.
+	 *
+	 * @param string $principalUri
+	 * @param string $uid
+	 * @return string|null
+	 */
+	function getCalendarObjectByUID($principalUri, $uid) {
+
+		$query = $this->db->getQueryBuilder();
+		$query->select([$query->createFunction('c.`uri` AS `calendaruri`'), $query->createFunction('co.`uri` AS `objecturi`')])
+			->from('calendarobjects', 'co')
+			->leftJoin('co', 'calendars', 'c', 'co.`calendarid` = c.`id`')
+			->where($query->expr()->eq('c.principaluri', $query->createNamedParameter($principalUri)))
+			->andWhere($query->expr()->eq('co.uid', $query->createNamedParameter($uid)));
+
+		$stmt = $query->execute();
+
+		if ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+			return $row['calendaruri'] . '/' . $row['objecturi'];
+		}
+
+		return null;
+	}
+
+	/**
+	 * The getChanges method returns all the changes that have happened, since
+	 * the specified syncToken in the specified calendar.
+	 *
+	 * This function should return an array, such as the following:
+	 *
+	 * [
+	 *   'syncToken' => 'The current synctoken',
+	 *   'added'   => [
+	 *      'new.txt',
+	 *   ],
+	 *   'modified'   => [
+	 *      'modified.txt',
+	 *   ],
+	 *   'deleted' => [
+	 *      'foo.php.bak',
+	 *      'old.txt'
+	 *   ]
+	 * );
+	 *
+	 * The returned syncToken property should reflect the *current* syncToken
+	 * of the calendar, as reported in the {http://sabredav.org/ns}sync-token
+	 * property This is * needed here too, to ensure the operation is atomic.
+	 *
+	 * If the $syncToken argument is specified as null, this is an initial
+	 * sync, and all members should be reported.
+	 *
+	 * The modified property is an array of nodenames that have changed since
+	 * the last token.
+	 *
+	 * The deleted property is an array with nodenames, that have been deleted
+	 * from collection.
+	 *
+	 * The $syncLevel argument is basically the 'depth' of the report. If it's
+	 * 1, you only have to report changes that happened only directly in
+	 * immediate descendants. If it's 2, it should also include changes from
+	 * the nodes below the child collections. (grandchildren)
+	 *
+	 * The $limit argument allows a client to specify how many results should
+	 * be returned at most. If the limit is not specified, it should be treated
+	 * as infinite.
+	 *
+	 * If the limit (infinite or not) is higher than you're willing to return,
+	 * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
+	 *
+	 * If the syncToken is expired (due to data cleanup) or unknown, you must
+	 * return null.
+	 *
+	 * The limit is 'suggestive'. You are free to ignore it.
+	 *
+	 * @param string $calendarId
+	 * @param string $syncToken
+	 * @param int $syncLevel
+	 * @param int $limit
+	 * @return array
+	 */
+	function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit = null) {
+		// Current synctoken
+		$stmt = $this->db->prepare('SELECT `synctoken` FROM `*PREFIX*calendars` WHERE `id` = ?');
+		$stmt->execute([ $calendarId ]);
+		$currentToken = $stmt->fetchColumn(0);
+
+		if (is_null($currentToken)) {
+			return null;
+		}
+
+		$result = [
+			'syncToken' => $currentToken,
+			'added'     => [],
+			'modified'  => [],
+			'deleted'   => [],
+		];
+
+		if ($syncToken) {
+
+			$query = "SELECT `uri`, `operation` FROM `*PREFIX*calendarchanges` WHERE `synctoken` >= ? AND `synctoken` < ? AND `calendarid` = ? ORDER BY `synctoken`";
+			if ($limit>0) {
+				$query.= " `LIMIT` " . (int)$limit;
+			}
+
+			// Fetching all changes
+			$stmt = $this->db->prepare($query);
+			$stmt->execute([$syncToken, $currentToken, $calendarId]);
+
+			$changes = [];
+
+			// This loop ensures that any duplicates are overwritten, only the
+			// last change on a node is relevant.
+			while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+
+				$changes[$row['uri']] = $row['operation'];
+
+			}
+
+			foreach($changes as $uri => $operation) {
+
+				switch($operation) {
+					case 1 :
+						$result['added'][] = $uri;
+						break;
+					case 2 :
+						$result['modified'][] = $uri;
+						break;
+					case 3 :
+						$result['deleted'][] = $uri;
+						break;
+				}
+
+			}
+		} else {
+			// No synctoken supplied, this is the initial sync.
+			$query = "SELECT `uri` FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ?";
+			$stmt = $this->db->prepare($query);
+			$stmt->execute([$calendarId]);
+
+			$result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
+		}
+		return $result;
+
+	}
+
+	/**
+	 * Returns a list of subscriptions for a principal.
+	 *
+	 * Every subscription is an array with the following keys:
+	 *  * id, a unique id that will be used by other functions to modify the
+	 *    subscription. This can be the same as the uri or a database key.
+	 *  * uri. This is just the 'base uri' or 'filename' of the subscription.
+	 *  * principaluri. The owner of the subscription. Almost always the same as
+	 *    principalUri passed to this method.
+	 *
+	 * Furthermore, all the subscription info must be returned too:
+	 *
+	 * 1. {DAV:}displayname
+	 * 2. {http://apple.com/ns/ical/}refreshrate
+	 * 3. {http://calendarserver.org/ns/}subscribed-strip-todos (omit if todos
+	 *    should not be stripped).
+	 * 4. {http://calendarserver.org/ns/}subscribed-strip-alarms (omit if alarms
+	 *    should not be stripped).
+	 * 5. {http://calendarserver.org/ns/}subscribed-strip-attachments (omit if
+	 *    attachments should not be stripped).
+	 * 6. {http://calendarserver.org/ns/}source (Must be a
+	 *     Sabre\DAV\Property\Href).
+	 * 7. {http://apple.com/ns/ical/}calendar-color
+	 * 8. {http://apple.com/ns/ical/}calendar-order
+	 * 9. {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
+	 *    (should just be an instance of
+	 *    Sabre\CalDAV\Property\SupportedCalendarComponentSet, with a bunch of
+	 *    default components).
+	 *
+	 * @param string $principalUri
+	 * @return array
+	 */
+	function getSubscriptionsForUser($principalUri) {
+		$fields = array_values($this->subscriptionPropertyMap);
+		$fields[] = 'id';
+		$fields[] = 'uri';
+		$fields[] = 'source';
+		$fields[] = 'principaluri';
+		$fields[] = 'lastmodified';
+
+		$query = $this->db->getQueryBuilder();
+		$query->select($fields)
+			->from('calendarsubscriptions')
+			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
+			->orderBy('calendarorder', 'asc');
+		$stmt =$query->execute();
+
+		$subscriptions = [];
+		while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+
+			$subscription = [
+				'id'           => $row['id'],
+				'uri'          => $row['uri'],
+				'principaluri' => $row['principaluri'],
+				'source'       => $row['source'],
+				'lastmodified' => $row['lastmodified'],
+
+				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
+			];
+
+			foreach($this->subscriptionPropertyMap as $xmlName=>$dbName) {
+				if (!is_null($row[$dbName])) {
+					$subscription[$xmlName] = $row[$dbName];
+				}
+			}
+
+			$subscriptions[] = $subscription;
+
+		}
+
+		return $subscriptions;
+	}
+
+	/**
+	 * Creates a new subscription for a principal.
+	 *
+	 * If the creation was a success, an id must be returned that can be used to reference
+	 * this subscription in other methods, such as updateSubscription.
+	 *
+	 * @param string $principalUri
+	 * @param string $uri
+	 * @param array $properties
+	 * @return mixed
+	 */
+	function createSubscription($principalUri, $uri, array $properties) {
+
+		if (!isset($properties['{http://calendarserver.org/ns/}source'])) {
+			throw new Forbidden('The {http://calendarserver.org/ns/}source property is required when creating subscriptions');
+		}
+
+		$values = [
+			'principaluri' => $principalUri,
+			'uri'          => $uri,
+			'source'       => $properties['{http://calendarserver.org/ns/}source']->getHref(),
+			'lastmodified' => time(),
+		];
+
+		foreach($this->subscriptionPropertyMap as $xmlName=>$dbName) {
+			if (isset($properties[$xmlName])) {
+
+				$values[$dbName] = $properties[$xmlName];
+				$fieldNames[] = $dbName;
+			}
+		}
+
+		$query = $this->db->getQueryBuilder();
+		$query->insert('calendarsubscriptions')
+			->values([
+				'principaluri' => $query->createNamedParameter($values['principaluri']),
+				'uri'          => $query->createNamedParameter($values['uri']),
+				'source'       => $query->createNamedParameter($values['source']),
+				'lastmodified' => $query->createNamedParameter($values['lastmodified']),
+			])
+			->execute();
+
+		return $this->db->lastInsertId('*PREFIX*calendarsubscriptions');
+	}
+
+	/**
+	 * Updates a subscription
+	 *
+	 * The list of mutations is stored in a Sabre\DAV\PropPatch object.
+	 * To do the actual updates, you must tell this object which properties
+	 * you're going to process with the handle() method.
+	 *
+	 * Calling the handle method is like telling the PropPatch object "I
+	 * promise I can handle updating this property".
+	 *
+	 * Read the PropPatch documentation for more info and examples.
+	 *
+	 * @param mixed $subscriptionId
+	 * @param \Sabre\DAV\PropPatch $propPatch
+	 * @return void
+	 */
+	function updateSubscription($subscriptionId, DAV\PropPatch $propPatch) {
+		$supportedProperties = array_keys($this->subscriptionPropertyMap);
+		$supportedProperties[] = '{http://calendarserver.org/ns/}source';
+
+		$propPatch->handle($supportedProperties, function($mutations) use ($subscriptionId) {
+
+			$newValues = [];
+
+			foreach($mutations as $propertyName=>$propertyValue) {
+			if ($propertyName === '{http://calendarserver.org/ns/}source') {
+					$newValues['source'] = $propertyValue->getHref();
+				} else {
+					$fieldName = $this->subscriptionPropertyMap[$propertyName];
+					$newValues[$fieldName] = $propertyValue;
+				}
+			}
+
+			$query = $this->db->getQueryBuilder();
+			$query->update('calendarsubscriptions')
+				->set('lastmodified', $query->createNamedParameter(time()));
+			foreach($newValues as $fieldName=>$value) {
+				$query->set($fieldName, $query->createNamedParameter($value));
+			}
+			$query->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
+				->execute();
+
+			return true;
+
+		});
+	}
+
+	/**
+	 * Deletes a subscription.
+	 *
+	 * @param mixed $subscriptionId
+	 * @return void
+	 */
+	function deleteSubscription($subscriptionId) {
+		$query = $this->db->getQueryBuilder();
+		$query->delete('calendarsubscriptions')
+			->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
+			->execute();
+	}
+
+	/**
+	 * Returns a single scheduling object for the inbox collection.
+	 *
+	 * The returned array should contain the following elements:
+	 *   * uri - A unique basename for the object. This will be used to
+	 *           construct a full uri.
+	 *   * calendardata - The iCalendar object
+	 *   * lastmodified - The last modification date. Can be an int for a unix
+	 *                    timestamp, or a PHP DateTime object.
+	 *   * etag - A unique token that must change if the object changed.
+	 *   * size - The size of the object, in bytes.
+	 *
+	 * @param string $principalUri
+	 * @param string $objectUri
+	 * @return array
+	 */
+	function getSchedulingObject($principalUri, $objectUri) {
+		$query = $this->db->getQueryBuilder();
+		$stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size'])
+			->from('schedulingobjects')
+			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
+			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
+			->execute();
+
+		$row = $stmt->fetch(\PDO::FETCH_ASSOC);
+
+		if(!$row) {
+			return null;
+		}
+
+		return [
+				'uri'          => $row['uri'],
+				'calendardata' => $row['calendardata'],
+				'lastmodified' => $row['lastmodified'],
+				'etag'         => '"' . $row['etag'] . '"',
+				'size'         => (int)$row['size'],
+		];
+	}
+
+	/**
+	 * Returns all scheduling objects for the inbox collection.
+	 *
+	 * These objects should be returned as an array. Every item in the array
+	 * should follow the same structure as returned from getSchedulingObject.
+	 *
+	 * The main difference is that 'calendardata' is optional.
+	 *
+	 * @param string $principalUri
+	 * @return array
+	 */
+	function getSchedulingObjects($principalUri) {
+		$query = $this->db->getQueryBuilder();
+		$stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size'])
+				->from('schedulingobjects')
+				->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
+				->execute();
+
+		$result = [];
+		foreach($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) {
+			$result[] = [
+					'calendardata' => $row['calendardata'],
+					'uri'          => $row['uri'],
+					'lastmodified' => $row['lastmodified'],
+					'etag'         => '"' . $row['etag'] . '"',
+					'size'         => (int)$row['size'],
+			];
+		}
+
+		return $result;
+	}
+
+	/**
+	 * Deletes a scheduling object from the inbox collection.
+	 *
+	 * @param string $principalUri
+	 * @param string $objectUri
+	 * @return void
+	 */
+	function deleteSchedulingObject($principalUri, $objectUri) {
+		$query = $this->db->getQueryBuilder();
+		$query->delete('schedulingobjects')
+				->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
+				->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
+				->execute();
+	}
+
+	/**
+	 * Creates a new scheduling object. This should land in a users' inbox.
+	 *
+	 * @param string $principalUri
+	 * @param string $objectUri
+	 * @param string $objectData
+	 * @return void
+	 */
+	function createSchedulingObject($principalUri, $objectUri, $objectData) {
+		$query = $this->db->getQueryBuilder();
+		$query->insert('schedulingobjects')
+			->values([
+				'principaluri' => $query->createNamedParameter($principalUri),
+				'calendardata' => $query->createNamedParameter($objectData),
+				'uri' => $query->createNamedParameter($objectUri),
+				'lastmodified' => $query->createNamedParameter(time()),
+				'etag' => $query->createNamedParameter(md5($objectData)),
+				'size' => $query->createNamedParameter(strlen($objectData))
+			])
+			->execute();
+	}
+
+	/**
+	 * Adds a change record to the calendarchanges table.
+	 *
+	 * @param mixed $calendarId
+	 * @param string $objectUri
+	 * @param int $operation 1 = add, 2 = modify, 3 = delete.
+	 * @return void
+	 */
+	protected function addChange($calendarId, $objectUri, $operation) {
+
+		$stmt = $this->db->prepare('INSERT INTO `*PREFIX*calendarchanges` (`uri`, `synctoken`, `calendarid`, `operation`) SELECT ?, `synctoken`, ?, ? FROM `*PREFIX*calendars` WHERE `id` = ?');
+		$stmt->execute([
+			$objectUri,
+			$calendarId,
+			$operation,
+			$calendarId
+		]);
+		$stmt = $this->db->prepare('UPDATE `*PREFIX*calendars` SET `synctoken` = `synctoken` + 1 WHERE `id` = ?');
+		$stmt->execute([
+			$calendarId
+		]);
+
+	}
+
+	/**
+	 * Parses some information from calendar objects, used for optimized
+	 * calendar-queries.
+	 *
+	 * Returns an array with the following keys:
+	 *   * etag - An md5 checksum of the object without the quotes.
+	 *   * size - Size of the object in bytes
+	 *   * componentType - VEVENT, VTODO or VJOURNAL
+	 *   * firstOccurence
+	 *   * lastOccurence
+	 *   * uid - value of the UID property
+	 *
+	 * @param string $calendarData
+	 * @return array
+	 */
+	protected function getDenormalizedData($calendarData) {
+
+		$vObject = Reader::read($calendarData);
+		$componentType = null;
+		$component = null;
+		$firstOccurence = null;
+		$lastOccurence = null;
+		$uid = null;
+		foreach($vObject->getComponents() as $component) {
+			if ($component->name!=='VTIMEZONE') {
+				$componentType = $component->name;
+				$uid = (string)$component->UID;
+				break;
+			}
+		}
+		if (!$componentType) {
+			throw new \Sabre\DAV\Exception\BadRequest('Calendar objects must have a VJOURNAL, VEVENT or VTODO component');
+		}
+		if ($componentType === 'VEVENT') {
+			$firstOccurence = $component->DTSTART->getDateTime()->getTimeStamp();
+			// Finding the last occurence is a bit harder
+			if (!isset($component->RRULE)) {
+				if (isset($component->DTEND)) {
+					$lastOccurence = $component->DTEND->getDateTime()->getTimeStamp();
+				} elseif (isset($component->DURATION)) {
+					$endDate = clone $component->DTSTART->getDateTime();
+					$endDate->add(DateTimeParser::parse($component->DURATION->getValue()));
+					$lastOccurence = $endDate->getTimeStamp();
+				} elseif (!$component->DTSTART->hasTime()) {
+					$endDate = clone $component->DTSTART->getDateTime();
+					$endDate->modify('+1 day');
+					$lastOccurence = $endDate->getTimeStamp();
+				} else {
+					$lastOccurence = $firstOccurence;
+				}
+			} else {
+				$it = new RecurrenceIterator($vObject, (string)$component->UID);
+				$maxDate = new \DateTime(self::MAX_DATE);
+				if ($it->isInfinite()) {
+					$lastOccurence = $maxDate->getTimeStamp();
+				} else {
+					$end = $it->getDtEnd();
+					while($it->valid() && $end < $maxDate) {
+						$end = $it->getDtEnd();
+						$it->next();
+
+					}
+					$lastOccurence = $end->getTimeStamp();
+				}
+
+			}
+		}
+
+		return [
+				'etag' => md5($calendarData),
+				'size' => strlen($calendarData),
+				'componentType' => $componentType,
+				'firstOccurence' => $firstOccurence,
+				'lastOccurence'  => $lastOccurence,
+				'uid' => $uid,
+		];
+
+	}
+
+	private function readBlob($cardData) {
+		if (is_resource($cardData)) {
+			return stream_get_contents($cardData);
+		}
+
+		return $cardData;
+	}
+}
diff --git a/apps/dav/lib/rootcollection.php b/apps/dav/lib/rootcollection.php
index 850180d8481bedf6d54c2053f019619733f05804..10baff072cce7253d333e10327c07d8a77cdd6e3 100644
--- a/apps/dav/lib/rootcollection.php
+++ b/apps/dav/lib/rootcollection.php
@@ -2,8 +2,10 @@
 
 namespace OCA\DAV;
 
+use OCA\DAV\CalDAV\CalDavBackend;
 use OCA\DAV\CardDAV\CardDavBackend;
 use OCA\DAV\Connector\Sabre\Principal;
+use Sabre\CalDAV\CalendarRoot;
 use Sabre\CalDAV\Principal\Collection;
 use Sabre\CardDAV\AddressBookRoot;
 use Sabre\DAV\SimpleCollection;
@@ -12,9 +14,10 @@ class RootCollection extends SimpleCollection {
 
 	public function __construct() {
 		$config = \OC::$server->getConfig();
+		$db = \OC::$server->getDatabaseConnection();
 		$principalBackend = new Principal(
-			$config,
-			\OC::$server->getUserManager()
+				$config,
+				\OC::$server->getUserManager()
 		);
 		// as soon as debug mode is enabled we allow listing of principals
 		$disableListing = !$config->getSystemValue('debug', false);
@@ -24,14 +27,18 @@ class RootCollection extends SimpleCollection {
 		$principalCollection->disableListing = $disableListing;
 		$filesCollection = new Files\RootCollection($principalBackend);
 		$filesCollection->disableListing = $disableListing;
-		$cardDavBackend = new CardDavBackend(\OC::$server->getDatabaseConnection());
+		$caldavBackend = new CalDavBackend($db);
+		$calendarRoot = new CalendarRoot($principalBackend, $caldavBackend);
+		$calendarRoot->disableListing = $disableListing;
+		$cardDavBackend = new CardDavBackend($db);
 		$addressBookRoot = new AddressBookRoot($principalBackend, $cardDavBackend);
 		$addressBookRoot->disableListing = $disableListing;
 
 		$children = [
-			$principalCollection,
-			$filesCollection,
-			$addressBookRoot,
+				$principalCollection,
+				$filesCollection,
+				$calendarRoot,
+				$addressBookRoot,
 		];
 
 		parent::__construct('root', $children);
diff --git a/apps/dav/lib/server.php b/apps/dav/lib/server.php
index 395544761abc5a72c3f86d7e51e50720a0e09aad..ae0fe5123f416bb3721397fbfff080922272c31b 100644
--- a/apps/dav/lib/server.php
+++ b/apps/dav/lib/server.php
@@ -33,8 +33,18 @@ class Server {
 		$this->server->addPlugin(new BlockLegacyClientPlugin(\OC::$server->getConfig()));
 		$this->server->addPlugin(new Plugin($authBackend, 'ownCloud'));
 
+		// calendar plugins
+		$this->server->addPlugin(new \Sabre\CalDAV\Plugin());
 		$this->server->addPlugin(new \Sabre\DAVACL\Plugin());
+		$this->server->addPlugin(new \Sabre\CalDAV\ICSExportPlugin());
+		$senderEmail = \OCP\Util::getDefaultEmailAddress('no-reply');
+		$this->server->addPlugin(new \Sabre\CalDAV\Schedule\Plugin());
+		$this->server->addPlugin(new \Sabre\CalDAV\Schedule\IMipPlugin($senderEmail));
+		$this->server->addPlugin(new \Sabre\CalDAV\SharingPlugin());
+		$this->server->addPlugin(new \Sabre\CalDAV\Subscriptions\Plugin());
+		$this->server->addPlugin(new \Sabre\CalDAV\Notifications\Plugin());
 
+		// addressbook plugins
 		$this->server->addPlugin(new \Sabre\CardDAV\Plugin());
 
 		// Finder on OS X requires Class 2 WebDAV support (locking), since we do
diff --git a/apps/dav/tests/unit/caldav/caldavbackendtest.php b/apps/dav/tests/unit/caldav/caldavbackendtest.php
new file mode 100644
index 0000000000000000000000000000000000000000..258c5627ad9a624250de4695de7df8171a1ee737
--- /dev/null
+++ b/apps/dav/tests/unit/caldav/caldavbackendtest.php
@@ -0,0 +1,348 @@
+<?php
+/**
+ * @author Lukas Reschke <lukas@owncloud.com>
+ *
+ * @copyright Copyright (c) 2015, ownCloud, Inc.
+ * @license AGPL-3.0
+ *
+ * This code is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License, version 3,
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>
+ *
+ */
+namespace Tests\Connector\Sabre;
+
+use DateTime;
+use DateTimeZone;
+use OCA\DAV\CalDAV\CalDavBackend;
+use Sabre\CalDAV\Property\SupportedCalendarComponentSet;
+use Sabre\DAV\Property\Href;
+use Sabre\DAV\PropPatch;
+use Test\TestCase;
+
+/**
+ * Class CalDavBackendTest
+ *
+ * @group DB
+ *
+ * @package Tests\Connector\Sabre
+ */
+class CalDavBackendTest extends TestCase {
+
+	/** @var CalDavBackend */
+	private $backend;
+
+	const UNIT_TEST_USER = 'caldav-unit-test';
+
+
+	public function setUp() {
+		parent::setUp();
+
+		$db = \OC::$server->getDatabaseConnection();
+		$this->backend = new CalDavBackend($db);
+
+		$this->tearDown();
+	}
+
+	public function tearDown() {
+		parent::tearDown();
+
+		if (is_null($this->backend)) {
+			return;
+		}
+		$books = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER);
+		foreach ($books as $book) {
+			$this->backend->deleteCalendar($book['id']);
+		}
+		$subscriptions = $this->backend->getSubscriptionsForUser(self::UNIT_TEST_USER);
+		foreach ($subscriptions as $subscription) {
+			$this->backend->deleteSubscription($subscription['id']);
+		}
+	}
+
+	public function testCalendarOperations() {
+
+		$calendarId = $this->createTestCalendar();
+
+		// update it's display name
+		$patch = new PropPatch([
+			'{DAV:}displayname' => 'Unit test',
+			'{urn:ietf:params:xml:ns:caldav}calendar-description' => 'Calendar used for unit testing'
+		]);
+		$this->backend->updateCalendar($calendarId, $patch);
+		$patch->commit();
+		$books = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER);
+		$this->assertEquals(1, count($books));
+		$this->assertEquals('Unit test', $books[0]['{DAV:}displayname']);
+		$this->assertEquals('Calendar used for unit testing', $books[0]['{urn:ietf:params:xml:ns:caldav}calendar-description']);
+
+		// delete the address book
+		$this->backend->deleteCalendar($books[0]['id']);
+		$books = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER);
+		$this->assertEquals(0, count($books));
+	}
+
+	public function testCalendarObjectsOperations() {
+
+		$calendarId = $this->createTestCalendar();
+
+		// create a card
+		$uri = $this->getUniqueID('calobj');
+		$calData = <<<'EOD'
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:ownCloud Calendar
+BEGIN:VEVENT
+CREATED;VALUE=DATE-TIME:20130910T125139Z
+UID:47d15e3ec8
+LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z
+DTSTAMP;VALUE=DATE-TIME:20130910T125139Z
+SUMMARY:Test Event
+DTSTART;VALUE=DATE-TIME:20130912T130000Z
+DTEND;VALUE=DATE-TIME:20130912T140000Z
+CLASS:PUBLIC
+END:VEVENT
+END:VCALENDAR
+EOD;
+
+		$this->backend->createCalendarObject($calendarId, $uri, $calData);
+
+		// get all the cards
+		$calendarObjects = $this->backend->getCalendarObjects($calendarId);
+		$this->assertEquals(1, count($calendarObjects));
+		$this->assertEquals($calendarId, $calendarObjects[0]['calendarid']);
+
+		// get the cards
+		$calendarObject = $this->backend->getCalendarObject($calendarId, $uri);
+		$this->assertNotNull($calendarObject);
+		$this->assertArrayHasKey('id', $calendarObject);
+		$this->assertArrayHasKey('uri', $calendarObject);
+		$this->assertArrayHasKey('lastmodified', $calendarObject);
+		$this->assertArrayHasKey('etag', $calendarObject);
+		$this->assertArrayHasKey('size', $calendarObject);
+		$this->assertEquals($calData, $calendarObject['calendardata']);
+
+		// update the card
+		$calData = <<<'EOD'
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:ownCloud Calendar
+BEGIN:VEVENT
+CREATED;VALUE=DATE-TIME:20130910T125139Z
+UID:47d15e3ec8
+LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z
+DTSTAMP;VALUE=DATE-TIME:20130910T125139Z
+SUMMARY:Test Event
+DTSTART;VALUE=DATE-TIME:20130912T130000Z
+DTEND;VALUE=DATE-TIME:20130912T140000Z
+END:VEVENT
+END:VCALENDAR
+EOD;
+		$this->backend->updateCalendarObject($calendarId, $uri, $calData);
+		$calendarObject = $this->backend->getCalendarObject($calendarId, $uri);
+		$this->assertEquals($calData, $calendarObject['calendardata']);
+
+		// delete the card
+		$this->backend->deleteCalendarObject($calendarId, $uri);
+		$calendarObjects = $this->backend->getCalendarObjects($calendarId);
+		$this->assertEquals(0, count($calendarObjects));
+	}
+
+	public function testMultiCalendarObjects() {
+
+		$calendarId = $this->createTestCalendar();
+
+		// create an event
+		$calData = <<<'EOD'
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:ownCloud Calendar
+BEGIN:VEVENT
+CREATED;VALUE=DATE-TIME:20130910T125139Z
+UID:47d15e3ec8
+LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z
+DTSTAMP;VALUE=DATE-TIME:20130910T125139Z
+SUMMARY:Test Event
+DTSTART;VALUE=DATE-TIME:20130912T130000Z
+DTEND;VALUE=DATE-TIME:20130912T140000Z
+CLASS:PUBLIC
+END:VEVENT
+END:VCALENDAR
+EOD;
+		$uri0 = $this->getUniqueID('card');
+		$this->backend->createCalendarObject($calendarId, $uri0, $calData);
+		$uri1 = $this->getUniqueID('card');
+		$this->backend->createCalendarObject($calendarId, $uri1, $calData);
+		$uri2 = $this->getUniqueID('card');
+		$this->backend->createCalendarObject($calendarId, $uri2, $calData);
+
+		// get all the cards
+		$calendarObjects = $this->backend->getCalendarObjects($calendarId);
+		$this->assertEquals(3, count($calendarObjects));
+
+		// get the cards
+		$calendarObjects = $this->backend->getMultipleCalendarObjects($calendarId, [$uri1, $uri2]);
+		$this->assertEquals(2, count($calendarObjects));
+		foreach($calendarObjects as $card) {
+			$this->assertArrayHasKey('id', $card);
+			$this->assertArrayHasKey('uri', $card);
+			$this->assertArrayHasKey('lastmodified', $card);
+			$this->assertArrayHasKey('etag', $card);
+			$this->assertArrayHasKey('size', $card);
+			$this->assertEquals($calData, $card['calendardata']);
+		}
+
+		// delete the card
+		$this->backend->deleteCalendarObject($calendarId, $uri0);
+		$this->backend->deleteCalendarObject($calendarId, $uri1);
+		$this->backend->deleteCalendarObject($calendarId, $uri2);
+		$calendarObjects = $this->backend->getCalendarObjects($calendarId);
+		$this->assertEquals(0, count($calendarObjects));
+	}
+
+	/**
+	 * @dataProvider providesCalendarQueryParameters
+	 */
+	public function testCalendarQuery($expectedEventsInResult, $propFilters, $compFilter) {
+		$calendarId = $this->createTestCalendar();
+		$events = [];
+		$events[0] = $this->createEvent($calendarId, '20130912T130000Z', '20130912T140000Z');
+		$events[1] = $this->createEvent($calendarId, '20130912T150000Z', '20130912T170000Z');
+		$events[2] = $this->createEvent($calendarId, '20130912T173000Z', '20130912T220000Z');
+
+		$result = $this->backend->calendarQuery($calendarId, [
+			'name' => '',
+			'prop-filters' => $propFilters,
+			'comp-filters' => $compFilter
+		]);
+
+		$expectedEventsInResult = array_map(function($index) use($events) {
+			return $events[$index];
+		}, $expectedEventsInResult);
+		$this->assertEquals($expectedEventsInResult, $result, '', 0.0, 10, true);
+	}
+
+	public function testGetCalendarObjectByUID() {
+		$calendarId = $this->createTestCalendar();
+		$this->createEvent($calendarId, '20130912T130000Z', '20130912T140000Z');
+
+		$co = $this->backend->getCalendarObjectByUID(self::UNIT_TEST_USER, '47d15e3ec8');
+		$this->assertNotNull($co);
+	}
+
+	public function providesCalendarQueryParameters() {
+		return [
+			'all' => [[0, 1, 2], [], []],
+			'only-todos' => [[], ['name' => 'VTODO'], []],
+			'only-events' => [[0, 1, 2], [], [['name' => 'VEVENT', 'is-not-defined' => false, 'comp-filters' => [], 'time-range' => ['start' => null, 'end' => null], 'prop-filters' => []]],],
+			'start' => [[1, 2], [], [['name' => 'VEVENT', 'is-not-defined' => false, 'comp-filters' => [], 'time-range' => ['start' => new DateTime('2013-09-12 14:00:00', new DateTimeZone('UTC')), 'end' => null], 'prop-filters' => []]],],
+			'end' => [[0], [], [['name' => 'VEVENT', 'is-not-defined' => false, 'comp-filters' => [], 'time-range' => ['start' => null, 'end' => new DateTime('2013-09-12 14:00:00', new DateTimeZone('UTC'))], 'prop-filters' => []]],],
+		];
+	}
+
+	private function createTestCalendar() {
+		$this->backend->createCalendar(self::UNIT_TEST_USER, 'Example', [
+			'{http://apple.com/ns/ical/}calendar-color' => '#1C4587FF'
+		]);
+		$calendars = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER);
+		$this->assertEquals(1, count($calendars));
+		$this->assertEquals(self::UNIT_TEST_USER, $calendars[0]['principaluri']);
+		/** @var SupportedCalendarComponentSet $components */
+		$components = $calendars[0]['{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set'];
+		$this->assertEquals(['VEVENT','VTODO'], $components->getValue());
+		$color = $calendars[0]['{http://apple.com/ns/ical/}calendar-color'];
+		$this->assertEquals('#1C4587FF', $color);
+		$this->assertEquals('Example', $calendars[0]['uri']);
+		$this->assertEquals('Example', $calendars[0]['{DAV:}displayname']);
+		$calendarId = $calendars[0]['id'];
+
+		return $calendarId;
+	}
+
+	private function createEvent($calendarId, $start = '20130912T130000Z', $end = '20130912T140000Z') {
+
+		$calData = <<<EOD
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:ownCloud Calendar
+BEGIN:VEVENT
+CREATED;VALUE=DATE-TIME:20130910T125139Z
+UID:47d15e3ec8
+LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z
+DTSTAMP;VALUE=DATE-TIME:20130910T125139Z
+SUMMARY:Test Event
+DTSTART;VALUE=DATE-TIME:$start
+DTEND;VALUE=DATE-TIME:$end
+CLASS:PUBLIC
+END:VEVENT
+END:VCALENDAR
+EOD;
+		$uri0 = $this->getUniqueID('event');
+		$this->backend->createCalendarObject($calendarId, $uri0, $calData);
+
+		return $uri0;
+	}
+
+	public function testSyncSupport() {
+		$calendarId = $this->createTestCalendar();
+
+		// fist call without synctoken
+		$changes = $this->backend->getChangesForCalendar($calendarId, '', 1);
+		$syncToken = $changes['syncToken'];
+
+		// add a change
+		$event = $this->createEvent($calendarId, '20130912T130000Z', '20130912T140000Z');
+
+		// look for changes
+		$changes = $this->backend->getChangesForCalendar($calendarId, $syncToken, 1);
+		$this->assertEquals($event, $changes['added'][0]);
+	}
+
+	public function testSubscriptions() {
+		$id = $this->backend->createSubscription(self::UNIT_TEST_USER, 'Subscription', [
+			'{http://calendarserver.org/ns/}source' => new Href('test-source')
+		]);
+
+		$subscriptions = $this->backend->getSubscriptionsForUser(self::UNIT_TEST_USER);
+		$this->assertEquals(1, count($subscriptions));
+		$this->assertEquals($id, $subscriptions[0]['id']);
+
+		$patch = new PropPatch([
+				'{DAV:}displayname' => 'Unit test',
+		]);
+		$this->backend->updateSubscription($id, $patch);
+		$patch->commit();
+
+		$subscriptions = $this->backend->getSubscriptionsForUser(self::UNIT_TEST_USER);
+		$this->assertEquals(1, count($subscriptions));
+		$this->assertEquals($id, $subscriptions[0]['id']);
+		$this->assertEquals('Unit test', $subscriptions[0]['{DAV:}displayname']);
+
+		$this->backend->deleteSubscription($id);
+		$subscriptions = $this->backend->getSubscriptionsForUser(self::UNIT_TEST_USER);
+		$this->assertEquals(0, count($subscriptions));
+	}
+
+	public function testScheduling() {
+		$this->backend->createSchedulingObject(self::UNIT_TEST_USER, 'Sample Schedule', '');
+
+		$sos = $this->backend->getSchedulingObjects(self::UNIT_TEST_USER);
+		$this->assertEquals(1, count($sos));
+
+		$so = $this->backend->getSchedulingObject(self::UNIT_TEST_USER, 'Sample Schedule');
+		$this->assertNotNull($so);
+
+		$this->backend->deleteSchedulingObject(self::UNIT_TEST_USER, 'Sample Schedule');
+
+		$sos = $this->backend->getSchedulingObjects(self::UNIT_TEST_USER);
+		$this->assertEquals(0, count($sos));
+	}
+}