diff --git a/apps/dav/appinfo/app.php b/apps/dav/appinfo/app.php new file mode 100644 index 0000000000..950754ee94 --- /dev/null +++ b/apps/dav/appinfo/app.php @@ -0,0 +1,42 @@ + + * + * @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 + * + */ + +$cm = \OC::$server->getContactsManager(); +$cm->register(function() use ($cm) { + $db = \OC::$server->getDatabaseConnection(); + $userId = \OC::$server->getUserSession()->getUser()->getUID(); + $principal = new \OCA\DAV\Connector\Sabre\Principal( + \OC::$server->getConfig(), + \OC::$server->getUserManager() + ); + $cardDav = new \OCA\DAV\CardDAV\CardDavBackend($db, $principal, \OC::$server->getLogger()); + $addressBooks = $cardDav->getAddressBooksForUser("principals/$userId"); + foreach ($addressBooks as $addressBookInfo) { + $addressBook = new \OCA\DAV\CardDAV\AddressBook($cardDav, $addressBookInfo); + $cm->registerAddressBook( + new OCA\DAV\CardDAV\AddressBookImpl( + $addressBook, + $addressBookInfo, + $cardDav + ) + ); + } +}); diff --git a/apps/dav/appinfo/database.xml b/apps/dav/appinfo/database.xml index 48641c2be6..50c8aa7d8c 100644 --- a/apps/dav/appinfo/database.xml +++ b/apps/dav/appinfo/database.xml @@ -571,6 +571,78 @@ CREATE TABLE calendarobjects ( + + *dbprefix*cards_properties + + + id + integer + 0 + true + 1 + true + 11 + + + addressbookid + integer + + true + 11 + + + cardid + integer + + true + true + 11 + + + name + text + + false + 64 + + + value + text + + false + 255 + + + preferred + integer + 1 + true + 4 + + + card_contactid_index + + cardid + ascending + + + + card_name_index + + name + ascending + + + + card_value_index + + value + ascending + + + +
+ *dbprefix*dav_shares diff --git a/apps/dav/appinfo/register_command.php b/apps/dav/appinfo/register_command.php index af41036cdd..603832e0c4 100644 --- a/apps/dav/appinfo/register_command.php +++ b/apps/dav/appinfo/register_command.php @@ -8,8 +8,9 @@ $config = \OC::$server->getConfig(); $dbConnection = \OC::$server->getDatabaseConnection(); $userManager = OC::$server->getUserManager(); $config = \OC::$server->getConfig(); +$logger = \OC::$server->getLogger(); /** @var Symfony\Component\Console\Application $application */ -$application->add(new CreateAddressBook($userManager, $dbConnection, $config)); +$application->add(new CreateAddressBook($userManager, $dbConnection, $config, $logger)); $application->add(new CreateCalendar($userManager, $dbConnection)); $application->add(new SyncSystemAddressBook($userManager, $dbConnection, $config)); diff --git a/apps/dav/command/createaddressbook.php b/apps/dav/command/createaddressbook.php index ea89e7aa0a..7b70cea7f8 100644 --- a/apps/dav/command/createaddressbook.php +++ b/apps/dav/command/createaddressbook.php @@ -6,6 +6,7 @@ use OCA\DAV\CardDAV\CardDavBackend; use OCA\DAV\Connector\Sabre\Principal; use OCP\IConfig; use OCP\IDBConnection; +use OCP\ILogger; use OCP\IUserManager; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; @@ -23,15 +24,25 @@ class CreateAddressBook extends Command { /** @var IConfig */ private $config; + /** @var ILogger */ + private $logger; + /** * @param IUserManager $userManager * @param IDBConnection $dbConnection + * @param IConfig $config + * @param ILogger $logger */ - function __construct(IUserManager $userManager, IDBConnection $dbConnection, IConfig $config) { + function __construct(IUserManager $userManager, + IDBConnection $dbConnection, + IConfig $config, + ILogger $logger + ) { parent::__construct(); $this->userManager = $userManager; $this->dbConnection = $dbConnection; $this->config = $config; + $this->logger = $logger; } protected function configure() { diff --git a/apps/dav/lib/carddav/addressbookimpl.php b/apps/dav/lib/carddav/addressbookimpl.php new file mode 100644 index 0000000000..838ef5aec6 --- /dev/null +++ b/apps/dav/lib/carddav/addressbookimpl.php @@ -0,0 +1,219 @@ + + * @author Björn Schießle + * + * @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 OCP\Constants; +use OCP\IAddressBook; +use Sabre\VObject\Component\VCard; +use Sabre\VObject\Property\Text; +use Sabre\VObject\Reader; +use Sabre\VObject\UUIDUtil; + +class AddressBookImpl implements IAddressBook { + + /** @var CardDavBackend */ + private $backend; + + /** @var array */ + private $addressBookInfo; + + /** @var AddressBook */ + private $addressBook; + + /** + * AddressBookImpl constructor. + * + * @param AddressBook $addressBook + * @param array $addressBookInfo + * @param CardDavBackend $backend + */ + public function __construct( + AddressBook $addressBook, + array $addressBookInfo, + CardDavBackend $backend) { + + $this->addressBook = $addressBook; + $this->addressBookInfo = $addressBookInfo; + $this->backend = $backend; + } + + /** + * @return string defining the technical unique key + * @since 5.0.0 + */ + public function getKey() { + return $this->addressBookInfo['id']; + } + + /** + * In comparison to getKey() this function returns a human readable (maybe translated) name + * + * @return mixed + * @since 5.0.0 + */ + public function getDisplayName() { + return $this->addressBookInfo['{DAV:}displayname']; + } + + /** + * @param string $pattern which should match within the $searchProperties + * @param array $searchProperties defines the properties within the query pattern should match + * @param array $options - for future use. One should always have options! + * @return array an array of contacts which are arrays of key-value-pairs + * @since 5.0.0 + */ + public function search($pattern, $searchProperties, $options) { + $result = $this->backend->search($this->getKey(), $pattern, $searchProperties); + + $vCards = []; + foreach ($result as $cardData) { + $vCards[] = $this->vCard2Array($this->readCard($cardData)); + } + + return $vCards; + } + + /** + * @param array $properties this array if key-value-pairs defines a contact + * @return array an array representing the contact just created or updated + * @since 5.0.0 + */ + public function createOrUpdate($properties) { + $update = false; + if (!isset($properties['UID'])) { // create a new contact + $uid = $this->createUid(); + $uri = $uid . '.vcf'; + $vCard = $this->createEmptyVCard($uid); + } else { // update existing contact + $uid = $properties['UID']; + $uri = $uid . '.vcf'; + $vCardData = $this->backend->getCard($this->getKey(), $uri); + $vCard = $this->readCard($vCardData['carddata']); + $update = true; + } + + foreach ($properties as $key => $value) { + $vCard->$key = $vCard->createProperty($key, $value); + } + + if ($update) { + $this->backend->updateCard($this->getKey(), $uri, $vCard->serialize()); + } else { + $this->backend->createCard($this->getKey(), $uri, $vCard->serialize()); + } + + return $this->vCard2Array($vCard); + + } + + /** + * @return mixed + * @since 5.0.0 + */ + public function getPermissions() { + $permissions = $this->addressBook->getACL(); + $result = 0; + foreach ($permissions as $permission) { + switch($permission['privilege']) { + case '{DAV:}read': + $result |= Constants::PERMISSION_READ; + break; + case '{DAV:}write': + $result |= Constants::PERMISSION_CREATE; + $result |= Constants::PERMISSION_UPDATE; + break; + case '{DAV:}all': + $result |= Constants::PERMISSION_ALL; + break; + } + } + + return $result; + } + + /** + * @param object $id the unique identifier to a contact + * @return bool successful or not + * @since 5.0.0 + */ + public function delete($id) { + $uri = $this->backend->getCardUri($id); + return $this->backend->deleteCard($this->addressBookInfo['id'], $uri); + } + + /** + * read vCard data into a vCard object + * + * @param string $cardData + * @return VCard + */ + protected function readCard($cardData) { + return Reader::read($cardData); + } + + /** + * create UID for contact + * + * @return string + */ + protected function createUid() { + do { + $uid = $this->getUid(); + } while (!empty($this->backend->getContact($uid . '.vcf'))); + + return $uid; + } + + /** + * getUid is only there for testing, use createUid instead + */ + protected function getUid() { + return UUIDUtil::getUUID(); + } + + /** + * create empty vcard + * + * @param string $uid + * @return VCard + */ + protected function createEmptyVCard($uid) { + $vCard = new VCard(); + $vCard->add(new Text($vCard, 'UID', $uid)); + return $vCard; + } + + /** + * create array with all vCard properties + * + * @param VCard $vCard + * @return array + */ + protected function vCard2Array(VCard $vCard) { + $result = []; + foreach ($vCard->children as $property) { + $result[$property->name] = $property->getValue(); + } + return $result; + } +} diff --git a/apps/dav/lib/carddav/carddavbackend.php b/apps/dav/lib/carddav/carddavbackend.php index 29b056672b..742d29e92c 100644 --- a/apps/dav/lib/carddav/carddavbackend.php +++ b/apps/dav/lib/carddav/carddavbackend.php @@ -23,19 +23,48 @@ namespace OCA\DAV\CardDAV; use OCA\DAV\Connector\Sabre\Principal; +use OCP\IDBConnection; +use OCP\ILogger; use Sabre\CardDAV\Backend\BackendInterface; use Sabre\CardDAV\Backend\SyncSupport; use Sabre\CardDAV\Plugin; use Sabre\DAV\Exception\BadRequest; +use Sabre\VObject\Component\VCard; +use Sabre\VObject\Reader; class CardDavBackend implements BackendInterface, SyncSupport { /** @var Principal */ private $principalBackend; - public function __construct(\OCP\IDBConnection $db, Principal $principalBackend) { + /** @var ILogger */ + private $logger; + + /** @var string */ + private $dbCardsTable = 'cards'; + + /** @var string */ + private $dbCardsPropertiesTable = 'cards_properties'; + + /** @var IDBConnection */ + private $db; + + /** @var array properties to index */ + public static $indexProperties = array( + 'BDAY', 'UID', 'N', 'FN', 'TITLE', 'ROLE', 'NOTE', 'NICKNAME', + 'ORG', 'CATEGORIES', 'EMAIL', 'TEL', 'IMPP', 'ADR', 'URL', 'GEO', 'CLOUD'); + + /** + * CardDavBackend constructor. + * + * @param IDBConnection $db + * @param Principal $principalBackend + * @param ILogger $logger + */ + public function __construct(IDBConnection $db, Principal $principalBackend, ILogger $logger) { $this->db = $db; $this->principalBackend = $principalBackend; + $this->logger = $logger; } /** @@ -263,6 +292,11 @@ class CardDavBackend implements BackendInterface, SyncSupport { ->where($query->expr()->eq('resourceid', $query->createNamedParameter($addressBookId))) ->andWhere($query->expr()->eq('type', $query->createNamedParameter('addressbook'))) ->execute(); + + $query->delete($this->dbCardsPropertiesTable) + ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId))) + ->execute(); + } /** @@ -398,7 +432,7 @@ class CardDavBackend implements BackendInterface, SyncSupport { $query = $this->db->getQueryBuilder(); $query->insert('cards') ->values([ - 'carddata' => $query->createNamedParameter($cardData), + 'carddata' => $query->createNamedParameter($cardData, \PDO::PARAM_LOB), 'uri' => $query->createNamedParameter($cardUri), 'lastmodified' => $query->createNamedParameter(time()), 'addressbookid' => $query->createNamedParameter($addressBookId), @@ -408,6 +442,7 @@ class CardDavBackend implements BackendInterface, SyncSupport { ->execute(); $this->addChange($addressBookId, $cardUri, 1); + $this->updateProperties($addressBookId, $cardUri, $cardData); return '"' . $etag . '"'; } @@ -451,6 +486,7 @@ class CardDavBackend implements BackendInterface, SyncSupport { ->execute(); $this->addChange($addressBookId, $cardUri, 2); + $this->updateProperties($addressBookId, $cardUri, $cardData); return '"' . $etag . '"'; } @@ -463,6 +499,7 @@ class CardDavBackend implements BackendInterface, SyncSupport { * @return bool */ function deleteCard($addressBookId, $cardUri) { + $cardId = $this->getCardId($cardUri); $query = $this->db->getQueryBuilder(); $ret = $query->delete('cards') ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId))) @@ -471,7 +508,12 @@ class CardDavBackend implements BackendInterface, SyncSupport { $this->addChange($addressBookId, $cardUri, 3); - return $ret === 1; + if ($ret === 1) { + $this->purgeProperties($addressBookId, $cardId); + return true; + } + + return false; } /** @@ -637,6 +679,87 @@ class CardDavBackend implements BackendInterface, SyncSupport { } } + /** + * search contact + * + * @param int $addressBookId + * @param string $pattern which should match within the $searchProperties + * @param array $searchProperties defines the properties within the query pattern should match + * @return array an array of contacts which are arrays of key-value-pairs + */ + public function search($addressBookId, $pattern, $searchProperties) { + $query = $this->db->getQueryBuilder(); + $query2 = $this->db->getQueryBuilder(); + $query2->selectDistinct('cp.cardid')->from($this->dbCardsPropertiesTable, 'cp'); + foreach ($searchProperties as $property) { + $query2->orWhere( + $query2->expr()->andX( + $query2->expr()->eq('cp.name', $query->createNamedParameter($property)), + $query2->expr()->like('cp.value', $query->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%')) + ) + ); + } + $query2->andWhere($query2->expr()->eq('cp.addressbookid', $query->createNamedParameter($addressBookId))); + + $query->select('c.carddata')->from($this->dbCardsTable, 'c') + ->where($query->expr()->in('c.id', $query->createFunction($query2->getSQL()))); + + $result = $query->execute(); + $cards = $result->fetchAll(); + + $result->closeCursor(); + + return array_map(function($array) {return $this->readBlob($array['carddata']);}, $cards); + + } + + /** + * get URI from a given contact + * + * @param int $id + * @return string + */ + public function getCardUri($id) { + $query = $this->db->getQueryBuilder(); + $query->select('uri')->from($this->dbCardsTable) + ->where($query->expr()->eq('id', $query->createParameter('id'))) + ->setParameter('id', $id); + + $result = $query->execute(); + $uri = $result->fetch(); + $result->closeCursor(); + + if (!isset($uri['uri'])) { + throw new \InvalidArgumentException('Card does not exists: ' . $id); + } + + return $uri['uri']; + } + + /** + * return contact with the given URI + * + * @param string $uri + * @returns array + */ + public function getContact($uri) { + $result = []; + $query = $this->db->getQueryBuilder(); + $query->select('*')->from($this->dbCardsTable) + ->where($query->expr()->eq('uri', $query->createParameter('uri'))) + ->setParameter('uri', $uri); + $queryResult = $query->execute(); + $contact = $queryResult->fetch(); + $queryResult->closeCursor(); + + if (is_array($contact)) { + $result = $contact; + } + + return $result; + } + + /** * @param string $addressBookUri * @param string $element @@ -734,4 +857,93 @@ class CardDavBackend implements BackendInterface, SyncSupport { return $shares; } + + /** + * update properties table + * + * @param int $addressBookId + * @param string $cardUri + * @param string $vCardSerialized + */ + protected function updateProperties($addressBookId, $cardUri, $vCardSerialized) { + $cardId = $this->getCardId($cardUri); + $vCard = $this->readCard($vCardSerialized); + + $this->purgeProperties($addressBookId, $cardId); + + $query = $this->db->getQueryBuilder(); + $query->insert($this->dbCardsPropertiesTable) + ->values( + [ + 'addressbookid' => $query->createNamedParameter($addressBookId), + 'cardid' => $query->createNamedParameter($cardId), + 'name' => $query->createParameter('name'), + 'value' => $query->createParameter('value'), + 'preferred' => $query->createParameter('preferred') + ] + ); + + foreach ($vCard->children as $property) { + if(!in_array($property->name, self::$indexProperties)) { + continue; + } + $preferred = 0; + foreach($property->parameters as $parameter) { + if ($parameter->name == 'TYPE' && strtoupper($parameter->getValue()) == 'PREF') { + $preferred = 1; + break; + } + } + $query->setParameter('name', $property->name); + $query->setParameter('value', substr($property->getValue(), 0, 254)); + $query->setParameter('preferred', $preferred); + $query->execute(); + } + } + + /** + * read vCard data into a vCard object + * + * @param string $cardData + * @return VCard + */ + protected function readCard($cardData) { + return Reader::read($cardData); + } + + /** + * delete all properties from a given card + * + * @param int $addressBookId + * @param int $cardId + */ + protected function purgeProperties($addressBookId, $cardId) { + $query = $this->db->getQueryBuilder(); + $query->delete($this->dbCardsPropertiesTable) + ->where($query->expr()->eq('cardid', $query->createNamedParameter($cardId))) + ->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId))); + $query->execute(); + } + + /** + * get ID from a given contact + * + * @param string $uri + * @return int + */ + protected function getCardId($uri) { + $query = $this->db->getQueryBuilder(); + $query->select('id')->from($this->dbCardsTable) + ->where($query->expr()->eq('uri', $query->createNamedParameter($uri))); + + $result = $query->execute(); + $cardIds = $result->fetch(); + $result->closeCursor(); + + if (!isset($cardIds['id'])) { + throw new \InvalidArgumentException('Card does not exists: ' . $uri); + } + + return (int)$cardIds['id']; + } } diff --git a/apps/dav/lib/rootcollection.php b/apps/dav/lib/rootcollection.php index 9ee32822bb..96cc2bbc46 100644 --- a/apps/dav/lib/rootcollection.php +++ b/apps/dav/lib/rootcollection.php @@ -41,11 +41,11 @@ class RootCollection extends SimpleCollection { \OC::$server->getSystemTagObjectMapper() ); - $usersCardDavBackend = new CardDavBackend($db, $principalBackend); + $usersCardDavBackend = new CardDavBackend($db, $principalBackend, \OC::$server->getLogger()); $usersAddressBookRoot = new AddressBookRoot($principalBackend, $usersCardDavBackend, 'principals/users'); $usersAddressBookRoot->disableListing = $disableListing; - $systemCardDavBackend = new CardDavBackend($db, $principalBackend); + $systemCardDavBackend = new CardDavBackend($db, $principalBackend, \OC::$server->getLogger()); $systemAddressBookRoot = new AddressBookRoot(new SystemPrincipalBackend(), $systemCardDavBackend, 'principals/system'); $systemAddressBookRoot->disableListing = $disableListing; diff --git a/apps/dav/tests/unit/carddav/addressbookimpltest.php b/apps/dav/tests/unit/carddav/addressbookimpltest.php new file mode 100644 index 0000000000..73053888b9 --- /dev/null +++ b/apps/dav/tests/unit/carddav/addressbookimpltest.php @@ -0,0 +1,287 @@ + + * + * @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\AddressBook; +use OCA\DAV\CardDAV\AddressBookImpl; +use OCA\DAV\CardDAV\CardDavBackend; +use Sabre\VObject\Component\VCard; +use Sabre\VObject\Property\Text; +use Test\TestCase; + +class AddressBookImplTest extends TestCase { + + /** @var AddressBookImpl */ + private $addressBookImpl; + + /** @var array */ + private $addressBookInfo; + + /** @var AddressBook | \PHPUnit_Framework_MockObject_MockObject */ + private $addressBook; + + /** @var CardDavBackend | \PHPUnit_Framework_MockObject_MockObject */ + private $backend; + + /** @var VCard | \PHPUnit_Framework_MockObject_MockObject */ + private $vCard; + + public function setUp() { + parent::setUp(); + + $this->addressBookInfo = [ + 'id' => 42, + '{DAV:}displayname' => 'display name' + ]; + $this->addressBook = $this->getMockBuilder('OCA\DAV\CardDAV\AddressBook') + ->disableOriginalConstructor()->getMock(); + $this->backend = $this->getMockBuilder('\OCA\DAV\CardDAV\CardDavBackend') + ->disableOriginalConstructor()->getMock(); + $this->vCard = $this->getMock('Sabre\VObject\Component\VCard'); + + $this->addressBookImpl = new AddressBookImpl( + $this->addressBook, + $this->addressBookInfo, + $this->backend + ); + } + + public function testGetKey() { + $this->assertSame($this->addressBookInfo['id'], + $this->addressBookImpl->getKey()); + } + + public function testGetDisplayName() { + $this->assertSame($this->addressBookInfo['{DAV:}displayname'], + $this->addressBookImpl->getDisplayName()); + } + + public function testSearch() { + + /** @var \PHPUnit_Framework_MockObject_MockObject | AddressBookImpl $addressBookImpl */ + $addressBookImpl = $this->getMockBuilder('OCA\DAV\CardDAV\AddressBookImpl') + ->setConstructorArgs( + [ + $this->addressBook, + $this->addressBookInfo, + $this->backend + ] + ) + ->setMethods(['vCard2Array', 'readCard']) + ->getMock(); + + $pattern = 'pattern'; + $searchProperties = 'properties'; + + $this->backend->expects($this->once())->method('search') + ->with($this->addressBookInfo['id'], $pattern, $searchProperties) + ->willReturn( + [ + 'cardData1', + 'cardData2' + ] + ); + + $addressBookImpl->expects($this->exactly(2))->method('readCard') + ->willReturn($this->vCard); + $addressBookImpl->expects($this->exactly(2))->method('vCard2Array') + ->with($this->vCard)->willReturn('vCard'); + + $result = $addressBookImpl->search($pattern, $searchProperties, []); + $this->assertTrue((is_array($result))); + $this->assertSame(2, count($result)); + } + + /** + * @dataProvider dataTestCreate + * + * @param array $properties + */ + public function testCreate($properties) { + + $uid = 'uid'; + + /** @var \PHPUnit_Framework_MockObject_MockObject | AddressBookImpl $addressBookImpl */ + $addressBookImpl = $this->getMockBuilder('OCA\DAV\CardDAV\AddressBookImpl') + ->setConstructorArgs( + [ + $this->addressBook, + $this->addressBookInfo, + $this->backend + ] + ) + ->setMethods(['vCard2Array', 'createUid', 'createEmptyVCard']) + ->getMock(); + + $addressBookImpl->expects($this->once())->method('createUid') + ->willReturn($uid); + $addressBookImpl->expects($this->once())->method('createEmptyVCard') + ->with($uid)->willReturn($this->vCard); + $this->vCard->expects($this->exactly(count($properties))) + ->method('createProperty'); + $this->backend->expects($this->once())->method('createCard'); + $this->backend->expects($this->never())->method('updateCard'); + $this->backend->expects($this->never())->method('getCard'); + $addressBookImpl->expects($this->once())->method('vCard2Array') + ->with($this->vCard)->willReturn(true); + + $this->assertTrue($addressBookImpl->createOrUpdate($properties)); + } + + public function dataTestCreate() { + return [ + [[]], + [['FN' => 'John Doe']] + ]; + } + + public function testUpdate() { + + $uid = 'uid'; + $properties = ['UID' => $uid, 'FN' => 'John Doe']; + + /** @var \PHPUnit_Framework_MockObject_MockObject | AddressBookImpl $addressBookImpl */ + $addressBookImpl = $this->getMockBuilder('OCA\DAV\CardDAV\AddressBookImpl') + ->setConstructorArgs( + [ + $this->addressBook, + $this->addressBookInfo, + $this->backend + ] + ) + ->setMethods(['vCard2Array', 'createUid', 'createEmptyVCard', 'readCard']) + ->getMock(); + + $addressBookImpl->expects($this->never())->method('createUid'); + $addressBookImpl->expects($this->never())->method('createEmptyVCard'); + $this->backend->expects($this->once())->method('getCard') + ->with($this->addressBookInfo['id'], $uid . '.vcf') + ->willReturn(['carddata' => 'data']); + $addressBookImpl->expects($this->once())->method('readCard') + ->with('data')->willReturn($this->vCard); + $this->vCard->expects($this->exactly(count($properties))) + ->method('createProperty'); + $this->backend->expects($this->never())->method('createCard'); + $this->backend->expects($this->once())->method('updateCard'); + $addressBookImpl->expects($this->once())->method('vCard2Array') + ->with($this->vCard)->willReturn(true); + + $this->assertTrue($addressBookImpl->createOrUpdate($properties)); + } + + /** + * @dataProvider dataTestGetPermissions + * + * @param array $permissions + * @param int $expected + */ + public function testGetPermissions($permissions, $expected) { + $this->addressBook->expects($this->once())->method('getACL') + ->willReturn($permissions); + + $this->assertSame($expected, + $this->addressBookImpl->getPermissions() + ); + } + + public function dataTestGetPermissions() { + return [ + [[], 0], + [[['privilege' => '{DAV:}read']], 1], + [[['privilege' => '{DAV:}write']], 6], + [[['privilege' => '{DAV:}all']], 31], + [[['privilege' => '{DAV:}read'],['privilege' => '{DAV:}write']], 7], + [[['privilege' => '{DAV:}read'],['privilege' => '{DAV:}all']], 31], + [[['privilege' => '{DAV:}all'],['privilege' => '{DAV:}write']], 31], + [[['privilege' => '{DAV:}read'],['privilege' => '{DAV:}write'],['privilege' => '{DAV:}all']], 31], + [[['privilege' => '{DAV:}all'],['privilege' => '{DAV:}read'],['privilege' => '{DAV:}write']], 31], + ]; + } + + public function testDelete() { + $cardId = 1; + $cardUri = 'cardUri'; + $this->backend->expects($this->once())->method('getCardUri') + ->with($cardId)->willReturn($cardUri); + $this->backend->expects($this->once())->method('deleteCard') + ->with($this->addressBookInfo['id'], $cardUri) + ->willReturn(true); + + $this->assertTrue($this->addressBookImpl->delete($cardId)); + } + + public function testReadCard() { + $vCard = new VCard(); + $vCard->add(new Text($vCard, 'UID', 'uid')); + $vCardSerialized = $vCard->serialize(); + + $result = $this->invokePrivate($this->addressBookImpl, 'readCard', [$vCardSerialized]); + $resultSerialized = $result->serialize(); + + $this->assertSame($vCardSerialized, $resultSerialized); + } + + public function testCreateUid() { + /** @var \PHPUnit_Framework_MockObject_MockObject | AddressBookImpl $addressBookImpl */ + $addressBookImpl = $this->getMockBuilder('OCA\DAV\CardDAV\AddressBookImpl') + ->setConstructorArgs( + [ + $this->addressBook, + $this->addressBookInfo, + $this->backend + ] + ) + ->setMethods(['getUid']) + ->getMock(); + + $addressBookImpl->expects($this->at(0))->method('getUid')->willReturn('uid0'); + $addressBookImpl->expects($this->at(1))->method('getUid')->willReturn('uid1'); + + // simulate that 'uid0' already exists, so the second uid will be returned + $this->backend->expects($this->exactly(2))->method('getContact') + ->willReturnCallback( + function($uid) { + return ($uid === 'uid0.vcf'); + } + ); + + $this->assertSame('uid1', + $this->invokePrivate($addressBookImpl, 'createUid', []) + ); + + } + + public function testCreateEmptyVCard() { + $uid = 'uid'; + $expectedVCard = new VCard(); + $expectedVCard->add(new Text($expectedVCard, 'UID', $uid)); + $expectedVCardSerialized = $expectedVCard->serialize(); + + $result = $this->invokePrivate($this->addressBookImpl, 'createEmptyVCard', [$uid]); + $resultSerialized = $result->serialize(); + + $this->assertSame($expectedVCardSerialized, $resultSerialized); + } + +} diff --git a/apps/dav/tests/unit/carddav/carddavbackendtest.php b/apps/dav/tests/unit/carddav/carddavbackendtest.php index dd5e205242..56d04a8cd4 100644 --- a/apps/dav/tests/unit/carddav/carddavbackendtest.php +++ b/apps/dav/tests/unit/carddav/carddavbackendtest.php @@ -20,8 +20,14 @@ */ namespace OCA\DAV\Tests\Unit\CardDAV; +use InvalidArgumentException; use OCA\DAV\CardDAV\CardDavBackend; +use OCA\DAV\Connector\Sabre\Principal; +use OCP\IDBConnection; +use OCP\ILogger; use Sabre\DAV\PropPatch; +use Sabre\VObject\Component\VCard; +use Sabre\VObject\Property\Text; use Test\TestCase; /** @@ -36,22 +42,46 @@ class CardDavBackendTest extends TestCase { /** @var CardDavBackend */ private $backend; + /** @var Principal | \PHPUnit_Framework_MockObject_MockObject */ + private $principal; + + /** @var ILogger | \PHPUnit_Framework_MockObject_MockObject */ + private $logger; + + /** @var IDBConnection */ + private $db; + + /** @var string */ + private $dbCardsTable = 'cards'; + + /** @var string */ + private $dbCardsPropertiesTable = 'cards_properties'; + const UNIT_TEST_USER = 'carddav-unit-test'; public function setUp() { parent::setUp(); - $principal = $this->getMockBuilder('OCA\DAV\Connector\Sabre\Principal') + $this->principal = $this->getMockBuilder('OCA\DAV\Connector\Sabre\Principal') ->disableOriginalConstructor() ->setMethods(['getPrincipalByPath']) ->getMock(); - $principal->method('getPrincipalByPath') + $this->principal->method('getPrincipalByPath') ->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); + + // start every test with a empty cards_properties and cards table + $query = $this->db->getQueryBuilder(); + $query->delete('cards_properties')->execute(); + $query = $this->db->getQueryBuilder(); + $query->delete('cards')->execute(); - $db = \OC::$server->getDatabaseConnection(); - $this->backend = new CardDavBackend($db, $principal); $this->tearDown(); } @@ -96,23 +126,32 @@ class CardDavBackendTest extends TestCase { } public function testCardOperations() { + + /** @var CardDavBackend | \PHPUnit_Framework_MockObject_MockObject $backend */ + $backend = $this->getMockBuilder('OCA\DAV\CardDAV\CardDavBackend') + ->setConstructorArgs([$this->db, $this->principal, $this->logger]) + ->setMethods(['updateProperties', 'purgeProperties'])->getMock(); + // create a new address book - $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []); - $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER); + $backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []); + $books = $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, ''); + // updateProperties is expected twice, once for createCard and once for updateCard + $backend->expects($this->at(0))->method('updateProperties')->with($bookId, $uri, ''); + $backend->expects($this->at(1))->method('updateProperties')->with($bookId, $uri, '***'); + // create a card + $backend->createCard($bookId, $uri, ''); // get all the cards - $cards = $this->backend->getCards($bookId); + $cards = $backend->getCards($bookId); $this->assertEquals(1, count($cards)); $this->assertEquals('', $cards[0]['carddata']); // get the cards - $card = $this->backend->getCard($bookId, $uri); + $card = $backend->getCard($bookId, $uri); $this->assertNotNull($card); $this->assertArrayHasKey('id', $card); $this->assertArrayHasKey('uri', $card); @@ -122,17 +161,23 @@ class CardDavBackendTest extends TestCase { $this->assertEquals('', $card['carddata']); // update the card - $this->backend->updateCard($bookId, $uri, '***'); - $card = $this->backend->getCard($bookId, $uri); + $backend->updateCard($bookId, $uri, '***'); + $card = $backend->getCard($bookId, $uri); $this->assertEquals('***', $card['carddata']); // delete the card - $this->backend->deleteCard($bookId, $uri); - $cards = $this->backend->getCards($bookId); + $backend->expects($this->once())->method('purgeProperties')->with($bookId, $card['id']); + $backend->deleteCard($bookId, $uri); + $cards = $backend->getCards($bookId); $this->assertEquals(0, count($cards)); } public function testMultiCard() { + + $this->backend = $this->getMockBuilder('OCA\DAV\CardDAV\CardDavBackend') + ->setConstructorArgs([$this->db, $this->principal, $this->logger]) + ->setMethods(['updateProperties'])->getMock(); + // create a new address book $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []); $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER); @@ -175,6 +220,11 @@ class CardDavBackendTest extends TestCase { } public function testSyncSupport() { + + $this->backend = $this->getMockBuilder('OCA\DAV\CardDAV\CardDavBackend') + ->setConstructorArgs([$this->db, $this->principal, $this->logger]) + ->setMethods(['updateProperties'])->getMock(); + // create a new address book $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []); $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER); @@ -221,4 +271,273 @@ class CardDavBackendTest extends TestCase { $books = $this->backend->getAddressBooksForUser('principals/best-friend'); $this->assertEquals(0, count($books)); } + + public function testUpdateProperties() { + + $bookId = 42; + $cardUri = 'card-uri'; + $cardId = 2; + + $backend = $this->getMockBuilder('OCA\DAV\CardDAV\CardDavBackend') + ->setConstructorArgs([$this->db, $this->principal, $this->logger]) + ->setMethods(['getCardId'])->getMock(); + + $backend->expects($this->any())->method('getCardId')->willReturn($cardId); + + // add properties for new vCard + $vCard = new VCard(); + $vCard->add(new Text($vCard, 'UID', $cardUri)); + $vCard->add(new Text($vCard, 'FN', 'John Doe')); + $this->invokePrivate($backend, 'updateProperties', [$bookId, $cardUri, $vCard->serialize()]); + + $query = $this->db->getQueryBuilder(); + $result = $query->select('*')->from('cards_properties')->execute()->fetchAll(); + + $this->assertSame(2, count($result)); + + $this->assertSame('UID', $result[0]['name']); + $this->assertSame($cardUri, $result[0]['value']); + $this->assertSame($bookId, (int)$result[0]['addressbookid']); + $this->assertSame($cardId, (int)$result[0]['cardid']); + + $this->assertSame('FN', $result[1]['name']); + $this->assertSame('John Doe', $result[1]['value']); + $this->assertSame($bookId, (int)$result[1]['addressbookid']); + $this->assertSame($cardId, (int)$result[1]['cardid']); + + // update properties for existing vCard + $vCard = new VCard(); + $vCard->add(new Text($vCard, 'FN', 'John Doe')); + $this->invokePrivate($backend, 'updateProperties', [$bookId, $cardUri, $vCard->serialize()]); + + $query = $this->db->getQueryBuilder(); + $result = $query->select('*')->from('cards_properties')->execute()->fetchAll(); + + $this->assertSame(1, count($result)); + + $this->assertSame('FN', $result[0]['name']); + $this->assertSame('John Doe', $result[0]['value']); + $this->assertSame($bookId, (int)$result[0]['addressbookid']); + $this->assertSame($cardId, (int)$result[0]['cardid']); + } + + public function testPurgeProperties() { + + $query = $this->db->getQueryBuilder(); + $query->insert('cards_properties') + ->values( + [ + 'addressbookid' => $query->createNamedParameter(1), + 'cardid' => $query->createNamedParameter(1), + 'name' => $query->createNamedParameter('name1'), + 'value' => $query->createNamedParameter('value1'), + 'preferred' => $query->createNamedParameter(0) + ] + ); + $query->execute(); + + $query = $this->db->getQueryBuilder(); + $query->insert('cards_properties') + ->values( + [ + 'addressbookid' => $query->createNamedParameter(1), + 'cardid' => $query->createNamedParameter(2), + 'name' => $query->createNamedParameter('name2'), + 'value' => $query->createNamedParameter('value2'), + 'preferred' => $query->createNamedParameter(0) + ] + ); + $query->execute(); + + $this->invokePrivate($this->backend, 'purgeProperties', [1, 1]); + + $query = $this->db->getQueryBuilder(); + $result = $query->select('*')->from('cards_properties')->execute()->fetchAll(); + $this->assertSame(1, count($result)); + $this->assertSame(1 ,(int)$result[0]['addressbookid']); + $this->assertSame(2 ,(int)$result[0]['cardid']); + + } + + public function testGetCardId() { + $query = $this->db->getQueryBuilder(); + + $query->insert('cards') + ->values( + [ + 'addressbookid' => $query->createNamedParameter(1), + 'carddata' => $query->createNamedParameter(''), + 'uri' => $query->createNamedParameter('uri'), + 'lastmodified' => $query->createNamedParameter(4738743), + 'etag' => $query->createNamedParameter('etag'), + 'size' => $query->createNamedParameter(120) + ] + ); + $query->execute(); + $id = $query->getLastInsertId(); + + $this->assertSame($id, + $this->invokePrivate($this->backend, 'getCardId', ['uri'])); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testGetCardIdFailed() { + $this->invokePrivate($this->backend, 'getCardId', ['uri']); + } + + /** + * @dataProvider dataTestSearch + * + * @param string $pattern + * @param array $expected + */ + public function testSearch($pattern, $properties, $expected) { + /** @var VCard $vCards */ + $vCards = []; + $vCards[0] = new VCard(); + $vCards[0]->add(new Text($vCards[0], 'UID', 'uid')); + $vCards[0]->add(new Text($vCards[0], 'FN', 'John Doe')); + $vCards[0]->add(new Text($vCards[0], 'CLOUD', 'john@owncloud.org')); + $vCards[1] = new VCard(); + $vCards[1]->add(new Text($vCards[1], 'UID', 'uid')); + $vCards[1]->add(new Text($vCards[1], 'FN', 'John M. Doe')); + + $vCardIds = []; + $query = $this->db->getQueryBuilder(); + for($i=0; $i<2; $i++) { + $query->insert($this->dbCardsTable) + ->values( + [ + 'addressbookid' => $query->createNamedParameter(0), + 'carddata' => $query->createNamedParameter($vCards[$i]->serialize(), \PDO::PARAM_LOB), + 'uri' => $query->createNamedParameter('uri' . $i), + 'lastmodified' => $query->createNamedParameter(time()), + 'etag' => $query->createNamedParameter('etag' . $i), + 'size' => $query->createNamedParameter(120), + ] + ); + $query->execute(); + $vCardIds[] = $query->getLastInsertId(); + } + + $query->insert($this->dbCardsPropertiesTable) + ->values( + [ + 'addressbookid' => $query->createNamedParameter(0), + 'cardid' => $query->createNamedParameter($vCardIds[0]), + 'name' => $query->createNamedParameter('FN'), + 'value' => $query->createNamedParameter('John Doe'), + 'preferred' => $query->createNamedParameter(0) + ] + ); + $query->execute(); + $query->insert($this->dbCardsPropertiesTable) + ->values( + [ + 'addressbookid' => $query->createNamedParameter(0), + 'cardid' => $query->createNamedParameter($vCardIds[0]), + 'name' => $query->createNamedParameter('CLOUD'), + 'value' => $query->createNamedParameter('John@owncloud.org'), + 'preferred' => $query->createNamedParameter(0) + ] + ); + $query->execute(); + $query->insert($this->dbCardsPropertiesTable) + ->values( + [ + 'addressbookid' => $query->createNamedParameter(0), + 'cardid' => $query->createNamedParameter($vCardIds[1]), + 'name' => $query->createNamedParameter('FN'), + 'value' => $query->createNamedParameter('John M. Doe'), + 'preferred' => $query->createNamedParameter(0) + ] + ); + $query->execute(); + + $result = $this->backend->search(0, $pattern, $properties); + + // check result + $this->assertSame(count($expected), count($result)); + $found = []; + foreach ($result as $r) { + foreach ($expected as $exp) { + if (strpos($r, $exp) > 0) { + $found[$exp] = true; + break; + } + } + } + + $this->assertSame(count($expected), count($found)); + } + + public function dataTestSearch() { + return [ + ['John', ['FN'], ['John Doe', 'John M. Doe']], + ['M. Doe', ['FN'], ['John M. Doe']], + ['Do', ['FN'], ['John Doe', 'John M. Doe']], + // check if duplicates are handled correctly + ['John', ['FN', 'CLOUD'], ['John Doe', 'John M. Doe']], + ]; + } + + public function testGetCardUri() { + $query = $this->db->getQueryBuilder(); + $query->insert($this->dbCardsTable) + ->values( + [ + 'addressbookid' => $query->createNamedParameter(1), + 'carddata' => $query->createNamedParameter('carddata', \PDO::PARAM_LOB), + 'uri' => $query->createNamedParameter('uri'), + 'lastmodified' => $query->createNamedParameter(5489543), + 'etag' => $query->createNamedParameter('etag'), + 'size' => $query->createNamedParameter(120), + ] + ); + $query->execute(); + + $id = $query->getLastInsertId(); + + $this->assertSame('uri', $this->backend->getCardUri($id)); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testGetCardUriFailed() { + $this->backend->getCardUri(1); + } + + public function testGetContact() { + $query = $this->db->getQueryBuilder(); + for($i=0; $i<2; $i++) { + $query->insert($this->dbCardsTable) + ->values( + [ + 'addressbookid' => $query->createNamedParameter($i), + 'carddata' => $query->createNamedParameter('carddata' . $i, \PDO::PARAM_LOB), + 'uri' => $query->createNamedParameter('uri' . $i), + 'lastmodified' => $query->createNamedParameter(5489543), + 'etag' => $query->createNamedParameter('etag' . $i), + 'size' => $query->createNamedParameter(120), + ] + ); + $query->execute(); + } + + $result = $this->backend->getContact('uri0'); + $this->assertSame(7, count($result)); + $this->assertSame(0, (int)$result['addressbookid']); + $this->assertSame('uri0', $result['uri']); + $this->assertSame(5489543, (int)$result['lastmodified']); + $this->assertSame('etag0', $result['etag']); + $this->assertSame(120, (int)$result['size']); + } + + public function testGetContactFail() { + $this->assertEmpty($this->backend->getContact('uri')); + } + }