From 4eb15885c9a7e930670ed58af2e566c1928bc059 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Thu, 5 Nov 2015 16:46:37 +0100 Subject: [PATCH] Addressbook sharing added based on a simplified approach which is based on calendar sharing standard --- apps/dav/appinfo/database.xml | 51 +++++ apps/dav/lib/carddav/addressbook.php | 58 ++++++ apps/dav/lib/carddav/addressbookroot.php | 23 +++ apps/dav/lib/carddav/carddavbackend.php | 159 +++++++++++++- .../carddav/sharing/ishareableaddressbook.php | 46 +++++ apps/dav/lib/carddav/sharing/plugin.php | 194 ++++++++++++++++++ apps/dav/lib/carddav/useraddressbooks.php | 23 +++ apps/dav/lib/rootcollection.php | 6 +- apps/dav/tests/misc/sharing.xml | 7 + .../tests/unit/carddav/carddavbackendtest.php | 43 +++- 10 files changed, 605 insertions(+), 5 deletions(-) create mode 100644 apps/dav/lib/carddav/addressbook.php create mode 100644 apps/dav/lib/carddav/addressbookroot.php create mode 100644 apps/dav/lib/carddav/sharing/ishareableaddressbook.php create mode 100644 apps/dav/lib/carddav/sharing/plugin.php create mode 100644 apps/dav/lib/carddav/useraddressbooks.php create mode 100644 apps/dav/tests/misc/sharing.xml 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)); + + + } }