aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--apps/dav/.gitignore1
-rw-r--r--apps/dav/appinfo/database.xml186
-rw-r--r--apps/dav/appinfo/info.xml2
-rw-r--r--apps/dav/lib/carddav/carddavbackend.php558
-rw-r--r--apps/dav/lib/rootcollection.php6
-rw-r--r--apps/dav/tests/unit/carddav/carddavbackendtest.php180
-rw-r--r--apps/dav/tests/unit/connector/sabre/BlockLegacyClientPluginTest.php2
-rw-r--r--apps/dav/tests/unit/connector/sabre/DummyGetResponsePluginTest.php2
-rw-r--r--apps/dav/tests/unit/connector/sabre/MaintenancePluginTest.php2
-rw-r--r--apps/dav/tests/unit/connector/sabre/auth.php3
-rw-r--r--apps/dav/tests/unit/connector/sabre/copyetagheaderplugintest.php2
-rw-r--r--apps/dav/tests/unit/connector/sabre/custompropertiesbackend.php4
-rw-r--r--apps/dav/tests/unit/connector/sabre/directory.php9
-rw-r--r--apps/dav/tests/unit/connector/sabre/exception/invalidpathtest.php2
-rw-r--r--apps/dav/tests/unit/connector/sabre/exceptionloggerplugin.php2
-rw-r--r--apps/dav/tests/unit/connector/sabre/file.php2
-rw-r--r--apps/dav/tests/unit/connector/sabre/filesplugin.php2
-rw-r--r--apps/dav/tests/unit/connector/sabre/node.php2
-rw-r--r--apps/dav/tests/unit/connector/sabre/objecttree.php5
-rw-r--r--apps/dav/tests/unit/connector/sabre/principal.php2
-rw-r--r--apps/dav/tests/unit/connector/sabre/quotaplugin.php3
-rw-r--r--apps/dav/tests/unit/connector/sabre/tagsplugin.php4
22 files changed, 958 insertions, 23 deletions
diff --git a/apps/dav/.gitignore b/apps/dav/.gitignore
new file mode 100644
index 00000000000..885b6b3e6de
--- /dev/null
+++ b/apps/dav/.gitignore
@@ -0,0 +1 @@
+tests/travis/CalDAVTester
diff --git a/apps/dav/appinfo/database.xml b/apps/dav/appinfo/database.xml
new file mode 100644
index 00000000000..f3fd5079949
--- /dev/null
+++ b/apps/dav/appinfo/database.xml
@@ -0,0 +1,186 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<database>
+
+ <!--
+CREATE TABLE addressbooks (
+ id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ principaluri VARBINARY(255),
+ displayname VARCHAR(255),
+ uri VARBINARY(200),
+ description TEXT,
+ synctoken INT(11) UNSIGNED NOT NULL DEFAULT '1',
+ UNIQUE(principaluri(100), uri(100))
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+ -->
+ <table>
+
+ <name>*dbprefix*addressbooks</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>description</name>
+ <type>text</type>
+ </field>
+ <field>
+ <name>synctoken</name>
+ <type>integer</type>
+ <default>1</default>
+ <notnull>true</notnull>
+ <unsigned>true</unsigned>
+ </field>
+ <index>
+ <name>addressbook_index</name>
+ <unique>true</unique>
+ <field>
+ <name>principaluri</name>
+ </field>
+ <field>
+ <name>uri</name>
+ </field>
+ </index>
+ </declaration>
+ </table>
+
+ <!--
+
+CREATE TABLE cards (
+ id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ addressbookid INT(11) UNSIGNED NOT NULL,
+ carddata 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*cards</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>addressbookid</name>
+ <type>integer</type>
+ <default>0</default>
+ <notnull>true</notnull>
+ </field>
+ <field>
+ <name>carddata</name>
+ <type>blob</type>
+ </field>
+ <field>
+ <name>uri</name>
+ <type>text</type>
+ </field>
+ <field>
+ <name>lastmodified</name>
+ <type>integer</type>
+ <unsigned>true</unsigned>
+ <length>11</length>
+ </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>
+
+ <!--
+CREATE TABLE addressbookchanges (
+ id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ uri VARBINARY(200) NOT NULL,
+ synctoken INT(11) UNSIGNED NOT NULL,
+ addressbookid INT(11) UNSIGNED NOT NULL,
+ operation TINYINT(1) NOT NULL,
+ INDEX addressbookid_synctoken (addressbookid, synctoken)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+ -->
+
+ <table>
+ <name>*dbprefix*addressbookchanges</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>addressbookid</name>
+ <type>integer</type>
+ <notnull>true</notnull>
+ </field>
+ <field>
+ <name>operation</name>
+ <type>integer</type>
+ <notnull>true</notnull>
+ <length>1</length>
+ </field>
+
+ <index>
+ <name>addressbookid_synctoken</name>
+ <field>
+ <name>addressbookid</name>
+ </field>
+ <field>
+ <name>synctoken</name>
+ </field>
+ </index>
+
+ </declaration>
+ </table>
+
+</database>
diff --git a/apps/dav/appinfo/info.xml b/apps/dav/appinfo/info.xml
index 8f378f5e18d..11025115691 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.1</version>
+ <version>0.1.2</version>
<requiremin>9.0</requiremin>
<shipped>true</shipped>
<standalone/>
diff --git a/apps/dav/lib/carddav/carddavbackend.php b/apps/dav/lib/carddav/carddavbackend.php
new file mode 100644
index 00000000000..7b16262a680
--- /dev/null
+++ b/apps/dav/lib/carddav/carddavbackend.php
@@ -0,0 +1,558 @@
+<?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\CardDAV;
+
+use Sabre\CardDAV\Backend\BackendInterface;
+use Sabre\CardDAV\Backend\SyncSupport;
+use Sabre\CardDAV\Plugin;
+use Sabre\DAV\Exception\BadRequest;
+
+class CardDavBackend implements BackendInterface, SyncSupport {
+
+ public function __construct(\OCP\IDBConnection $db) {
+ $this->db = $db;
+ }
+
+ /**
+ * Returns the list of addressbooks for a specific user.
+ *
+ * Every addressbook should have the following properties:
+ * id - an arbitrary unique id
+ * uri - the 'basename' part of the url
+ * principaluri - Same as the passed parameter
+ *
+ * Any additional clark-notation property may be passed besides this. Some
+ * common ones are :
+ * {DAV:}displayname
+ * {urn:ietf:params:xml:ns:carddav}addressbook-description
+ * {http://calendarserver.org/ns/}getctag
+ *
+ * @param string $principalUri
+ * @return array
+ */
+ function getAddressBooksForUser($principalUri) {
+ $query = $this->db->getQueryBuilder();
+ $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
+ ->from('addressbooks')
+ ->where($query->expr()->eq('principaluri', $query->createParameter('principaluri')))
+ ->setParameter('principaluri', $principalUri);
+
+ $addressBooks = [];
+
+ $result = $query->execute();
+ while($row = $result->fetch()) {
+ $addressBooks[] = [
+ 'id' => $row['id'],
+ 'uri' => $row['uri'],
+ 'principaluri' => $row['principaluri'],
+ '{DAV:}displayname' => $row['displayname'],
+ '{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
+ '{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
+ '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
+ ];
+ }
+ $result->closeCursor();
+
+ return $addressBooks;
+ }
+
+ /**
+ * Updates properties for an address book.
+ *
+ * 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 documenation for more info and examples.
+ *
+ * @param string $addressBookId
+ * @param \Sabre\DAV\PropPatch $propPatch
+ * @return void
+ */
+ function updateAddressBook($addressBookId, \Sabre\DAV\PropPatch $propPatch) {
+ $supportedProperties = [
+ '{DAV:}displayname',
+ '{' . Plugin::NS_CARDDAV . '}addressbook-description',
+ ];
+
+ $propPatch->handle($supportedProperties, function($mutations) use ($addressBookId) {
+
+ $updates = [];
+ foreach($mutations as $property=>$newValue) {
+
+ switch($property) {
+ case '{DAV:}displayname' :
+ $updates['displayname'] = $newValue;
+ break;
+ case '{' . Plugin::NS_CARDDAV . '}addressbook-description' :
+ $updates['description'] = $newValue;
+ break;
+ }
+ }
+ $query = $this->db->getQueryBuilder();
+ $query->update('addressbooks');
+
+ foreach($updates as $key=>$value) {
+ $query->set($key, $query->createNamedParameter($value));
+ }
+ $query->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId)))
+ ->execute();
+
+ $this->addChange($addressBookId, "", 2);
+
+ return true;
+
+ });
+ }
+
+ /**
+ * Creates a new address book
+ *
+ * @param string $principalUri
+ * @param string $url Just the 'basename' of the url.
+ * @param array $properties
+ * @return void
+ */
+ function createAddressBook($principalUri, $url, array $properties) {
+ $values = [
+ 'displayname' => null,
+ 'description' => null,
+ 'principaluri' => $principalUri,
+ 'uri' => $url,
+ 'synctoken' => 1
+ ];
+
+ foreach($properties as $property=>$newValue) {
+
+ switch($property) {
+ case '{DAV:}displayname' :
+ $values['displayname'] = $newValue;
+ break;
+ case '{' . Plugin::NS_CARDDAV . '}addressbook-description' :
+ $values['description'] = $newValue;
+ break;
+ default :
+ throw new BadRequest('Unknown property: ' . $property);
+ }
+
+ }
+
+ $query = $this->db->getQueryBuilder();
+ $query->insert('addressbooks')
+ ->values([
+ 'uri' => $query->createParameter('uri'),
+ 'displayname' => $query->createParameter('displayname'),
+ 'description' => $query->createParameter('description'),
+ 'principaluri' => $query->createParameter('principaluri'),
+ 'synctoken' => $query->createParameter('synctoken'),
+ ])
+ ->setParameters($values)
+ ->execute();
+ }
+
+ /**
+ * Deletes an entire addressbook and all its contents
+ *
+ * @param mixed $addressBookId
+ * @return void
+ */
+ function deleteAddressBook($addressBookId) {
+ $query = $this->db->getQueryBuilder();
+ $query->delete('cards')
+ ->where($query->expr()->eq('addressbookid', $query->createParameter('addressbookid')))
+ ->setParameter('addressbookid', $addressBookId)
+ ->execute();
+
+ $query->delete('addressbookchanges')
+ ->where($query->expr()->eq('addressbookid', $query->createParameter('addressbookid')))
+ ->setParameter('addressbookid', $addressBookId)
+ ->execute();
+
+ $query->delete('addressbooks')
+ ->where($query->expr()->eq('id', $query->createParameter('id')))
+ ->setParameter('id', $addressBookId)
+ ->execute();
+ }
+
+ /**
+ * Returns all cards for a specific addressbook id.
+ *
+ * This method should return the following properties for each card:
+ * * carddata - raw vcard data
+ * * uri - Some unique url
+ * * lastmodified - A unix timestamp
+ *
+ * It's recommended to also return the following properties:
+ * * etag - A unique etag. This must change every time the card changes.
+ * * size - The size of the card in bytes.
+ *
+ * If these last two properties are provided, less time will be spent
+ * calculating them. If they are specified, you can also ommit carddata.
+ * This may speed up certain requests, especially with large cards.
+ *
+ * @param mixed $addressBookId
+ * @return array
+ */
+ function getCards($addressBookId) {
+ $query = $this->db->getQueryBuilder();
+ $query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata'])
+ ->from('cards')
+ ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
+
+ $cards = [];
+
+ $result = $query->execute();
+ while($row = $result->fetch()) {
+ $row['etag'] = '"' . $row['etag'] . '"';
+ $row['carddata'] = $this->readBlob($row['carddata']);
+ $cards[] = $row;
+ }
+ $result->closeCursor();
+
+ return $cards;
+ }
+
+ /**
+ * Returns a specfic card.
+ *
+ * The same set of properties must be returned as with getCards. The only
+ * exception is that 'carddata' is absolutely required.
+ *
+ * If the card does not exist, you must return false.
+ *
+ * @param mixed $addressBookId
+ * @param string $cardUri
+ * @return array
+ */
+ function getCard($addressBookId, $cardUri) {
+ $query = $this->db->getQueryBuilder();
+ $query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata'])
+ ->from('cards')
+ ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
+ ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
+ ->setMaxResults(1);
+
+ $result = $query->execute();
+ $row = $result->fetch();
+ if (!$row) {
+ return false;
+ }
+ $row['etag'] = '"' . $row['etag'] . '"';
+ $row['carddata'] = $this->readBlob($row['carddata']);
+
+ return $row;
+ }
+
+ /**
+ * Returns a list of cards.
+ *
+ * This method should work identical to getCard, but instead return all the
+ * cards in the list as an array.
+ *
+ * If the backend supports this, it may allow for some speed-ups.
+ *
+ * @param mixed $addressBookId
+ * @param array $uris
+ * @return array
+ */
+ function getMultipleCards($addressBookId, array $uris) {
+ $query = $this->db->getQueryBuilder();
+ $query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata'])
+ ->from('cards')
+ ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
+ ->andWhere($query->expr()->in('uri', $query->createParameter('uri')))
+ ->setParameter('uri', $uris, \Doctrine\DBAL\Connection::PARAM_STR_ARRAY);
+
+ $cards = [];
+
+ $result = $query->execute();
+ while($row = $result->fetch()) {
+ $row['etag'] = '"' . $row['etag'] . '"';
+ $row['carddata'] = $this->readBlob($row['carddata']);
+ $cards[] = $row;
+ }
+ $result->closeCursor();
+
+ return $cards;
+ }
+
+ /**
+ * Creates a new card.
+ *
+ * The addressbook id will be passed as the first argument. This is the
+ * same id as it is returned from the getAddressBooksForUser method.
+ *
+ * The cardUri is a base uri, and doesn't include the full path. The
+ * cardData argument is the vcard body, and is passed as a string.
+ *
+ * It is possible to return an ETag from this method. This ETag is for the
+ * newly created resource, and must be enclosed with double quotes (that
+ * is, the string itself must contain the double quotes).
+ *
+ * You should only return the ETag if you store the carddata as-is. If a
+ * subsequent GET request on the same card does not have the same body,
+ * byte-by-byte and you did return an ETag here, clients tend to get
+ * confused.
+ *
+ * If you don't return an ETag, you can just return null.
+ *
+ * @param mixed $addressBookId
+ * @param string $cardUri
+ * @param string $cardData
+ * @return string|null
+ */
+ function createCard($addressBookId, $cardUri, $cardData) {
+ $etag = md5($cardData);
+
+ $query = $this->db->getQueryBuilder();
+ $query->insert('cards')
+ ->values([
+ 'carddata' => $query->createNamedParameter($cardData),
+ 'uri' => $query->createNamedParameter($cardUri),
+ 'lastmodified' => $query->createNamedParameter(time()),
+ 'addressbookid' => $query->createNamedParameter($addressBookId),
+ 'size' => $query->createNamedParameter(strlen($cardData)),
+ 'etag' => $query->createNamedParameter($etag),
+ ])
+ ->execute();
+
+ $this->addChange($addressBookId, $cardUri, 1);
+
+ return '"' . $etag . '"';
+ }
+
+ /**
+ * Updates a card.
+ *
+ * The addressbook id will be passed as the first argument. This is the
+ * same id as it is returned from the getAddressBooksForUser method.
+ *
+ * The cardUri is a base uri, and doesn't include the full path. The
+ * cardData argument is the vcard body, and is passed as a string.
+ *
+ * It is possible to return an ETag from this method. This ETag should
+ * match that of the updated resource, and must be enclosed with double
+ * quotes (that is: the string itself must contain the actual quotes).
+ *
+ * You should only return the ETag if you store the carddata as-is. If a
+ * subsequent GET request on the same card does not have the same body,
+ * byte-by-byte and you did return an ETag here, clients tend to get
+ * confused.
+ *
+ * If you don't return an ETag, you can just return null.
+ *
+ * @param mixed $addressBookId
+ * @param string $cardUri
+ * @param string $cardData
+ * @return string|null
+ */
+ function updateCard($addressBookId, $cardUri, $cardData) {
+
+ $etag = md5($cardData);
+ $query = $this->db->getQueryBuilder();
+ $query->update('cards')
+ ->set('carddata', $query->createNamedParameter($cardData, \PDO::PARAM_LOB))
+ ->set('lastmodified', $query->createNamedParameter(time()))
+ ->set('size', $query->createNamedParameter(strlen($cardData)))
+ ->set('etag', $query->createNamedParameter($etag))
+ ->where($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
+ ->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
+ ->execute();
+
+ $this->addChange($addressBookId, $cardUri, 2);
+
+ return '"' . $etag . '"';
+ }
+
+ /**
+ * Deletes a card
+ *
+ * @param mixed $addressBookId
+ * @param string $cardUri
+ * @return bool
+ */
+ function deleteCard($addressBookId, $cardUri) {
+ $query = $this->db->getQueryBuilder();
+ $ret = $query->delete('cards')
+ ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
+ ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
+ ->execute();
+
+ $this->addChange($addressBookId, $cardUri, 3);
+
+ return $ret === 1;
+ }
+
+ /**
+ * The getChanges method returns all the changes that have happened, since
+ * the specified syncToken in the specified address book.
+ *
+ * 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 $addressBookId
+ * @param string $syncToken
+ * @param int $syncLevel
+ * @param int $limit
+ * @return array
+ */
+ function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, $limit = null) {
+ // Current synctoken
+ $stmt = $this->db->prepare('SELECT `synctoken` FROM `*PREFIX*addressbooks` WHERE `id` = ?');
+ $stmt->execute([ $addressBookId ]);
+ $currentToken = $stmt->fetchColumn(0);
+
+ if (is_null($currentToken)) return null;
+
+ $result = [
+ 'syncToken' => $currentToken,
+ 'added' => [],
+ 'modified' => [],
+ 'deleted' => [],
+ ];
+
+ if ($syncToken) {
+
+ $query = "SELECT `uri`, `operation` FROM `*PREFIX*addressbookchanges` WHERE `synctoken` >= ? AND `synctoken` < ? AND `addressbookid` = ? ORDER BY `synctoken`";
+ if ($limit>0) {
+ $query .= " `LIMIT` " . (int)$limit;
+ }
+
+ // Fetching all changes
+ $stmt = $this->db->prepare($query);
+ $stmt->execute([$syncToken, $currentToken, $addressBookId]);
+
+ $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*cards` WHERE `addressbookid` = ?";
+ $stmt = $this->db->prepare($query);
+ $stmt->execute([$addressBookId]);
+
+ $result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
+ }
+ return $result;
+ }
+
+ /**
+ * Adds a change record to the addressbookchanges table.
+ *
+ * @param mixed $addressBookId
+ * @param string $objectUri
+ * @param int $operation 1 = add, 2 = modify, 3 = delete
+ * @return void
+ */
+ protected function addChange($addressBookId, $objectUri, $operation) {
+ $sql = 'INSERT INTO `*PREFIX*addressbookchanges`(`uri`, `synctoken`, `addressbookid`, `operation`) SELECT ?, `synctoken`, ?, ? FROM `*PREFIX*addressbooks` WHERE `id` = ?';
+ $stmt = $this->db->prepare($sql);
+ $stmt->execute([
+ $objectUri,
+ $addressBookId,
+ $operation,
+ $addressBookId
+ ]);
+ $stmt = $this->db->prepare('UPDATE `*PREFIX*addressbooks` SET `synctoken` = `synctoken` + 1 WHERE `id` = ?');
+ $stmt->execute([
+ $addressBookId
+ ]);
+ }
+
+ 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 7de2c2aabe3..850180d8481 100644
--- a/apps/dav/lib/rootcollection.php
+++ b/apps/dav/lib/rootcollection.php
@@ -2,8 +2,10 @@
namespace OCA\DAV;
+use OCA\DAV\CardDAV\CardDavBackend;
use OCA\DAV\Connector\Sabre\Principal;
use Sabre\CalDAV\Principal\Collection;
+use Sabre\CardDAV\AddressBookRoot;
use Sabre\DAV\SimpleCollection;
class RootCollection extends SimpleCollection {
@@ -22,10 +24,14 @@ class RootCollection extends SimpleCollection {
$principalCollection->disableListing = $disableListing;
$filesCollection = new Files\RootCollection($principalBackend);
$filesCollection->disableListing = $disableListing;
+ $cardDavBackend = new CardDavBackend(\OC::$server->getDatabaseConnection());
+ $addressBookRoot = new AddressBookRoot($principalBackend, $cardDavBackend);
+ $addressBookRoot->disableListing = $disableListing;
$children = [
$principalCollection,
$filesCollection,
+ $addressBookRoot,
];
parent::__construct('root', $children);
diff --git a/apps/dav/tests/unit/carddav/carddavbackendtest.php b/apps/dav/tests/unit/carddav/carddavbackendtest.php
new file mode 100644
index 00000000000..f7456e9634c
--- /dev/null
+++ b/apps/dav/tests/unit/carddav/carddavbackendtest.php
@@ -0,0 +1,180 @@
+<?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 OCA\DAV\Tests\Unit\CardDAV;
+
+use OCA\DAV\CardDAV\CardDavBackend;
+use Sabre\DAV\PropPatch;
+use Test\TestCase;
+
+class CardDavBackendTest extends TestCase {
+
+ /** @var CardDavBackend */
+ private $backend;
+
+ const UNIT_TEST_USER = 'carddav-unit-test';
+
+
+ public function setUp() {
+ parent::setUp();
+
+ $db = \OC::$server->getDatabaseConnection();
+ $this->backend = new CardDavBackend($db);
+
+ $this->tearDown();
+ }
+
+ public function tearDown() {
+ parent::tearDown();
+
+ if (is_null($this->backend)) {
+ return;
+ }
+ $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER);
+ foreach ($books as $book) {
+ $this->backend->deleteAddressBook($book['id']);
+ }
+ }
+
+ public function testAddressBookOperations() {
+
+ // create a new address book
+ $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []);
+
+ $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER);
+ $this->assertEquals(1, count($books));
+
+ // update it's display name
+ $patch = new PropPatch([
+ '{DAV:}displayname' => 'Unit test',
+ '{urn:ietf:params:xml:ns:carddav}addressbook-description' => 'Addressbook used for unit testing'
+ ]);
+ $this->backend->updateAddressBook($books[0]['id'], $patch);
+ $patch->commit();
+ $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER);
+ $this->assertEquals(1, count($books));
+ $this->assertEquals('Unit test', $books[0]['{DAV:}displayname']);
+ $this->assertEquals('Addressbook used for unit testing', $books[0]['{urn:ietf:params:xml:ns:carddav}addressbook-description']);
+
+ // delete the address book
+ $this->backend->deleteAddressBook($books[0]['id']);
+ $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER);
+ $this->assertEquals(0, count($books));
+ }
+
+ public function testCardOperations() {
+ // create a new address book
+ $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []);
+ $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER);
+ $this->assertEquals(1, count($books));
+ $bookId = $books[0]['id'];
+
+ // create a card
+ $uri = $this->getUniqueID('card');
+ $this->backend->createCard($bookId, $uri, '');
+
+ // get all the cards
+ $cards = $this->backend->getCards($bookId);
+ $this->assertEquals(1, count($cards));
+ $this->assertEquals('', $cards[0]['carddata']);
+
+ // get the cards
+ $card = $this->backend->getCard($bookId, $uri);
+ $this->assertNotNull($card);
+ $this->assertArrayHasKey('id', $card);
+ $this->assertArrayHasKey('uri', $card);
+ $this->assertArrayHasKey('lastmodified', $card);
+ $this->assertArrayHasKey('etag', $card);
+ $this->assertArrayHasKey('size', $card);
+ $this->assertEquals('', $card['carddata']);
+
+ // update the card
+ $this->backend->updateCard($bookId, $uri, '***');
+ $card = $this->backend->getCard($bookId, $uri);
+ $this->assertEquals('***', $card['carddata']);
+
+ // delete the card
+ $this->backend->deleteCard($bookId, $uri);
+ $cards = $this->backend->getCards($bookId);
+ $this->assertEquals(0, count($cards));
+ }
+
+ public function testMultiCard() {
+ // create a new address book
+ $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []);
+ $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER);
+ $this->assertEquals(1, count($books));
+ $bookId = $books[0]['id'];
+
+ // create a card
+ $uri0 = $this->getUniqueID('card');
+ $this->backend->createCard($bookId, $uri0, '');
+ $uri1 = $this->getUniqueID('card');
+ $this->backend->createCard($bookId, $uri1, '');
+ $uri2 = $this->getUniqueID('card');
+ $this->backend->createCard($bookId, $uri2, '');
+
+ // get all the cards
+ $cards = $this->backend->getCards($bookId);
+ $this->assertEquals(3, count($cards));
+ $this->assertEquals('', $cards[0]['carddata']);
+ $this->assertEquals('', $cards[1]['carddata']);
+ $this->assertEquals('', $cards[2]['carddata']);
+
+ // get the cards
+ $cards = $this->backend->getMultipleCards($bookId, [$uri1, $uri2]);
+ $this->assertEquals(2, count($cards));
+ foreach($cards as $card) {
+ $this->assertArrayHasKey('id', $card);
+ $this->assertArrayHasKey('uri', $card);
+ $this->assertArrayHasKey('lastmodified', $card);
+ $this->assertArrayHasKey('etag', $card);
+ $this->assertArrayHasKey('size', $card);
+ $this->assertEquals('', $card['carddata']);
+ }
+
+ // delete the card
+ $this->backend->deleteCard($bookId, $uri0);
+ $this->backend->deleteCard($bookId, $uri1);
+ $this->backend->deleteCard($bookId, $uri2);
+ $cards = $this->backend->getCards($bookId);
+ $this->assertEquals(0, count($cards));
+ }
+
+ public function testSyncSupport() {
+ // create a new address book
+ $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []);
+ $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER);
+ $this->assertEquals(1, count($books));
+ $bookId = $books[0]['id'];
+
+ // fist call without synctoken
+ $changes = $this->backend->getChangesForAddressBook($bookId, '', 1);
+ $syncToken = $changes['syncToken'];
+
+ // add a change
+ $uri0 = $this->getUniqueID('card');
+ $this->backend->createCard($bookId, $uri0, '');
+
+ // look for changes
+ $changes = $this->backend->getChangesForAddressBook($bookId, $syncToken, 1);
+ $this->assertEquals($uri0, $changes['added'][0]);
+ }
+}
diff --git a/apps/dav/tests/unit/connector/sabre/BlockLegacyClientPluginTest.php b/apps/dav/tests/unit/connector/sabre/BlockLegacyClientPluginTest.php
index 1e390cf15f7..3004c03b266 100644
--- a/apps/dav/tests/unit/connector/sabre/BlockLegacyClientPluginTest.php
+++ b/apps/dav/tests/unit/connector/sabre/BlockLegacyClientPluginTest.php
@@ -19,7 +19,7 @@
*
*/
-namespace Test\Connector\Sabre;
+namespace OCA\DAV\Tests\Unit\Connector\Sabre;
use OCA\DAV\Connector\Sabre\BlockLegacyClientPlugin;
use Test\TestCase;
diff --git a/apps/dav/tests/unit/connector/sabre/DummyGetResponsePluginTest.php b/apps/dav/tests/unit/connector/sabre/DummyGetResponsePluginTest.php
index 1fd89c84ff6..d2d4a849a51 100644
--- a/apps/dav/tests/unit/connector/sabre/DummyGetResponsePluginTest.php
+++ b/apps/dav/tests/unit/connector/sabre/DummyGetResponsePluginTest.php
@@ -19,7 +19,7 @@
*
*/
-namespace Test\Connector\Sabre;
+namespace OCA\DAV\Tests\Unit\Connector\Sabre;
use OCA\DAV\Connector\Sabre\DummyGetResponsePlugin;
use Test\TestCase;
diff --git a/apps/dav/tests/unit/connector/sabre/MaintenancePluginTest.php b/apps/dav/tests/unit/connector/sabre/MaintenancePluginTest.php
index c0acd4fc3de..34fa7f7eef9 100644
--- a/apps/dav/tests/unit/connector/sabre/MaintenancePluginTest.php
+++ b/apps/dav/tests/unit/connector/sabre/MaintenancePluginTest.php
@@ -19,7 +19,7 @@
*
*/
-namespace Test\Connector\Sabre;
+namespace OCA\DAV\Tests\Unit\Connector\Sabre;
use OCA\DAV\Connector\Sabre\MaintenancePlugin;
use Test\TestCase;
diff --git a/apps/dav/tests/unit/connector/sabre/auth.php b/apps/dav/tests/unit/connector/sabre/auth.php
index 0466f3aab77..d18747d732a 100644
--- a/apps/dav/tests/unit/connector/sabre/auth.php
+++ b/apps/dav/tests/unit/connector/sabre/auth.php
@@ -18,7 +18,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
-namespace Tests\Connector\Sabre;
+
+namespace OCA\DAV\Tests\Unit\Connector\Sabre;
use Test\TestCase;
use OCP\ISession;
diff --git a/apps/dav/tests/unit/connector/sabre/copyetagheaderplugintest.php b/apps/dav/tests/unit/connector/sabre/copyetagheaderplugintest.php
index 2080755cd51..74dd4edd8cf 100644
--- a/apps/dav/tests/unit/connector/sabre/copyetagheaderplugintest.php
+++ b/apps/dav/tests/unit/connector/sabre/copyetagheaderplugintest.php
@@ -1,6 +1,6 @@
<?php
-namespace Tests\Connector\Sabre;
+namespace OCA\DAV\Tests\Unit\Connector\Sabre;
/**
* Copyright (c) 2015 Vincent Petry <pvince81@owncloud.com>
diff --git a/apps/dav/tests/unit/connector/sabre/custompropertiesbackend.php b/apps/dav/tests/unit/connector/sabre/custompropertiesbackend.php
index 973a5d4c27b..e1bcc996908 100644
--- a/apps/dav/tests/unit/connector/sabre/custompropertiesbackend.php
+++ b/apps/dav/tests/unit/connector/sabre/custompropertiesbackend.php
@@ -1,6 +1,6 @@
<?php
-namespace Tests\Connector\Sabre;
+namespace OCA\DAV\Tests\Unit\Connector\Sabre;
/**
* Copyright (c) 2015 Vincent Petry <pvince81@owncloud.com>
@@ -16,7 +16,7 @@ class CustomPropertiesBackend extends \Test\TestCase {
private $server;
/**
- * @var \Sabre\DAV\ObjectTree
+ * @var \Sabre\DAV\Tree
*/
private $tree;
diff --git a/apps/dav/tests/unit/connector/sabre/directory.php b/apps/dav/tests/unit/connector/sabre/directory.php
index d85290df80a..148a91d26db 100644
--- a/apps/dav/tests/unit/connector/sabre/directory.php
+++ b/apps/dav/tests/unit/connector/sabre/directory.php
@@ -6,11 +6,14 @@
* later.
* See the COPYING-README file.
*/
-class Test_OC_Connector_Sabre_Directory extends \Test\TestCase {
- /** @var OC\Files\View | PHPUnit_Framework_MockObject_MockObject */
+namespace OCA\DAV\Tests\Unit\Connector\Sabre;
+
+class Directory extends \Test\TestCase {
+
+ /** @var \OC\Files\View | \PHPUnit_Framework_MockObject_MockObject */
private $view;
- /** @var OC\Files\FileInfo | PHPUnit_Framework_MockObject_MockObject */
+ /** @var \OC\Files\FileInfo | \PHPUnit_Framework_MockObject_MockObject */
private $info;
protected function setUp() {
diff --git a/apps/dav/tests/unit/connector/sabre/exception/invalidpathtest.php b/apps/dav/tests/unit/connector/sabre/exception/invalidpathtest.php
index 4c0af58ffea..19e82320d55 100644
--- a/apps/dav/tests/unit/connector/sabre/exception/invalidpathtest.php
+++ b/apps/dav/tests/unit/connector/sabre/exception/invalidpathtest.php
@@ -1,6 +1,6 @@
<?php
-namespace Test\Connector\Sabre\Exception;
+namespace OCA\DAV\Tests\Unit\Connector\Sabre\Exception;
use OCA\DAV\Connector\Sabre\Exception\InvalidPath;
diff --git a/apps/dav/tests/unit/connector/sabre/exceptionloggerplugin.php b/apps/dav/tests/unit/connector/sabre/exceptionloggerplugin.php
index d85aa5a9cc3..0c364df012b 100644
--- a/apps/dav/tests/unit/connector/sabre/exceptionloggerplugin.php
+++ b/apps/dav/tests/unit/connector/sabre/exceptionloggerplugin.php
@@ -7,7 +7,7 @@
* See the COPYING-README file.
*/
-namespace Test\Connector\Sabre;
+namespace OCA\DAV\Tests\Unit\Connector\Sabre;
use OCA\DAV\Connector\Sabre\Exception\InvalidPath;
use OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin as PluginToTest;
diff --git a/apps/dav/tests/unit/connector/sabre/file.php b/apps/dav/tests/unit/connector/sabre/file.php
index d874b7f33c2..94dadf88fe4 100644
--- a/apps/dav/tests/unit/connector/sabre/file.php
+++ b/apps/dav/tests/unit/connector/sabre/file.php
@@ -6,7 +6,7 @@
* See the COPYING-README file.
*/
-namespace Test\Connector\Sabre;
+namespace OCA\DAV\Tests\Unit\Connector\Sabre;
use OC\Files\Storage\Local;
use Test\HookHelper;
diff --git a/apps/dav/tests/unit/connector/sabre/filesplugin.php b/apps/dav/tests/unit/connector/sabre/filesplugin.php
index db3bbabefd0..f3c862941c0 100644
--- a/apps/dav/tests/unit/connector/sabre/filesplugin.php
+++ b/apps/dav/tests/unit/connector/sabre/filesplugin.php
@@ -1,6 +1,6 @@
<?php
-namespace Tests\Connector\Sabre;
+namespace OCA\DAV\Tests\Unit\Connector\Sabre;
/**
* Copyright (c) 2015 Vincent Petry <pvince81@owncloud.com>
diff --git a/apps/dav/tests/unit/connector/sabre/node.php b/apps/dav/tests/unit/connector/sabre/node.php
index a9610fd84b3..cee64fb7dff 100644
--- a/apps/dav/tests/unit/connector/sabre/node.php
+++ b/apps/dav/tests/unit/connector/sabre/node.php
@@ -7,7 +7,7 @@
* See the COPYING-README file.
*/
-namespace Test\Connector\Sabre;
+namespace OCA\DAV\Tests\Unit\Connector\Sabre;
class Node extends \Test\TestCase {
public function davPermissionsProvider() {
diff --git a/apps/dav/tests/unit/connector/sabre/objecttree.php b/apps/dav/tests/unit/connector/sabre/objecttree.php
index 2691385c1c1..3a56404e552 100644
--- a/apps/dav/tests/unit/connector/sabre/objecttree.php
+++ b/apps/dav/tests/unit/connector/sabre/objecttree.php
@@ -6,11 +6,10 @@
* See the COPYING-README file.
*/
-namespace Test\OCA\DAV\Connector\Sabre;
+namespace OCA\DAV\Tests\Unit\Connector\Sabre;
use OC\Files\FileInfo;
-use OCA\DAV\Connector\Sabre\Directory;
use OC\Files\Storage\Temporary;
class TestDoubleFileView extends \OC\Files\View {
@@ -103,7 +102,7 @@ class ObjectTree extends \Test\TestCase {
$info = new FileInfo('', null, null, array(), null);
- $rootDir = new Directory($view, $info);
+ $rootDir = new \OCA\DAV\Connector\Sabre\Directory($view, $info);
$objectTree = $this->getMock('\OCA\DAV\Connector\Sabre\ObjectTree',
array('nodeExists', 'getNodeForPath'),
array($rootDir, $view));
diff --git a/apps/dav/tests/unit/connector/sabre/principal.php b/apps/dav/tests/unit/connector/sabre/principal.php
index 3c0abeac3f1..2fbab124fb7 100644
--- a/apps/dav/tests/unit/connector/sabre/principal.php
+++ b/apps/dav/tests/unit/connector/sabre/principal.php
@@ -8,7 +8,7 @@
* See the COPYING-README file.
*/
-namespace Test\Connector\Sabre;
+namespace OCA\DAV\Tests\Unit\Connector\Sabre;
use \Sabre\DAV\PropPatch;
use OCP\IUserManager;
diff --git a/apps/dav/tests/unit/connector/sabre/quotaplugin.php b/apps/dav/tests/unit/connector/sabre/quotaplugin.php
index 5d3364e1f8c..470fd9cbf85 100644
--- a/apps/dav/tests/unit/connector/sabre/quotaplugin.php
+++ b/apps/dav/tests/unit/connector/sabre/quotaplugin.php
@@ -1,12 +1,13 @@
<?php
+namespace OCA\DAV\Tests\Unit\Connector\Sabre;
/**
* Copyright (c) 2013 Thomas Müller <thomas.mueller@tmit.eu>
* This file is licensed under the Affero General Public License version 3 or
* later.
* See the COPYING-README file.
*/
-class Test_OC_Connector_Sabre_QuotaPlugin extends \Test\TestCase {
+class QuotaPlugin extends \Test\TestCase {
/**
* @var \Sabre\DAV\Server
diff --git a/apps/dav/tests/unit/connector/sabre/tagsplugin.php b/apps/dav/tests/unit/connector/sabre/tagsplugin.php
index 4731e770cfa..f1f6cc40dab 100644
--- a/apps/dav/tests/unit/connector/sabre/tagsplugin.php
+++ b/apps/dav/tests/unit/connector/sabre/tagsplugin.php
@@ -1,6 +1,6 @@
<?php
-namespace Tests\Connector\Sabre;
+namespace OCA\DAV\Tests\Unit\Connector\Sabre;
/**
* Copyright (c) 2014 Vincent Petry <pvince81@owncloud.com>
@@ -20,7 +20,7 @@ class TagsPlugin extends \Test\TestCase {
private $server;
/**
- * @var \Sabre\DAV\ObjectTree
+ * @var \Sabre\DAV\Tree
*/
private $tree;