diff options
24 files changed, 939 insertions, 75 deletions
diff --git a/apps/dav/appinfo/register_command.php b/apps/dav/appinfo/register_command.php index 1f0df054110..af41036cddc 100644 --- a/apps/dav/appinfo/register_command.php +++ b/apps/dav/appinfo/register_command.php @@ -2,7 +2,9 @@ use OCA\DAV\Command\CreateAddressBook; use OCA\DAV\Command\CreateCalendar; +use OCA\DAV\Command\SyncSystemAddressBook; +$config = \OC::$server->getConfig(); $dbConnection = \OC::$server->getDatabaseConnection(); $userManager = OC::$server->getUserManager(); $config = \OC::$server->getConfig(); @@ -10,3 +12,4 @@ $config = \OC::$server->getConfig(); /** @var Symfony\Component\Console\Application $application */ $application->add(new CreateAddressBook($userManager, $dbConnection, $config)); $application->add(new CreateCalendar($userManager, $dbConnection)); +$application->add(new SyncSystemAddressBook($userManager, $dbConnection, $config)); diff --git a/apps/dav/command/syncsystemaddressbook.php b/apps/dav/command/syncsystemaddressbook.php new file mode 100644 index 00000000000..bb2896abc60 --- /dev/null +++ b/apps/dav/command/syncsystemaddressbook.php @@ -0,0 +1,107 @@ +<?php + +namespace OCA\DAV\Command; + +use OCA\DAV\CardDAV\CardDavBackend; +use OCA\DAV\CardDAV\Converter; +use OCA\DAV\Connector\Sabre\Principal; +use OCP\IConfig; +use OCP\IDBConnection; +use OCP\IUser; +use OCP\IUserManager; +use Sabre\CardDAV\Plugin; +use Sabre\VObject\Component\VCard; +use Sabre\VObject\Property\Text; +use Sabre\VObject\Reader; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\ProgressBar; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class SyncSystemAddressBook extends Command { + + /** @var IUserManager */ + protected $userManager; + + /** @var \OCP\IDBConnection */ + protected $dbConnection; + + /** @var IConfig */ + protected $config; + + /** @var CardDavBackend */ + private $backend; + + /** + * @param IUserManager $userManager + * @param IDBConnection $dbConnection + * @param IConfig $config + */ + function __construct(IUserManager $userManager, IDBConnection $dbConnection, IConfig $config) { + parent::__construct(); + $this->userManager = $userManager; + $this->dbConnection = $dbConnection; + $this->config = $config; + } + + protected function configure() { + $this + ->setName('dav:sync-system-addressbook') + ->setDescription('Synchronizes users to the system addressbook'); + } + + /** + * @param InputInterface $input + * @param OutputInterface $output + */ + protected function execute(InputInterface $input, OutputInterface $output) { + $principalBackend = new Principal( + $this->config, + $this->userManager + ); + + $this->backend = new CardDavBackend($this->dbConnection, $principalBackend); + + // ensure system addressbook exists + $systemAddressBook = $this->ensureSystemAddressBookExists(); + $converter = new Converter(); + + $output->writeln('Syncing users ...'); + $progress = new ProgressBar($output); + $progress->start(); + $this->userManager->callForAllUsers(function($user) use ($systemAddressBook, $converter, $progress) { + /** @var IUser $user */ + $name = $user->getBackendClassName(); + $userId = $user->getUID(); + + $cardId = "$name:$userId.vcf"; + $card = $this->backend->getCard($systemAddressBook['id'], $cardId); + if ($card === false) { + $vCard = $converter->createCardFromUser($user); + $this->backend->createCard($systemAddressBook['id'], $cardId, $vCard->serialize()); + } else { + $vCard = Reader::read($card['carddata']); + if ($converter->updateCard($vCard, $user)) { + $this->backend->updateCard($systemAddressBook['id'], $cardId, $vCard->serialize()); + } + } + $progress->advance(); + }); + $progress->finish(); + $output->writeln(''); + } + + protected function ensureSystemAddressBookExists() { + $book = $this->backend->getAddressBooksByUri('system'); + if (!is_null($book)) { + return $book; + } + $systemPrincipal = "principals/system/system"; + $this->backend->createAddressBook($systemPrincipal, 'system', [ + '{' . Plugin::NS_CARDDAV . '}addressbook-description' => 'System addressbook which holds all users of this instance' + ]); + + return $this->backend->getAddressBooksByUri('system'); + } +} diff --git a/apps/dav/lib/carddav/addressbook.php b/apps/dav/lib/carddav/addressbook.php index eff1ad321e5..507657e9682 100644 --- a/apps/dav/lib/carddav/addressbook.php +++ b/apps/dav/lib/carddav/addressbook.php @@ -3,6 +3,7 @@ namespace OCA\DAV\CardDAV; use OCA\DAV\CardDAV\Sharing\IShareableAddressBook; +use Sabre\DAV\Exception\NotFound; class AddressBook extends \Sabre\CardDAV\AddressBook implements IShareableAddressBook { @@ -51,4 +52,39 @@ class AddressBook extends \Sabre\CardDAV\AddressBook implements IShareableAddres $carddavBackend = $this->carddavBackend; $carddavBackend->getShares($this->getName()); } + + function getACL() { + $acl = parent::getACL(); + if ($this->getOwner() === 'principals/system/system') { + $acl[] = [ + 'privilege' => '{DAV:}read', + 'principal' => '{DAV:}authenticated', + 'protected' => true, + ]; + } + + return $acl; + } + + function getChildACL() { + $acl = parent::getChildACL(); + if ($this->getOwner() === 'principals/system/system') { + $acl[] = [ + 'privilege' => '{DAV:}read', + 'principal' => '{DAV:}authenticated', + 'protected' => true, + ]; + } + + return $acl; + } + + function getChild($name) { + $obj = $this->carddavBackend->getCard($this->addressBookInfo['id'], $name); + if (!$obj) { + throw new NotFound('Card not found'); + } + return new Card($this->carddavBackend, $this->addressBookInfo, $obj); + } + } diff --git a/apps/dav/lib/carddav/addressbookroot.php b/apps/dav/lib/carddav/addressbookroot.php index ee99ac8d798..8c78d024556 100644 --- a/apps/dav/lib/carddav/addressbookroot.php +++ b/apps/dav/lib/carddav/addressbookroot.php @@ -20,4 +20,14 @@ class AddressBookRoot extends \Sabre\CardDAV\AddressBookRoot { } -}
\ No newline at end of file + function getName() { + + // Grabbing all the components of the principal path. + $parts = explode('/', $this->principalPrefix); + + // We are only interested in the second part. + return $parts[1]; + + } + +} diff --git a/apps/dav/lib/carddav/card.php b/apps/dav/lib/carddav/card.php new file mode 100644 index 00000000000..cea0b1e41c7 --- /dev/null +++ b/apps/dav/lib/carddav/card.php @@ -0,0 +1,39 @@ +<?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 Card extends \Sabre\CardDAV\Card { + + function getACL() { + $acl = parent::getACL(); + if ($this->getOwner() === 'principals/system/system') { + $acl[] = [ + 'privilege' => '{DAV:}read', + 'principal' => '{DAV:}authenticated', + 'protected' => true, + ]; + } + + return $acl; + } + +} diff --git a/apps/dav/lib/carddav/carddavbackend.php b/apps/dav/lib/carddav/carddavbackend.php index daa31725fa1..29b056672b4 100644 --- a/apps/dav/lib/carddav/carddavbackend.php +++ b/apps/dav/lib/carddav/carddavbackend.php @@ -108,7 +108,7 @@ class CardDavBackend implements BackendInterface, SyncSupport { return $addressBooks; } - private function getAddressBooksByUri($addressBookUri) { + public function getAddressBooksByUri($addressBookUri) { $query = $this->db->getQueryBuilder(); $result = $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken']) ->from('addressbooks') @@ -117,10 +117,10 @@ class CardDavBackend implements BackendInterface, SyncSupport { ->execute(); $row = $result->fetch(); - if (is_null($row)) { + $result->closeCursor(); + if ($row === false) { return null; } - $result->closeCursor(); return [ 'id' => $row['id'], diff --git a/apps/dav/lib/carddav/converter.php b/apps/dav/lib/carddav/converter.php new file mode 100644 index 00000000000..56b73eba4c0 --- /dev/null +++ b/apps/dav/lib/carddav/converter.php @@ -0,0 +1,158 @@ +<?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 OCP\IImage; +use OCP\IUser; +use Sabre\VObject\Component\VCard; +use Sabre\VObject\Property\Text; + +class Converter { + + /** + * @param IUser $user + * @return VCard + */ + public function createCardFromUser(IUser $user) { + + $uid = $user->getUID(); + $displayName = $user->getDisplayName(); + $displayName = empty($displayName ) ? $uid : $displayName; + $emailAddress = $user->getEMailAddress(); + $cloudId = $user->getCloudId(); + $image = $user->getAvatarImage(-1); + + $vCard = new VCard(); + $vCard->add(new Text($vCard, 'UID', $uid)); + if (!empty($displayName)) { + $vCard->add(new Text($vCard, 'FN', $displayName)); + $vCard->add(new Text($vCard, 'N', $this->splitFullName($displayName))); + } + if (!empty($emailAddress)) { + $vCard->add(new Text($vCard, 'EMAIL', $emailAddress, ['TYPE' => 'OTHER'])); + } + if (!empty($cloudId)) { + $vCard->add(new Text($vCard, 'CLOUD', $cloudId)); + } + if ($image) { + $vCard->add('PHOTO', $image->data(), ['ENCODING' => 'b', 'TYPE' => $image->mimeType()]); + } + $vCard->validate(); + + return $vCard; + } + + /** + * @param VCard $vCard + * @param IUser $user + * @return bool + */ + public function updateCard(VCard $vCard, IUser $user) { + $uid = $user->getUID(); + $displayName = $user->getDisplayName(); + $displayName = empty($displayName ) ? $uid : $displayName; + $emailAddress = $user->getEMailAddress(); + $cloudId = $user->getCloudId(); + $image = $user->getAvatarImage(-1); + + $updated = false; + if($this->propertyNeedsUpdate($vCard, 'FN', $displayName)) { + $vCard->FN = new Text($vCard, 'FN', $displayName); + unset($vCard->N); + $vCard->add(new Text($vCard, 'N', $this->splitFullName($displayName))); + $updated = true; + } + if($this->propertyNeedsUpdate($vCard, 'EMAIL', $emailAddress)) { + $vCard->EMAIL = new Text($vCard, 'EMAIL', $emailAddress); + $updated = true; + } + if($this->propertyNeedsUpdate($vCard, 'CLOUD', $cloudId)) { + $vCard->CLOUD = new Text($vCard, 'CLOUD', $cloudId); + $updated = true; + } + + if($this->propertyNeedsUpdate($vCard, 'PHOTO', $image)) { + $vCard->add('PHOTO', $image->data(), ['ENCODING' => 'b', 'TYPE' => $image->mimeType()]); + $updated = true; + } + + if (empty($emailAddress) && !is_null($vCard->EMAIL)) { + unset($vCard->EMAIL); + $updated = true; + } + if (empty($cloudId) && !is_null($vCard->CLOUD)) { + unset($vCard->CLOUD); + $updated = true; + } + if (empty($image) && !is_null($vCard->PHOTO)) { + unset($vCard->PHOTO); + $updated = true; + } + + return $updated; + } + + /** + * @param VCard $vCard + * @param string $name + * @param string|IImage $newValue + * @return bool + */ + private function propertyNeedsUpdate(VCard $vCard, $name, $newValue) { + if (is_null($newValue)) { + return false; + } + $value = $vCard->__get($name); + if (!is_null($value)) { + $value = $value->getValue(); + $newValue = $newValue instanceof IImage ? $newValue->data() : $newValue; + + return $value !== $newValue; + } + return true; + } + + /** + * @param string $fullName + * @return string[] + */ + public function splitFullName($fullName) { + // Very basic western style parsing. I'm not gonna implement + // https://github.com/android/platform_packages_providers_contactsprovider/blob/master/src/com/android/providers/contacts/NameSplitter.java ;) + + $elements = explode(' ', $fullName); + $result = ['', '', '', '', '']; + if (count($elements) > 2) { + $result[0] = implode(' ', array_slice($elements, count($elements)-1)); + $result[1] = $elements[0]; + $result[2] = implode(' ', array_slice($elements, 1, count($elements)-2)); + } elseif (count($elements) === 2) { + $result[0] = $elements[1]; + $result[1] = $elements[0]; + } else { + $result[0] = $elements[0]; + } + + return $result; + } + +} diff --git a/apps/dav/lib/carddav/plugin.php b/apps/dav/lib/carddav/plugin.php new file mode 100644 index 00000000000..f2b3dcbfda9 --- /dev/null +++ b/apps/dav/lib/carddav/plugin.php @@ -0,0 +1,47 @@ +<?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\HTTP\URLUtil; + +class Plugin extends \Sabre\CardDAV\Plugin { + + /** + * Returns the addressbook home for a given principal + * + * @param string $principal + * @return string + */ + protected function getAddressbookHomeForPrincipal($principal) { + + if (strrpos($principal, 'principals/users', -strlen($principal)) !== FALSE) { + list(, $principalId) = URLUtil::splitPath($principal); + return self::ADDRESSBOOK_ROOT . '/users/' . $principalId; + } + if (strrpos($principal, 'principals/system', -strlen($principal)) !== FALSE) { + list(, $principalId) = URLUtil::splitPath($principal); + return self::ADDRESSBOOK_ROOT . '/system/' . $principalId; + } + + throw new \LogicException('This is not supposed to happen'); + } +} diff --git a/apps/dav/lib/carddav/useraddressbooks.php b/apps/dav/lib/carddav/useraddressbooks.php index 5f618a0ece3..093cee0e1b2 100644 --- a/apps/dav/lib/carddav/useraddressbooks.php +++ b/apps/dav/lib/carddav/useraddressbooks.php @@ -11,13 +11,39 @@ class UserAddressBooks extends \Sabre\CardDAV\AddressBookHome { */ function getChildren() { - $addressbooks = $this->carddavBackend->getAddressBooksForUser($this->principalUri); - $objs = []; - foreach($addressbooks as $addressbook) { - $objs[] = new AddressBook($this->carddavBackend, $addressbook); + $addressBooks = $this->carddavBackend->getAddressBooksForUser($this->principalUri); + $objects = []; + foreach($addressBooks as $addressBook) { + $objects[] = new AddressBook($this->carddavBackend, $addressBook); } - return $objs; + return $objects; } + /** + * Returns a list of ACE's for this node. + * + * Each ACE has the following properties: + * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are + * currently the only supported privileges + * * 'principal', a url to the principal who owns the node + * * 'protected' (optional), indicating that this ACE is not allowed to + * be updated. + * + * @return array + */ + function getACL() { + + $acl = parent::getACL(); + if ($this->principalUri === 'principals/system/system') { + $acl[] = [ + 'privilege' => '{DAV:}read', + 'principal' => '{DAV:}authenticated', + 'protected' => true, + ]; + } + + return $acl; + } + } diff --git a/apps/dav/lib/dav/systemprincipalbackend.php b/apps/dav/lib/dav/systemprincipalbackend.php new file mode 100644 index 00000000000..2c2049ace60 --- /dev/null +++ b/apps/dav/lib/dav/systemprincipalbackend.php @@ -0,0 +1,183 @@ +<?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\DAV; + +use Sabre\DAVACL\PrincipalBackend\AbstractBackend; +use Sabre\HTTP\URLUtil; + +class SystemPrincipalBackend extends AbstractBackend { + + /** + * Returns a list of principals based on a prefix. + * + * This prefix will often contain something like 'principals'. You are only + * expected to return principals that are in this base path. + * + * You are expected to return at least a 'uri' for every user, you can + * return any additional properties if you wish so. Common properties are: + * {DAV:}displayname + * {http://sabredav.org/ns}email-address - This is a custom SabreDAV + * field that's actually injected in a number of other properties. If + * you have an email address, use this property. + * + * @param string $prefixPath + * @return array + */ + function getPrincipalsByPrefix($prefixPath) { + $principals = []; + + if ($prefixPath === 'principals/system') { + $principals[] = [ + 'uri' => 'principals/system/system', + '{DAV:}displayname' => 'system', + ]; + } + + return $principals; + } + + /** + * Returns a specific principal, specified by it's path. + * The returned structure should be the exact same as from + * getPrincipalsByPrefix. + * + * @param string $path + * @return array + */ + function getPrincipalByPath($path) { + + $elements = explode('/', $path); + if ($elements[0] !== 'principals') { + return null; + } + if ($elements[1] === 'system') { + $principal = [ + 'uri' => 'principals/system/system', + '{DAV:}displayname' => 'system', + ]; + return $principal; + } + + return null; + } + + /** + * Updates one ore more webdav properties on a principal. + * + * 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 documentation for more info and examples. + * + * @param string $path + * @param \Sabre\DAV\PropPatch $propPatch + * @return void + */ + function updatePrincipal($path, \Sabre\DAV\PropPatch $propPatch) { + } + + /** + * This method is used to search for principals matching a set of + * properties. + * + * This search is specifically used by RFC3744's principal-property-search + * REPORT. + * + * The actual search should be a unicode-non-case-sensitive search. The + * keys in searchProperties are the WebDAV property names, while the values + * are the property values to search on. + * + * By default, if multiple properties are submitted to this method, the + * various properties should be combined with 'AND'. If $test is set to + * 'anyof', it should be combined using 'OR'. + * + * This method should simply return an array with full principal uri's. + * + * If somebody attempted to search on a property the backend does not + * support, you should simply return 0 results. + * + * You can also just return 0 results if you choose to not support + * searching at all, but keep in mind that this may stop certain features + * from working. + * + * @param string $prefixPath + * @param array $searchProperties + * @param string $test + * @return array + */ + function searchPrincipals($prefixPath, array $searchProperties, $test = 'allof') { + return []; + } + + /** + * Returns the list of members for a group-principal + * + * @param string $principal + * @return array + */ + function getGroupMemberSet($principal) { + // TODO: for now the group principal has only one member, the user itself + $principal = $this->getPrincipalByPath($principal); + if (!$principal) { + throw new \Sabre\DAV\Exception('Principal not found'); + } + + return [$principal['uri']]; + } + + /** + * Returns the list of groups a principal is a member of + * + * @param string $principal + * @return array + */ + function getGroupMembership($principal) { + list($prefix, $name) = URLUtil::splitPath($principal); + + if ($prefix === 'principals/system') { + $principal = $this->getPrincipalByPath($principal); + if (!$principal) { + throw new \Sabre\DAV\Exception('Principal not found'); + } + + return []; + } + return []; + } + + /** + * Updates the list of group members for a group principal. + * + * The principals should be passed as a list of uri's. + * + * @param string $principal + * @param array $members + * @return void + */ + function setGroupMemberSet($principal, array $members) { + throw new \Sabre\DAV\Exception('Setting members of the group is not supported yet'); + } +} diff --git a/apps/dav/lib/rootcollection.php b/apps/dav/lib/rootcollection.php index 3e349fa31c9..c1635c9cde5 100644 --- a/apps/dav/lib/rootcollection.php +++ b/apps/dav/lib/rootcollection.php @@ -6,6 +6,7 @@ use OCA\DAV\CalDAV\CalDavBackend; use OCA\DAV\CardDAV\AddressBookRoot; use OCA\DAV\CardDAV\CardDavBackend; use OCA\DAV\Connector\Sabre\Principal; +use OCA\DAV\DAV\SystemPrincipalBackend; use Sabre\CalDAV\CalendarRoot; use Sabre\CalDAV\Principal\Collection; use Sabre\DAV\SimpleCollection; @@ -23,24 +24,33 @@ class RootCollection extends SimpleCollection { $disableListing = !$config->getSystemValue('debug', false); // setup the first level of the dav tree - $principalCollection = new Collection($principalBackend, 'principals/users'); - $principalCollection->disableListing = $disableListing; + $userPrincipals = new Collection($principalBackend, 'principals/users'); + $userPrincipals->disableListing = $disableListing; + $systemPrincipals = new Collection(new SystemPrincipalBackend(), 'principals/system'); + $systemPrincipals->disableListing = $disableListing; $filesCollection = new Files\RootCollection($principalBackend, 'principals/users'); $filesCollection->disableListing = $disableListing; $caldavBackend = new CalDavBackend($db); $calendarRoot = new CalendarRoot($principalBackend, $caldavBackend, 'principals/users'); $calendarRoot->disableListing = $disableListing; - $cardDavBackend = new CardDavBackend(\OC::$server->getDatabaseConnection(), $principalBackend); + $usersCardDavBackend = new CardDavBackend($db, $principalBackend); + $usersAddressBookRoot = new AddressBookRoot($principalBackend, $usersCardDavBackend, 'principals/users'); + $usersAddressBookRoot->disableListing = $disableListing; - $addressBookRoot = new AddressBookRoot($principalBackend, $cardDavBackend, 'principals/users'); - $addressBookRoot->disableListing = $disableListing; + $systemCardDavBackend = new CardDavBackend($db, $principalBackend); + $systemAddressBookRoot = new AddressBookRoot(new SystemPrincipalBackend(), $systemCardDavBackend, 'principals/system'); + $systemAddressBookRoot->disableListing = $disableListing; $children = [ - new SimpleCollection('principals', [$principalCollection]), + new SimpleCollection('principals', [ + $userPrincipals, + $systemPrincipals]), $filesCollection, $calendarRoot, - $addressBookRoot, + new SimpleCollection('addressbooks', [ + $usersAddressBookRoot, + $systemAddressBookRoot]), ]; parent::__construct('root', $children); diff --git a/apps/dav/lib/server.php b/apps/dav/lib/server.php index ffdb917085e..a031f2c442b 100644 --- a/apps/dav/lib/server.php +++ b/apps/dav/lib/server.php @@ -58,7 +58,7 @@ class Server { $this->server->addPlugin(new CardDAV\Sharing\Plugin($authBackend, \OC::$server->getRequest())); // addressbook plugins - $this->server->addPlugin(new \Sabre\CardDAV\Plugin()); + $this->server->addPlugin(new \OCA\DAV\CardDAV\Plugin()); // Finder on OS X requires Class 2 WebDAV support (locking), since we do // not provide locking we emulate it using a fake locking plugin. diff --git a/apps/dav/tests/travis/caldavtest/config/serverinfo.xml b/apps/dav/tests/travis/caldavtest/config/serverinfo.xml index a474bb7135c..c80e47f9481 100644 --- a/apps/dav/tests/travis/caldavtest/config/serverinfo.xml +++ b/apps/dav/tests/travis/caldavtest/config/serverinfo.xml @@ -569,7 +569,7 @@ <!-- relative path to user addressbook home--> <substitution> <key>$addressbookhome%d:</key> - <value>$addressbooks:$userid%d:</value> + <value>$addressbooks:users/$userid%d:</value> </substitution> <!-- relative path to user addressbook--> <substitution> diff --git a/apps/dav/tests/travis/caldavtest/tests/CardDAV/sync-report.xml b/apps/dav/tests/travis/caldavtest/tests/CardDAV/sync-report.xml index 0321e61edbc..ffa6662981c 100644 --- a/apps/dav/tests/travis/caldavtest/tests/CardDAV/sync-report.xml +++ b/apps/dav/tests/travis/caldavtest/tests/CardDAV/sync-report.xml @@ -181,8 +181,7 @@ </verify> </request> </test> - <!-- - <test name='6'> + <!-- test name='6'> <require-feature> <feature>sync-report-home</feature> </require-feature> @@ -236,7 +235,7 @@ </arg> </verify> </request> - </test> + </test --> <test name='8'> <description>remove new resource</description> <request> @@ -264,14 +263,15 @@ <callback>multistatusItems</callback> <arg> <name>okhrefs</name> - <value>$calendar_sync_extra_items:</value> + <!-- no sync on addressbook level --> + <!--<value>$calendar_sync_extra_items:</value>--> <value>1.vcf</value> <value>2.vcf</value> </arg> </verify> </request> </test> - <test name='10'> + <!--test name='10'> <require-feature> <feature>sync-report-home</feature> </require-feature> diff --git a/apps/dav/tests/unit/carddav/convertertest.php b/apps/dav/tests/unit/carddav/convertertest.php new file mode 100644 index 00000000000..f4e2ea3f002 --- /dev/null +++ b/apps/dav/tests/unit/carddav/convertertest.php @@ -0,0 +1,136 @@ +<?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\Tests\Unit; + +use OCA\DAV\CardDAV\Converter; +use Test\TestCase; + +class ConverterTests extends TestCase { + + /** + * @dataProvider providesNewUsers + */ + public function testCreation($expectedVCard, $displayName = null, $eMailAddress = null, $cloudId = null) { + $user = $this->getUserMock($displayName, $eMailAddress, $cloudId); + + $converter = new Converter(); + $vCard = $converter->createCardFromUser($user); + $cardData = $vCard->serialize(); + + $this->assertEquals($expectedVCard, $cardData); + } + + public function providesNewUsers() { + return [ + ["BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 3.4.7//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nPHOTO;ENCODING=b;TYPE=JPEG:MTIzNDU2Nzg5\r\nEND:VCARD\r\n"], + ["BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 3.4.7//EN\r\nUID:12345\r\nFN:Dr. Foo Bar\r\nN:Bar;Dr.;Foo;;\r\nPHOTO;ENCODING=b;TYPE=JPEG:MTIzNDU2Nzg5\r\nEND:VCARD\r\n", "Dr. Foo Bar"], + ["BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 3.4.7//EN\r\nUID:12345\r\nFN:Dr. Foo Bar\r\nN:Bar;Dr.;Foo;;\r\nEMAIL;TYPE=OTHER:foo@bar.net\r\nPHOTO;ENCODING=b;TYPE=JPEG:MTIzNDU2Nzg5\r\nEND:VCARD\r\n", "Dr. Foo Bar", "foo@bar.net"], + ["BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 3.4.7//EN\r\nUID:12345\r\nFN:Dr. Foo Bar\r\nN:Bar;Dr.;Foo;;\r\nCLOUD:foo@bar.net\r\nPHOTO;ENCODING=b;TYPE=JPEG:MTIzNDU2Nzg5\r\nEND:VCARD\r\n", "Dr. Foo Bar", null, "foo@bar.net"], + ]; + } + + /** + * @dataProvider providesNewUsers + */ + public function testUpdateOfUnchangedUser($expectedVCard, $displayName = null, $eMailAddress = null, $cloudId = null) { + $user = $this->getUserMock($displayName, $eMailAddress, $cloudId); + + $converter = new Converter(); + $vCard = $converter->createCardFromUser($user); + $updated = $converter->updateCard($vCard, $user); + $this->assertFalse($updated); + $cardData = $vCard->serialize(); + + $this->assertEquals($expectedVCard, $cardData); + } + + /** + * @dataProvider providesUsersForUpdateOfRemovedElement + */ + public function testUpdateOfRemovedElement($expectedVCard, $displayName = null, $eMailAddress = null, $cloudId = null) { + $user = $this->getUserMock($displayName, $eMailAddress, $cloudId); + + $converter = new Converter(); + $vCard = $converter->createCardFromUser($user); + + $user1 = $this->getMockBuilder('OCP\IUser')->disableOriginalConstructor()->getMock(); + $user1->method('getUID')->willReturn('12345'); + $user1->method('getDisplayName')->willReturn(null); + $user1->method('getEMailAddress')->willReturn(null); + $user1->method('getCloudId')->willReturn(null); + $user1->method('getAvatarImage')->willReturn(null); + + $updated = $converter->updateCard($vCard, $user1); + $this->assertTrue($updated); + $cardData = $vCard->serialize(); + + $this->assertEquals($expectedVCard, $cardData); + } + + public function providesUsersForUpdateOfRemovedElement() { + return [ + ["BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 3.4.7//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nEND:VCARD\r\n", "Dr. Foo Bar"], + ["BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 3.4.7//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nEND:VCARD\r\n", "Dr. Foo Bar", "foo@bar.net"], + ["BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 3.4.7//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nEND:VCARD\r\n", "Dr. Foo Bar", null, "foo@bar.net"], + ]; + } + + /** + * @dataProvider providesNames + * @param $expected + * @param $fullName + */ + public function testNameSplitter($expected, $fullName) { + + $converter = new Converter(); + $r = $converter->splitFullName($fullName); + $r = implode(';', $r); + $this->assertEquals($expected, $r); + } + + public function providesNames() { + return [ + ['Sauron;;;;', 'Sauron'], + ['Baggins;Bilbo;;;', 'Bilbo Baggins'], + ['Tolkien;John;Ronald Reuel;;', 'John Ronald Reuel Tolkien'], + ]; + } + + /** + * @param $displayName + * @param $eMailAddress + * @param $cloudId + * @return \PHPUnit_Framework_MockObject_MockObject + */ + protected function getUserMock($displayName, $eMailAddress, $cloudId) { + $image0 = $this->getMockBuilder('OCP\IImage')->disableOriginalConstructor()->getMock(); + $image0->method('mimeType')->willReturn('JPEG'); + $image0->method('data')->willReturn('123456789'); + $user = $this->getMockBuilder('OCP\IUser')->disableOriginalConstructor()->getMock(); + $user->method('getUID')->willReturn('12345'); + $user->method('getDisplayName')->willReturn($displayName); + $user->method('getEMailAddress')->willReturn($eMailAddress); + $user->method('getCloudId')->willReturn($cloudId); + $user->method('getAvatarImage')->willReturn($image0); + return $user; + } +} diff --git a/apps/files_sharing/lib/helper.php b/apps/files_sharing/lib/helper.php index a804737c490..391b491e1ff 100644 --- a/apps/files_sharing/lib/helper.php +++ b/apps/files_sharing/lib/helper.php @@ -310,20 +310,4 @@ class Helper { \OC::$server->getConfig()->setSystemValue('share_folder', $shareFolder); } - /** - * remove protocol from URL - * - * @param string $url - * @return string - */ - public static function removeProtocolFromUrl($url) { - if (strpos($url, 'https://') === 0) { - return substr($url, strlen('https://')); - } else if (strpos($url, 'http://') === 0) { - return substr($url, strlen('http://')); - } - - return $url; - } - } diff --git a/apps/files_sharing/settings-personal.php b/apps/files_sharing/settings-personal.php index deaa7b92ac7..85fad9c3eaf 100644 --- a/apps/files_sharing/settings-personal.php +++ b/apps/files_sharing/settings-personal.php @@ -32,9 +32,7 @@ if (count($matches) > 0 && $matches[1] <= 9) { $isIE8 = true; } -$uid = \OC::$server->getUserSession()->getUser()->getUID(); -$server = \OC::$server->getURLGenerator()->getAbsoluteURL('/'); -$cloudID = $uid . '@' . rtrim(\OCA\Files_Sharing\Helper::removeProtocolFromUrl($server), '/'); +$cloudID = \OC::$server->getUserSession()->getUser()->getCloudId(); $url = 'https://owncloud.org/federation#' . $cloudID; $ownCloudLogoPath = \OC::$server->getURLGenerator()->imagePath('core', 'logo-icon.svg'); diff --git a/lib/private/avatar.php b/lib/private/avatar.php index 872da35f947..37a813f3ff8 100644 --- a/lib/private/avatar.php +++ b/lib/private/avatar.php @@ -79,7 +79,9 @@ class Avatar implements \OCP\IAvatar { /** @var File $node */ $node = $this->folder->get('avatar.' . $ext); $avatar->loadFromData($node->getContent()); - $avatar->resize($size); + if ($size > 0) { + $avatar->resize($size); + } $this->folder->newFile('avatar.' . $size . '.' . $ext)->putContent($avatar->data()); } return $avatar; diff --git a/lib/private/user/manager.php b/lib/private/user/manager.php index 109f08f47a0..2cb866d0056 100644 --- a/lib/private/user/manager.php +++ b/lib/private/user/manager.php @@ -294,21 +294,47 @@ class Manager extends PublicEmitter implements IUserManager { $userCountStatistics = array(); foreach ($this->backends as $backend) { if ($backend->implementsActions(\OC_User_Backend::COUNT_USERS)) { - $backendusers = $backend->countUsers(); - if($backendusers !== false) { + $backendUsers = $backend->countUsers(); + if($backendUsers !== false) { if($backend instanceof \OCP\IUserBackend) { $name = $backend->getBackendName(); } else { $name = get_class($backend); } if(isset($userCountStatistics[$name])) { - $userCountStatistics[$name] += $backendusers; + $userCountStatistics[$name] += $backendUsers; } else { - $userCountStatistics[$name] = $backendusers; + $userCountStatistics[$name] = $backendUsers; } } } } return $userCountStatistics; } + + /** + * The callback is executed for each user on each backend. + * If the callback returns false no further users will be retrieved. + * + * @param \Closure $callback + * @return void + * @since 9.0.0 + */ + public function callForAllUsers(\Closure $callback, $search = '') { + foreach($this->getBackends() as $backend) { + $limit = 500; + $offset = 0; + do { + $users = $backend->getUsers($search, $limit, $offset); + foreach ($users as $user) { + $user = $this->get($user); + $return = $callback($user); + if ($return === false) { + break; + } + } + $offset += $limit; + } while (count($users) >= $limit); + } + } } diff --git a/lib/private/user/user.php b/lib/private/user/user.php index 2740b25d5d3..d1fa641504c 100644 --- a/lib/private/user/user.php +++ b/lib/private/user/user.php @@ -30,61 +30,56 @@ namespace OC\User; use OC\Hooks\Emitter; +use OCP\IAvatarManager; +use OCP\IImage; +use OCP\IURLGenerator; use OCP\IUser; use OCP\IConfig; class User implements IUser { - /** - * @var string $uid - */ + /** @var string $uid */ private $uid; - /** - * @var string $displayName - */ + /** @var string $displayName */ private $displayName; - /** - * @var \OC_User_Interface $backend - */ + /** @var \OC_User_Interface $backend */ private $backend; - /** - * @var bool $enabled - */ + /** @var bool $enabled */ private $enabled; - /** - * @var Emitter|Manager $emitter - */ + /** @var Emitter|Manager $emitter */ private $emitter; - /** - * @var string $home - */ + /** @var string $home */ private $home; - /** - * @var int $lastLogin - */ + /** @var int $lastLogin */ private $lastLogin; - /** - * @var \OCP\IConfig $config - */ + /** @var \OCP\IConfig $config */ private $config; + /** @var IAvatarManager */ + private $avatarManager; + + /** @var IURLGenerator */ + private $urlGenerator; + /** * @param string $uid * @param \OC_User_Interface $backend * @param \OC\Hooks\Emitter $emitter - * @param \OCP\IConfig $config + * @param IConfig|null $config + * @param IURLGenerator $urlGenerator */ - public function __construct($uid, $backend, $emitter = null, IConfig $config = null) { + public function __construct($uid, $backend, $emitter = null, IConfig $config = null, $urlGenerator = null) { $this->uid = $uid; $this->backend = $backend; $this->emitter = $emitter; $this->config = $config; + $this->urlGenerator = $urlGenerator; if ($this->config) { $enabled = $this->config->getUserValue($uid, 'core', 'enabled', 'true'); $this->enabled = ($enabled === 'true'); @@ -93,6 +88,9 @@ class User implements IUser { $this->enabled = true; $this->lastLogin = \OC::$server->getConfig()->getUserValue($uid, 'login', 'lastLogin', 0); } + if (is_null($this->urlGenerator)) { + $this->urlGenerator = \OC::$server->getURLGenerator(); + } } /** @@ -105,7 +103,7 @@ class User implements IUser { } /** - * get the displayname for the user, if no specific displayname is set it will fallback to the user id + * get the display name for the user, if no specific display name is set it will fallback to the user id * * @return string */ @@ -316,4 +314,52 @@ class User implements IUser { public function getEMailAddress() { return $this->config->getUserValue($this->uid, 'settings', 'email'); } + + /** + * get the avatar image if it exists + * + * @param int $size + * @return IImage|null + * @since 9.0.0 + */ + public function getAvatarImage($size) { + // delay the initialization + if (is_null($this->avatarManager)) { + $this->avatarManager = \OC::$server->getAvatarManager(); + } + + $avatar = $this->avatarManager->getAvatar($this->uid); + $image = $avatar->get(-1); + if ($image) { + return $image; + } + + return null; + } + + /** + * get the federation cloud id + * + * @return string + * @since 9.0.0 + */ + public function getCloudId() { + $uid = $this->getUID(); + $server = $this->urlGenerator->getAbsoluteURL('/'); + return $uid . '@' . rtrim( $this->removeProtocolFromUrl($server), '/'); + } + + /** + * @param string $url + * @return string + */ + private function removeProtocolFromUrl($url) { + if (strpos($url, 'https://') === 0) { + return substr($url, strlen('https://')); + } else if (strpos($url, 'http://') === 0) { + return substr($url, strlen('http://')); + } + + return $url; + } } diff --git a/lib/public/iuser.php b/lib/public/iuser.php index 1e52cd59036..67e2b107e0b 100644 --- a/lib/public/iuser.php +++ b/lib/public/iuser.php @@ -152,4 +152,21 @@ interface IUser { * @since 9.0.0 */ public function getEMailAddress(); + + /** + * get the avatar image if it exists + * + * @param int $size + * @return IImage|null + * @since 9.0.0 + */ + public function getAvatarImage($size); + + /** + * get the federation cloud id + * + * @return string + * @since 9.0.0 + */ + public function getCloudId(); } diff --git a/lib/public/iusermanager.php b/lib/public/iusermanager.php index e3857d6231a..3a0abaca7b7 100644 --- a/lib/public/iusermanager.php +++ b/lib/public/iusermanager.php @@ -134,4 +134,11 @@ interface IUserManager { * @since 8.0.0 */ public function countUsers(); + + /** + * @param \Closure $callback + * @return void + * @since 9.0.0 + */ + public function callForAllUsers (\Closure $callback, $search = ''); } diff --git a/tests/lib/contacts/localadressbook.php b/tests/lib/contacts/localadressbook.php index 6bfcf3d890a..e5c43460835 100644 --- a/tests/lib/contacts/localadressbook.php +++ b/tests/lib/contacts/localadressbook.php @@ -98,6 +98,11 @@ class SimpleUserForTesting implements IUser { } public function getEMailAddress() { - // TODO: Implement getEMailAddress() method. + } + + public function getAvatarImage($size) { + } + + public function getCloudId() { } } diff --git a/tests/lib/user/user.php b/tests/lib/user/user.php index bc1365d35bf..1f613edc4e6 100644 --- a/tests/lib/user/user.php +++ b/tests/lib/user/user.php @@ -11,6 +11,13 @@ namespace Test\User; use OC\Hooks\PublicEmitter; +/** + * Class User + * + * @group DB + * + * @package Test\User + */ class User extends \Test\TestCase { public function testDisplayName() { /** @@ -454,4 +461,21 @@ class User extends \Test\TestCase { $this->assertTrue($user->delete()); $this->assertEquals(2, $hooksCalled); } + + public function testGetCloudId() { + /** + * @var \OC_User_Backend | \PHPUnit_Framework_MockObject_MockObject $backend + */ + $backend = $this->getMock('\Test\Util\User\Dummy'); + $urlGenerator = $this->getMockBuilder('\OC\URLGenerator') + ->setMethods(['getAbsoluteURL']) + ->disableOriginalConstructor()->getMock(); + $urlGenerator + ->expects($this->any()) + ->method('getAbsoluteURL') + ->withAnyParameters() + ->willReturn('http://localhost:8888/owncloud'); + $user = new \OC\User\User('foo', $backend, null, null, $urlGenerator); + $this->assertEquals("foo@localhost:8888/owncloud", $user->getCloudId()); + } } |