diff --git a/apps/dav/appinfo/database.xml b/apps/dav/appinfo/database.xml
index 5e2dad097e..48641c2be6 100644
--- a/apps/dav/appinfo/database.xml
+++ b/apps/dav/appinfo/database.xml
@@ -570,4 +570,55 @@ CREATE TABLE calendarobjects (
+
+
+ *dbprefix*dav_shares
+
+
+ id
+ integer
+ 0
+ true
+ 1
+ true
+ 11
+
+
+ uri
+ text
+
+
+ principaluri
+ text
+
+
+ type
+ text
+
+
+ access
+ integer
+ 1
+
+
+ resourceid
+ integer
+ true
+ true
+
+
+ dav_shares_index
+ true
+
+ principaluri
+
+
+ uri
+
+
+ type
+
+
+
+
diff --git a/apps/dav/lib/carddav/addressbook.php b/apps/dav/lib/carddav/addressbook.php
new file mode 100644
index 0000000000..e50f6f4adf
--- /dev/null
+++ b/apps/dav/lib/carddav/addressbook.php
@@ -0,0 +1,58 @@
+carddavBackend;
+ $carddavBackend->updateShares($this->getName(), $add, $remove);
+ }
+
+ /**
+ * Returns the list of people whom this addressbook is shared with.
+ *
+ * Every element in this array should have the following properties:
+ * * href - Often a mailto: address
+ * * commonName - Optional, for example a first + last name
+ * * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants.
+ * * readOnly - boolean
+ * * summary - Optional, a description for the share
+ *
+ * @return array
+ */
+ function getShares() {
+ /** @var CardDavBackend $carddavBackend */
+ $carddavBackend = $this->carddavBackend;
+ $carddavBackend->getShares($this->getName());
+ }
+}
\ No newline at end of file
diff --git a/apps/dav/lib/carddav/addressbookroot.php b/apps/dav/lib/carddav/addressbookroot.php
new file mode 100644
index 0000000000..ee99ac8d79
--- /dev/null
+++ b/apps/dav/lib/carddav/addressbookroot.php
@@ -0,0 +1,23 @@
+carddavBackend, $principal['uri']);
+
+ }
+
+}
\ No newline at end of file
diff --git a/apps/dav/lib/carddav/carddavbackend.php b/apps/dav/lib/carddav/carddavbackend.php
index b2597baedc..3af7057161 100644
--- a/apps/dav/lib/carddav/carddavbackend.php
+++ b/apps/dav/lib/carddav/carddavbackend.php
@@ -22,6 +22,7 @@
namespace OCA\DAV\CardDAV;
+use OCA\DAV\Connector\Sabre\Principal;
use Sabre\CardDAV\Backend\BackendInterface;
use Sabre\CardDAV\Backend\SyncSupport;
use Sabre\CardDAV\Plugin;
@@ -29,8 +30,12 @@ use Sabre\DAV\Exception\BadRequest;
class CardDavBackend implements BackendInterface, SyncSupport {
- public function __construct(\OCP\IDBConnection $db) {
+ /** @var Principal */
+ private $principalBackend;
+
+ public function __construct(\OCP\IDBConnection $db, Principal $principalBackend) {
$this->db = $db;
+ $this->principalBackend = $principalBackend;
}
/**
@@ -73,9 +78,61 @@ class CardDavBackend implements BackendInterface, SyncSupport {
}
$result->closeCursor();
+ // query for shared calendars
+ $query = $this->db->getQueryBuilder();
+ $query2 = $this->db->getQueryBuilder();
+ $query2->select(['resourceid'])
+ ->from('dav_shares')
+ ->where($query2->expr()->eq('principaluri', $query2->createParameter('principaluri')))
+ ->andWhere($query2->expr()->eq('type', $query2->createParameter('type')));
+ $result = $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
+ ->from('addressbooks')
+ ->where($query->expr()->in('id', $query->createFunction($query2->getSQL())))
+ ->setParameter('type', 'addressbook')
+ ->setParameter('principaluri', $principalUri)
+ ->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;
}
+ private function getAddressBooksByUri($addressBookUri) {
+ $query = $this->db->getQueryBuilder();
+ $result = $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
+ ->from('addressbooks')
+ ->where($query->expr()->eq('uri', $query->createNamedParameter($addressBookUri)))
+ ->setMaxResults(1)
+ ->execute();
+
+ $row = $result->fetch();
+ if (is_null($row)) {
+ return null;
+ }
+ $result->closeCursor();
+
+ return [
+ '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',
+ ];
+ }
+
/**
* Updates properties for an address book.
*
@@ -201,6 +258,11 @@ class CardDavBackend implements BackendInterface, SyncSupport {
->where($query->expr()->eq('id', $query->createParameter('id')))
->setParameter('id', $addressBookId)
->execute();
+
+ $query->delete('dav_shares')
+ ->where($query->expr()->eq('resourceid', $query->createNamedParameter($addressBookId)))
+ ->andWhere($query->expr()->eq('type', $query->createNamedParameter('addressbook')))
+ ->execute();
}
/**
@@ -561,4 +623,99 @@ class CardDavBackend implements BackendInterface, SyncSupport {
return $cardData;
}
+ public function updateShares($path, $add, $remove) {
+ foreach($add as $element) {
+ $this->shareWith($path, $element);
+ }
+ foreach($remove as $element) {
+ $this->unshare($path, $element);
+ }
+ }
+
+ private function shareWith($addressBookUri, $element) {
+ $user = $element['href'];
+ $parts = explode(':', $user, 2);
+ if ($parts[0] !== 'principal') {
+ return;
+ }
+ $p = $this->principalBackend->getPrincipalByPath($parts[1]);
+ if (is_null($p)) {
+ return;
+ }
+
+ $addressbook = $this->getAddressBooksByUri($addressBookUri);
+ if (is_null($addressbook)) {
+ return;
+ }
+
+ $query = $this->db->getQueryBuilder();
+ $query->insert('dav_shares')
+ ->values([
+ 'principaluri' => $query->createNamedParameter($parts[1]),
+ 'uri' => $query->createNamedParameter($addressBookUri),
+ 'type' => $query->createNamedParameter('addressbook'),
+ 'access' => $query->createNamedParameter(0),
+ 'resourceid' => $query->createNamedParameter($addressbook['id'])
+ ]);
+ $query->execute();
+ }
+
+ private function unshare($addressBookUri, $element) {
+ $user = $element['href'];
+ $parts = explode(':', $user, 2);
+ if ($parts[0] !== 'principal') {
+ return;
+ }
+ $p = $this->principalBackend->getPrincipalByPath($parts[1]);
+ if (is_null($p)) {
+ return;
+ }
+
+ $addressbook = $this->getAddressBooksByUri($addressBookUri);
+ if (is_null($addressbook)) {
+ return;
+ }
+
+ $query = $this->db->getQueryBuilder();
+ $query->delete('dav_shares')
+ ->where($query->expr()->eq('resourceid', $query->createNamedParameter($addressbook['id'])))
+ ->andWhere($query->expr()->eq('type', $query->createNamedParameter('addressbook')))
+ ->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($parts[1])))
+ ;
+ $query->execute();
+ }
+
+ /**
+ * Returns the list of people whom this addressbook is shared with.
+ *
+ * Every element in this array should have the following properties:
+ * * href - Often a mailto: address
+ * * commonName - Optional, for example a first + last name
+ * * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants.
+ * * readOnly - boolean
+ * * summary - Optional, a description for the share
+ *
+ * @return array
+ */
+ public function getShares($addressBookUri) {
+ $query = $this->db->getQueryBuilder();
+ $result = $query->select(['principaluri', 'access'])
+ ->from('dav_shares')
+ ->where($query->expr()->eq('uri', $query->createNamedParameter($addressBookUri)))
+ ->andWhere($query->expr()->eq('type', $query->createNamedParameter('addressbook')))
+ ->execute();
+
+ $shares = [];
+ while($row = $result->fetch()) {
+ $p = $this->principalBackend->getPrincipalByPath($row['principaluri']);
+ $shares[]= [
+ 'href' => "principal:${p['uri']}",
+ 'commonName' => isset($p['{DAV:}displayname']) ? $p['{DAV:}displayname'] : '',
+ 'status' => 1,
+ 'readOnly' => ($row['access'] === 1)
+ ];
+ }
+
+ return $shares;
+ }
}
diff --git a/apps/dav/lib/carddav/sharing/ishareableaddressbook.php b/apps/dav/lib/carddav/sharing/ishareableaddressbook.php
new file mode 100644
index 0000000000..856a9ed18e
--- /dev/null
+++ b/apps/dav/lib/carddav/sharing/ishareableaddressbook.php
@@ -0,0 +1,46 @@
+server = $server;
+ $server->resourceTypeMapping['OCA\\DAV\CardDAV\\ISharedAddressbook'] = '{' . \Sabre\CardDAV\Plugin::NS_CARDDAV . '}shared';
+
+ $this->server->on('method:POST', [$this, 'httpPost']);
+ }
+
+ /**
+ * We intercept this to handle POST requests on calendars.
+ *
+ * @param RequestInterface $request
+ * @param ResponseInterface $response
+ * @return null|bool
+ */
+ function httpPost(RequestInterface $request, ResponseInterface $response) {
+
+ $path = $request->getPath();
+
+ // Only handling xml
+ $contentType = $request->getHeader('Content-Type');
+ if (strpos($contentType, 'application/xml') === false && strpos($contentType, 'text/xml') === false)
+ return;
+
+ // Making sure the node exists
+ try {
+ $node = $this->server->tree->getNodeForPath($path);
+ } catch (NotFound $e) {
+ return;
+ }
+
+ $requestBody = $request->getBodyAsString();
+
+ // If this request handler could not deal with this POST request, it
+ // will return 'null' and other plugins get a chance to handle the
+ // request.
+ //
+ // However, we already requested the full body. This is a problem,
+ // because a body can only be read once. This is why we preemptively
+ // re-populated the request body with the existing data.
+ $request->setBody($requestBody);
+
+ $dom = XMLUtil::loadDOMDocument($requestBody);
+
+ $documentType = XMLUtil::toClarkNotation($dom->firstChild);
+
+ switch ($documentType) {
+
+ // Dealing with the 'share' document, which modified invitees on a
+ // calendar.
+ case '{' . \Sabre\CardDAV\Plugin::NS_CARDDAV . '}share' :
+
+ // We can only deal with IShareableCalendar objects
+ if (!$node instanceof IShareableAddressBook) {
+ return;
+ }
+
+ $this->server->transactionType = 'post-calendar-share';
+
+ // Getting ACL info
+ $acl = $this->server->getPlugin('acl');
+
+ // If there's no ACL support, we allow everything
+ if ($acl) {
+ $acl->checkPrivileges($path, '{DAV:}write');
+ }
+
+ $mutations = $this->parseShareRequest($dom);
+
+ $node->updateShares($mutations[0], $mutations[1]);
+
+ $response->setStatus(200);
+ // Adding this because sending a response body may cause issues,
+ // and I wanted some type of indicator the response was handled.
+ $response->setHeader('X-Sabre-Status', 'everything-went-well');
+
+ // Breaking the event chain
+ return false;
+ }
+ }
+
+ /**
+ * Parses the 'share' POST request.
+ *
+ * This method returns an array, containing two arrays.
+ * The first array is a list of new sharees. Every element is a struct
+ * containing a:
+ * * href element. (usually a mailto: address)
+ * * commonName element (often a first and lastname, but can also be
+ * false)
+ * * readOnly (true or false)
+ * * summary (A description of the share, can also be false)
+ *
+ * The second array is a list of sharees that are to be removed. This is
+ * just a simple array with 'hrefs'.
+ *
+ * @param \DOMDocument $dom
+ * @return array
+ */
+ function parseShareRequest(\DOMDocument $dom) {
+
+ $xpath = new \DOMXPath($dom);
+ $xpath->registerNamespace('cs', \Sabre\CardDAV\Plugin::NS_CARDDAV);
+ $xpath->registerNamespace('d', 'urn:DAV');
+
+ $set = [];
+ $elems = $xpath->query('cs:set');
+
+ for ($i = 0; $i < $elems->length; $i++) {
+
+ $xset = $elems->item($i);
+ $set[] = [
+ 'href' => $xpath->evaluate('string(d:href)', $xset),
+ 'commonName' => $xpath->evaluate('string(cs:common-name)', $xset),
+ 'summary' => $xpath->evaluate('string(cs:summary)', $xset),
+ 'readOnly' => $xpath->evaluate('boolean(cs:read)', $xset) !== false
+ ];
+
+ }
+
+ $remove = [];
+ $elems = $xpath->query('cs:remove');
+
+ for ($i = 0; $i < $elems->length; $i++) {
+
+ $xremove = $elems->item($i);
+ $remove[] = $xpath->evaluate('string(d:href)', $xremove);
+
+ }
+
+ return [$set, $remove];
+
+ }
+
+
+}
diff --git a/apps/dav/lib/carddav/useraddressbooks.php b/apps/dav/lib/carddav/useraddressbooks.php
new file mode 100644
index 0000000000..adbb0292fa
--- /dev/null
+++ b/apps/dav/lib/carddav/useraddressbooks.php
@@ -0,0 +1,23 @@
+carddavBackend->getAddressBooksForUser($this->principalUri);
+ $objs = [];
+ foreach($addressbooks as $addressbook) {
+ $objs[] = new AddressBook($this->carddavBackend, $addressbook);
+ }
+ return $objs;
+
+ }
+
+}
diff --git a/apps/dav/lib/rootcollection.php b/apps/dav/lib/rootcollection.php
index 10baff072c..672e0a9868 100644
--- a/apps/dav/lib/rootcollection.php
+++ b/apps/dav/lib/rootcollection.php
@@ -3,11 +3,11 @@
namespace OCA\DAV;
use OCA\DAV\CalDAV\CalDavBackend;
+use OCA\DAV\CardDAV\AddressBookRoot;
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;
class RootCollection extends SimpleCollection {
@@ -30,7 +30,9 @@ class RootCollection extends SimpleCollection {
$caldavBackend = new CalDavBackend($db);
$calendarRoot = new CalendarRoot($principalBackend, $caldavBackend);
$calendarRoot->disableListing = $disableListing;
- $cardDavBackend = new CardDavBackend($db);
+
+ $cardDavBackend = new CardDavBackend(\OC::$server->getDatabaseConnection(), $principalBackend);
+
$addressBookRoot = new AddressBookRoot($principalBackend, $cardDavBackend);
$addressBookRoot->disableListing = $disableListing;
diff --git a/apps/dav/tests/misc/sharing.xml b/apps/dav/tests/misc/sharing.xml
new file mode 100644
index 0000000000..8771256ce7
--- /dev/null
+++ b/apps/dav/tests/misc/sharing.xml
@@ -0,0 +1,7 @@
+
+
+
+ principal:principals/admin
+
+
+
diff --git a/apps/dav/tests/unit/carddav/carddavbackendtest.php b/apps/dav/tests/unit/carddav/carddavbackendtest.php
index 79ef36d809..d76db5a91e 100644
--- a/apps/dav/tests/unit/carddav/carddavbackendtest.php
+++ b/apps/dav/tests/unit/carddav/carddavbackendtest.php
@@ -24,6 +24,13 @@ use OCA\DAV\CardDAV\CardDavBackend;
use Sabre\DAV\PropPatch;
use Test\TestCase;
+/**
+ * Class CardDavBackendTest
+ *
+ * @group DB
+ *
+ * @package OCA\DAV\Tests\Unit\CardDAV
+ */
class CardDavBackendTest extends TestCase {
/** @var CardDavBackend */
@@ -31,12 +38,20 @@ class CardDavBackendTest extends TestCase {
const UNIT_TEST_USER = 'carddav-unit-test';
-
public function setUp() {
parent::setUp();
+ $principal = $this->getMockBuilder('OCA\DAV\Connector\Sabre\Principal')
+ ->disableOriginalConstructor()
+ ->setMethods(['getPrincipalByPath'])
+ ->getMock();
+ $principal->method('getPrincipalByPath')
+ ->willReturn([
+ 'uri' => 'principals/best-friend'
+ ]);
+
$db = \OC::$server->getDatabaseConnection();
- $this->backend = new CardDavBackend($db);
+ $this->backend = new CardDavBackend($db, $principal);
$this->tearDown();
}
@@ -178,4 +193,28 @@ class CardDavBackendTest extends TestCase {
$changes = $this->backend->getChangesForAddressBook($bookId, $syncToken, 1);
$this->assertEquals($uri0, $changes['added'][0]);
}
+
+ public function testSharing() {
+ $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []);
+ $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER);
+ $this->assertEquals(1, count($books));
+
+ $this->backend->updateShares('Example', [['href' => 'principal:principals/best-friend']], []);
+
+ $shares = $this->backend->getShares('Example');
+ $this->assertEquals(1, count($shares));
+
+ $books = $this->backend->getAddressBooksForUser('principals/best-friend');
+ $this->assertEquals(1, count($books));
+
+ $this->backend->updateShares('Example', [], [['href' => 'principal:principals/best-friend']]);
+
+ $shares = $this->backend->getShares('Example');
+ $this->assertEquals(0, count($shares));
+
+ $books = $this->backend->getAddressBooksForUser('principals/best-friend');
+ $this->assertEquals(0, count($books));
+
+
+ }
}