diff --git a/apps/dav/.gitignore b/apps/dav/.gitignore new file mode 100644 index 0000000000..885b6b3e6d --- /dev/null +++ b/apps/dav/.gitignore @@ -0,0 +1 @@ +tests/travis/CalDAVTester diff --git a/apps/dav/appinfo/database.xml b/apps/dav/appinfo/database.xml new file mode 100644 index 0000000000..f3fd507994 --- /dev/null +++ b/apps/dav/appinfo/database.xml @@ -0,0 +1,186 @@ + + + + + + + *dbprefix*addressbooks + + + + + id + integer + 0 + true + 1 + true + 11 + + + + principaluri + text + + + displayname + text + + + uri + text + + + description + text + + + synctoken + integer + 1 + true + true + + + addressbook_index + true + + principaluri + + + uri + + + +
+ + + + *dbprefix*cards + + + id + integer + 0 + true + 1 + true + 11 + + + addressbookid + integer + 0 + true + + + carddata + blob + + + uri + text + + + lastmodified + integer + true + 11 + + + etag + text + 32 + + + size + integer + true + true + 11 + + +
+ + + + + *dbprefix*addressbookchanges + + + id + integer + 0 + true + 1 + true + 11 + + + uri + text + + + synctoken + integer + 1 + true + true + + + addressbookid + integer + true + + + operation + integer + true + 1 + + + + addressbookid_synctoken + + addressbookid + + + synctoken + + + + +
+ +
diff --git a/apps/dav/appinfo/info.xml b/apps/dav/appinfo/info.xml index 8f378f5e18..1102511569 100644 --- a/apps/dav/appinfo/info.xml +++ b/apps/dav/appinfo/info.xml @@ -5,7 +5,7 @@ ownCloud WebDAV endpoint AGPL owncloud.org - 0.1.1 + 0.1.2 9.0 true diff --git a/apps/dav/appinfo/register_command.php b/apps/dav/appinfo/register_command.php new file mode 100644 index 0000000000..c996dd4406 --- /dev/null +++ b/apps/dav/appinfo/register_command.php @@ -0,0 +1,8 @@ +getDatabaseConnection(); +$userManager = OC::$server->getUserManager(); +/** @var Symfony\Component\Console\Application $application */ +$application->add(new CreateAddressBook($userManager, $dbConnection)); diff --git a/apps/dav/command/createaddressbook.php b/apps/dav/command/createaddressbook.php new file mode 100644 index 0000000000..286871b39e --- /dev/null +++ b/apps/dav/command/createaddressbook.php @@ -0,0 +1,52 @@ +userManager = $userManager; + $this->dbConnection = $dbConnection; + } + + protected function configure() { + $this + ->setName('dav:create-addressbook') + ->setDescription('Create a dav addressbook') + ->addArgument('user', + InputArgument::REQUIRED, + 'User for whom the addressbook will be created') + ->addArgument('name', + InputArgument::REQUIRED, + 'Name of the addressbook'); + } + + protected function execute(InputInterface $input, OutputInterface $output) { + $user = $input->getArgument('user'); + if (!$this->userManager->userExists($user)) { + throw new \InvalidArgumentException("User <$user> in unknown."); + } + $name = $input->getArgument('name'); + $carddav = new CardDavBackend($this->dbConnection); + $carddav->createAddressBook("principals/$user", $name, []); + } +} diff --git a/apps/dav/lib/carddav/carddavbackend.php b/apps/dav/lib/carddav/carddavbackend.php new file mode 100644 index 0000000000..7b16262a68 --- /dev/null +++ b/apps/dav/lib/carddav/carddavbackend.php @@ -0,0 +1,558 @@ + + * + * @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 + * + */ + +namespace OCA\DAV\CardDAV; + +use Sabre\CardDAV\Backend\BackendInterface; +use Sabre\CardDAV\Backend\SyncSupport; +use Sabre\CardDAV\Plugin; +use Sabre\DAV\Exception\BadRequest; + +class CardDavBackend implements BackendInterface, SyncSupport { + + public function __construct(\OCP\IDBConnection $db) { + $this->db = $db; + } + + /** + * Returns the list of addressbooks for a specific user. + * + * Every addressbook should have the following properties: + * id - an arbitrary unique id + * uri - the 'basename' part of the url + * principaluri - Same as the passed parameter + * + * Any additional clark-notation property may be passed besides this. Some + * common ones are : + * {DAV:}displayname + * {urn:ietf:params:xml:ns:carddav}addressbook-description + * {http://calendarserver.org/ns/}getctag + * + * @param string $principalUri + * @return array + */ + function getAddressBooksForUser($principalUri) { + $query = $this->db->getQueryBuilder(); + $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken']) + ->from('addressbooks') + ->where($query->expr()->eq('principaluri', $query->createParameter('principaluri'))) + ->setParameter('principaluri', $principalUri); + + $addressBooks = []; + + $result = $query->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; + } + + /** + * Updates properties for an address book. + * + * The list of mutations is stored in a Sabre\DAV\PropPatch object. + * To do the actual updates, you must tell this object which properties + * you're going to process with the handle() method. + * + * Calling the handle method is like telling the PropPatch object "I + * promise I can handle updating this property". + * + * Read the PropPatch documenation for more info and examples. + * + * @param string $addressBookId + * @param \Sabre\DAV\PropPatch $propPatch + * @return void + */ + function updateAddressBook($addressBookId, \Sabre\DAV\PropPatch $propPatch) { + $supportedProperties = [ + '{DAV:}displayname', + '{' . Plugin::NS_CARDDAV . '}addressbook-description', + ]; + + $propPatch->handle($supportedProperties, function($mutations) use ($addressBookId) { + + $updates = []; + foreach($mutations as $property=>$newValue) { + + switch($property) { + case '{DAV:}displayname' : + $updates['displayname'] = $newValue; + break; + case '{' . Plugin::NS_CARDDAV . '}addressbook-description' : + $updates['description'] = $newValue; + break; + } + } + $query = $this->db->getQueryBuilder(); + $query->update('addressbooks'); + + foreach($updates as $key=>$value) { + $query->set($key, $query->createNamedParameter($value)); + } + $query->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId))) + ->execute(); + + $this->addChange($addressBookId, "", 2); + + return true; + + }); + } + + /** + * Creates a new address book + * + * @param string $principalUri + * @param string $url Just the 'basename' of the url. + * @param array $properties + * @return void + */ + function createAddressBook($principalUri, $url, array $properties) { + $values = [ + 'displayname' => null, + 'description' => null, + 'principaluri' => $principalUri, + 'uri' => $url, + 'synctoken' => 1 + ]; + + foreach($properties as $property=>$newValue) { + + switch($property) { + case '{DAV:}displayname' : + $values['displayname'] = $newValue; + break; + case '{' . Plugin::NS_CARDDAV . '}addressbook-description' : + $values['description'] = $newValue; + break; + default : + throw new BadRequest('Unknown property: ' . $property); + } + + } + + $query = $this->db->getQueryBuilder(); + $query->insert('addressbooks') + ->values([ + 'uri' => $query->createParameter('uri'), + 'displayname' => $query->createParameter('displayname'), + 'description' => $query->createParameter('description'), + 'principaluri' => $query->createParameter('principaluri'), + 'synctoken' => $query->createParameter('synctoken'), + ]) + ->setParameters($values) + ->execute(); + } + + /** + * Deletes an entire addressbook and all its contents + * + * @param mixed $addressBookId + * @return void + */ + function deleteAddressBook($addressBookId) { + $query = $this->db->getQueryBuilder(); + $query->delete('cards') + ->where($query->expr()->eq('addressbookid', $query->createParameter('addressbookid'))) + ->setParameter('addressbookid', $addressBookId) + ->execute(); + + $query->delete('addressbookchanges') + ->where($query->expr()->eq('addressbookid', $query->createParameter('addressbookid'))) + ->setParameter('addressbookid', $addressBookId) + ->execute(); + + $query->delete('addressbooks') + ->where($query->expr()->eq('id', $query->createParameter('id'))) + ->setParameter('id', $addressBookId) + ->execute(); + } + + /** + * Returns all cards for a specific addressbook id. + * + * This method should return the following properties for each card: + * * carddata - raw vcard data + * * uri - Some unique url + * * lastmodified - A unix timestamp + * + * It's recommended to also return the following properties: + * * etag - A unique etag. This must change every time the card changes. + * * size - The size of the card in bytes. + * + * If these last two properties are provided, less time will be spent + * calculating them. If they are specified, you can also ommit carddata. + * This may speed up certain requests, especially with large cards. + * + * @param mixed $addressBookId + * @return array + */ + function getCards($addressBookId) { + $query = $this->db->getQueryBuilder(); + $query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata']) + ->from('cards') + ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId))); + + $cards = []; + + $result = $query->execute(); + while($row = $result->fetch()) { + $row['etag'] = '"' . $row['etag'] . '"'; + $row['carddata'] = $this->readBlob($row['carddata']); + $cards[] = $row; + } + $result->closeCursor(); + + return $cards; + } + + /** + * Returns a specfic card. + * + * The same set of properties must be returned as with getCards. The only + * exception is that 'carddata' is absolutely required. + * + * If the card does not exist, you must return false. + * + * @param mixed $addressBookId + * @param string $cardUri + * @return array + */ + function getCard($addressBookId, $cardUri) { + $query = $this->db->getQueryBuilder(); + $query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata']) + ->from('cards') + ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId))) + ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($cardUri))) + ->setMaxResults(1); + + $result = $query->execute(); + $row = $result->fetch(); + if (!$row) { + return false; + } + $row['etag'] = '"' . $row['etag'] . '"'; + $row['carddata'] = $this->readBlob($row['carddata']); + + return $row; + } + + /** + * Returns a list of cards. + * + * This method should work identical to getCard, but instead return all the + * cards in the list as an array. + * + * If the backend supports this, it may allow for some speed-ups. + * + * @param mixed $addressBookId + * @param array $uris + * @return array + */ + function getMultipleCards($addressBookId, array $uris) { + $query = $this->db->getQueryBuilder(); + $query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata']) + ->from('cards') + ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId))) + ->andWhere($query->expr()->in('uri', $query->createParameter('uri'))) + ->setParameter('uri', $uris, \Doctrine\DBAL\Connection::PARAM_STR_ARRAY); + + $cards = []; + + $result = $query->execute(); + while($row = $result->fetch()) { + $row['etag'] = '"' . $row['etag'] . '"'; + $row['carddata'] = $this->readBlob($row['carddata']); + $cards[] = $row; + } + $result->closeCursor(); + + return $cards; + } + + /** + * Creates a new card. + * + * The addressbook id will be passed as the first argument. This is the + * same id as it is returned from the getAddressBooksForUser method. + * + * The cardUri is a base uri, and doesn't include the full path. The + * cardData argument is the vcard body, and is passed as a string. + * + * It is possible to return an ETag from this method. This ETag is for the + * newly created resource, and must be enclosed with double quotes (that + * is, the string itself must contain the double quotes). + * + * You should only return the ETag if you store the carddata as-is. If a + * subsequent GET request on the same card does not have the same body, + * byte-by-byte and you did return an ETag here, clients tend to get + * confused. + * + * If you don't return an ETag, you can just return null. + * + * @param mixed $addressBookId + * @param string $cardUri + * @param string $cardData + * @return string|null + */ + function createCard($addressBookId, $cardUri, $cardData) { + $etag = md5($cardData); + + $query = $this->db->getQueryBuilder(); + $query->insert('cards') + ->values([ + 'carddata' => $query->createNamedParameter($cardData), + 'uri' => $query->createNamedParameter($cardUri), + 'lastmodified' => $query->createNamedParameter(time()), + 'addressbookid' => $query->createNamedParameter($addressBookId), + 'size' => $query->createNamedParameter(strlen($cardData)), + 'etag' => $query->createNamedParameter($etag), + ]) + ->execute(); + + $this->addChange($addressBookId, $cardUri, 1); + + return '"' . $etag . '"'; + } + + /** + * Updates a card. + * + * The addressbook id will be passed as the first argument. This is the + * same id as it is returned from the getAddressBooksForUser method. + * + * The cardUri is a base uri, and doesn't include the full path. The + * cardData argument is the vcard body, and is passed as a string. + * + * It is possible to return an ETag from this method. This ETag should + * match that of the updated resource, and must be enclosed with double + * quotes (that is: the string itself must contain the actual quotes). + * + * You should only return the ETag if you store the carddata as-is. If a + * subsequent GET request on the same card does not have the same body, + * byte-by-byte and you did return an ETag here, clients tend to get + * confused. + * + * If you don't return an ETag, you can just return null. + * + * @param mixed $addressBookId + * @param string $cardUri + * @param string $cardData + * @return string|null + */ + function updateCard($addressBookId, $cardUri, $cardData) { + + $etag = md5($cardData); + $query = $this->db->getQueryBuilder(); + $query->update('cards') + ->set('carddata', $query->createNamedParameter($cardData, \PDO::PARAM_LOB)) + ->set('lastmodified', $query->createNamedParameter(time())) + ->set('size', $query->createNamedParameter(strlen($cardData))) + ->set('etag', $query->createNamedParameter($etag)) + ->where($query->expr()->eq('uri', $query->createNamedParameter($cardUri))) + ->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId))) + ->execute(); + + $this->addChange($addressBookId, $cardUri, 2); + + return '"' . $etag . '"'; + } + + /** + * Deletes a card + * + * @param mixed $addressBookId + * @param string $cardUri + * @return bool + */ + function deleteCard($addressBookId, $cardUri) { + $query = $this->db->getQueryBuilder(); + $ret = $query->delete('cards') + ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId))) + ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($cardUri))) + ->execute(); + + $this->addChange($addressBookId, $cardUri, 3); + + return $ret === 1; + } + + /** + * The getChanges method returns all the changes that have happened, since + * the specified syncToken in the specified address book. + * + * This function should return an array, such as the following: + * + * [ + * 'syncToken' => 'The current synctoken', + * 'added' => [ + * 'new.txt', + * ], + * 'modified' => [ + * 'modified.txt', + * ], + * 'deleted' => [ + * 'foo.php.bak', + * 'old.txt' + * ] + * ]; + * + * The returned syncToken property should reflect the *current* syncToken + * of the calendar, as reported in the {http://sabredav.org/ns}sync-token + * property. This is needed here too, to ensure the operation is atomic. + * + * If the $syncToken argument is specified as null, this is an initial + * sync, and all members should be reported. + * + * The modified property is an array of nodenames that have changed since + * the last token. + * + * The deleted property is an array with nodenames, that have been deleted + * from collection. + * + * The $syncLevel argument is basically the 'depth' of the report. If it's + * 1, you only have to report changes that happened only directly in + * immediate descendants. If it's 2, it should also include changes from + * the nodes below the child collections. (grandchildren) + * + * The $limit argument allows a client to specify how many results should + * be returned at most. If the limit is not specified, it should be treated + * as infinite. + * + * If the limit (infinite or not) is higher than you're willing to return, + * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception. + * + * If the syncToken is expired (due to data cleanup) or unknown, you must + * return null. + * + * The limit is 'suggestive'. You are free to ignore it. + * + * @param string $addressBookId + * @param string $syncToken + * @param int $syncLevel + * @param int $limit + * @return array + */ + function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, $limit = null) { + // Current synctoken + $stmt = $this->db->prepare('SELECT `synctoken` FROM `*PREFIX*addressbooks` WHERE `id` = ?'); + $stmt->execute([ $addressBookId ]); + $currentToken = $stmt->fetchColumn(0); + + if (is_null($currentToken)) return null; + + $result = [ + 'syncToken' => $currentToken, + 'added' => [], + 'modified' => [], + 'deleted' => [], + ]; + + if ($syncToken) { + + $query = "SELECT `uri`, `operation` FROM `*PREFIX*addressbookchanges` WHERE `synctoken` >= ? AND `synctoken` < ? AND `addressbookid` = ? ORDER BY `synctoken`"; + if ($limit>0) { + $query .= " `LIMIT` " . (int)$limit; + } + + // Fetching all changes + $stmt = $this->db->prepare($query); + $stmt->execute([$syncToken, $currentToken, $addressBookId]); + + $changes = []; + + // This loop ensures that any duplicates are overwritten, only the + // last change on a node is relevant. + while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + + $changes[$row['uri']] = $row['operation']; + + } + + foreach($changes as $uri => $operation) { + + switch($operation) { + case 1: + $result['added'][] = $uri; + break; + case 2: + $result['modified'][] = $uri; + break; + case 3: + $result['deleted'][] = $uri; + break; + } + + } + } else { + // No synctoken supplied, this is the initial sync. + $query = "SELECT `uri` FROM `*PREFIX*cards` WHERE `addressbookid` = ?"; + $stmt = $this->db->prepare($query); + $stmt->execute([$addressBookId]); + + $result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN); + } + return $result; + } + + /** + * Adds a change record to the addressbookchanges table. + * + * @param mixed $addressBookId + * @param string $objectUri + * @param int $operation 1 = add, 2 = modify, 3 = delete + * @return void + */ + protected function addChange($addressBookId, $objectUri, $operation) { + $sql = 'INSERT INTO `*PREFIX*addressbookchanges`(`uri`, `synctoken`, `addressbookid`, `operation`) SELECT ?, `synctoken`, ?, ? FROM `*PREFIX*addressbooks` WHERE `id` = ?'; + $stmt = $this->db->prepare($sql); + $stmt->execute([ + $objectUri, + $addressBookId, + $operation, + $addressBookId + ]); + $stmt = $this->db->prepare('UPDATE `*PREFIX*addressbooks` SET `synctoken` = `synctoken` + 1 WHERE `id` = ?'); + $stmt->execute([ + $addressBookId + ]); + } + + private function readBlob($cardData) { + if (is_resource($cardData)) { + return stream_get_contents($cardData); + } + + return $cardData; + } + +} diff --git a/apps/dav/lib/rootcollection.php b/apps/dav/lib/rootcollection.php index 7de2c2aabe..850180d848 100644 --- a/apps/dav/lib/rootcollection.php +++ b/apps/dav/lib/rootcollection.php @@ -2,8 +2,10 @@ namespace OCA\DAV; +use OCA\DAV\CardDAV\CardDavBackend; use OCA\DAV\Connector\Sabre\Principal; use Sabre\CalDAV\Principal\Collection; +use Sabre\CardDAV\AddressBookRoot; use Sabre\DAV\SimpleCollection; class RootCollection extends SimpleCollection { @@ -22,10 +24,14 @@ class RootCollection extends SimpleCollection { $principalCollection->disableListing = $disableListing; $filesCollection = new Files\RootCollection($principalBackend); $filesCollection->disableListing = $disableListing; + $cardDavBackend = new CardDavBackend(\OC::$server->getDatabaseConnection()); + $addressBookRoot = new AddressBookRoot($principalBackend, $cardDavBackend); + $addressBookRoot->disableListing = $disableListing; $children = [ $principalCollection, $filesCollection, + $addressBookRoot, ]; parent::__construct('root', $children); diff --git a/apps/dav/tests/unit/bootstrap.php b/apps/dav/tests/unit/bootstrap.php new file mode 100644 index 0000000000..28f6b971de --- /dev/null +++ b/apps/dav/tests/unit/bootstrap.php @@ -0,0 +1,13 @@ + + * + * @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 + * + */ +namespace OCA\DAV\Tests\Unit\CardDAV; + +use OCA\DAV\CardDAV\CardDavBackend; +use Sabre\DAV\PropPatch; +use Test\TestCase; + +class CardDavBackendTest extends TestCase { + + /** @var CardDavBackend */ + private $backend; + + const UNIT_TEST_USER = 'carddav-unit-test'; + + + public function setUp() { + parent::setUp(); + + $db = \OC::$server->getDatabaseConnection(); + $this->backend = new CardDavBackend($db); + + $this->tearDown(); + } + + public function tearDown() { + parent::tearDown(); + + if (is_null($this->backend)) { + return; + } + $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER); + foreach ($books as $book) { + $this->backend->deleteAddressBook($book['id']); + } + } + + public function testAddressBookOperations() { + + // create a new address book + $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []); + + $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER); + $this->assertEquals(1, count($books)); + + // update it's display name + $patch = new PropPatch([ + '{DAV:}displayname' => 'Unit test', + '{urn:ietf:params:xml:ns:carddav}addressbook-description' => 'Addressbook used for unit testing' + ]); + $this->backend->updateAddressBook($books[0]['id'], $patch); + $patch->commit(); + $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER); + $this->assertEquals(1, count($books)); + $this->assertEquals('Unit test', $books[0]['{DAV:}displayname']); + $this->assertEquals('Addressbook used for unit testing', $books[0]['{urn:ietf:params:xml:ns:carddav}addressbook-description']); + + // delete the address book + $this->backend->deleteAddressBook($books[0]['id']); + $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER); + $this->assertEquals(0, count($books)); + } + + public function testCardOperations() { + // create a new address book + $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []); + $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER); + $this->assertEquals(1, count($books)); + $bookId = $books[0]['id']; + + // create a card + $uri = $this->getUniqueID('card'); + $this->backend->createCard($bookId, $uri, ''); + + // get all the cards + $cards = $this->backend->getCards($bookId); + $this->assertEquals(1, count($cards)); + $this->assertEquals('', $cards[0]['carddata']); + + // get the cards + $card = $this->backend->getCard($bookId, $uri); + $this->assertNotNull($card); + $this->assertArrayHasKey('id', $card); + $this->assertArrayHasKey('uri', $card); + $this->assertArrayHasKey('lastmodified', $card); + $this->assertArrayHasKey('etag', $card); + $this->assertArrayHasKey('size', $card); + $this->assertEquals('', $card['carddata']); + + // update the card + $this->backend->updateCard($bookId, $uri, '***'); + $card = $this->backend->getCard($bookId, $uri); + $this->assertEquals('***', $card['carddata']); + + // delete the card + $this->backend->deleteCard($bookId, $uri); + $cards = $this->backend->getCards($bookId); + $this->assertEquals(0, count($cards)); + } + + public function testMultiCard() { + // create a new address book + $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []); + $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER); + $this->assertEquals(1, count($books)); + $bookId = $books[0]['id']; + + // create a card + $uri0 = $this->getUniqueID('card'); + $this->backend->createCard($bookId, $uri0, ''); + $uri1 = $this->getUniqueID('card'); + $this->backend->createCard($bookId, $uri1, ''); + $uri2 = $this->getUniqueID('card'); + $this->backend->createCard($bookId, $uri2, ''); + + // get all the cards + $cards = $this->backend->getCards($bookId); + $this->assertEquals(3, count($cards)); + $this->assertEquals('', $cards[0]['carddata']); + $this->assertEquals('', $cards[1]['carddata']); + $this->assertEquals('', $cards[2]['carddata']); + + // get the cards + $cards = $this->backend->getMultipleCards($bookId, [$uri1, $uri2]); + $this->assertEquals(2, count($cards)); + foreach($cards as $card) { + $this->assertArrayHasKey('id', $card); + $this->assertArrayHasKey('uri', $card); + $this->assertArrayHasKey('lastmodified', $card); + $this->assertArrayHasKey('etag', $card); + $this->assertArrayHasKey('size', $card); + $this->assertEquals('', $card['carddata']); + } + + // delete the card + $this->backend->deleteCard($bookId, $uri0); + $this->backend->deleteCard($bookId, $uri1); + $this->backend->deleteCard($bookId, $uri2); + $cards = $this->backend->getCards($bookId); + $this->assertEquals(0, count($cards)); + } + + public function testSyncSupport() { + // create a new address book + $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []); + $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER); + $this->assertEquals(1, count($books)); + $bookId = $books[0]['id']; + + // fist call without synctoken + $changes = $this->backend->getChangesForAddressBook($bookId, '', 1); + $syncToken = $changes['syncToken']; + + // add a change + $uri0 = $this->getUniqueID('card'); + $this->backend->createCard($bookId, $uri0, ''); + + // look for changes + $changes = $this->backend->getChangesForAddressBook($bookId, $syncToken, 1); + $this->assertEquals($uri0, $changes['added'][0]); + } +} diff --git a/apps/dav/tests/unit/connector/sabre/BlockLegacyClientPluginTest.php b/apps/dav/tests/unit/connector/sabre/BlockLegacyClientPluginTest.php index 1e390cf15f..3004c03b26 100644 --- a/apps/dav/tests/unit/connector/sabre/BlockLegacyClientPluginTest.php +++ b/apps/dav/tests/unit/connector/sabre/BlockLegacyClientPluginTest.php @@ -19,7 +19,7 @@ * */ -namespace Test\Connector\Sabre; +namespace OCA\DAV\Tests\Unit\Connector\Sabre; use OCA\DAV\Connector\Sabre\BlockLegacyClientPlugin; use Test\TestCase; diff --git a/apps/dav/tests/unit/connector/sabre/DummyGetResponsePluginTest.php b/apps/dav/tests/unit/connector/sabre/DummyGetResponsePluginTest.php index 1fd89c84ff..d2d4a849a5 100644 --- a/apps/dav/tests/unit/connector/sabre/DummyGetResponsePluginTest.php +++ b/apps/dav/tests/unit/connector/sabre/DummyGetResponsePluginTest.php @@ -19,7 +19,7 @@ * */ -namespace Test\Connector\Sabre; +namespace OCA\DAV\Tests\Unit\Connector\Sabre; use OCA\DAV\Connector\Sabre\DummyGetResponsePlugin; use Test\TestCase; diff --git a/apps/dav/tests/unit/connector/sabre/MaintenancePluginTest.php b/apps/dav/tests/unit/connector/sabre/MaintenancePluginTest.php index c0acd4fc3d..34fa7f7eef 100644 --- a/apps/dav/tests/unit/connector/sabre/MaintenancePluginTest.php +++ b/apps/dav/tests/unit/connector/sabre/MaintenancePluginTest.php @@ -19,7 +19,7 @@ * */ -namespace Test\Connector\Sabre; +namespace OCA\DAV\Tests\Unit\Connector\Sabre; use OCA\DAV\Connector\Sabre\MaintenancePlugin; use Test\TestCase; diff --git a/apps/dav/tests/unit/connector/sabre/auth.php b/apps/dav/tests/unit/connector/sabre/auth.php index 0466f3aab7..d18747d732 100644 --- a/apps/dav/tests/unit/connector/sabre/auth.php +++ b/apps/dav/tests/unit/connector/sabre/auth.php @@ -18,7 +18,8 @@ * along with this program. If not, see * */ -namespace Tests\Connector\Sabre; + +namespace OCA\DAV\Tests\Unit\Connector\Sabre; use Test\TestCase; use OCP\ISession; diff --git a/apps/dav/tests/unit/connector/sabre/copyetagheaderplugintest.php b/apps/dav/tests/unit/connector/sabre/copyetagheaderplugintest.php index 2080755cd5..74dd4edd8c 100644 --- a/apps/dav/tests/unit/connector/sabre/copyetagheaderplugintest.php +++ b/apps/dav/tests/unit/connector/sabre/copyetagheaderplugintest.php @@ -1,6 +1,6 @@ diff --git a/apps/dav/tests/unit/connector/sabre/custompropertiesbackend.php b/apps/dav/tests/unit/connector/sabre/custompropertiesbackend.php index 973a5d4c27..e1bcc99690 100644 --- a/apps/dav/tests/unit/connector/sabre/custompropertiesbackend.php +++ b/apps/dav/tests/unit/connector/sabre/custompropertiesbackend.php @@ -1,6 +1,6 @@ @@ -16,7 +16,7 @@ class CustomPropertiesBackend extends \Test\TestCase { private $server; /** - * @var \Sabre\DAV\ObjectTree + * @var \Sabre\DAV\Tree */ private $tree; diff --git a/apps/dav/tests/unit/connector/sabre/directory.php b/apps/dav/tests/unit/connector/sabre/directory.php index d85290df80..148a91d26d 100644 --- a/apps/dav/tests/unit/connector/sabre/directory.php +++ b/apps/dav/tests/unit/connector/sabre/directory.php @@ -6,11 +6,14 @@ * later. * See the COPYING-README file. */ -class Test_OC_Connector_Sabre_Directory extends \Test\TestCase { - /** @var OC\Files\View | PHPUnit_Framework_MockObject_MockObject */ +namespace OCA\DAV\Tests\Unit\Connector\Sabre; + +class Directory extends \Test\TestCase { + + /** @var \OC\Files\View | \PHPUnit_Framework_MockObject_MockObject */ private $view; - /** @var OC\Files\FileInfo | PHPUnit_Framework_MockObject_MockObject */ + /** @var \OC\Files\FileInfo | \PHPUnit_Framework_MockObject_MockObject */ private $info; protected function setUp() { diff --git a/apps/dav/tests/unit/connector/sabre/exception/invalidpathtest.php b/apps/dav/tests/unit/connector/sabre/exception/invalidpathtest.php index 4c0af58ffe..19e82320d5 100644 --- a/apps/dav/tests/unit/connector/sabre/exception/invalidpathtest.php +++ b/apps/dav/tests/unit/connector/sabre/exception/invalidpathtest.php @@ -1,6 +1,6 @@ diff --git a/apps/dav/tests/unit/connector/sabre/node.php b/apps/dav/tests/unit/connector/sabre/node.php index a9610fd84b..cee64fb7df 100644 --- a/apps/dav/tests/unit/connector/sabre/node.php +++ b/apps/dav/tests/unit/connector/sabre/node.php @@ -7,7 +7,7 @@ * See the COPYING-README file. */ -namespace Test\Connector\Sabre; +namespace OCA\DAV\Tests\Unit\Connector\Sabre; class Node extends \Test\TestCase { public function davPermissionsProvider() { diff --git a/apps/dav/tests/unit/connector/sabre/objecttree.php b/apps/dav/tests/unit/connector/sabre/objecttree.php index 2691385c1c..3a56404e55 100644 --- a/apps/dav/tests/unit/connector/sabre/objecttree.php +++ b/apps/dav/tests/unit/connector/sabre/objecttree.php @@ -6,11 +6,10 @@ * See the COPYING-README file. */ -namespace Test\OCA\DAV\Connector\Sabre; +namespace OCA\DAV\Tests\Unit\Connector\Sabre; use OC\Files\FileInfo; -use OCA\DAV\Connector\Sabre\Directory; use OC\Files\Storage\Temporary; class TestDoubleFileView extends \OC\Files\View { @@ -103,7 +102,7 @@ class ObjectTree extends \Test\TestCase { $info = new FileInfo('', null, null, array(), null); - $rootDir = new Directory($view, $info); + $rootDir = new \OCA\DAV\Connector\Sabre\Directory($view, $info); $objectTree = $this->getMock('\OCA\DAV\Connector\Sabre\ObjectTree', array('nodeExists', 'getNodeForPath'), array($rootDir, $view)); diff --git a/apps/dav/tests/unit/connector/sabre/principal.php b/apps/dav/tests/unit/connector/sabre/principal.php index 3c0abeac3f..2fbab124fb 100644 --- a/apps/dav/tests/unit/connector/sabre/principal.php +++ b/apps/dav/tests/unit/connector/sabre/principal.php @@ -8,7 +8,7 @@ * See the COPYING-README file. */ -namespace Test\Connector\Sabre; +namespace OCA\DAV\Tests\Unit\Connector\Sabre; use \Sabre\DAV\PropPatch; use OCP\IUserManager; diff --git a/apps/dav/tests/unit/connector/sabre/quotaplugin.php b/apps/dav/tests/unit/connector/sabre/quotaplugin.php index 5d3364e1f8..470fd9cbf8 100644 --- a/apps/dav/tests/unit/connector/sabre/quotaplugin.php +++ b/apps/dav/tests/unit/connector/sabre/quotaplugin.php @@ -1,12 +1,13 @@ * This file is licensed under the Affero General Public License version 3 or * later. * See the COPYING-README file. */ -class Test_OC_Connector_Sabre_QuotaPlugin extends \Test\TestCase { +class QuotaPlugin extends \Test\TestCase { /** * @var \Sabre\DAV\Server diff --git a/apps/dav/tests/unit/connector/sabre/tagsplugin.php b/apps/dav/tests/unit/connector/sabre/tagsplugin.php index 4731e770cf..f1f6cc40da 100644 --- a/apps/dav/tests/unit/connector/sabre/tagsplugin.php +++ b/apps/dav/tests/unit/connector/sabre/tagsplugin.php @@ -1,6 +1,6 @@ @@ -20,7 +20,7 @@ class TagsPlugin extends \Test\TestCase { private $server; /** - * @var \Sabre\DAV\ObjectTree + * @var \Sabre\DAV\Tree */ private $tree; diff --git a/apps/dav/tests/unit/phpunit.xml b/apps/dav/tests/unit/phpunit.xml new file mode 100644 index 0000000000..46c3cdfb34 --- /dev/null +++ b/apps/dav/tests/unit/phpunit.xml @@ -0,0 +1,25 @@ + + + + . + + + + + ../../dav + + ../../dav/tests + + + + + + + + +