Merge pull request #20184 from owncloud/add-carddav

Introducing CardDAV into core
This commit is contained in:
Thomas Müller 2015-11-06 20:54:16 +01:00
commit ab0c9da4f9
26 changed files with 1056 additions and 23 deletions

1
apps/dav/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
tests/travis/CalDAVTester

View file

@ -0,0 +1,186 @@
<?xml version="1.0" encoding="ISO-8859-1" ?>
<database>
<!--
CREATE TABLE addressbooks (
id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
principaluri VARBINARY(255),
displayname VARCHAR(255),
uri VARBINARY(200),
description TEXT,
synctoken INT(11) UNSIGNED NOT NULL DEFAULT '1',
UNIQUE(principaluri(100), uri(100))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-->
<table>
<name>*dbprefix*addressbooks</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>principaluri</name>
<type>text</type>
</field>
<field>
<name>displayname</name>
<type>text</type>
</field>
<field>
<name>uri</name>
<type>text</type>
</field>
<field>
<name>description</name>
<type>text</type>
</field>
<field>
<name>synctoken</name>
<type>integer</type>
<default>1</default>
<notnull>true</notnull>
<unsigned>true</unsigned>
</field>
<index>
<name>addressbook_index</name>
<unique>true</unique>
<field>
<name>principaluri</name>
</field>
<field>
<name>uri</name>
</field>
</index>
</declaration>
</table>
<!--
CREATE TABLE cards (
id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
addressbookid INT(11) UNSIGNED NOT NULL,
carddata MEDIUMBLOB,
uri VARBINARY(200),
lastmodified INT(11) UNSIGNED,
etag VARBINARY(32),
size INT(11) UNSIGNED NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-->
<table>
<name>*dbprefix*cards</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>0</default>
<notnull>true</notnull>
</field>
<field>
<name>carddata</name>
<type>blob</type>
</field>
<field>
<name>uri</name>
<type>text</type>
</field>
<field>
<name>lastmodified</name>
<type>integer</type>
<unsigned>true</unsigned>
<length>11</length>
</field>
<field>
<name>etag</name>
<type>text</type>
<length>32</length>
</field>
<field>
<name>size</name>
<type>integer</type>
<notnull>true</notnull>
<unsigned>true</unsigned>
<length>11</length>
</field>
</declaration>
</table>
<!--
CREATE TABLE addressbookchanges (
id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
uri VARBINARY(200) NOT NULL,
synctoken INT(11) UNSIGNED NOT NULL,
addressbookid INT(11) UNSIGNED NOT NULL,
operation TINYINT(1) NOT NULL,
INDEX addressbookid_synctoken (addressbookid, synctoken)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-->
<table>
<name>*dbprefix*addressbookchanges</name>
<declaration>
<field>
<name>id</name>
<type>integer</type>
<default>0</default>
<notnull>true</notnull>
<autoincrement>1</autoincrement>
<unsigned>true</unsigned>
<length>11</length>
</field>
<field>
<name>uri</name>
<type>text</type>
</field>
<field>
<name>synctoken</name>
<type>integer</type>
<default>1</default>
<notnull>true</notnull>
<unsigned>true</unsigned>
</field>
<field>
<name>addressbookid</name>
<type>integer</type>
<notnull>true</notnull>
</field>
<field>
<name>operation</name>
<type>integer</type>
<notnull>true</notnull>
<length>1</length>
</field>
<index>
<name>addressbookid_synctoken</name>
<field>
<name>addressbookid</name>
</field>
<field>
<name>synctoken</name>
</field>
</index>
</declaration>
</table>
</database>

View file

@ -5,7 +5,7 @@
<description>ownCloud WebDAV endpoint</description> <description>ownCloud WebDAV endpoint</description>
<licence>AGPL</licence> <licence>AGPL</licence>
<author>owncloud.org</author> <author>owncloud.org</author>
<version>0.1.1</version> <version>0.1.2</version>
<requiremin>9.0</requiremin> <requiremin>9.0</requiremin>
<shipped>true</shipped> <shipped>true</shipped>
<standalone/> <standalone/>

View file

@ -0,0 +1,8 @@
<?php
use OCA\DAV\Command\CreateAddressBook;
$dbConnection = \OC::$server->getDatabaseConnection();
$userManager = OC::$server->getUserManager();
/** @var Symfony\Component\Console\Application $application */
$application->add(new CreateAddressBook($userManager, $dbConnection));

View file

@ -0,0 +1,52 @@
<?php
namespace OCA\DAV\Command;
use OCA\DAV\CardDAV\CardDavBackend;
use OCP\IDBConnection;
use OCP\IUserManager;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class CreateAddressBook extends Command {
/** @var IUserManager */
protected $userManager;
/** @var \OCP\IDBConnection */
protected $dbConnection;
/**
* @param IUserManager $userManager
* @param IDBConnection $dbConnection
*/
function __construct(IUserManager $userManager, IDBConnection $dbConnection) {
parent::__construct();
$this->userManager = $userManager;
$this->dbConnection = $dbConnection;
}
protected function configure() {
$this
->setName('dav:create-addressbook')
->setDescription('Create a dav addressbook')
->addArgument('user',
InputArgument::REQUIRED,
'User for whom the addressbook will be created')
->addArgument('name',
InputArgument::REQUIRED,
'Name of the addressbook');
}
protected function execute(InputInterface $input, OutputInterface $output) {
$user = $input->getArgument('user');
if (!$this->userManager->userExists($user)) {
throw new \InvalidArgumentException("User <$user> in unknown.");
}
$name = $input->getArgument('name');
$carddav = new CardDavBackend($this->dbConnection);
$carddav->createAddressBook("principals/$user", $name, []);
}
}

View file

@ -0,0 +1,558 @@
<?php
/**
* @author Thomas Müller <thomas.mueller@tmit.eu>
*
* @copyright Copyright (c) 2015, ownCloud, Inc.
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\DAV\CardDAV;
use Sabre\CardDAV\Backend\BackendInterface;
use Sabre\CardDAV\Backend\SyncSupport;
use Sabre\CardDAV\Plugin;
use Sabre\DAV\Exception\BadRequest;
class CardDavBackend implements BackendInterface, SyncSupport {
public function __construct(\OCP\IDBConnection $db) {
$this->db = $db;
}
/**
* Returns the list of addressbooks for a specific user.
*
* Every addressbook should have the following properties:
* id - an arbitrary unique id
* uri - the 'basename' part of the url
* principaluri - Same as the passed parameter
*
* Any additional clark-notation property may be passed besides this. Some
* common ones are :
* {DAV:}displayname
* {urn:ietf:params:xml:ns:carddav}addressbook-description
* {http://calendarserver.org/ns/}getctag
*
* @param string $principalUri
* @return array
*/
function getAddressBooksForUser($principalUri) {
$query = $this->db->getQueryBuilder();
$query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken'])
->from('addressbooks')
->where($query->expr()->eq('principaluri', $query->createParameter('principaluri')))
->setParameter('principaluri', $principalUri);
$addressBooks = [];
$result = $query->execute();
while($row = $result->fetch()) {
$addressBooks[] = [
'id' => $row['id'],
'uri' => $row['uri'],
'principaluri' => $row['principaluri'],
'{DAV:}displayname' => $row['displayname'],
'{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
];
}
$result->closeCursor();
return $addressBooks;
}
/**
* Updates properties for an address book.
*
* The list of mutations is stored in a Sabre\DAV\PropPatch object.
* To do the actual updates, you must tell this object which properties
* you're going to process with the handle() method.
*
* Calling the handle method is like telling the PropPatch object "I
* promise I can handle updating this property".
*
* Read the PropPatch documenation for more info and examples.
*
* @param string $addressBookId
* @param \Sabre\DAV\PropPatch $propPatch
* @return void
*/
function updateAddressBook($addressBookId, \Sabre\DAV\PropPatch $propPatch) {
$supportedProperties = [
'{DAV:}displayname',
'{' . Plugin::NS_CARDDAV . '}addressbook-description',
];
$propPatch->handle($supportedProperties, function($mutations) use ($addressBookId) {
$updates = [];
foreach($mutations as $property=>$newValue) {
switch($property) {
case '{DAV:}displayname' :
$updates['displayname'] = $newValue;
break;
case '{' . Plugin::NS_CARDDAV . '}addressbook-description' :
$updates['description'] = $newValue;
break;
}
}
$query = $this->db->getQueryBuilder();
$query->update('addressbooks');
foreach($updates as $key=>$value) {
$query->set($key, $query->createNamedParameter($value));
}
$query->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId)))
->execute();
$this->addChange($addressBookId, "", 2);
return true;
});
}
/**
* Creates a new address book
*
* @param string $principalUri
* @param string $url Just the 'basename' of the url.
* @param array $properties
* @return void
*/
function createAddressBook($principalUri, $url, array $properties) {
$values = [
'displayname' => null,
'description' => null,
'principaluri' => $principalUri,
'uri' => $url,
'synctoken' => 1
];
foreach($properties as $property=>$newValue) {
switch($property) {
case '{DAV:}displayname' :
$values['displayname'] = $newValue;
break;
case '{' . Plugin::NS_CARDDAV . '}addressbook-description' :
$values['description'] = $newValue;
break;
default :
throw new BadRequest('Unknown property: ' . $property);
}
}
$query = $this->db->getQueryBuilder();
$query->insert('addressbooks')
->values([
'uri' => $query->createParameter('uri'),
'displayname' => $query->createParameter('displayname'),
'description' => $query->createParameter('description'),
'principaluri' => $query->createParameter('principaluri'),
'synctoken' => $query->createParameter('synctoken'),
])
->setParameters($values)
->execute();
}
/**
* Deletes an entire addressbook and all its contents
*
* @param mixed $addressBookId
* @return void
*/
function deleteAddressBook($addressBookId) {
$query = $this->db->getQueryBuilder();
$query->delete('cards')
->where($query->expr()->eq('addressbookid', $query->createParameter('addressbookid')))
->setParameter('addressbookid', $addressBookId)
->execute();
$query->delete('addressbookchanges')
->where($query->expr()->eq('addressbookid', $query->createParameter('addressbookid')))
->setParameter('addressbookid', $addressBookId)
->execute();
$query->delete('addressbooks')
->where($query->expr()->eq('id', $query->createParameter('id')))
->setParameter('id', $addressBookId)
->execute();
}
/**
* Returns all cards for a specific addressbook id.
*
* This method should return the following properties for each card:
* * carddata - raw vcard data
* * uri - Some unique url
* * lastmodified - A unix timestamp
*
* It's recommended to also return the following properties:
* * etag - A unique etag. This must change every time the card changes.
* * size - The size of the card in bytes.
*
* If these last two properties are provided, less time will be spent
* calculating them. If they are specified, you can also ommit carddata.
* This may speed up certain requests, especially with large cards.
*
* @param mixed $addressBookId
* @return array
*/
function getCards($addressBookId) {
$query = $this->db->getQueryBuilder();
$query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata'])
->from('cards')
->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
$cards = [];
$result = $query->execute();
while($row = $result->fetch()) {
$row['etag'] = '"' . $row['etag'] . '"';
$row['carddata'] = $this->readBlob($row['carddata']);
$cards[] = $row;
}
$result->closeCursor();
return $cards;
}
/**
* Returns a specfic card.
*
* The same set of properties must be returned as with getCards. The only
* exception is that 'carddata' is absolutely required.
*
* If the card does not exist, you must return false.
*
* @param mixed $addressBookId
* @param string $cardUri
* @return array
*/
function getCard($addressBookId, $cardUri) {
$query = $this->db->getQueryBuilder();
$query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata'])
->from('cards')
->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
->andWhere($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
->setMaxResults(1);
$result = $query->execute();
$row = $result->fetch();
if (!$row) {
return false;
}
$row['etag'] = '"' . $row['etag'] . '"';
$row['carddata'] = $this->readBlob($row['carddata']);
return $row;
}
/**
* Returns a list of cards.
*
* This method should work identical to getCard, but instead return all the
* cards in the list as an array.
*
* If the backend supports this, it may allow for some speed-ups.
*
* @param mixed $addressBookId
* @param array $uris
* @return array
*/
function getMultipleCards($addressBookId, array $uris) {
$query = $this->db->getQueryBuilder();
$query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata'])
->from('cards')
->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
->andWhere($query->expr()->in('uri', $query->createParameter('uri')))
->setParameter('uri', $uris, \Doctrine\DBAL\Connection::PARAM_STR_ARRAY);
$cards = [];
$result = $query->execute();
while($row = $result->fetch()) {
$row['etag'] = '"' . $row['etag'] . '"';
$row['carddata'] = $this->readBlob($row['carddata']);
$cards[] = $row;
}
$result->closeCursor();
return $cards;
}
/**
* Creates a new card.
*
* The addressbook id will be passed as the first argument. This is the
* same id as it is returned from the getAddressBooksForUser method.
*
* The cardUri is a base uri, and doesn't include the full path. The
* cardData argument is the vcard body, and is passed as a string.
*
* It is possible to return an ETag from this method. This ETag is for the
* newly created resource, and must be enclosed with double quotes (that
* is, the string itself must contain the double quotes).
*
* You should only return the ETag if you store the carddata as-is. If a
* subsequent GET request on the same card does not have the same body,
* byte-by-byte and you did return an ETag here, clients tend to get
* confused.
*
* If you don't return an ETag, you can just return null.
*
* @param mixed $addressBookId
* @param string $cardUri
* @param string $cardData
* @return string|null
*/
function createCard($addressBookId, $cardUri, $cardData) {
$etag = md5($cardData);
$query = $this->db->getQueryBuilder();
$query->insert('cards')
->values([
'carddata' => $query->createNamedParameter($cardData),
'uri' => $query->createNamedParameter($cardUri),
'lastmodified' => $query->createNamedParameter(time()),
'addressbookid' => $query->createNamedParameter($addressBookId),
'size' => $query->createNamedParameter(strlen($cardData)),
'etag' => $query->createNamedParameter($etag),
])
->execute();
$this->addChange($addressBookId, $cardUri, 1);
return '"' . $etag . '"';
}
/**
* Updates a card.
*
* The addressbook id will be passed as the first argument. This is the
* same id as it is returned from the getAddressBooksForUser method.
*
* The cardUri is a base uri, and doesn't include the full path. The
* cardData argument is the vcard body, and is passed as a string.
*
* It is possible to return an ETag from this method. This ETag should
* match that of the updated resource, and must be enclosed with double
* quotes (that is: the string itself must contain the actual quotes).
*
* You should only return the ETag if you store the carddata as-is. If a
* subsequent GET request on the same card does not have the same body,
* byte-by-byte and you did return an ETag here, clients tend to get
* confused.
*
* If you don't return an ETag, you can just return null.
*
* @param mixed $addressBookId
* @param string $cardUri
* @param string $cardData
* @return string|null
*/
function updateCard($addressBookId, $cardUri, $cardData) {
$etag = md5($cardData);
$query = $this->db->getQueryBuilder();
$query->update('cards')
->set('carddata', $query->createNamedParameter($cardData, \PDO::PARAM_LOB))
->set('lastmodified', $query->createNamedParameter(time()))
->set('size', $query->createNamedParameter(strlen($cardData)))
->set('etag', $query->createNamedParameter($etag))
->where($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
->execute();
$this->addChange($addressBookId, $cardUri, 2);
return '"' . $etag . '"';
}
/**
* Deletes a card
*
* @param mixed $addressBookId
* @param string $cardUri
* @return bool
*/
function deleteCard($addressBookId, $cardUri) {
$query = $this->db->getQueryBuilder();
$ret = $query->delete('cards')
->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
->andWhere($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
->execute();
$this->addChange($addressBookId, $cardUri, 3);
return $ret === 1;
}
/**
* The getChanges method returns all the changes that have happened, since
* the specified syncToken in the specified address book.
*
* This function should return an array, such as the following:
*
* [
* 'syncToken' => 'The current synctoken',
* 'added' => [
* 'new.txt',
* ],
* 'modified' => [
* 'modified.txt',
* ],
* 'deleted' => [
* 'foo.php.bak',
* 'old.txt'
* ]
* ];
*
* The returned syncToken property should reflect the *current* syncToken
* of the calendar, as reported in the {http://sabredav.org/ns}sync-token
* property. This is needed here too, to ensure the operation is atomic.
*
* If the $syncToken argument is specified as null, this is an initial
* sync, and all members should be reported.
*
* The modified property is an array of nodenames that have changed since
* the last token.
*
* The deleted property is an array with nodenames, that have been deleted
* from collection.
*
* The $syncLevel argument is basically the 'depth' of the report. If it's
* 1, you only have to report changes that happened only directly in
* immediate descendants. If it's 2, it should also include changes from
* the nodes below the child collections. (grandchildren)
*
* The $limit argument allows a client to specify how many results should
* be returned at most. If the limit is not specified, it should be treated
* as infinite.
*
* If the limit (infinite or not) is higher than you're willing to return,
* you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
*
* If the syncToken is expired (due to data cleanup) or unknown, you must
* return null.
*
* The limit is 'suggestive'. You are free to ignore it.
*
* @param string $addressBookId
* @param string $syncToken
* @param int $syncLevel
* @param int $limit
* @return array
*/
function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, $limit = null) {
// Current synctoken
$stmt = $this->db->prepare('SELECT `synctoken` FROM `*PREFIX*addressbooks` WHERE `id` = ?');
$stmt->execute([ $addressBookId ]);
$currentToken = $stmt->fetchColumn(0);
if (is_null($currentToken)) return null;
$result = [
'syncToken' => $currentToken,
'added' => [],
'modified' => [],
'deleted' => [],
];
if ($syncToken) {
$query = "SELECT `uri`, `operation` FROM `*PREFIX*addressbookchanges` WHERE `synctoken` >= ? AND `synctoken` < ? AND `addressbookid` = ? ORDER BY `synctoken`";
if ($limit>0) {
$query .= " `LIMIT` " . (int)$limit;
}
// Fetching all changes
$stmt = $this->db->prepare($query);
$stmt->execute([$syncToken, $currentToken, $addressBookId]);
$changes = [];
// This loop ensures that any duplicates are overwritten, only the
// last change on a node is relevant.
while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
$changes[$row['uri']] = $row['operation'];
}
foreach($changes as $uri => $operation) {
switch($operation) {
case 1:
$result['added'][] = $uri;
break;
case 2:
$result['modified'][] = $uri;
break;
case 3:
$result['deleted'][] = $uri;
break;
}
}
} else {
// No synctoken supplied, this is the initial sync.
$query = "SELECT `uri` FROM `*PREFIX*cards` WHERE `addressbookid` = ?";
$stmt = $this->db->prepare($query);
$stmt->execute([$addressBookId]);
$result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
}
return $result;
}
/**
* Adds a change record to the addressbookchanges table.
*
* @param mixed $addressBookId
* @param string $objectUri
* @param int $operation 1 = add, 2 = modify, 3 = delete
* @return void
*/
protected function addChange($addressBookId, $objectUri, $operation) {
$sql = 'INSERT INTO `*PREFIX*addressbookchanges`(`uri`, `synctoken`, `addressbookid`, `operation`) SELECT ?, `synctoken`, ?, ? FROM `*PREFIX*addressbooks` WHERE `id` = ?';
$stmt = $this->db->prepare($sql);
$stmt->execute([
$objectUri,
$addressBookId,
$operation,
$addressBookId
]);
$stmt = $this->db->prepare('UPDATE `*PREFIX*addressbooks` SET `synctoken` = `synctoken` + 1 WHERE `id` = ?');
$stmt->execute([
$addressBookId
]);
}
private function readBlob($cardData) {
if (is_resource($cardData)) {
return stream_get_contents($cardData);
}
return $cardData;
}
}

