@@ -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> |
@@ -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()); | |||
} | |||
} |
@@ -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']); | |||
} | |||
} |
@@ -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; | |||
} | |||
} |
@@ -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(); | |||
} |
@@ -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]; | |||
} | |||
} |
@@ -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; | |||
} | |||
} |
@@ -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; | |||
@@ -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> |
@@ -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)); | |||
} | |||
} |