]> source.dussan.org Git - nextcloud-server.git/commitdiff
implement delete, create, update, search, get permissions
authorBjörn Schießle <bjoern@schiessle.org>
Wed, 25 Nov 2015 14:24:50 +0000 (15:24 +0100)
committerBjörn Schießle <bjoern@schiessle.org>
Tue, 15 Dec 2015 13:52:27 +0000 (14:52 +0100)
apps/dav/appinfo/app.php
apps/dav/appinfo/database.xml
apps/dav/appinfo/register_command.php
apps/dav/command/createaddressbook.php
apps/dav/lib/carddav/AddressBookImpl.php [deleted file]
apps/dav/lib/carddav/addressbookimpl.php [new file with mode: 0644]
apps/dav/lib/carddav/carddavbackend.php
apps/dav/lib/rootcollection.php
apps/dav/tests/unit/carddav/addressbookimpltest.php [new file with mode: 0644]
apps/dav/tests/unit/carddav/carddavbackendtest.php

index 0a54df19c725dbd4b7b0619d28c9041a34f77524..950754ee941e3710b641c7bffe168c9c8a1b29a3 100644 (file)
@@ -23,9 +23,20 @@ $cm = \OC::$server->getContactsManager();
 $cm->register(function() use ($cm) {
        $db = \OC::$server->getDatabaseConnection();
        $userId = \OC::$server->getUserSession()->getUser()->getUID();
-       $cardDav = new \OCA\DAV\CardDAV\CardDavBackend($db);
+       $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 $addressBook)  {
-               $cm->registerAddressBook(new OCA\DAV\CardDAV\AddressBookImpl($addressBook));
+       foreach ($addressBooks as $addressBookInfo)  {
+               $addressBook = new \OCA\DAV\CardDAV\AddressBook($cardDav, $addressBookInfo);
+               $cm->registerAddressBook(
+                               new OCA\DAV\CardDAV\AddressBookImpl(
+                                               $addressBook,
+                                               $addressBookInfo,
+                                               $cardDav
+                               )
+               );
        }
 });