View file

@ -2,8 +2,10 @@
namespace OCA\DAV; namespace OCA\DAV;
use OCA\DAV\CardDAV\CardDavBackend;
use OCA\DAV\Connector\Sabre\Principal; use OCA\DAV\Connector\Sabre\Principal;
use Sabre\CalDAV\Principal\Collection; use Sabre\CalDAV\Principal\Collection;
use Sabre\CardDAV\AddressBookRoot;
use Sabre\DAV\SimpleCollection; use Sabre\DAV\SimpleCollection;
class RootCollection extends SimpleCollection { class RootCollection extends SimpleCollection {
@ -22,10 +24,14 @@ class RootCollection extends SimpleCollection {
$principalCollection->disableListing = $disableListing; $principalCollection->disableListing = $disableListing;
$filesCollection = new Files\RootCollection($principalBackend); $filesCollection = new Files\RootCollection($principalBackend);
$filesCollection->disableListing = $disableListing; $filesCollection->disableListing = $disableListing;
$cardDavBackend = new CardDavBackend(\OC::$server->getDatabaseConnection());
$addressBookRoot = new AddressBookRoot($principalBackend, $cardDavBackend);
$addressBookRoot->disableListing = $disableListing;
$children = [ $children = [
$principalCollection, $principalCollection,
$filesCollection, $filesCollection,
$addressBookRoot,
]; ];
parent::__construct('root', $children); parent::__construct('root', $children);

View file

@ -0,0 +1,13 @@
<?php
define('PHPUNIT_RUN', 1);
require_once __DIR__.'/../../../../lib/base.php';
if(!class_exists('PHPUnit_Framework_TestCase')) {
require_once('PHPUnit/Autoload.php');
}
\OC_App::loadApp('dav');
OC_Hook::clear();

View file

@ -0,0 +1,180 @@
<?php
/**
* @author Lukas Reschke <lukas@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\CardDavBackend;
use Sabre\DAV\PropPatch;
use Test\TestCase;
class CardDavBackendTest extends TestCase {
/** @var CardDavBackend */
private $backend;
const UNIT_TEST_USER = 'carddav-unit-test';
public function setUp() {
parent::setUp();
$db = \OC::$server->getDatabaseConnection();
$this->backend = new CardDavBackend($db);
$this->tearDown();
}
public function tearDown() {
parent::tearDown();
if (is_null($this->backend)) {
return;
}
$books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER);
foreach ($books as $book) {
$this->backend->deleteAddressBook($book['id']);
}
}
public function testAddressBookOperations() {
// create a new address book
$this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []);
$books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER);
$this->assertEquals(1, count($books));
// update it's display name
$patch = new PropPatch([
'{DAV:}displayname' => 'Unit test',
'{urn:ietf:params:xml:ns:carddav}addressbook-description' => 'Addressbook used for unit testing'
]);
$this->backend->updateAddressBook($books[0]['id'], $patch);
$patch->commit();
$books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER);
$this->assertEquals(1, count($books));
$this->assertEquals('Unit test', $books[0]['{DAV:}displayname']);
$this->assertEquals('Addressbook used for unit testing', $books[0]['{urn:ietf:params:xml:ns:carddav}addressbook-description']);
// delete the address book
$this->backend->deleteAddressBook($books[0]['id']);
$books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER);
$this->assertEquals(0, count($books));
}
public function testCardOperations() {
// create a new address book
$this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []);
$books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER);
$this->assertEquals(1, count($books));
$bookId = $books[0]['id'];
// create a card
$uri = $this->getUniqueID('card');
$this->backend->createCard($bookId, $uri, '');
// get all the cards
$cards = $this->backend->getCards($bookId);
$this->assertEquals(1, count($cards));
$this->assertEquals('', $cards[0]['carddata']);
// get the cards
$card = $this->backend->getCard($bookId, $uri);
$this->assertNotNull($card);
$this->assertArrayHasKey('id', $card);
$this->assertArrayHasKey('uri', $card);
$this->assertArrayHasKey('lastmodified', $card);
$this->assertArrayHasKey('etag', $card);
$this->assertArrayHasKey('size', $card);
$this->assertEquals('', $card['carddata']);
// update the card
$this->backend->updateCard($bookId, $uri, '***');
$card = $this->backend->getCard($bookId, $uri);
$this->assertEquals('***', $card['carddata']);
// delete the card
$this->backend->deleteCard($bookId, $uri);
$cards = $this->backend->getCards($bookId);
$this->assertEquals(0, count($cards));
}
public function testMultiCard() {
// create a new address book
$this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []);
$books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER);
$this->assertEquals(1, count($books));
$bookId = $books[0]['id'];
// create a card
$uri0 = $this->getUniqueID('card');
$this->backend->createCard($bookId, $uri0, '');
$uri1 = $this->getUniqueID('card');
$this->backend->createCard($bookId, $uri1, '');
$uri2 = $this->getUniqueID('card');
$this->backend->createCard($bookId, $uri2, '');
// get all the cards
$cards = $this->backend->getCards($bookId);
$this->assertEquals(3, count($cards));
$this->assertEquals('', $cards[0]['carddata']);
$this->assertEquals('', $cards[1]['carddata']);
$this->assertEquals('', $cards[2]['carddata']);
// get the cards
$cards = $this->backend->getMultipleCards($bookId, [$uri1, $uri2]);
$this->assertEquals(2, count($cards));
foreach($cards as $card) {
$this->assertArrayHasKey('id', $card);
$this->assertArrayHasKey('uri', $card);
$this->assertArrayHasKey('lastmodified', $card);
$this->assertArrayHasKey('etag', $card);
$this->assertArrayHasKey('size', $card);
$this->assertEquals('', $card['carddata']);
}
// delete the card
$this->backend->deleteCard($bookId, $uri0);
$this->backend->deleteCard($bookId, $uri1);
$this->backend->deleteCard($bookId, $uri2);
$cards = $this->backend->getCards($bookId);
$this->assertEquals(0, count($cards));
}
public function testSyncSupport() {
// create a new address book
$this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []);
$books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER);
$this->assertEquals(1, count($books));
$bookId = $books[0]['id'];
// fist call without synctoken
$changes = $this->backend->getChangesForAddressBook($bookId, '', 1);
$syncToken = $changes['syncToken'];
// add a change
$uri0 = $this->getUniqueID('card');
$this->backend->createCard($bookId, $uri0, '');
// look for changes
$changes = $this->backend->getChangesForAddressBook($bookId, $syncToken, 1);
$this->assertEquals($uri0, $changes['added'][0]);
}
}

