diff options
author | Thomas Müller <thomas.mueller@tmit.eu> | 2015-11-05 16:46:37 +0100 |
---|---|---|
committer | Thomas Müller <thomas.mueller@tmit.eu> | 2015-11-19 11:34:31 +0100 |
commit | 4eb15885c9a7e930670ed58af2e566c1928bc059 (patch) | |
tree | 14f21c884aad92fba5fe00fc31429b90eca42d08 | |
parent | 58eaeb267c60ed2aed4b5592a28d16f5453bb773 (diff) | |
download | nextcloud-server-4eb15885c9a7e930670ed58af2e566c1928bc059.tar.gz nextcloud-server-4eb15885c9a7e930670ed58af2e566c1928bc059.zip |
Addressbook sharing added based on a simplified approach which is based on calendar sharing standard
-rw-r--r-- | apps/dav/appinfo/database.xml | 51 | ||||
-rw-r--r-- | apps/dav/lib/carddav/addressbook.php | 58 | ||||
-rw-r--r-- | apps/dav/lib/carddav/addressbookroot.php | 23 | ||||
-rw-r--r-- | apps/dav/lib/carddav/carddavbackend.php | 159 | ||||
-rw-r--r-- | apps/dav/lib/carddav/sharing/ishareableaddressbook.php | 46 | ||||
-rw-r--r-- | apps/dav/lib/carddav/sharing/plugin.php | 194 | ||||
-rw-r--r-- | apps/dav/lib/carddav/useraddressbooks.php | 23 | ||||
-rw-r--r-- | apps/dav/lib/rootcollection.php | 6 | ||||
-rw-r--r-- | apps/dav/tests/misc/sharing.xml | 7 | ||||
-rw-r--r-- | apps/dav/tests/unit/carddav/carddavbackendtest.php | 43 |
10 files changed, 605 insertions, 5 deletions
diff --git a/apps/dav/appinfo/database.xml b/apps/dav/appinfo/database.xml index 5e2dad097e4..48641c2be6f 100644 --- a/apps/dav/appinfo/database.xml +++ b/apps/dav/appinfo/database.xml @@ -570,4 +570,55 @@ CREATE TABLE calendarobjects ( </declaration> </table> + + <table> + <name>*dbprefix*dav_shares</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>type</name> + <type>text</type> + </field> + <field> + <name>access</name> + <type>integer</type> + <length>1</length> + </field> + <field> + <name>resourceid</name> + <type>integer</type> + <notnull>true</notnull> + <unsigned>true</unsigned> + </field> + <index> + <name>dav_shares_index</name> + <unique>true</unique> + <field> + <name>principaluri</name> + </field> + <field> + <name>uri</name> + </field> + <field> + <name>type</name> + </field> + </index> + </declaration> + </table> </database> diff --git a/apps/dav/lib/carddav/addressbook.php b/apps/dav/lib/carddav/addressbook.php new file mode 100644 index 00000000000..e50f6f4adf6 --- /dev/null +++ b/apps/dav/lib/carddav/addressbook.php @@ -0,0 +1,58 @@ +<?php + +namespace OCA\DAV\CardDAV; + +use OCA\DAV\CardDAV\Sharing\IShareableAddressBook; +use OCP\IUserManager; + +class AddressBook extends \Sabre\CardDAV\AddressBook implements IShareableAddressBook { + + /** @var IUserManager */ + private $userManager; + + public function __construct(CardDavBackend $carddavBackend, array $addressBookInfo) { + parent::__construct($carddavBackend, $addressBookInfo); + } + + /** + * Updates the list of shares. + * + * The first array is a list of people that are to be added to the + * addressbook. + * + * Every element in the add array has the following properties: + * * href - A url. Usually a mailto: address + * * commonName - Usually a first and last name, or false + * * summary - A description of the share, can also be false + * * readOnly - A boolean value + * + * Every element in the remove array is just the address string. + * + * @param array $add + * @param array $remove + * @return void + */ + function updateShares(array $add, array $remove) { + /** @var CardDavBackend $carddavBackend */ + $carddavBackend = $this->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 00000000000..ee99ac8d798 --- /dev/null +++ b/apps/dav/lib/carddav/addressbookroot.php @@ -0,0 +1,23 @@ +<?php + +namespace OCA\DAV\CardDAV; + +class AddressBookRoot extends \Sabre\CardDAV\AddressBookRoot { + + /** + * This method returns a node for a principal. + * + * The passed array contains principal information, and is guaranteed to + * at least contain a uri item. Other properties may or may not be + * supplied by the authentication backend. + * + * @param array $principal + * @return \Sabre\DAV\INode + */ + function getChildForPrincipal(array $principal) { + + return new UserAddressBooks($this->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 b2597baedc6..3af70571610 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 00000000000..856a9ed18e6 --- /dev/null +++ b/apps/dav/lib/carddav/sharing/ishareableaddressbook.php @@ -0,0 +1,46 @@ +<?php + +namespace OCA\DAV\CardDAV\Sharing; +use Sabre\CardDAV\IAddressBook; + +/** + * This interface represents a Calendar that can be shared with other users. + * + */ +interface IShareableAddressBook extends IAddressBook { + + /** + * Updates the list of shares. + * + * The first array is a list of people that are to be added to the + * addressbook. + * + * Every element in the add array has the following properties: + * * href - A url. Usually a mailto: address + * * commonName - Usually a first and last name, or false + * * summary - A description of the share, can also be false + * * readOnly - A boolean value + * + * Every element in the remove array is just the address string. + * + * @param array $add + * @param array $remove + * @return void + */ + function updateShares(array $add, array $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(); + +}
\ No newline at end of file diff --git a/apps/dav/lib/carddav/sharing/plugin.php b/apps/dav/lib/carddav/sharing/plugin.php new file mode 100644 index 00000000000..edc1a5fc117 --- /dev/null +++ b/apps/dav/lib/carddav/sharing/plugin.php @@ -0,0 +1,194 @@ +<?php + +namespace OCA\DAV\CardDAV\Sharing; + +use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\Server; +use Sabre\DAV\ServerPlugin; +use Sabre\DAV\XMLUtil; +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; + +class Plugin extends ServerPlugin { + + /** + * Reference to SabreDAV server object. + * + * @var \Sabre\DAV\Server + */ + protected $server; + + /** + * This method should return a list of server-features. + * + * This is for example 'versioning' and is added to the DAV: header + * in an OPTIONS response. + * + * @return array + */ + function getFeatures() { + + return ['oc-addressbook-sharing']; + + } + + /** + * Returns a plugin name. + * + * Using this name other plugins will be able to access other plugins + * using Sabre\DAV\Server::getPlugin + * + * @return string + */ + function getPluginName() { + + return 'carddav-sharing'; + + } + + /** + * This initializes the plugin. + * + * This function is called by Sabre\DAV\Server, after + * addPlugin is called. + * + * This method should set up the required event subscriptions. + * + * @param Server $server + * @return void + */ + function initialize(Server $server) { + $this->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 00000000000..adbb0292fa7 --- /dev/null +++ b/apps/dav/lib/carddav/useraddressbooks.php @@ -0,0 +1,23 @@ +<?php + +namespace OCA\DAV\CardDAV; + +class UserAddressBooks extends \Sabre\CardDAV\UserAddressBooks { + + /** + * Returns a list of addressbooks + * + * @return array + */ + function getChildren() { + + $addressbooks = $this->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 10baff072cc..672e0a98684 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 00000000000..8771256ce79 --- /dev/null +++ b/apps/dav/tests/misc/sharing.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8" ?> + <CS:share xmlns:D="DAV:" xmlns:CS="urn:ietf:params:xml:ns:carddav"> + <CS:set> + <D:href>principal:principals/admin</D:href> + <CS:read-write /> + </CS:set> + </CS:share> diff --git a/apps/dav/tests/unit/carddav/carddavbackendtest.php b/apps/dav/tests/unit/carddav/carddavbackendtest.php index 79ef36d8097..d76db5a91e2 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)); + + + } } |