index 48641c2be6ffc5d32608550b0c0528d319f69b3c..50c8aa7d8ca547b4232f476e795fbe885d25bd83 100644 (file)
@@ -571,6 +571,78 @@ CREATE TABLE calendarobjects (
                </declaration>
        </table>
 
+       <table>
+               <name>*dbprefix*cards_properties</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>addressbookid</name>
+                               <type>integer</type>
+                               <default></default>
+                               <notnull>true</notnull>
+                               <length>11</length>
+                       </field>
+                       <field>
+                               <name>cardid</name>
+                               <type>integer</type>
+                               <default></default>
+                               <notnull>true</notnull>
+                               <unsigned>true</unsigned>
+                               <length>11</length>
+                       </field>
+                       <field>
+                               <name>name</name>
+                               <type>text</type>
+                               <default></default>
+                               <notnull>false</notnull>
+                               <length>64</length>
+                       </field>
+                       <field>
+                               <name>value</name>
+                               <type>text</type>
+                               <default></default>
+                               <notnull>false</notnull>
+                               <length>255</length>
+                       </field>
+                       <field>
+                               <name>preferred</name>
+                               <type>integer</type>
+                               <default>1</default>
+                               <notnull>true</notnull>
+                               <length>4</length>
+                       </field>
+                       <index>
+                               <name>card_contactid_index</name>
+                               <field>
+                                       <name>cardid</name>
+                                       <sorting>ascending</sorting>
+                               </field>
+                       </index>
+                       <index>
+                               <name>card_name_index</name>
+                               <field>
+                                       <name>name</name>
+                                       <sorting>ascending</sorting>
+                               </field>
+                       </index>
+                       <index>
+                               <name>card_value_index</name>
+                               <field>
+                                       <name>value</name>
+                                       <sorting>ascending</sorting>
+                               </field>
+                       </index>
+               </declaration>
+       </table>
+
        <table>
                <name>*dbprefix*dav_shares</name>
                <declaration>
index af41036cddcd37a02249f86457bf4ec17c1c7dae..603832e0c4806ae47cb1b497371b843c3e19cf19 100644 (file)
@@ -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));
index ea89e7aa0a8eb47e7ab9b5ebf3ac1bda819fc772..7b70cea7f80515ade21df363dd934d5c1ff12a62 100644 (file)
@@ -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
deleted file mode 100644 (file)
index 7eee731..0000000
+++ /dev/null
@@ -1,100 +0,0 @@
-<?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;
-
-class AddressBookImpl implements \OCP\IAddressBook {
-
-       public function __construct(array $values, CardDavBackend $backend) {
-               $this->values = $values;
-               $this->backend = $backend;
-               /*
-                *                              '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',
-*/
-       }
-
-       /**
-        * @return string defining the technical unique key
-        * @since 5.0.0
-        */
-       public function getKey() {
-               return $this->values['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->values['{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) {
-               // TODO: Implement search() method.
-               $cards = $this->backend->getCards($this->values['id']);
-
-               //
-               // TODO: search now
-               //
-
-       }
-
-       /**
-        * @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) {
-               // TODO: Implement createOrUpdate() method.
-       }
-
-       /**
-        * @return mixed
-        * @since 5.0.0
-        */
-       public function getPermissions() {
-               // TODO: Implement getPermissions() method.
-       }
-
-       /**
-        * @param object $id the unique identifier to a contact
-        * @return bool successful or not
-        * @since 5.0.0
-        */
-       public function delete($id) {
-               // TODO: Implement delete() method.
-       }
-}
diff --git a/apps/dav/lib/carddav/addressbookimpl.php b/apps/dav/lib/carddav/addressbookimpl.php
new file mode 100644 (file)
index 0000000..838ef5a
--- /dev/null
@@ -0,0 +1,219 @@
+<?php
+/**
+ * @author Thomas Müller <thomas.mueller@tmit.eu>
+ * @author Björn Schießle <schiessle@owncloud.com>
+ *
+ * @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 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;
+       }
+}
index 29b056672b4ebde520649428b17452e1bb84bba8..bbd76ae37acc79007a5445dc51ddb62019317702 100644 (file)
 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\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 +291,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();
+
        }
 
        /**
@@ -408,6 +441,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
                        ->execute();
 
                $this->addChange($addressBookId, $cardUri, 1);
+               $this->updateProperties($addressBookId, $cardUri, $cardData);
 
                return '"' . $etag . '"';
        }
@@ -451,6 +485,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
                        ->execute();
 
                $this->addChange($addressBookId, $cardUri, 2);
+               $this->updateProperties($addressBookId, $cardUri, $cardData);
 
                return '"' . $etag . '"';
        }
@@ -463,6 +498,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 +507,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 +678,83 @@ 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();
+               $query->selectDistinct('carddata', 'c')
+                               ->from($this->dbCardsTable, 'c')
+                               ->leftJoin('c', $this->dbCardsPropertiesTable, 'cp', $query->expr()->eq('cp.cardid', 'c.id'));
+               foreach ($searchProperties as $property) {
+                       $query->orWhere(
+                                       $query->expr()->andX(
+                                                       $query->expr()->eq('cp.name', $query->createNamedParameter($property)),
+                                                       $query->expr()->like('cp.value', $query->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%'))
+                                       )
+                       );
+               }
+               $query->andWhere($query->expr()->eq('cp.addressbookid', $query->createNamedParameter($addressBookId)));
+               $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 +852,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'];
+       }
 }
index 9ee32822bbda9f1fed823adc2ac701afdeb7073f..96cc2bbc46a1089b6acf3479e1ffc98dac13dc16 100644 (file)
@@ -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 (file)
index 0000000..7305388
--- /dev/null
@@ -0,0 +1,287 @@
+<?php
+/**
+ * @author Björn Schießle <schiessle@owncloud.com>
+ *
+ * @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\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);
+       }
+
+}
index dd5e205242a15b498c6e65c9dc11656815f75c0e..0e17d8a40ba6b24335cf25aa932bac2d1eb7e1b5 100644 (file)
 namespace OCA\DAV\Tests\Unit\CardDAV;
 
 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 +41,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 +125,31 @@ class CardDavBackendTest extends TestCase {
        }
 
        public function testCardOperations() {
+
+               $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 +159,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 +218,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 +269,258 @@ 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, $expected) {
+               $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[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()),
+                                                                       'uri' => $query->createNamedParameter('uri' . $i),
+                                                                       'lastmodified' => $query->createNamedParameter(5489543),
+                                                                       '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[1]),
+                                       'name' => $query->createNamedParameter('FN'),
+                                       'value' => $query->createNamedParameter('John M. Doe'),
+                                       'preferred' => $query->createNamedParameter(0)
+                               ]
+                       );
+               $query->execute();
+
+               $result = $this->backend->search(0, $pattern, ['FN']);
+
+               // 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', ['John Doe', 'John M. Doe']],
+                               ['M. Doe', ['John M. Doe']],
+                               ['Do', ['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'));
+       }
+
 }