View file

@ -19,7 +19,7 @@
* *
*/ */
namespace Test\Connector\Sabre; namespace OCA\DAV\Tests\Unit\Connector\Sabre;
use OCA\DAV\Connector\Sabre\BlockLegacyClientPlugin; use OCA\DAV\Connector\Sabre\BlockLegacyClientPlugin;
use Test\TestCase; use Test\TestCase;

View file

@ -19,7 +19,7 @@
* *
*/ */
namespace Test\Connector\Sabre; namespace OCA\DAV\Tests\Unit\Connector\Sabre;
use OCA\DAV\Connector\Sabre\DummyGetResponsePlugin; use OCA\DAV\Connector\Sabre\DummyGetResponsePlugin;
use Test\TestCase; use Test\TestCase;

View file

@ -19,7 +19,7 @@
* *
*/ */
namespace Test\Connector\Sabre; namespace OCA\DAV\Tests\Unit\Connector\Sabre;
use OCA\DAV\Connector\Sabre\MaintenancePlugin; use OCA\DAV\Connector\Sabre\MaintenancePlugin;
use Test\TestCase; use Test\TestCase;

View file

@ -18,7 +18,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/> * along with this program. If not, see <http://www.gnu.org/licenses/>
* *
*/ */
namespace Tests\Connector\Sabre;
namespace OCA\DAV\Tests\Unit\Connector\Sabre;
use Test\TestCase; use Test\TestCase;
use OCP\ISession; use OCP\ISession;

