@@ -19,6 +19,21 @@ | |||
* | |||
*/ | |||
use OCA\DAV\CardDAV\CardDavBackend; | |||
use OCA\DAV\CardDAV\SyncService; | |||
use OCA\DAV\Connector\Sabre\Principal; | |||
\OC::$server->registerService('CardDAVSyncService', function() { | |||
$principalBackend = new Principal( | |||
$this->config, | |||
$this->userManager | |||
); | |||
$backend = new CardDavBackend($this->dbConnection, $principalBackend); | |||
return new SyncService($backend); | |||
}); | |||
$cm = \OC::$server->getContactsManager(); | |||
$cm->register(function() use ($cm) { | |||
$userId = \OC::$server->getUserSession()->getUser()->getUID(); |
@@ -37,9 +37,6 @@ class CardDavBackend implements BackendInterface, SyncSupport { | |||
/** @var Principal */ | |||
private $principalBackend; | |||
/** @var ILogger */ | |||
private $logger; | |||
/** @var string */ | |||
private $dbCardsTable = 'cards'; | |||
@@ -59,12 +56,10 @@ class CardDavBackend implements BackendInterface, SyncSupport { | |||
* | |||
* @param IDBConnection $db | |||
* @param Principal $principalBackend | |||
* @param ILogger $logger | |||
*/ | |||
public function __construct(IDBConnection $db, Principal $principalBackend, ILogger $logger) { | |||
public function __construct(IDBConnection $db, Principal $principalBackend) { | |||
$this->db = $db; | |||
$this->principalBackend = $principalBackend; | |||
$this->logger = $logger; | |||
} | |||
/** |
@@ -0,0 +1,155 @@ | |||
<?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\DAV\Client; | |||
use Sabre\DAV\Xml\Response\MultiStatus; | |||
use Sabre\DAV\Xml\Service; | |||
class SyncService { | |||
/** @var CardDavBackend */ | |||
private $backend; | |||
public function __construct(CardDavBackend $backend) { | |||
$this->backend = $backend; | |||
} | |||
public function syncRemoteAddressBook($url, $userName, $sharedSecret, $syncToken, $targetBookId, $targetPrincipal, $targetProperties) { | |||
// 1. create addressbook | |||
$book = $this->ensureSystemAddressBookExists($targetPrincipal, $targetBookId, $targetProperties); | |||
$addressBookId = $book['id']; | |||
// 2. query changes | |||
$response = $this->requestSyncReport($url, $userName, $sharedSecret, $syncToken); | |||
// 3. apply changes | |||
// TODO: use multi-get for download | |||
foreach ($response['response'] as $resource => $status) { | |||
$cardUri = basename($resource); | |||
if (isset($status[200])) { | |||
$vCard = $this->download($url, $sharedSecret, $resource); | |||
$existingCard = $this->backend->getCard($addressBookId, $cardUri); | |||
if ($existingCard === false) { | |||
$this->backend->createCard($addressBookId, $cardUri, $vCard['body']); | |||
} else { | |||
$this->backend->updateCard($addressBookId, $cardUri, $vCard['body']); | |||
} | |||
} else { | |||
$this->backend->deleteCard($addressBookId, $cardUri); | |||
} | |||
} | |||
return $response['token']; | |||
} | |||
protected function ensureSystemAddressBookExists($principal, $id, $properties) { | |||
$book = $this->backend->getAddressBooksByUri($id); | |||
if (!is_null($book)) { | |||
return $book; | |||
} | |||
$this->backend->createAddressBook($principal, $id, $properties); | |||
return $this->backend->getAddressBooksByUri($id); | |||
} | |||
/** | |||
* @param string $url | |||
* @param string $userName | |||
* @param string $sharedSecret | |||
* @param string $syncToken | |||
* @return array | |||
*/ | |||
private function requestSyncReport($url, $userName, $sharedSecret, $syncToken) { | |||
$settings = [ | |||
'baseUri' => $url, | |||
'userName' => $userName, | |||
'password' => $sharedSecret, | |||
]; | |||
$client = new Client($settings); | |||
$client->setThrowExceptions(true); | |||
$addressBookUrl = "/remote.php/dav/addressbooks/system/system/system"; | |||
$body = $this->buildSyncCollectionRequestBody($syncToken); | |||
$response = $client->request('REPORT', $addressBookUrl, $body, [ | |||
'Content-Type' => 'application/xml' | |||
]); | |||
// if ((int)$response->getStatus() >= 400) { | |||
// throw new Exception('HTTP error: ' . $response->getStatus()); | |||
// } | |||
$result = $this->parseMultiStatus($response['body']); | |||
return $result; | |||
} | |||
private function download($url, $sharedSecret, $changeSet) { | |||
$settings = [ | |||
'baseUri' => $url, | |||
'userName' => 'system', | |||
'password' => $sharedSecret, | |||
]; | |||
$client = new Client($settings); | |||
$client->setThrowExceptions(true); | |||
$response = $client->request('GET', $changeSet); | |||
return $response; | |||
} | |||
function buildSyncCollectionRequestBody($synToken) { | |||
$dom = new \DOMDocument('1.0', 'UTF-8'); | |||
$dom->formatOutput = true; | |||
$root = $dom->createElementNS('DAV:', 'd:sync-collection'); | |||
$sync = $dom->createElement('d:sync-token', $synToken); | |||
$prop = $dom->createElement('d:prop'); | |||
$cont = $dom->createElement('d:getcontenttype'); | |||
$etag = $dom->createElement('d:getetag'); | |||
$prop->appendChild($cont); | |||
$prop->appendChild($etag); | |||
$root->appendChild($sync); | |||
$root->appendChild($prop); | |||
$dom->appendChild($root); | |||
$body = $dom->saveXML(); | |||
return $body; | |||
} | |||
private function parseMultiStatus($body) { | |||
$xml = new Service(); | |||
/** @var MultiStatus $multiStatus */ | |||
$multiStatus = $xml->expect('{DAV:}multistatus', $body); | |||
$result = []; | |||
foreach ($multiStatus->getResponses() as $response) { | |||
$result[$response->getHref()] = $response->getResponseProperties(); | |||
} | |||
return ['response' => $result, 'token' => $multiStatus->getSyncToken()]; | |||
} | |||
} |
@@ -46,11 +46,11 @@ class RootCollection extends SimpleCollection { | |||
\OC::$server->getSystemTagObjectMapper() | |||
); | |||
$usersCardDavBackend = new CardDavBackend($db, $userPrincipalBackend, \OC::$server->getLogger()); | |||
$usersCardDavBackend = new CardDavBackend($db, $userPrincipalBackend); | |||
$usersAddressBookRoot = new AddressBookRoot($userPrincipalBackend, $usersCardDavBackend, 'principals/users'); | |||
$usersAddressBookRoot->disableListing = $disableListing; | |||
$systemCardDavBackend = new CardDavBackend($db, $userPrincipalBackend, \OC::$server->getLogger()); | |||
$systemCardDavBackend = new CardDavBackend($db, $userPrincipalBackend); | |||
$systemAddressBookRoot = new AddressBookRoot(new SystemPrincipalBackend(), $systemCardDavBackend, 'principals/system'); | |||
$systemAddressBookRoot->disableListing = $disableListing; | |||
@@ -45,9 +45,6 @@ class CardDavBackendTest extends TestCase { | |||
/** @var Principal | \PHPUnit_Framework_MockObject_MockObject */ | |||
private $principal; | |||
/** @var ILogger | \PHPUnit_Framework_MockObject_MockObject */ | |||
private $logger; | |||
/** @var IDBConnection */ | |||
private $db; | |||
@@ -70,11 +67,10 @@ class CardDavBackendTest extends TestCase { | |||
->willReturn([ | |||
'uri' => 'principals/best-friend' | |||
]); | |||
$this->logger = $this->getMock('\OCP\ILogger'); | |||
$this->db = \OC::$server->getDatabaseConnection(); | |||
$this->backend = new CardDavBackend($this->db, $this->principal, $this->logger); | |||
$this->backend = new CardDavBackend($this->db, $this->principal); | |||
// start every test with a empty cards_properties and cards table | |||
$query = $this->db->getQueryBuilder(); | |||
@@ -129,7 +125,7 @@ class CardDavBackendTest extends TestCase { | |||
/** @var CardDavBackend | \PHPUnit_Framework_MockObject_MockObject $backend */ | |||
$backend = $this->getMockBuilder('OCA\DAV\CardDAV\CardDavBackend') | |||
->setConstructorArgs([$this->db, $this->principal, $this->logger]) | |||
->setConstructorArgs([$this->db, $this->principal]) | |||
->setMethods(['updateProperties', 'purgeProperties'])->getMock(); | |||
// create a new address book | |||
@@ -175,7 +171,7 @@ class CardDavBackendTest extends TestCase { | |||
public function testMultiCard() { | |||
$this->backend = $this->getMockBuilder('OCA\DAV\CardDAV\CardDavBackend') | |||
->setConstructorArgs([$this->db, $this->principal, $this->logger]) | |||
->setConstructorArgs([$this->db, $this->principal]) | |||
->setMethods(['updateProperties'])->getMock(); | |||
// create a new address book | |||
@@ -222,7 +218,7 @@ class CardDavBackendTest extends TestCase { | |||
public function testSyncSupport() { | |||
$this->backend = $this->getMockBuilder('OCA\DAV\CardDAV\CardDavBackend') | |||
->setConstructorArgs([$this->db, $this->principal, $this->logger]) | |||
->setConstructorArgs([$this->db, $this->principal]) | |||
->setMethods(['updateProperties'])->getMock(); | |||
// create a new address book | |||
@@ -279,7 +275,7 @@ class CardDavBackendTest extends TestCase { | |||
$cardId = 2; | |||
$backend = $this->getMockBuilder('OCA\DAV\CardDAV\CardDavBackend') | |||
->setConstructorArgs([$this->db, $this->principal, $this->logger]) | |||
->setConstructorArgs([$this->db, $this->principal]) | |||
->setMethods(['getCardId'])->getMock(); | |||
$backend->expects($this->any())->method('getCardId')->willReturn($cardId); |
@@ -52,9 +52,8 @@ | |||
</field> | |||
<field> | |||
<name>sync_token</name> | |||
<type>integer</type> | |||
<notnull>true</notnull> | |||
<default>0</default> | |||
<type>text</type> | |||
<length>512</length> | |||
<comments>cardDav sync token</comments> | |||
</field> | |||
<index> |
@@ -5,7 +5,7 @@ | |||
<description>ownCloud Federation allows you to connect with other trusted ownClouds to exchange the user directory. For example this will be used to auto-complete external users for federated sharing.</description> | |||
<licence>AGPL</licence> | |||
<author>Bjoern Schiessle</author> | |||
<version>0.0.1</version> | |||
<version>0.0.2</version> | |||
<namespace>Federation</namespace> | |||
<category>other</category> | |||
<dependencies> |
@@ -0,0 +1,9 @@ | |||
<?php | |||
$config = \OC::$server->getConfig(); | |||
$dbConnection = \OC::$server->getDatabaseConnection(); | |||
$userManager = OC::$server->getUserManager(); | |||
$config = \OC::$server->getConfig(); | |||
/** @var Symfony\Component\Console\Application $application */ | |||
$application->add(new \OCA\Federation\Command\SyncFederationAddressBooks($userManager, $dbConnection, $config)); |
@@ -0,0 +1,75 @@ | |||
<?php | |||
namespace OCA\Federation\Command; | |||
use OCA\DAV\CardDAV\SyncService; | |||
use OCA\Federation\DbHandler; | |||
use OCA\Federation\TrustedServers; | |||
use OCP\IConfig; | |||
use OCP\IDBConnection; | |||
use OCP\IUserManager; | |||
use Symfony\Component\Console\Command\Command; | |||
use Symfony\Component\Console\Helper\ProgressBar; | |||
use Symfony\Component\Console\Input\InputArgument; | |||
use Symfony\Component\Console\Input\InputInterface; | |||
use Symfony\Component\Console\Output\OutputInterface; | |||
class SyncFederationAddressBooks extends Command { | |||
/** @var \OCP\IDBConnection */ | |||
protected $dbConnection; | |||
/** @var SyncService */ | |||
private $syncService; | |||
/** | |||
* @param IUserManager $userManager | |||
* @param IDBConnection $dbConnection | |||
* @param IConfig $config | |||
*/ | |||
function __construct(IDBConnection $dbConnection) { | |||
parent::__construct(); | |||
$this->syncService = \OC::$server->query('CardDAVSyncService'); | |||
$this->dbConnection = $dbConnection; | |||
} | |||
protected function configure() { | |||
$this | |||
->setName('federation:sync-addressbooks') | |||
->setDescription('Synchronizes addressbooks of all federated clouds'); | |||
} | |||
/** | |||
* @param InputInterface $input | |||
* @param OutputInterface $output | |||
*/ | |||
protected function execute(InputInterface $input, OutputInterface $output) { | |||
$progress = new ProgressBar($output); | |||
$progress->start(); | |||
$db = new DbHandler($this->dbConnection, null); | |||
$trustedServers = $db->getAllServer(); | |||
foreach ($trustedServers as $trustedServer) { | |||
$progress->advance(); | |||
$url = $trustedServer['url']; | |||
$sharedSecret = $trustedServer['shared_secret']; | |||
$syncToken = $trustedServer['sync_token']; | |||
if (is_null($sharedSecret)) { | |||
continue; | |||
} | |||
$targetBookId = md5($url); | |||
$targetPrincipal = "principals/system/system"; | |||
$targetBookProperties = [ | |||
'{DAV:}displayname' => $url | |||
]; | |||
$newToken = $this->syncService->syncRemoteAddressBook($url, 'system', $sharedSecret, $syncToken, $targetPrincipal, $targetBookId, $targetBookProperties); | |||
if ($newToken !== $syncToken) { | |||
$db->setServerStatus($url, TrustedServers::STATUS_OK, $newToken); | |||
} | |||
} | |||
$progress->finish(); | |||
$output->writeln(''); | |||
} | |||
} |
@@ -207,14 +207,15 @@ class DbHandler { | |||
* @param string $url | |||
* @param int $status | |||
*/ | |||
public function setServerStatus($url, $status) { | |||
public function setServerStatus($url, $status, $token = null) { | |||
$hash = $this->hash($url); | |||
$query = $this->connection->getQueryBuilder(); | |||
$query->update($this->dbTable) | |||
->set('status', $query->createParameter('status')) | |||
->where($query->expr()->eq('url_hash', $query->createParameter('url_hash'))) | |||
->setParameter('url_hash', $hash) | |||
->setParameter('status', $status); | |||
->set('status', $query->createNamedParameter($status)) | |||
->where($query->expr()->eq('url_hash', $query->createNamedParameter($hash))); | |||
if (!is_null($token)) { | |||
$query->set('sync_token', $query->createNamedParameter($token)); | |||
} | |||
$query->execute(); | |||
} | |||