View file

@ -1,6 +1,6 @@
<?php <?php
namespace Tests\Connector\Sabre; namespace OCA\DAV\Tests\Unit\Connector\Sabre;
/** /**
* Copyright (c) 2015 Vincent Petry <pvince81@owncloud.com> * Copyright (c) 2015 Vincent Petry <pvince81@owncloud.com>

View file

@ -1,6 +1,6 @@
<?php <?php
namespace Tests\Connector\Sabre; namespace OCA\DAV\Tests\Unit\Connector\Sabre;
/** /**
* Copyright (c) 2015 Vincent Petry <pvince81@owncloud.com> * Copyright (c) 2015 Vincent Petry <pvince81@owncloud.com>
@ -16,7 +16,7 @@ class CustomPropertiesBackend extends \Test\TestCase {
private $server; private $server;
/** /**
* @var \Sabre\DAV\ObjectTree * @var \Sabre\DAV\Tree
*/ */
private $tree; private $tree;

View file

@ -6,11 +6,14 @@
* later. * later.
* See the COPYING-README file. * See the COPYING-README file.
*/ */
class Test_OC_Connector_Sabre_Directory extends \Test\TestCase {
/** @var OC\Files\View | PHPUnit_Framework_MockObject_MockObject */ namespace OCA\DAV\Tests\Unit\Connector\Sabre;
class Directory extends \Test\TestCase {
/** @var \OC\Files\View | \PHPUnit_Framework_MockObject_MockObject */
private $view; private $view;
/** @var OC\Files\FileInfo | PHPUnit_Framework_MockObject_MockObject */ /** @var \OC\Files\FileInfo | \PHPUnit_Framework_MockObject_MockObject */
private $info; private $info;
protected function setUp() { protected function setUp() {

View file

@ -1,6 +1,6 @@
<?php <?php
namespace Test\Connector\Sabre\Exception; namespace OCA\DAV\Tests\Unit\Connector\Sabre\Exception;
use OCA\DAV\Connector\Sabre\Exception\InvalidPath; use OCA\DAV\Connector\Sabre\Exception\InvalidPath;

View file

@ -7,7 +7,7 @@
* See the COPYING-README file. * See the COPYING-README file.
*/ */
namespace Test\Connector\Sabre; namespace OCA\DAV\Tests\Unit\Connector\Sabre;
use OCA\DAV\Connector\Sabre\Exception\InvalidPath; use OCA\DAV\Connector\Sabre\Exception\InvalidPath;
use OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin as PluginToTest; use OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin as PluginToTest;

View file

@ -6,7 +6,7 @@
* See the COPYING-README file. * See the COPYING-README file.
*/ */
namespace Test\Connector\Sabre; namespace OCA\DAV\Tests\Unit\Connector\Sabre;
use OC\Files\Storage\Local; use OC\Files\Storage\Local;
use Test\HookHelper; use Test\HookHelper;

View file

@ -1,6 +1,6 @@
<?php <?php
namespace Tests\Connector\Sabre; namespace OCA\DAV\Tests\Unit\Connector\Sabre;
/** /**
* Copyright (c) 2015 Vincent Petry <pvince81@owncloud.com> * Copyright (c) 2015 Vincent Petry <pvince81@owncloud.com>

View file

@ -7,7 +7,7 @@
* See the COPYING-README file. * See the COPYING-README file.
*/ */
namespace Test\Connector\Sabre; namespace OCA\DAV\Tests\Unit\Connector\Sabre;
class Node extends \Test\TestCase { class Node extends \Test\TestCase {
public function davPermissionsProvider() { public function davPermissionsProvider() {

View file

@ -6,11 +6,10 @@
* See the COPYING-README file. * See the COPYING-README file.
*/ */
namespace Test\OCA\DAV\Connector\Sabre; namespace OCA\DAV\Tests\Unit\Connector\Sabre;
use OC\Files\FileInfo; use OC\Files\FileInfo;
use OCA\DAV\Connector\Sabre\Directory;
use OC\Files\Storage\Temporary; use OC\Files\Storage\Temporary;
class TestDoubleFileView extends \OC\Files\View { class TestDoubleFileView extends \OC\Files\View {
@ -103,7 +102,7 @@ class ObjectTree extends \Test\TestCase {
$info = new FileInfo('', null, null, array(), null); $info = new FileInfo('', null, null, array(), null);
$rootDir = new Directory($view, $info); $rootDir = new \OCA\DAV\Connector\Sabre\Directory($view, $info);
$objectTree = $this->getMock('\OCA\DAV\Connector\Sabre\ObjectTree', $objectTree = $this->getMock('\OCA\DAV\Connector\Sabre\ObjectTree',
array('nodeExists', 'getNodeForPath'), array('nodeExists', 'getNodeForPath'),
array($rootDir, $view)); array($rootDir, $view));

View file

@ -8,7 +8,7 @@
* See the COPYING-README file. * See the COPYING-README file.
*/ */
namespace Test\Connector\Sabre; namespace OCA\DAV\Tests\Unit\Connector\Sabre;
use \Sabre\DAV\PropPatch; use \Sabre\DAV\PropPatch;
use OCP\IUserManager; use OCP\IUserManager;

View file

@ -1,12 +1,13 @@
<?php <?php
namespace OCA\DAV\Tests\Unit\Connector\Sabre;
/** /**
* Copyright (c) 2013 Thomas Müller <thomas.mueller@tmit.eu> * Copyright (c) 2013 Thomas Müller <thomas.mueller@tmit.eu>
* This file is licensed under the Affero General Public License version 3 or * This file is licensed under the Affero General Public License version 3 or
* later. * later.
* See the COPYING-README file. * See the COPYING-README file.
*/ */
class Test_OC_Connector_Sabre_QuotaPlugin extends \Test\TestCase { class QuotaPlugin extends \Test\TestCase {
/** /**
* @var \Sabre\DAV\Server * @var \Sabre\DAV\Server

View file

@ -1,6 +1,6 @@
<?php <?php
namespace Tests\Connector\Sabre; namespace OCA\DAV\Tests\Unit\Connector\Sabre;
/** /**
* Copyright (c) 2014 Vincent Petry <pvince81@owncloud.com> * Copyright (c) 2014 Vincent Petry <pvince81@owncloud.com>
@ -20,7 +20,7 @@ class TagsPlugin extends \Test\TestCase {
private $server; private $server;
/** /**
* @var \Sabre\DAV\ObjectTree * @var \Sabre\DAV\Tree
*/ */
private $tree; private $tree;

View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8" ?>
<phpunit bootstrap="bootstrap.php"
verbose="true"
timeoutForSmallTests="900"
timeoutForMediumTests="900"
timeoutForLargeTests="900"
>
<testsuite name='unit'>
<directory suffix='test.php'>.</directory>
</testsuite>
<!-- filters for code coverage -->
<filter>
<whitelist>
<directory suffix=".php">../../dav</directory>
<exclude>
<directory suffix=".php">../../dav/tests</directory>
</exclude>
</whitelist>
</filter>
<logging>
<!-- and this is where your report will be written -->
<log type="coverage-clover" target="./clover.xml"/>
</logging>
</phpunit>