diff --git a/.gitignore b/.gitignore index 964701eea63ae..fa49588fad4b2 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ !/apps/files_sharing !/apps/files_trashbin !/apps/files_versions +!/apps/lookup_server_connector !/apps/user_ldap !/apps/provisioning_api !/apps/systemtags diff --git a/apps/dav/lib/AppInfo/Application.php b/apps/dav/lib/AppInfo/Application.php index 844e0780ffb92..7c32fda8f5a7e 100644 --- a/apps/dav/lib/AppInfo/Application.php +++ b/apps/dav/lib/AppInfo/Application.php @@ -101,6 +101,12 @@ public function registerHooks() { } }); + $dispatcher->addListener('OC\AccountManager::userUpdated', function(GenericEvent $event) { + $user = $event->getSubject(); + $syncService = $this->getContainer()->query(SyncService::class); + $syncService->updateUser($user); + }); + $dispatcher->addListener('\OCA\DAV\CalDAV\CalDavBackend::createCalendar', function(GenericEvent $event) { /** @var Backend $backend */ $backend = $this->getContainer()->query(Backend::class); diff --git a/apps/dav/lib/CardDAV/Converter.php b/apps/dav/lib/CardDAV/Converter.php index d1fb754017e17..065c5494ec38f 100644 --- a/apps/dav/lib/CardDAV/Converter.php +++ b/apps/dav/lib/CardDAV/Converter.php @@ -22,6 +22,7 @@ namespace OCA\DAV\CardDAV; +use OC\Accounts\AccountManager; use OCP\IImage; use OCP\IUser; use Sabre\VObject\Component\VCard; @@ -29,109 +30,76 @@ class Converter { + /** @var AccountManager */ + private $accountManager; + /** - * @param IUser $user - * @return VCard + * Converter constructor. + * + * @param AccountManager $accountManager */ - public function createCardFromUser(IUser $user) { - - $uid = $user->getUID(); - $displayName = $user->getDisplayName(); - $displayName = empty($displayName ) ? $uid : $displayName; - $emailAddress = $user->getEMailAddress(); - $cloudId = $user->getCloudId(); - $image = $this->getAvatarImage($user); - - $vCard = new VCard(); - $vCard->VERSION = '3.0'; - $vCard->UID = $uid; - if (!empty($displayName)) { - $vCard->FN = $displayName; - $vCard->N = $this->splitFullName($displayName); - } - if (!empty($emailAddress)) { - $vCard->add(new Text($vCard, 'EMAIL', $emailAddress, ['TYPE' => 'OTHER'])); - } - if (!empty($cloudId)) { - $vCard->CLOUD = $cloudId; - } - if ($image) { - $vCard->add('PHOTO', $image->data(), ['ENCODING' => 'b', 'TYPE' => $image->mimeType()]); - } - $vCard->validate(); - - return $vCard; + public function __construct(AccountManager $accountManager) { + $this->accountManager = $accountManager; } /** - * @param VCard $vCard * @param IUser $user - * @return bool + * @return VCard|null */ - public function updateCard(VCard $vCard, IUser $user) { + public function createCardFromUser(IUser $user) { + + $userData = $this->accountManager->getUser($user); + $uid = $user->getUID(); - $displayName = $user->getDisplayName(); - $displayName = empty($displayName ) ? $uid : $displayName; - $emailAddress = $user->getEMailAddress(); $cloudId = $user->getCloudId(); $image = $this->getAvatarImage($user); - $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)) { - unset($vCard->PHOTO); - $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; + $vCard = new VCard(); + $vCard->add(new Text($vCard, 'UID', $uid)); + + $publish = false; + + foreach ($userData as $property => $value) { + if ($value['scope'] === AccountManager::VISIBILITY_CONTACTS_ONLY || + $value['scope'] === AccountManager::VISIBILITY_PUBLIC + ) { + $publish = true; + switch ($property) { + case AccountManager::PROPERTY_DISPLAYNAME: + $vCard->add(new Text($vCard, 'FN', $value['value'])); + $vCard->add(new Text($vCard, 'N', $this->splitFullName($value['value']))); + break; + case AccountManager::PROPERTY_AVATAR: + if ($image !== null) { + $vCard->add('PHOTO', $image->data(), ['ENCODING' => 'b', 'TYPE' => $image->mimeType()]); + } + break; + case AccountManager::PROPERTY_EMAIL: + $vCard->add(new Text($vCard, 'EMAIL', $value['value'], ['TYPE' => 'OTHER'])); + break; + case AccountManager::PROPERTY_WEBSITE: + $vCard->add(new Text($vCard, 'URL', $value['value'])); + break; + case AccountManager::PROPERTY_PHONE: + $vCard->add(new Text($vCard, 'TEL', $value['value'], ['TYPE' => 'OTHER'])); + break; + case AccountManager::PROPERTY_ADDRESS: + $vCard->add(new Text($vCard, 'ADR', $value['value'], ['TYPE' => 'OTHER'])); + break; + case AccountManager::PROPERTY_TWITTER: + $vCard->add(new Text($vCard, 'X-SOCIALPROFILE', $value['value'], ['TYPE' => 'TWITTER'])); + break; + } + } } - 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; + if ($publish && !empty($cloudId)) { + $vCard->add(new Text($vCard, 'CLOUD', $cloudId)); + $vCard->validate(); + return $vCard; } - $value = $vCard->__get($name); - if (!is_null($value)) { - $value = $value->getValue(); - $newValue = $newValue instanceof IImage ? $newValue->data() : $newValue; - return $value !== $newValue; - } - return true; + return null; } /** diff --git a/apps/dav/lib/CardDAV/SyncService.php b/apps/dav/lib/CardDAV/SyncService.php index 1ad37da6bfe4b..1293d8ae8a005 100644 --- a/apps/dav/lib/CardDAV/SyncService.php +++ b/apps/dav/lib/CardDAV/SyncService.php @@ -24,6 +24,7 @@ namespace OCA\DAV\CardDAV; +use OC\Accounts\AccountManager; use OCP\AppFramework\Http; use OCP\ILogger; use OCP\IUser; @@ -48,10 +49,22 @@ class SyncService { /** @var array */ private $localSystemAddressBook; - public function __construct(CardDavBackend $backend, IUserManager $userManager, ILogger $logger) { + /** @var AccountManager */ + private $accountManager; + + /** + * SyncService constructor. + * + * @param CardDavBackend $backend + * @param IUserManager $userManager + * @param ILogger $logger + * @param AccountManager $accountManager + */ + public function __construct(CardDavBackend $backend, IUserManager $userManager, ILogger $logger, AccountManager $accountManager) { $this->backend = $backend; $this->userManager = $userManager; $this->logger = $logger; + $this->accountManager = $accountManager; } /** @@ -215,7 +228,7 @@ private function parseMultiStatus($body) { public function updateUser($user) { $systemAddressBook = $this->getLocalSystemAddressBook(); $addressBookId = $systemAddressBook['id']; - $converter = new Converter(); + $converter = new Converter($this->accountManager); $name = $user->getBackendClassName(); $userId = $user->getUID(); @@ -223,10 +236,14 @@ public function updateUser($user) { $card = $this->backend->getCard($addressBookId, $cardId); if ($card === false) { $vCard = $converter->createCardFromUser($user); - $this->backend->createCard($addressBookId, $cardId, $vCard->serialize()); + if ($vCard !== null) { + $this->backend->createCard($addressBookId, $cardId, $vCard->serialize()); + } } else { - $vCard = Reader::read($card['carddata']); - if ($converter->updateCard($vCard, $user)) { + $vCard = $converter->createCardFromUser($user); + if (is_null($vCard)) { + $this->backend->deleteCard($addressBookId, $cardId); + } else { $this->backend->updateCard($addressBookId, $cardId, $vCard->serialize()); } } diff --git a/apps/dav/lib/HookManager.php b/apps/dav/lib/HookManager.php index 247d4b291af15..26f3895a45986 100644 --- a/apps/dav/lib/HookManager.php +++ b/apps/dav/lib/HookManager.php @@ -27,6 +27,8 @@ use OCP\IUser; use OCP\IUserManager; use OCP\Util; +use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\EventDispatcher\GenericEvent; class HookManager { @@ -51,14 +53,19 @@ class HookManager { /** @var array */ private $addressBooksToDelete; + /** @var EventDispatcher */ + private $eventDispatcher; + public function __construct(IUserManager $userManager, SyncService $syncService, CalDavBackend $calDav, - CardDavBackend $cardDav) { + CardDavBackend $cardDav, + EventDispatcher $eventDispatcher) { $this->userManager = $userManager; $this->syncService = $syncService; $this->calDav = $calDav; $this->cardDav = $cardDav; + $this->eventDispatcher = $eventDispatcher; } public function setup() { diff --git a/apps/dav/tests/unit/CardDAV/ConverterTest.php b/apps/dav/tests/unit/CardDAV/ConverterTest.php index 7737b999507e4..737bbd96aaafb 100644 --- a/apps/dav/tests/unit/CardDAV/ConverterTest.php +++ b/apps/dav/tests/unit/CardDAV/ConverterTest.php @@ -24,79 +24,121 @@ namespace OCA\DAV\Tests\unit\CardDAV; +use OC\Accounts\AccountManager; use OCA\DAV\CardDAV\Converter; +use OCP\IDBConnection; use OCP\IImage; use OCP\IUser; +use OpenCloud\ObjectStore\Resource\Account; use PHPUnit_Framework_MockObject_MockObject; +use Symfony\Component\EventDispatcher\EventDispatcher; use Test\TestCase; class ConverterTest extends TestCase { - /** - * @dataProvider providesNewUsers - */ - public function testCreation($expectedVCard, $displayName = null, $eMailAddress = null, $cloudId = null) { - $user = $this->getUserMock($displayName, $eMailAddress, $cloudId); + /** @var AccountManager | PHPUnit_Framework_MockObject_MockObject */ + private $accountManager; - $converter = new Converter(); - $vCard = $converter->createCardFromUser($user); - $cardData = $vCard->serialize(); + /** @var EventDispatcher | PHPUnit_Framework_MockObject_MockObject */ + private $eventDispatcher; - $this->assertEquals($expectedVCard, $cardData); - } + /** @var IDBConnection | PHPUnit_Framework_MockObject_MockObject */ + private $databaseConnection; - public function providesNewUsers() { - return [ - ["BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//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 4.1.1//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 4.1.1//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 4.1.1//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"], - ]; + public function setUp() { + parent::setUp(); + $this->databaseConnection = $this->getMockBuilder('OCP\IDBConnection')->getMock(); + $this->eventDispatcher = $this->getMockBuilder('Symfony\Component\EventDispatcher\EventDispatcher') + ->disableOriginalConstructor()->getMock(); + $this->accountManager = $this->getMockBuilder('OC\Accounts\AccountManager') + ->disableOriginalConstructor()->getMock(); } - /** - * @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); + public function getAccountManager(IUser $user) { + $accountManager = $this->getMockBuilder('OC\Accounts\AccountManager') + ->disableOriginalConstructor()->getMock(); + $accountManager->expects($this->any())->method('getUser')->willReturn( + [ + AccountManager::PROPERTY_DISPLAYNAME => + [ + 'value' => $user->getDisplayName(), + 'scope' => AccountManager::VISIBILITY_CONTACTS_ONLY, + ], + AccountManager::PROPERTY_ADDRESS => + [ + 'value' => '', + 'scope' => AccountManager::VISIBILITY_PRIVATE, + ], + AccountManager::PROPERTY_WEBSITE => + [ + 'value' => '', + 'scope' => AccountManager::VISIBILITY_PRIVATE, + ], + AccountManager::PROPERTY_EMAIL => + [ + 'value' => $user->getEMailAddress(), + 'scope' => AccountManager::VISIBILITY_CONTACTS_ONLY, + ], + AccountManager::PROPERTY_AVATAR => + [ + 'scope' => AccountManager::VISIBILITY_CONTACTS_ONLY + ], + AccountManager::PROPERTY_PHONE => + [ + 'value' => '', + 'scope' => AccountManager::VISIBILITY_PRIVATE, + ], + AccountManager::PROPERTY_TWITTER => + [ + 'value' => '', + 'scope' => AccountManager::VISIBILITY_PRIVATE, + ], + ] + ); + + return $accountManager; } /** - * @dataProvider providesUsersForUpdateOfRemovedElement + * @dataProvider providesNewUsers */ - public function testUpdateOfRemovedElement($expectedVCard, $displayName = null, $eMailAddress = null, $cloudId = null) { + public function testCreation($expectedVCard, $displayName = null, $eMailAddress = null, $cloudId = null) { $user = $this->getUserMock($displayName, $eMailAddress, $cloudId); + $accountManager = $this->getAccountManager($user); - $converter = new Converter(); + $converter = new Converter($accountManager); $vCard = $converter->createCardFromUser($user); + if ($expectedVCard !== null) { + $this->assertInstanceOf('Sabre\VObject\Component\VCard', $vCard); + $cardData = $vCard->jsonSerialize(); + $this->compareData($expectedVCard, $cardData); - $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); + } else { + $this->assertSame($expectedVCard, $vCard); + } - $updated = $converter->updateCard($vCard, $user1); - $this->assertTrue($updated); - $cardData = $vCard->serialize(); + } - $this->assertEquals($expectedVCard, $cardData); + protected function compareData($expected, $data) { + foreach ($expected as $key => $value) { + $found = false; + foreach ($data[1] as $d) { + if($d[0] === $key && $d[3] === $value) { + $found = true; + break; + } + } + if (!$found) $this->assertTrue(false, 'Expected data: ' . $key . ' not found.'); + } } - public function providesUsersForUpdateOfRemovedElement() { + public function providesNewUsers() { return [ - ["BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//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 4.1.1//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 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nEND:VCARD\r\n", "Dr. Foo Bar", null, "foo@bar.net"], + [null], + [null, null, 'foo@bar.net'], + [['cloud' => 'foo@cloud.net', 'email' => 'foo@bar.net'], null, 'foo@bar.net', 'foo@cloud.net'], + [['cloud' => 'foo@cloud.net', 'email' => 'foo@bar.net', 'fn' => 'Dr. Foo Bar'], "Dr. Foo Bar", "foo@bar.net", 'foo@cloud.net'], + [['cloud' => 'foo@cloud.net', 'fn' => 'Dr. Foo Bar'], "Dr. Foo Bar", null, "foo@cloud.net"], ]; } @@ -107,7 +149,7 @@ public function providesUsersForUpdateOfRemovedElement() { */ public function testNameSplitter($expected, $fullName) { - $converter = new Converter(); + $converter = new Converter($this->accountManager); $r = $converter->splitFullName($fullName); $r = implode(';', $r); $this->assertEquals($expected, $r); diff --git a/apps/dav/tests/unit/CardDAV/SyncServiceTest.php b/apps/dav/tests/unit/CardDAV/SyncServiceTest.php index e6a5ac1f16ab9..68345def66b85 100644 --- a/apps/dav/tests/unit/CardDAV/SyncServiceTest.php +++ b/apps/dav/tests/unit/CardDAV/SyncServiceTest.php @@ -25,6 +25,7 @@ namespace OCA\DAV\Tests\unit\CardDAV; +use OC\Accounts\AccountManager; use OCA\DAV\CardDAV\CardDavBackend; use OCA\DAV\CardDAV\SyncService; use OCP\IUser; @@ -76,7 +77,8 @@ public function testEnsureSystemAddressBookExists() { /** @var IUserManager $userManager */ $userManager = $this->getMockBuilder('OCP\IUserManager')->disableOriginalConstructor()->getMock(); $logger = $this->getMockBuilder('OCP\ILogger')->disableOriginalConstructor()->getMock(); - $ss = new SyncService($backend, $userManager, $logger); + $accountManager = $this->getMockBuilder('OC\Accounts\AccountManager')->disableOriginalConstructor()->getMock(); + $ss = new SyncService($backend, $userManager, $logger, $accountManager); $book = $ss->ensureSystemAddressBookExists('principals/users/adam', 'contacts', []); } @@ -100,8 +102,47 @@ public function testUpdateAndDeleteUser() { $user = $this->getMockBuilder('OCP\IUser')->disableOriginalConstructor()->getMock(); $user->method('getBackendClassName')->willReturn('unittest'); $user->method('getUID')->willReturn('test-user'); - - $ss = new SyncService($backend, $userManager, $logger); + $user->method('getCloudId')->willReturn('cloudId'); + $accountManager = $this->getMockBuilder('OC\Accounts\AccountManager')->disableOriginalConstructor()->getMock(); + $accountManager->expects($this->any())->method('getUser') + ->willReturn([ + AccountManager::PROPERTY_DISPLAYNAME => + [ + 'value' => $user->getDisplayName(), + 'scope' => AccountManager::VISIBILITY_CONTACTS_ONLY, + ], + AccountManager::PROPERTY_ADDRESS => + [ + 'value' => '', + 'scope' => AccountManager::VISIBILITY_PRIVATE, + ], + AccountManager::PROPERTY_WEBSITE => + [ + 'value' => '', + 'scope' => AccountManager::VISIBILITY_PRIVATE, + ], + AccountManager::PROPERTY_EMAIL => + [ + 'value' => $user->getEMailAddress(), + 'scope' => AccountManager::VISIBILITY_CONTACTS_ONLY, + ], + AccountManager::PROPERTY_AVATAR => + [ + 'scope' => AccountManager::VISIBILITY_CONTACTS_ONLY + ], + AccountManager::PROPERTY_PHONE => + [ + 'value' => '', + 'scope' => AccountManager::VISIBILITY_PRIVATE, + ], + AccountManager::PROPERTY_TWITTER => + [ + 'value' => '', + 'scope' => AccountManager::VISIBILITY_PRIVATE, + ], + ]); + + $ss = new SyncService($backend, $userManager, $logger, $accountManager); $ss->updateUser($user); $user->method('getDisplayName')->willReturn('A test user for unit testing'); @@ -135,10 +176,11 @@ private function getBackendMock($createCount, $updateCount, $deleteCount) { private function getSyncServiceMock($backend, $response) { $userManager = $this->getMockBuilder('OCP\IUserManager')->disableOriginalConstructor()->getMock(); $logger = $this->getMockBuilder('OCP\ILogger')->disableOriginalConstructor()->getMock(); + $accountManager = $this->getMockBuilder('OC\Accounts\AccountManager')->disableOriginalConstructor()->getMock(); /** @var SyncService | \PHPUnit_Framework_MockObject_MockObject $ss */ $ss = $this->getMockBuilder(SyncService::class) ->setMethods(['ensureSystemAddressBookExists', 'requestSyncReport', 'download']) - ->setConstructorArgs([$backend, $userManager, $logger]) + ->setConstructorArgs([$backend, $userManager, $logger, $accountManager]) ->getMock(); $ss->method('requestSyncReport')->withAnyParameters()->willReturn(['response' => $response, 'token' => 'sync-token-1']); $ss->method('ensureSystemAddressBookExists')->willReturn(['id' => 1]); diff --git a/apps/dav/tests/unit/DAV/HookManagerTest.php b/apps/dav/tests/unit/DAV/HookManagerTest.php index f980e595bf917..9579ce1c6d3f0 100644 --- a/apps/dav/tests/unit/DAV/HookManagerTest.php +++ b/apps/dav/tests/unit/DAV/HookManagerTest.php @@ -31,14 +31,19 @@ use OCP\IL10N; use OCP\IUser; use OCP\IUserManager; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Test\TestCase; class HookManagerTest extends TestCase { /** @var IL10N */ private $l10n; + /** @var EventDispatcher | \PHPUnit_Framework_MockObject_MockObject */ + private $eventDispatcher; + public function setUp() { parent::setUp(); + $this->eventDispatcher = $this->getMockBuilder('Symfony\Component\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock(); $this->l10n = $this->createMock(IL10N::class); $this->l10n ->expects($this->any()) @@ -82,7 +87,7 @@ public function test() { 'principals/users/newUser', 'contacts', ['{DAV:}displayname' => 'Contacts']); - $hm = new HookManager($userManager, $syncService, $cal, $card); + $hm = new HookManager($userManager, $syncService, $cal, $card, $this->eventDispatcher); $hm->firstLogin($user); } @@ -116,7 +121,7 @@ public function testWithExisting() { $card->expects($this->once())->method('getAddressBooksForUserCount')->willReturn(1); $card->expects($this->never())->method('createAddressBook'); - $hm = new HookManager($userManager, $syncService, $cal, $card); + $hm = new HookManager($userManager, $syncService, $cal, $card, $this->eventDispatcher); $hm->firstLogin($user); } @@ -154,7 +159,7 @@ public function testWithBirthdayCalendar() { 'principals/users/newUser', 'contacts', ['{DAV:}displayname' => 'Contacts']); - $hm = new HookManager($userManager, $syncService, $cal, $card); + $hm = new HookManager($userManager, $syncService, $cal, $card, $this->eventDispatcher); $hm->firstLogin($user); } @@ -195,7 +200,7 @@ public function testDeleteCalendar() { ]); $card->expects($this->once())->method('deleteAddressBook'); - $hm = new HookManager($userManager, $syncService, $cal, $card, $this->l10n); + $hm = new HookManager($userManager, $syncService, $cal, $card, $this->eventDispatcher); $hm->preDeleteUser(['uid' => 'newUser']); $hm->postDeleteUser(['uid' => 'newUser']); } diff --git a/apps/federatedfilesharing/lib/FederatedShareProvider.php b/apps/federatedfilesharing/lib/FederatedShareProvider.php index 6481abe32e2c6..61f1b1c8f186e 100644 --- a/apps/federatedfilesharing/lib/FederatedShareProvider.php +++ b/apps/federatedfilesharing/lib/FederatedShareProvider.php @@ -943,4 +943,14 @@ public function isIncomingServer2serverShareEnabled() { $result = $this->config->getAppValue('files_sharing', 'incoming_server2server_share_enabled', 'yes'); return ($result === 'yes') ? true : false; } + + /** + * Check if querying sharees on the lookup server is enabled + * + * @return bool + */ + public function isLookupServerQueriesEnabled() { + $result = $this->config->getAppValue('files_sharing', 'lookupServerEnabled', 'no'); + return ($result === 'yes') ? true : false; + } } diff --git a/apps/federatedfilesharing/lib/Settings/Admin.php b/apps/federatedfilesharing/lib/Settings/Admin.php index 64619e329f371..20ff6ae6f4a62 100644 --- a/apps/federatedfilesharing/lib/Settings/Admin.php +++ b/apps/federatedfilesharing/lib/Settings/Admin.php @@ -43,6 +43,7 @@ public function getForm() { $parameters = [ 'outgoingServer2serverShareEnabled' => $this->fedShareProvider->isOutgoingServer2serverShareEnabled(), 'incomingServer2serverShareEnabled' => $this->fedShareProvider->isIncomingServer2serverShareEnabled(), + 'lookupServerEnabled' => $this->fedShareProvider->isLookupServerQueriesEnabled(), ]; return new TemplateResponse('federatedfilesharing', 'settings-admin', $parameters, ''); diff --git a/apps/federatedfilesharing/templates/settings-admin.php b/apps/federatedfilesharing/templates/settings-admin.php index 3ffd4bd5b2750..0670553e06112 100644 --- a/apps/federatedfilesharing/templates/settings-admin.php +++ b/apps/federatedfilesharing/templates/settings-admin.php @@ -25,4 +25,11 @@ t('Allow users on this server to receive shares from other servers'));?>

+

+ /> +
+

diff --git a/apps/federatedfilesharing/tests/Settings/AdminTest.php b/apps/federatedfilesharing/tests/Settings/AdminTest.php index 60fadca7b568a..c0b35a6427bbb 100644 --- a/apps/federatedfilesharing/tests/Settings/AdminTest.php +++ b/apps/federatedfilesharing/tests/Settings/AdminTest.php @@ -65,10 +65,15 @@ public function testGetForm($state) { ->expects($this->once()) ->method('isIncomingServer2serverShareEnabled') ->willReturn($state); + $this->federatedShareProvider + ->expects($this->once()) + ->method('isLookupServerQueriesEnabled') + ->willReturn($state); $params = [ 'outgoingServer2serverShareEnabled' => $state, 'incomingServer2serverShareEnabled' => $state, + 'lookupServerEnabled' => $state, ]; $expected = new TemplateResponse('federatedfilesharing', 'settings-admin', $params, ''); $this->assertEquals($expected, $this->admin->getForm()); diff --git a/apps/files/css/files.css b/apps/files/css/files.css index 9b844919c4e8d..c0460ff6058b6 100644 --- a/apps/files/css/files.css +++ b/apps/files/css/files.css @@ -550,7 +550,7 @@ html.ie8 #fileList tr.selected td.filename>.selectCheckBox { } .bubble:after, #app-navigation .app-navigation-entry-menu:after { - right: 6px; + right: 12px; } .bubble:before, #app-navigation .app-navigation-entry-menu:before { @@ -625,13 +625,6 @@ html.ie8 .column-mtime .selectedActions { padding-right: 14px; } -#fileList .popovermenu { - margin-right: 6px; -} -.ie8 #fileList .popovermenu { - margin-top: -10px; -} - .ie8 #fileList a.action img, #fileList tr:hover a.action, #fileList a.action.permanent, @@ -649,7 +642,6 @@ html.ie8 .column-mtime .selectedActions { -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=30)"; filter: alpha(opacity=30); opacity: .3; - display:inline; } .ie8 #fileList a.action:hover img, #fileList tr a.action.disabled.action-download, @@ -673,6 +665,7 @@ html.ie8 .column-mtime .selectedActions { -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=70)" !important; filter: alpha(opacity=70) !important; opacity: .7 !important; + display:inline; } /* always show actions on mobile, not only on hover */ #fileList a.action.action-menu.permanent { @@ -775,11 +768,6 @@ table.dragshadow td.size { padding: initial; } -#fileList .popovermenu a.action { - padding: 10px; - margin: -10px; -} - html.ie8 #controls .button.new { padding-right: 0; } @@ -826,11 +814,7 @@ html.ie8 #controls .button.new { } #fileList .popovermenu .action { - display: block; - line-height: 30px; - padding-left: 5px; color: #000; - padding: 0; } #filestable .filename .action .icon, diff --git a/apps/files_sharing/lib/Controller/ShareesAPIController.php b/apps/files_sharing/lib/Controller/ShareesAPIController.php index 09912b7758a05..6b3208dc289cf 100644 --- a/apps/files_sharing/lib/Controller/ShareesAPIController.php +++ b/apps/files_sharing/lib/Controller/ShareesAPIController.php @@ -28,6 +28,7 @@ use OCP\AppFramework\OCS\OCSBadRequestException; use OCP\AppFramework\OCSController; use OCP\Contacts\IManager; +use OCP\Http\Client\IClientService; use OCP\IGroup; use OCP\IGroupManager; use OCP\ILogger; @@ -65,6 +66,9 @@ class ShareesAPIController extends OCSController { /** @var \OCP\Share\IManager */ protected $shareManager; + /** @var IClientService */ + protected $clientService; + /** @var bool */ protected $shareWithGroupOnly = false; @@ -89,6 +93,7 @@ class ShareesAPIController extends OCSController { 'groups' => [], 'remotes' => [], 'emails' => [], + 'lookup' => [], ]; protected $reachedEndFor = []; @@ -104,6 +109,7 @@ class ShareesAPIController extends OCSController { * @param IURLGenerator $urlGenerator * @param ILogger $logger * @param \OCP\Share\IManager $shareManager + * @param IClientService $clientService */ public function __construct($appName, IRequest $request, @@ -114,7 +120,8 @@ public function __construct($appName, IUserSession $userSession, IURLGenerator $urlGenerator, ILogger $logger, - \OCP\Share\IManager $shareManager) { + \OCP\Share\IManager $shareManager, + IClientService $clientService) { parent::__construct($appName, $request); $this->groupManager = $groupManager; @@ -125,6 +132,7 @@ public function __construct($appName, $this->urlGenerator = $urlGenerator; $this->logger = $logger; $this->shareManager = $shareManager; + $this->clientService = $clientService; } /** @@ -414,10 +422,11 @@ protected function fixRemoteURL($remote) { * @param int $page * @param int $perPage * @param int|int[] $shareType + * @param bool $lookup * @return Http\DataResponse * @throws OCSBadRequestException */ - public function search($search = '', $itemType = null, $page = 1, $perPage = 200, $shareType = null) { + public function search($search = '', $itemType = null, $page = 1, $perPage = 200, $shareType = null, $lookup = true) { if ($perPage <= 0) { throw new OCSBadRequestException('Invalid perPage argument'); } @@ -459,7 +468,7 @@ public function search($search = '', $itemType = null, $page = 1, $perPage = 200 $this->limit = (int) $perPage; $this->offset = $perPage * ($page - 1); - return $this->searchSharees($search, $itemType, $shareTypes, $page, $perPage); + return $this->searchSharees($search, $itemType, $shareTypes, $page, $perPage, $lookup); } /** @@ -485,10 +494,11 @@ protected function isRemoteSharingAllowed($itemType) { * @param array $shareTypes * @param int $page * @param int $perPage + * @param bool $lookup * @return Http\DataResponse * @throws OCSBadRequestException */ - protected function searchSharees($search, $itemType, array $shareTypes, $page, $perPage) { + protected function searchSharees($search, $itemType, array $shareTypes, $page, $perPage, $lookup) { // Verify arguments if ($itemType === null) { throw new OCSBadRequestException('Missing itemType'); @@ -510,11 +520,17 @@ protected function searchSharees($search, $itemType, array $shareTypes, $page, $ $remoteResults = $this->getRemote($search); } + // Get emails $mailResults = ['results' => [], 'exact' => [], 'exactIdMatch' => false]; if (in_array(Share::SHARE_TYPE_EMAIL, $shareTypes)) { $mailResults = $this->getEmail($search); } + // Get from lookup server + if ($lookup) { + $this->getLookup($search); + } + // if we have a exact match, either for the federated cloud id or for the // email address we only return the exact match. It is highly unlikely // that the exact same email address and federated cloud id exists @@ -609,6 +625,40 @@ protected function getEmail($search) { return $result; } + protected function getLookup($search) { + $isEnabled = $this->config->getAppValue('files_sharing', 'lookupServerEnabled', 'no'); + $result = []; + + if($isEnabled === 'yes') { + try { + $client = $this->clientService->newClient(); + $response = $client->get( + 'https://lookup.nextcloud.com/users?search=' . urlencode($search), + [ + 'timeout' => 10, + 'connect_timeout' => 3, + ] + ); + + $body = json_decode($response->getBody(), true); + + $result = []; + foreach ($body as $lookup) { + $result[] = [ + 'label' => $lookup['federationId'], + 'value' => [ + 'shareType' => Share::SHARE_TYPE_REMOTE, + 'shareWith' => $lookup['federationId'], + ], + 'extra' => $lookup, + ]; + } + } catch (\Exception $e) {} + } + + $this->result['lookup'] = $result; + } + /** * Generates a bunch of pagination links for the current page * diff --git a/apps/files_sharing/tests/Controller/ShareesAPIControllerTest.php b/apps/files_sharing/tests/Controller/ShareesAPIControllerTest.php index 336dcb70f0e08..c570cb16980e3 100644 --- a/apps/files_sharing/tests/Controller/ShareesAPIControllerTest.php +++ b/apps/files_sharing/tests/Controller/ShareesAPIControllerTest.php @@ -29,6 +29,7 @@ use OCA\Files_Sharing\Tests\TestCase; use OCP\AppFramework\Http; use OCP\AppFramework\OCS\OCSBadRequestException; +use OCP\Http\Client\IClientService; use OCP\Share; /** @@ -60,6 +61,9 @@ class ShareesAPIControllerTest extends TestCase { /** @var \OCP\Share\IManager|\PHPUnit_Framework_MockObject_MockObject */ protected $shareManager; + /** @var IClientService|\PHPUnit_Framework_MockObject_MockObject */ + private $clientService; + protected function setUp() { parent::setUp(); @@ -87,6 +91,8 @@ protected function setUp() { ->disableOriginalConstructor() ->getMock(); + $this->clientService = $this->createMock(IClientService::class); + $this->sharees = new ShareesAPIController( 'files_sharing', $this->request, @@ -97,7 +103,8 @@ protected function setUp() { $this->session, $this->getMockBuilder('OCP\IURLGenerator')->disableOriginalConstructor()->getMock(), $this->getMockBuilder('OCP\ILogger')->disableOriginalConstructor()->getMock(), - $this->shareManager + $this->shareManager, + $this->clientService ); } @@ -1386,7 +1393,8 @@ public function testSearch($getData, $apiSetting, $enumSetting, $remoteSharingEn $this->session, $this->getMockBuilder('OCP\IURLGenerator')->disableOriginalConstructor()->getMock(), $this->getMockBuilder('OCP\ILogger')->disableOriginalConstructor()->getMock(), - $this->shareManager + $this->shareManager, + $this->clientService ]) ->setMethods(array('searchSharees', 'isRemoteSharingAllowed', 'shareProviderExists')) ->getMock(); @@ -1477,7 +1485,8 @@ public function testSearchInvalid($getData, $message) { $this->session, $this->getMockBuilder('OCP\IURLGenerator')->disableOriginalConstructor()->getMock(), $this->getMockBuilder('OCP\ILogger')->disableOriginalConstructor()->getMock(), - $this->shareManager + $this->shareManager, + $this->clientService ]) ->setMethods(array('searchSharees', 'isRemoteSharingAllowed')) ->getMock(); @@ -1522,6 +1531,7 @@ public function dataSearchSharees() { 'groups' => [], 'remotes' => [], 'emails' => [], + 'lookup' => [], ], false], ['test', 'folder', [Share::SHARE_TYPE_USER, Share::SHARE_TYPE_GROUP, Share::SHARE_TYPE_REMOTE], 1, 2, false, [], [], ['results' => [], 'exact' => [], 'exactIdMatch' => false], [ @@ -1530,6 +1540,7 @@ public function dataSearchSharees() { 'groups' => [], 'remotes' => [], 'emails' => [], + 'lookup' => [], ], false], [ 'test', 'folder', [Share::SHARE_TYPE_USER, Share::SHARE_TYPE_GROUP, Share::SHARE_TYPE_REMOTE], 1, 2, false, [ @@ -1551,6 +1562,7 @@ public function dataSearchSharees() { ['label' => 'testz@remote', 'value' => ['shareType' => Share::SHARE_TYPE_REMOTE, 'shareWith' => 'testz@remote']], ], 'emails' => [], + 'lookup' => [], ], true, ], // No groups requested @@ -1570,6 +1582,7 @@ public function dataSearchSharees() { ['label' => 'testz@remote', 'value' => ['shareType' => Share::SHARE_TYPE_REMOTE, 'shareWith' => 'testz@remote']], ], 'emails' => [], + 'lookup' => [], ], false, ], // Share type restricted to user - Only one user @@ -1585,6 +1598,7 @@ public function dataSearchSharees() { 'groups' => [], 'remotes' => [], 'emails' => [], + 'lookup' => [], ], false, ], // Share type restricted to user - Multipage result @@ -1602,6 +1616,7 @@ public function dataSearchSharees() { 'groups' => [], 'remotes' => [], 'emails' => [], + 'lookup' => [], ], true, ], ]; @@ -1636,7 +1651,8 @@ public function testSearchSharees($searchTerm, $itemType, array $shareTypes, $pa $this->session, $this->getMockBuilder('OCP\IURLGenerator')->disableOriginalConstructor()->getMock(), $this->getMockBuilder('OCP\ILogger')->disableOriginalConstructor()->getMock(), - $this->shareManager + $this->shareManager, + $this->clientService ]) ->setMethods(array('getShareesForShareIds', 'getUsers', 'getGroups', 'getRemote')) ->getMock(); diff --git a/apps/lookup_server_connector/appinfo/app.php b/apps/lookup_server_connector/appinfo/app.php new file mode 100644 index 0000000000000..6c63e9a04007d --- /dev/null +++ b/apps/lookup_server_connector/appinfo/app.php @@ -0,0 +1,46 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see . + * + */ + +$dispatcher = \OC::$server->getEventDispatcher(); + +$dispatcher->addListener('OC\AccountManager::userUpdated', function(\Symfony\Component\EventDispatcher\GenericEvent $event) { + $user = $event->getSubject(); + + $keyManager = new \OC\Security\IdentityProof\Manager( + \OC::$server->getAppDataDir('identityproof'), + \OC::$server->getCrypto() + ); + $updateLookupServer = new \OCA\LookupServerConnector\UpdateLookupServer( + new \OC\Accounts\AccountManager(\OC::$server->getDatabaseConnection(), \OC::$server->getEventDispatcher()), + \OC::$server->getConfig(), + \OC::$server->getSecureRandom(), + \OC::$server->getHTTPClientService(), + $keyManager, + new \OC\Security\IdentityProof\Signer( + $keyManager, + new \OC\AppFramework\Utility\TimeFactory(), + \OC::$server->getURLGenerator(), + \OC::$server->getUserManager() + ), + \OC::$server->getJobList() + ); + $updateLookupServer->userUpdated($user); +}); diff --git a/apps/lookup_server_connector/appinfo/info.xml b/apps/lookup_server_connector/appinfo/info.xml new file mode 100644 index 0000000000000..88898c0b71b21 --- /dev/null +++ b/apps/lookup_server_connector/appinfo/info.xml @@ -0,0 +1,18 @@ + + + lookup_server_connector + Lookup Server Connector + Sync public user information with the lookup server + AGPL + Bjoern Schiessle + LookupServerConnector + 1.0.0 + other + + + + + + + + diff --git a/apps/lookup_server_connector/lib/BackgroundJobs/RetryJob.php b/apps/lookup_server_connector/lib/BackgroundJobs/RetryJob.php new file mode 100644 index 0000000000000..f33323b2d4f37 --- /dev/null +++ b/apps/lookup_server_connector/lib/BackgroundJobs/RetryJob.php @@ -0,0 +1,81 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see . + * + */ + + +namespace OCA\LookupServerConnector\BackgroundJobs; + + +use OC\BackgroundJob\Job; +use OCP\BackgroundJob\IJobList; +use OCP\Http\Client\IClientService; + +class RetryJob extends Job { + /** @var IClientService */ + private $clientService; + /** @var IJobList */ + private $jobList; + /** @var string */ + private $lookupServer = 'https://lookup.nextcloud.com/users'; + + /** + * @param IClientService|null $clientService + * @param IJobList|null $jobList + */ + public function __construct(IClientService $clientService = null, + IJobList $jobList = null) { + if($clientService !== null) { + $this->clientService = $clientService; + } else { + $this->clientService = \OC::$server->getHTTPClientService(); + } + if($jobList !== null) { + $this->jobList = $jobList; + } else { + $this->jobList = \OC::$server->getJobList(); + } + } + + protected function run($argument) { + if($argument['retryNo'] === 5) { + return; + } + + $client = $this->clientService->newClient(); + + try { + $client->post($this->lookupServer, + [ + 'body' => json_encode($argument['dataArray']), + 'timeout' => 10, + 'connect_timeout' => 3, + ] + ); + } catch (\Exception $e) { + $this->jobList->add(RetryJob::class, + [ + 'dataArray' => $argument['dataArray'], + 'retryNo' => $argument['retryNo'] + 1, + ] + ); + + } + } +} diff --git a/apps/lookup_server_connector/lib/UpdateLookupServer.php b/apps/lookup_server_connector/lib/UpdateLookupServer.php new file mode 100644 index 0000000000000..7ba63c8556715 --- /dev/null +++ b/apps/lookup_server_connector/lib/UpdateLookupServer.php @@ -0,0 +1,136 @@ + + * @copyright Copyright (c) 2016 Lukas Reschke + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see . + * + */ + +namespace OCA\LookupServerConnector; + +use OC\Accounts\AccountManager; +use OC\Security\IdentityProof\Manager; +use OC\Security\IdentityProof\Signer; +use OCA\LookupServerConnector\BackgroundJobs\RetryJob; +use OCP\BackgroundJob\IJobList; +use OCP\Http\Client\IClientService; +use OCP\IConfig; +use OCP\IUser; +use OCP\Security\ISecureRandom; + +/** + * Class UpdateLookupServer + * + * @package OCA\LookupServerConnector + */ +class UpdateLookupServer { + /** @var AccountManager */ + private $accountManager; + /** @var IConfig */ + private $config; + /** @var ISecureRandom */ + private $secureRandom; + /** @var IClientService */ + private $clientService; + /** @var Manager */ + private $keyManager; + /** @var Signer */ + private $signer; + /** @var IJobList */ + private $jobList; + /** @var string URL point to lookup server */ + private $lookupServer = 'https://lookup.nextcloud.com/users'; + + /** + * @param AccountManager $accountManager + * @param IConfig $config + * @param ISecureRandom $secureRandom + * @param IClientService $clientService + * @param Manager $manager + * @param Signer $signer + * @param IJobList $jobList + */ + public function __construct(AccountManager $accountManager, + IConfig $config, + ISecureRandom $secureRandom, + IClientService $clientService, + Manager $manager, + Signer $signer, + IJobList $jobList) { + $this->accountManager = $accountManager; + $this->config = $config; + $this->secureRandom = $secureRandom; + $this->clientService = $clientService; + $this->keyManager = $manager; + $this->signer = $signer; + $this->jobList = $jobList; + } + + /** + * @param IUser $user + */ + public function userUpdated(IUser $user) { + $userData = $this->accountManager->getUser($user); + $publicData = []; + + foreach ($userData as $key => $data) { + if ($data['scope'] === AccountManager::VISIBILITY_PUBLIC) { + $publicData[$key] = $data; + } + } + + if (!empty($publicData)) { + $this->sendToLookupServer($user, $publicData); + } + } + + /** + * send public user data to the lookup server + * + * @param IUser $user + * @param array $publicData + */ + protected function sendToLookupServer(IUser $user, array $publicData) { + $dataArray = [ + 'federationId' => $user->getCloudId(), + 'name' => isset($publicData[AccountManager::PROPERTY_DISPLAYNAME]) ? $publicData[AccountManager::PROPERTY_DISPLAYNAME]['value'] : '', + 'email' => isset($publicData[AccountManager::PROPERTY_EMAIL]) ? $publicData[AccountManager::PROPERTY_EMAIL]['value'] : '', + 'address' => isset($publicData[AccountManager::PROPERTY_ADDRESS]) ? $publicData[AccountManager::PROPERTY_ADDRESS]['value'] : '', + 'website' => isset($publicData[AccountManager::PROPERTY_WEBSITE]) ? $publicData[AccountManager::PROPERTY_WEBSITE]['value'] : '', + 'twitter' => isset($publicData[AccountManager::PROPERTY_TWITTER]) ? $publicData[AccountManager::PROPERTY_TWITTER]['value'] : '', + 'phone' => isset($publicData[AccountManager::PROPERTY_PHONE]) ? $publicData[AccountManager::PROPERTY_PHONE]['value'] : '', + ]; + $dataArray = $this->signer->sign('lookupserver', $dataArray, $user); + $httpClient = $this->clientService->newClient(); + try { + $httpClient->post($this->lookupServer, + [ + 'body' => json_encode($dataArray), + 'timeout' => 10, + 'connect_timeout' => 3, + ] + ); + } catch (\Exception $e) { + $this->jobList->add(RetryJob::class, + [ + 'dataArray' => $dataArray, + 'retryNo' => 0, + ] + ); + } + } +} diff --git a/build/integration/features/provisioning-v1.feature b/build/integration/features/provisioning-v1.feature index 9103a71ab9265..a8501ee8873c7 100644 --- a/build/integration/features/provisioning-v1.feature +++ b/build/integration/features/provisioning-v1.feature @@ -290,6 +290,7 @@ Feature: provisioning | files_sharing | | files_trashbin | | files_versions | + | lookup_server_connector | | provisioning_api | | sharebymail | | systemtags | diff --git a/core/Application.php b/core/Application.php index f68f7929e6a71..545b5fe420b6d 100644 --- a/core/Application.php +++ b/core/Application.php @@ -30,7 +30,10 @@ namespace OC\Core; +use OC\AppFramework\Utility\SimpleContainer; +use OC\Security\IdentityProof\Manager; use OCP\AppFramework\App; +use OCP\Files\IAppData; use OCP\Util; /** @@ -45,8 +48,14 @@ public function __construct() { $container = $this->getContainer(); - $container->registerService('defaultMailAddress', function() { + $container->registerService('defaultMailAddress', function () { return Util::getDefaultEmailAddress('lostpassword-noreply'); }); + $container->registerService(Manager::class, function () { + return new Manager( + \OC::$server->getAppDataDir('identityproof'), + \OC::$server->getCrypto() + ); + }); } } diff --git a/core/Controller/OCSController.php b/core/Controller/OCSController.php index 27ab9deb08a10..c59b0d7ad3fc1 100644 --- a/core/Controller/OCSController.php +++ b/core/Controller/OCSController.php @@ -23,6 +23,7 @@ use OC\CapabilitiesManager; use OC\Security\Bruteforce\Throttler; +use OC\Security\IdentityProof\Manager; use OCP\AppFramework\Http\DataResponse; use OCP\IRequest; use OCP\IUserManager; @@ -32,13 +33,12 @@ class OCSController extends \OCP\AppFramework\OCSController { /** @var CapabilitiesManager */ private $capabilitiesManager; - /** @var IUserSession */ private $userSession; - /** @var IUserManager */ private $userManager; - + /** @var Manager */ + private $keyManager; /** @var Throttler */ private $throttler; @@ -51,19 +51,21 @@ class OCSController extends \OCP\AppFramework\OCSController { * @param IUserSession $userSession * @param IUserManager $userManager * @param Throttler $throttler + * @param Manager $keyManager */ public function __construct($appName, IRequest $request, CapabilitiesManager $capabilitiesManager, IUserSession $userSession, IUserManager $userManager, - Throttler $throttler) { + Throttler $throttler, + Manager $keyManager) { parent::__construct($appName, $request); - $this->capabilitiesManager = $capabilitiesManager; $this->userSession = $userSession; $this->userManager = $userManager; $this->throttler = $throttler; + $this->keyManager = $keyManager; } /** @@ -139,4 +141,24 @@ public function personCheck($login = '', $password = '') { } return new DataResponse(null, 101); } + + /** + * @PublicPage + * + * @param string $cloudId + * @return DataResponse + */ + public function getIdentityProof($cloudId) { + $userObject = $this->userManager->get($cloudId); + + if($userObject !== null) { + $key = $this->keyManager->getKey($userObject); + $data = [ + 'public' => $key->getPublic(), + ]; + return new DataResponse($data); + } + + return new DataResponse('User not found', 404); + } } diff --git a/core/css/apps.css b/core/css/apps.css index 852879aee89f6..bef5c7c4937ec 100644 --- a/core/css/apps.css +++ b/core/css/apps.css @@ -289,7 +289,7 @@ border-radius: 3px; border-top-right-radius: 0; z-index: 110; - margin: -5px 14px 5px 10px; + margin-top: -5px; right: 0; -webkit-filter: drop-shadow(0 0 5px rgba(150, 150, 150, 0.75)); -moz-filter: drop-shadow(0 0 5px rgba(150, 150, 150, 0.75)); @@ -333,7 +333,8 @@ opacity: .5 !important; } .bubble .action:hover, -.bubble .action:focus { +.bubble .action:focus, +.bubble .action.active { -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)" !important; filter: alpha(opacity=100) !important; opacity: 1 !important; @@ -646,20 +647,13 @@ em { } .popovermenu .menuitem:hover, -.popovermenu .menuitem:focus { +.popovermenu .menuitem:focus, +.popovermenu .menuitem.active { -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)"; filter: alpha(opacity=100); opacity: 1; } -.popovermenu { - padding: 4px 12px; -} - -.popovermenu li { - padding: 5px 0; -} - .popovermenu .menuitem img { padding: initial; } @@ -667,8 +661,8 @@ em { .popovermenu a.menuitem, .popovermenu label.menuitem, .popovermenu .menuitem { - padding: 10px; - margin: -10px; + padding: 10px !important; + width: auto; } .popovermenu.hidden { @@ -676,11 +670,10 @@ em { } .popovermenu .menuitem { - display: block; + display: flex !important; line-height: 30px; - padding-left: 5px; color: #000; - padding: 0; + align-items: center; } .popovermenu .menuitem .icon, diff --git a/core/css/header.css b/core/css/header.css index efdd3be7cebea..19ecf77bcd347 100644 --- a/core/css/header.css +++ b/core/css/header.css @@ -227,8 +227,8 @@ #apps-management a:hover span, #apps-management a:focus span, #apps-management a.active span { - -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=75)"; - opacity: .75; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)"; + opacity: 1; } #navigation .app-icon { @@ -372,8 +372,8 @@ #expanddiv a:focus, #expanddiv a:active, #expanddiv a.active { - -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=75)"; - opacity: .75; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)"; + opacity: 1; } /* do not show display name when profile picture is present */ diff --git a/core/js/sharedialogview.js b/core/js/sharedialogview.js index 0a29dec73ca26..6377d16dd4c70 100644 --- a/core/js/sharedialogview.js +++ b/core/js/sharedialogview.js @@ -149,6 +149,7 @@ var users = result.ocs.data.exact.users.concat(result.ocs.data.users); var groups = result.ocs.data.exact.groups.concat(result.ocs.data.groups); var remotes = result.ocs.data.exact.remotes.concat(result.ocs.data.remotes); + var lookup = result.ocs.data.lookup; if (typeof(result.ocs.data.emails) !== 'undefined') { var emails = result.ocs.data.exact.emails.concat(result.ocs.data.emails); } else { @@ -159,6 +160,7 @@ var groupsLength; var remotesLength; var emailsLength; + var lookupLength; var i, j; @@ -224,7 +226,7 @@ } } - var suggestions = users.concat(groups).concat(remotes).concat(emails); + var suggestions = users.concat(groups).concat(remotes).concat(emails).concat(lookup); if (suggestions.length > 0) { $('.shareWithField').removeClass('error') diff --git a/core/js/tests/specs/sharedialogviewSpec.js b/core/js/tests/specs/sharedialogviewSpec.js index 6e86cb8eff7a0..a44bb309c5cac 100644 --- a/core/js/tests/specs/sharedialogviewSpec.js +++ b/core/js/tests/specs/sharedialogviewSpec.js @@ -529,7 +529,8 @@ describe('OC.Share.ShareDialogView', function() { }, 'users' : [{'label': 'bob', 'value': {'shareType': 0, 'shareWith': 'test'}}], 'groups' : [], - 'remotes': [] + 'remotes': [], + 'lookup': [] } } }); @@ -577,7 +578,8 @@ describe('OC.Share.ShareDialogView', function() { } ], 'groups': [], - 'remotes': [] + 'remotes': [], + 'lookup': [] } } }); @@ -635,7 +637,8 @@ describe('OC.Share.ShareDialogView', function() { } ], 'groups': [], - 'remotes': [] + 'remotes': [], + 'lookup': [] } } }); @@ -715,7 +718,8 @@ describe('OC.Share.ShareDialogView', function() { } ], 'groups': [], - 'remotes': [] + 'remotes': [], + 'lookup': [] } } }); @@ -765,7 +769,8 @@ describe('OC.Share.ShareDialogView', function() { } } ], - 'remotes': [] + 'remotes': [], + 'lookup': [] } } }); @@ -815,7 +820,8 @@ describe('OC.Share.ShareDialogView', function() { 'shareWith': 'foo2@bar.com/baz' } } - ] + ], + 'lookup': [] } } }); diff --git a/core/routes.php b/core/routes.php index e5636ff6c007d..2b8080a3b7bee 100644 --- a/core/routes.php +++ b/core/routes.php @@ -61,6 +61,7 @@ ['root' => '/cloud', 'name' => 'OCS#getCurrentUser', 'url' => '/user', 'verb' => 'GET'], ['root' => '', 'name' => 'OCS#getConfig', 'url' => '/config', 'verb' => 'GET'], ['root' => '/person', 'name' => 'OCS#personCheck', 'url' => '/check', 'verb' => 'POST'], + ['root' => '/identityproof', 'name' => 'OCS#getIdentityProof', 'url' => '/key/{cloudId}', 'verb' => 'GET'], ], ]); diff --git a/core/shipped.json b/core/shipped.json index f831d17f36a5c..7fb87b7f17db7 100644 --- a/core/shipped.json +++ b/core/shipped.json @@ -22,6 +22,7 @@ "firstrunwizard", "gallery", "logreader", + "lookup_server_connector", "notifications", "password_policy", "provisioning_api", @@ -42,6 +43,7 @@ "files", "dav", "federatedfilesharing", + "lookup_server_connector", "provisioning_api", "twofactor_backupcodes", "workflowengine" diff --git a/db_structure.xml b/db_structure.xml index c7e1e072a8eff..545628a9233a7 100644 --- a/db_structure.xml +++ b/db_structure.xml @@ -2113,4 +2113,36 @@ + + + *dbprefix*accounts + + + + uid + text + + true + 64 + + + data + clob + + true + + + + uid_index + true + true + + uid + ascending + + + + +
+ diff --git a/lib/private/Accounts/AccountManager.php b/lib/private/Accounts/AccountManager.php new file mode 100644 index 0000000000000..6496a52132658 --- /dev/null +++ b/lib/private/Accounts/AccountManager.php @@ -0,0 +1,201 @@ + + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @copyright Copyright (c) 2016, Björn Schießle + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + + +namespace OC\Accounts; + +use OCP\IDBConnection; +use OCP\IUser; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\EventDispatcher\GenericEvent; + +/** + * Class AccountManager + * + * Manage system accounts table + * + * @group DB + * @package OC\Accounts + */ +class AccountManager { + + /** nobody can see my account details */ + const VISIBILITY_PRIVATE = 'private'; + /** only contacts, especially trusted servers can see my contact details */ + const VISIBILITY_CONTACTS_ONLY = 'contacts'; + /** every body ca see my contact detail, will be published to the lookup server */ + const VISIBILITY_PUBLIC = 'public'; + + const PROPERTY_AVATAR = 'avatar'; + const PROPERTY_DISPLAYNAME = 'displayname'; + const PROPERTY_PHONE = 'phone'; + const PROPERTY_EMAIL = 'email'; + const PROPERTY_WEBSITE = 'website'; + const PROPERTY_ADDRESS = 'address'; + const PROPERTY_TWITTER = 'twitter'; + + /** @var IDBConnection database connection */ + private $connection; + + /** @var string table name */ + private $table = 'accounts'; + + /** @var EventDispatcherInterface */ + private $eventDispatcher; + + /** + * AccountManager constructor. + * + * @param IDBConnection $connection + * @param EventDispatcherInterface $eventDispatcher + */ + public function __construct(IDBConnection $connection, EventDispatcherInterface $eventDispatcher) { + $this->connection = $connection; + $this->eventDispatcher = $eventDispatcher; + } + + /** + * update user record + * + * @param IUser $user + * @param $data + */ + public function updateUser(IUser $user, $data) { + $userData = $this->getUser($user); + if (empty($userData)) { + $this->insertNewUser($user, $data); + } else { + $this->updateExistingUser($user, $data); + } + + $this->eventDispatcher->dispatch( + 'OC\AccountManager::userUpdated', + new GenericEvent($user) + ); + } + + /** + * get stored data from a given user + * + * @param IUser $user + * @return array + */ + public function getUser(IUser $user) { + $uid = $user->getUID(); + $query = $this->connection->getQueryBuilder(); + $query->select('data')->from($this->table) + ->where($query->expr()->eq('uid', $query->createParameter('uid'))) + ->setParameter('uid', $uid); + $query->execute(); + $result = $query->execute()->fetchAll(); + + if (empty($result)) { + $userData = $this->buildDefaultUserRecord($user); + $this->insertNewUser($user, $userData); + return $userData; + } + + return json_decode($result[0]['data'], true); + } + + /** + * add new user to accounts table + * + * @param IUser $user + * @param array $data + */ + protected function insertNewUser(IUser $user, $data) { + $uid = $user->getUID(); + $jsonEncodedData = json_encode($data); + $query = $this->connection->getQueryBuilder(); + $query->insert($this->table) + ->values( + [ + 'uid' => $query->createNamedParameter($uid), + 'data' => $query->createNamedParameter($jsonEncodedData), + ] + ) + ->execute(); + } + + /** + * update existing user in accounts table + * + * @param IUser $user + * @param array $data + */ + protected function updateExistingUser(IUser $user, $data) { + $uid = $user->getUID(); + $jsonEncodedData = json_encode($data); + $query = $this->connection->getQueryBuilder(); + $query->update($this->table) + ->set('data', $query->createNamedParameter($jsonEncodedData)) + ->where($query->expr()->eq('uid', $query->createNamedParameter($uid))) + ->execute(); + } + + /** + * build default user record in case not data set exists yet + * + * @param IUser $user + * @return array + */ + protected function buildDefaultUserRecord(IUser $user) { + return [ + self::PROPERTY_DISPLAYNAME => + [ + 'value' => $user->getDisplayName(), + 'scope' => self::VISIBILITY_CONTACTS_ONLY, + ], + self::PROPERTY_ADDRESS => + [ + 'value' => '', + 'scope' => self::VISIBILITY_PRIVATE, + ], + self::PROPERTY_WEBSITE => + [ + 'value' => '', + 'scope' => self::VISIBILITY_PRIVATE, + ], + self::PROPERTY_EMAIL => + [ + 'value' => $user->getEMailAddress(), + 'scope' => self::VISIBILITY_CONTACTS_ONLY, + ], + self::PROPERTY_AVATAR => + [ + 'scope' => self::VISIBILITY_CONTACTS_ONLY + ], + self::PROPERTY_PHONE => + [ + 'value' => '', + 'scope' => self::VISIBILITY_PRIVATE, + ], + self::PROPERTY_TWITTER => + [ + 'value' => '', + 'scope' => self::VISIBILITY_PRIVATE, + ], + ]; + } + +} diff --git a/lib/private/Security/IdentityProof/Key.php b/lib/private/Security/IdentityProof/Key.php new file mode 100644 index 0000000000000..9739a9571bb1f --- /dev/null +++ b/lib/private/Security/IdentityProof/Key.php @@ -0,0 +1,46 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see . + * + */ + +namespace OC\Security\IdentityProof; + +class Key { + /** @var string */ + private $publicKey; + /** @var string */ + private $privateKey; + + /** + * @param string $publicKey + * @param string $privateKey + */ + public function __construct($publicKey, $privateKey) { + $this->publicKey = $publicKey; + $this->privateKey = $privateKey; + } + + public function getPrivate() { + return $this->privateKey; + } + + public function getPublic() { + return $this->publicKey; + } +} diff --git a/lib/private/Security/IdentityProof/Manager.php b/lib/private/Security/IdentityProof/Manager.php new file mode 100644 index 0000000000000..d2a9e57e3387e --- /dev/null +++ b/lib/private/Security/IdentityProof/Manager.php @@ -0,0 +1,108 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see . + * + */ + +namespace OC\Security\IdentityProof; + +use OCP\Files\IAppData; +use OCP\IUser; +use OCP\Security\ICrypto; + +class Manager { + /** @var IAppData */ + private $appData; + /** @var ICrypto */ + private $crypto; + + /** + * @param IAppData $appData + * @param ICrypto $crypto + */ + public function __construct(IAppData $appData, + ICrypto $crypto) { + $this->appData = $appData; + $this->crypto = $crypto; + } + + /** + * Calls the openssl functions to generate a public and private key. + * In a separate function for unit testing purposes. + * + * @return array [$publicKey, $privateKey] + */ + protected function generateKeyPair() { + $config = [ + 'digest_alg' => 'sha512', + 'private_key_bits' => 2048, + ]; + + // Generate new key + $res = openssl_pkey_new($config); + openssl_pkey_export($res, $privateKey); + + // Extract the public key from $res to $pubKey + $publicKey = openssl_pkey_get_details($res); + $publicKey = $publicKey['key']; + + return [$publicKey, $privateKey]; + } + + /** + * Generate a key for $user + * Note: If a key already exists it will be overwritten + * + * @param IUser $user + * @return Key + */ + protected function generateKey(IUser $user) { + list($publicKey, $privateKey) = $this->generateKeyPair(); + + // Write the private and public key to the disk + try { + $this->appData->newFolder($user->getUID()); + } catch (\Exception $e) {} + $folder = $this->appData->getFolder($user->getUID()); + $folder->newFile('private') + ->putContent($this->crypto->encrypt($privateKey)); + $folder->newFile('public') + ->putContent($publicKey); + + return new Key($publicKey, $privateKey); + } + + /** + * Get public and private key for $user + * + * @param IUser $user + * @return Key + */ + public function getKey(IUser $user) { + try { + $folder = $this->appData->getFolder($user->getUID()); + $privateKey = $this->crypto->decrypt( + $folder->getFile('private')->getContent() + ); + $publicKey = $folder->getFile('public')->getContent(); + return new Key($publicKey, $privateKey); + } catch (\Exception $e) { + return $this->generateKey($user); + } + } +} diff --git a/lib/private/Security/IdentityProof/Signer.php b/lib/private/Security/IdentityProof/Signer.php new file mode 100644 index 0000000000000..50c36b26966ac --- /dev/null +++ b/lib/private/Security/IdentityProof/Signer.php @@ -0,0 +1,120 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see . + * + */ + +namespace OC\Security\IdentityProof; + +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\IURLGenerator; +use OCP\IUser; +use OCP\IUserManager; + +class Signer { + /** @var Manager */ + private $keyManager; + /** @var ITimeFactory */ + private $timeFactory; + /** @var IURLGenerator */ + private $urlGenerator; + /** @var IUserManager */ + private $userManager; + + /** + * @param Manager $keyManager + * @param ITimeFactory $timeFactory + * @param IURLGenerator $urlGenerator + * @param IUserManager $userManager + */ + public function __construct(Manager $keyManager, + ITimeFactory $timeFactory, + IURLGenerator $urlGenerator, + IUserManager $userManager) { + $this->keyManager = $keyManager; + $this->timeFactory = $timeFactory; + $this->userManager = $userManager; + } + + /** + * Returns a signed blob for $data + * + * @param string $type + * @param array $data + * @param IUser $user + * @return array ['message', 'signature'] + */ + public function sign($type, array $data, IUser $user) { + $privateKey = $this->keyManager->getKey($user)->getPrivate(); + $data = [ + 'data' => $data, + 'type' => $type, + 'signer' => $user->getCloudId(), + 'timestamp' => $this->timeFactory->getTime(), + ]; + openssl_sign(json_encode($data), $signature, $privateKey, OPENSSL_ALGO_SHA512); + + return [ + 'message' => $data, + 'signature' => base64_encode($signature), + ]; + } + + /** + * @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; + } + + /** + * Whether the data is signed properly + * + * @param array $data + * @return bool + */ + public function verify(array $data) { + if(isset($data['message']) + && isset($data['signature']) + && isset($data['message']['signer']) + ) { + $server = $this->urlGenerator->getAbsoluteURL('/'); + $postfix = strlen('@' . rtrim($this->removeProtocolFromUrl($server), '/')); + $userId = substr($data['message']['signer'], -$postfix); + + $user = $this->userManager->get($userId); + if($user !== null) { + $key = $this->keyManager->getKey($user); + return (bool)openssl_verify( + json_encode($data['message']), + base64_decode($data['signature']), + $key->getPublic() + ); + } + } + + return false; + } +} diff --git a/lib/private/Updater.php b/lib/private/Updater.php index 2fed67988b889..c3d8ef9fea372 100644 --- a/lib/private/Updater.php +++ b/lib/private/Updater.php @@ -53,7 +53,7 @@ class Updater extends BasicEmitter { /** @var ILogger $log */ private $log; - + /** @var IConfig */ private $config; diff --git a/settings/Application.php b/settings/Application.php index d907cd666fb76..ca2b2b91f7195 100644 --- a/settings/Application.php +++ b/settings/Application.php @@ -34,9 +34,21 @@ use OC\App\AppStore\Fetcher\CategoryFetcher; use OC\AppFramework\Utility\TimeFactory; use OC\Authentication\Token\IProvider; +use OC\Files\View; use OC\Server; +use OC\Settings\Controller\AppSettingsController; +use OC\Settings\Controller\AuthSettingsController; +use OC\Settings\Controller\CertificateController; +use OC\Settings\Controller\CheckSetupController; +use OC\Settings\Controller\EncryptionController; +use OC\Settings\Controller\GroupsController; +use OC\Settings\Controller\LogSettingsController; +use OC\Settings\Controller\MailSettingsController; +use OC\Settings\Controller\SecuritySettingsController; +use OC\Settings\Controller\UsersController; use OC\Settings\Middleware\SubadminMiddleware; use OCP\AppFramework\App; +use OCP\AppFramework\IAppContainer; use OCP\IContainer; use OCP\Settings\IManager; use OCP\Util; @@ -57,7 +69,129 @@ public function __construct(array $urlParams=[]){ // Register Middleware $container->registerAlias('SubadminMiddleware', SubadminMiddleware::class); - $container->registerMiddleWare('SubadminMiddleware'); + /** + * Controllers + */ + $container->registerService('MailSettingsController', function(IContainer $c) { + return new MailSettingsController( + $c->query('AppName'), + $c->query('Request'), + $c->query('L10N'), + $c->query('Config'), + $c->query('UserSession'), + $c->query('Defaults'), + $c->query('Mailer'), + $c->query('DefaultMailAddress') + ); + }); + $container->registerService('EncryptionController', function(IContainer $c) { + return new EncryptionController( + $c->query('AppName'), + $c->query('Request'), + $c->query('L10N'), + $c->query('Config'), + $c->query('DatabaseConnection'), + $c->query('UserManager'), + new View(), + $c->query('Logger') + ); + }); + + $container->registerService('AppSettingsController', function(IContainer $c) { + return new AppSettingsController( + $c->query('AppName'), + $c->query('Request'), + $c->query('L10N'), + $c->query('Config'), + $c->query('INavigationManager'), + $c->query('IAppManager'), + $c->query('CategoryFetcher'), + $c->query('AppFetcher'), + \OC::$server->getL10NFactory() + ); + }); + $container->registerService('AuthSettingsController', function(IContainer $c) { + return new AuthSettingsController( + $c->query('AppName'), + $c->query('Request'), + $c->query('ServerContainer')->query('OC\Authentication\Token\IProvider'), + $c->query('UserManager'), + $c->query('ServerContainer')->getSession(), + $c->query('ServerContainer')->getSecureRandom(), + $c->query('UserId') + ); + }); + $container->registerService('SecuritySettingsController', function(IContainer $c) { + return new SecuritySettingsController( + $c->query('AppName'), + $c->query('Request'), + $c->query('Config') + ); + }); + $container->registerService('AccountManager', function(IAppContainer $c) { + return new AccountManager($c->getServer()->getDatabaseConnection()); + }); + $container->registerService('CertificateController', function(IContainer $c) { + return new CertificateController( + $c->query('AppName'), + $c->query('Request'), + $c->query('CertificateManager'), + $c->query('SystemCertificateManager'), + $c->query('L10N'), + $c->query('IAppManager') + ); + }); + $container->registerService('GroupsController', function(IContainer $c) { + return new GroupsController( + $c->query('AppName'), + $c->query('Request'), + $c->query('GroupManager'), + $c->query('UserSession'), + $c->query('IsAdmin'), + $c->query('L10N') + ); + }); + $container->registerService('UsersController', function(IContainer $c) { + return new UsersController( + $c->query('AppName'), + $c->query('Request'), + $c->query('UserManager'), + $c->query('GroupManager'), + $c->query('UserSession'), + $c->query('Config'), + $c->query('IsAdmin'), + $c->query('L10N'), + $c->query('Logger'), + $c->query('Defaults'), + $c->query('Mailer'), + $c->query('DefaultMailAddress'), + $c->query('URLGenerator'), + $c->query('OCP\\App\\IAppManager'), + $c->query('OCP\\IAvatarManager'), + $c->query('AccountManager') + ); + }); + $container->registerService('LogSettingsController', function(IContainer $c) { + return new LogSettingsController( + $c->query('AppName'), + $c->query('Request'), + $c->query('Config'), + $c->query('L10N') + ); + }); + $container->registerService('CheckSetupController', function(IContainer $c) { + return new CheckSetupController( + $c->query('AppName'), + $c->query('Request'), + $c->query('Config'), + $c->query('ClientService'), + $c->query('URLGenerator'), + $c->query('Util'), + $c->query('L10N'), + $c->query('Checker') + ); + }); + /** * Core class wrappers diff --git a/settings/Controller/UsersController.php b/settings/Controller/UsersController.php index 89831a66abad0..206f187254232 100644 --- a/settings/Controller/UsersController.php +++ b/settings/Controller/UsersController.php @@ -30,7 +30,9 @@ namespace OC\Settings\Controller; +use OC\Accounts\AccountManager; use OC\AppFramework\Http; +use OC\ForbiddenException; use OC\User\User; use OCP\App\IAppManager; use OCP\AppFramework\Controller; @@ -47,6 +49,7 @@ use OCP\IUserSession; use OCP\Mail\IMailer; use OCP\IAvatarManager; +use Punic\Exception; /** * @package OC\Settings\Controller @@ -80,6 +83,8 @@ class UsersController extends Controller { private $isRestoreEnabled = false; /** @var IAvatarManager */ private $avatarManager; + /** @var AccountManager */ + private $accountManager; /** * @param string $appName @@ -97,6 +102,7 @@ class UsersController extends Controller { * @param IURLGenerator $urlGenerator * @param IAppManager $appManager * @param IAvatarManager $avatarManager + * @param AccountManager $accountManager */ public function __construct($appName, IRequest $request, @@ -112,7 +118,9 @@ public function __construct($appName, $fromMailAddress, IURLGenerator $urlGenerator, IAppManager $appManager, - IAvatarManager $avatarManager) { + IAvatarManager $avatarManager, + AccountManager $accountManager +) { parent::__construct($appName, $request); $this->userManager = $userManager; $this->groupManager = $groupManager; @@ -126,6 +134,7 @@ public function __construct($appName, $this->fromMailAddress = $fromMailAddress; $this->urlGenerator = $urlGenerator; $this->avatarManager = $avatarManager; + $this->accountManager = $accountManager; // check for encryption state - TODO see formatUserForIndex $this->isEncryptionAppEnabled = $appManager->isEnabledForUser('encryption'); @@ -493,35 +502,42 @@ public function destroy($id) { } /** - * Set the mail address of a user - * * @NoAdminRequired * @NoSubadminRequired * @PasswordConfirmationRequired * - * @param string $id - * @param string $mailAddress + * @param string $avatarScope + * @param string $displayname + * @param string $displaynameScope + * @param string $phone + * @param string $phoneScope + * @param string $email + * @param string $emailScope + * @param string $website + * @param string $websiteScope + * @param string $address + * @param string $addressScope + * @param string $twitter + * @param string $twitterScope * @return DataResponse */ - public function setMailAddress($id, $mailAddress) { - $userId = $this->userSession->getUser()->getUID(); - $user = $this->userManager->get($id); - - if($userId !== $id - && !$this->isAdmin - && !$this->groupManager->getSubAdmin()->isUserAccessible($this->userSession->getUser(), $user)) { - return new DataResponse( - array( - 'status' => 'error', - 'data' => array( - 'message' => (string)$this->l10n->t('Forbidden') - ) - ), - Http::STATUS_FORBIDDEN - ); - } - - if($mailAddress !== '' && !$this->mailer->validateMailAddress($mailAddress)) { + public function setUserSettings($avatarScope, + $displayname, + $displaynameScope, + $phone, + $phoneScope, + $email, + $emailScope, + $website, + $websiteScope, + $address, + $addressScope, + $twitter, + $twitterScope + ) { + + + if(!empty($email) && !$this->mailer->validateMailAddress($email)) { return new DataResponse( array( 'status' => 'error', @@ -533,46 +549,80 @@ public function setMailAddress($id, $mailAddress) { ); } - if(!$user){ + $user = $this->userSession->getUser(); + + $data = [ + AccountManager::PROPERTY_AVATAR => ['scope' => $avatarScope], + AccountManager::PROPERTY_DISPLAYNAME => ['value' => $displayname, 'scope' => $displaynameScope], + AccountManager::PROPERTY_EMAIL=> ['value' => $email, 'scope' => $emailScope], + AccountManager::PROPERTY_WEBSITE => ['value' => $website, 'scope' => $websiteScope], + AccountManager::PROPERTY_ADDRESS => ['value' => $address, 'scope' => $addressScope], + AccountManager::PROPERTY_PHONE => ['value' => $phone, 'scope' => $phoneScope], + AccountManager::PROPERTY_TWITTER => ['value' => $twitter, 'scope' => $twitterScope] + ]; + + $this->accountManager->updateUser($user, $data); + + try { + $this->saveUserSettings($user, $data); return new DataResponse( array( - 'status' => 'error', + 'status' => 'success', 'data' => array( - 'message' => (string)$this->l10n->t('Invalid user') + 'userId' => $user->getUID(), + 'avatarScope' => $avatarScope, + 'displayname' => $displayname, + 'displaynameScope' => $displaynameScope, + 'email' => $email, + 'emailScope' => $emailScope, + 'website' => $website, + 'websiteScope' => $websiteScope, + 'address' => $address, + 'addressScope' => $addressScope, + 'message' => (string)$this->l10n->t('Settings saved') ) ), - Http::STATUS_UNPROCESSABLE_ENTITY + Http::STATUS_OK ); + } catch (ForbiddenException $e) { + return new DataResponse([ + 'status' => 'error', + 'data' => [ + 'message' => $e->getMessage() + ], + ]); } - // this is the only permission a backend provides and is also used - // for the permission of setting a email address - if(!$user->canChangeDisplayName()){ - return new DataResponse( - array( - 'status' => 'error', - 'data' => array( - 'message' => (string)$this->l10n->t('Unable to change mail address') - ) - ), - Http::STATUS_FORBIDDEN - ); + } + + + /** + * update account manager with new user data + * + * @param IUser $user + * @param array $data + * @throws ForbiddenException + */ + private function saveUserSettings(IUser $user, $data) { + + // keep the user back-end up-to-date with the latest display name and email + // address + $oldDisplayName = $user->getDisplayName(); + if (isset($data[AccountManager::PROPERTY_DISPLAYNAME]['value']) && $oldDisplayName !== $data[AccountManager::PROPERTY_DISPLAYNAME]['value']) { + $result = $user->setDisplayName($data[AccountManager::PROPERTY_DISPLAYNAME]['value']); + if ($result === false) { + throw new ForbiddenException($this->l10n->t('Unable to change full name')); + } } - // delete user value if email address is empty - $user->setEMailAddress($mailAddress); + if (isset($data['email'][0]['value']) && $user->getEMailAddress() !== $data['email'][0]['value']) { + $result = $user->setEMailAddress($data['email'][0]['value']); + if ($result === false) { + throw new ForbiddenException($this->l10n->t('Unable to change mail address')); + } + } - return new DataResponse( - array( - 'status' => 'success', - 'data' => array( - 'username' => $id, - 'mailAddress' => $mailAddress, - 'message' => (string)$this->l10n->t('Email saved') - ) - ), - Http::STATUS_OK - ); + $this->accountManager->updateUser($user, $data); } /** @@ -619,6 +669,7 @@ public function stats() { * @NoAdminRequired * @NoSubadminRequired * @PasswordConfirmationRequired + * @todo merge into saveUserSettings * * @param string $username * @param string $displayName @@ -626,11 +677,6 @@ public function stats() { */ public function setDisplayName($username, $displayName) { $currentUser = $this->userSession->getUser(); - - if ($username === null) { - $username = $currentUser->getUID(); - } - $user = $this->userManager->get($username); if ($user === null || @@ -638,8 +684,10 @@ public function setDisplayName($username, $displayName) { ( !$this->groupManager->isAdmin($currentUser->getUID()) && !$this->groupManager->getSubAdmin()->isUserAccessible($currentUser, $user) && - $currentUser !== $user) - ) { + $currentUser->getUID() !== $username + + ) + ) { return new DataResponse([ 'status' => 'error', 'data' => [ @@ -648,7 +696,12 @@ public function setDisplayName($username, $displayName) { ]); } - if ($user->setDisplayName($displayName)) { + $userData = $this->accountManager->getUser($user); + $userData[AccountManager::PROPERTY_DISPLAYNAME]['value'] = $displayName; + + + try { + $this->saveUserSettings($user, $userData); return new DataResponse([ 'status' => 'success', 'data' => [ @@ -657,11 +710,11 @@ public function setDisplayName($username, $displayName) { 'displayName' => $displayName, ], ]); - } else { + } catch (ForbiddenException $e) { return new DataResponse([ 'status' => 'error', 'data' => [ - 'message' => $this->l10n->t('Unable to change full name'), + 'message' => $e->getMessage(), 'displayName' => $user->getDisplayName(), ], ]); diff --git a/settings/css/settings.css b/settings/css/settings.css index 9008ba5a985ba..23d6cd98007b0 100644 --- a/settings/css/settings.css +++ b/settings/css/settings.css @@ -7,16 +7,14 @@ input#openid, input#webdav { width:20em; } /* PERSONAL */ -#avatar { - display: inline-block; - float: left; +#avatarform { width: 160px; padding-right: 0; } -#avatar .avatardiv { +#avatarform .avatardiv { margin-bottom: 10px; } -#avatar .warning { +#avatarform .warning { width: 100%; } #uploadavatarbutton, @@ -28,7 +26,7 @@ input#openid, input#webdav { width:20em; } .jcrop-holder { z-index: 500; } -#avatar #cropper { +#cropper { float: left; z-index: 500; /* float cropper above settings page to prevent unexpected flowing from dynamically sized element */ @@ -40,7 +38,7 @@ input#openid, input#webdav { width:20em; } width: 100%; height: calc(100% - 45px); } -#avatar #cropper .inner-container { +#cropper .inner-container { z-index: 2001; /* above the top bar if needed */ position: absolute; top: 50%; @@ -53,17 +51,80 @@ input#openid, input#webdav { width:20em; } padding: 15px; } -#avatar #cropper .inner-container .jcrop-holder { +#cropper .inner-container .jcrop-holder { box-shadow: 0 0 7px #888; } -#avatar #cropper .inner-container .button { +#cropper .inner-container .button { margin-top: 15px; } -#avatar #cropper .inner-container .primary { +#cropper .inner-container .primary { float: right; } -#displaynameform, +#personal-settings-avatar-container { + float: left; +} +#personal-settings-container { + position: relative; + float: left; + min-width: 280px; + max-width: 700px; + width: calc(100% - 200px); +} +#personal-settings-container:after { + clear: both; +} +#personal-settings-container > div { + float: left; + height: 100px; + min-width: 300px; +} +#personal-settings-container.no-edit > div { + height: 20px; + min-width: 200px; +} +#avatarform > h2, +#personal-settings-container > div h2 { + position: relative; +} +#personal-settings-container > div h2 span[class^="icon-"], +#personal-settings-avatar-container h2 span[class^="icon-"] { + display: inline-block; + padding: 8px; + margin-left: -8px; + margin-bottom: -10px; + background-size: 16px; + opacity: .3; + cursor: pointer; +} +.personal-settings-setting-box input[type="text"], +.personal-settings-setting-box input[type="email"], +.personal-settings-setting-box input[type="tel"] { + width: 17em; +} +#personal-settings-container > div > form span[class^="icon-checkmark"] { + position: absolute; + left: 239px; + top: 82px; + pointer-events: none; +} +.federationScopeMenu { + top: 44px; +} +.federationScopeMenu.bubble::after { + right: 50%; + transform: translate(50%, 0); +} +.federationScopeMenu.popovermenu a.menuitem, +.federationScopeMenu.popovermenu label.menuitem, +.federationScopeMenu.popovermenu .menuitem { + font-size: 12.8px; + line-height: 1.6em; +} +.federationScopeMenu.popovermenu .menuitem .menuitem-text-detail { + opacity: .75; +} + #lostpassword, #groups, #passwordform { @@ -73,7 +134,7 @@ input#openid, input#webdav { width:20em; } padding-right: 0; min-width: 60%; } -#avatar, +#avatarform, #passwordform { margin-bottom: 0; padding-bottom: 0; @@ -81,6 +142,8 @@ input#openid, input#webdav { width:20em; } #groups { overflow-wrap: break-word; max-width: 75%; + display: block;; + clear: both; } .clientsbox img { @@ -104,10 +167,6 @@ input#openid, input#webdav { width:20em; } input#identity { width: 20em; } -#displayName, -#email { - width: 17em; -} #showWizard { display: inline-block; diff --git a/settings/js/federationscopemenu.js b/settings/js/federationscopemenu.js new file mode 100644 index 0000000000000..066f648c104a0 --- /dev/null +++ b/settings/js/federationscopemenu.js @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2016 + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +/* global OC, Handlebars */ +(function() { + + var TEMPLATE_MENU = + ''; + + /** + * Construct a new FederationScopeMenu instance + * @constructs FederationScopeMenu + * @memberof OC.Settings + */ + var FederationScopeMenu = OC.Backbone.View.extend({ + tagName: 'div', + className: 'federationScopeMenu popovermenu bubble hidden open menu', + field: undefined, + _scopes: undefined, + + initialize: function(options) { + this.field = options.field; + this._scopes = [ + { + name: 'private', + displayName: (this.field === 'avatar' || this.field === 'displayname') ? t('core', 'Local') : t('core', 'Private'), + tooltip: (this.field === 'avatar' || this.field === 'displayname') ? t('core', 'Only visible to local users') : t('core', 'Only visible to you'), + icon: OC.imagePath('core', 'actions/password'), + active: false + }, + { + name: 'contacts', + displayName: t('core', 'Contacts'), + tooltip: t('core', 'Visible to local users and to trusted servers'), + icon: OC.imagePath('core', 'places/contacts-dark'), + active: false + }, + { + name: 'public', + displayName: t('core', 'Public'), + tooltip: t('core', 'Will be synced to a global and public address book'), + icon: OC.imagePath('core', 'places/link'), + active: false + } + ]; + }, + + /** + * Current context + * + * @type OCA.Files.FileActionContext + */ + _context: null, + + events: { + 'click a.action': '_onClickAction' + }, + + template: Handlebars.compile(TEMPLATE_MENU), + + /** + * Event handler whenever an action has been clicked within the menu + * + * @param {Object} event event object + */ + _onClickAction: function(event) { + var $target = $(event.currentTarget); + if (!$target.hasClass('menuitem')) { + $target = $target.closest('.menuitem'); + } + + this.trigger('select:scope', $target.data('action')); + + OC.hideMenus(); + }, + + /** + * Renders the menu with the currently set items + */ + render: function() { + this.$el.html(this.template({ + items: this._scopes + })); + }, + + /** + * Displays the menu + */ + show: function(context) { + this._context = context; + var currentlyActiveValue = $('#'+context.target.closest('form').id).find('.icon-checkmark > input')[0].value; + + for(var i = 0 in this._scopes) { + this._scopes[i].active = false; + } + + switch (currentlyActiveValue) { + case "private": + this._scopes[0].active = true; + break; + case "contacts": + this._scopes[1].active = true; + break; + case "public": + this._scopes[2].active = true; + break; + } + + var $el = $(context.target); + var offsetIcon = $el.offset(); + var offsetHeading = $el.closest('h2').offset(); + + this.render(); + this.$el.removeClass('hidden'); + + OC.showMenu(null, this.$el); + + //Set the menuwidth + var menuWidth = this.$el.width(); + this.$el.css('width', menuWidth); + + //Calculate menu position + var l = offsetIcon.left - offsetHeading.left; + l = l - (menuWidth / 2) + ($el.outerWidth()/2); + this.$el.css('left', l); + + } + }); + + OC.Settings = OC.Settings || {}; + OC.Settings.FederationScopeMenu = FederationScopeMenu; + +})(); diff --git a/settings/js/federationsettingsview.js b/settings/js/federationsettingsview.js new file mode 100644 index 0000000000000..7a629fc2ab6a9 --- /dev/null +++ b/settings/js/federationsettingsview.js @@ -0,0 +1,176 @@ +/* global OC, result, _ */ + +/** + * Copyright (c) 2016, Christoph Wurst + * + * This file is licensed under the Affero General Public License version 3 or later. + * See the COPYING-README file. + */ + +(function(_, $, OC) { + 'use strict'; + + var FederationSettingsView = OC.Backbone.View.extend({ + _inputFields: undefined, + + /** @var Backbone.Model */ + _config: undefined, + + initialize: function(options) { + options = options || {}; + + if (options.config) { + this._config = options.config; + } else { + this._config = new OC.Settings.UserSettings(); + } + + this._inputFields = [ + 'displayname', + 'phone', + 'email', + 'website', + 'twitter', + 'address', + 'avatar' + ]; + + var self = this; + _.each(this._inputFields, function(field) { + var scopeOnly = field === 'avatar'; + + // Initialize config model + if (!scopeOnly) { + self._config.set(field, $('#' + field).val()); + } + self._config.set(field + 'Scope', $('#' + field + 'scope').val()); + + // Set inputs whenever model values change + if (!scopeOnly) { + self.listenTo(self._config, 'change:' + field, function() { + self.$('#' + field).val(self._config.get(field)); + }); + } + self.listenTo(self._config, 'change:' + field + 'Scope', function() { + self._setFieldScopeIcon(field, self._config.get(field + 'Scope')); + }); + }); + + this._registerEvents(); + }, + + render: function() { + var self = this; + _.each(this._inputFields, function(field) { + var $heading = self.$('#' + field + 'form h2'); + var $icon = self.$('#' + field + 'form h2 > span'); + var scopeMenu = new OC.Settings.FederationScopeMenu({field: field}); + + self.listenTo(scopeMenu, 'select:scope', function(scope) { + self._onScopeChanged(field, scope); + }); + $heading.append(scopeMenu.$el); + $icon.on('click', _.bind(scopeMenu.show, scopeMenu)); + + // Restore initial state + self._setFieldScopeIcon(field, self._config.get(field + 'Scope')); + }); + }, + + _registerEvents: function() { + var self = this; + _.each(this._inputFields, function(field) { + if (field === 'avatar') { + return; + } + self.$('#' + field).keyUpDelayedOrEnter(_.bind(self._onInputChanged, self)); + }); + }, + + _onInputChanged: function(e) { + var self = this; + + var $dialog = $('.oc-dialog:visible'); + if (OC.PasswordConfirmation.requiresPasswordConfirmation()) { + if($dialog.length === 0) { + OC.PasswordConfirmation.requirePasswordConfirmation(_.bind(this._onInputChanged, this, e)); + } + return; + } + var $target = $(e.target); + var value = $target.val(); + var field = $target.attr('id'); + this._config.set(field, value); + + var savingData = this._config.save({ + error: function(jqXHR) { + OC.msg.finishedSaving('#personal-settings-container .msg', jqXHR); + } + }); + + $.when(savingData).done(function() { + //OC.msg.finishedSaving('#personal-settings-container .msg', result) + self._showInputChangeSuccess(field); + if (field === 'displayname') { + self._updateDisplayName(value); + } + }); + }, + + _updateDisplayName: function(displayName) { + // update displayName on the top right expand button + $('#expandDisplayName').text(displayName); + // update avatar if avatar is available + if (!$('#removeavatar').hasClass('hidden')) { + updateAvatar(); + } + }, + + _onScopeChanged: function(field, scope) { + var $dialog = $('.oc-dialog:visible'); + if (OC.PasswordConfirmation.requiresPasswordConfirmation()) { + if($dialog.length === 0) { + OC.PasswordConfirmation.requirePasswordConfirmation(_.bind(this._onScopeChanged, this, field, scope)); + } + return; + } + + this._config.set(field + 'Scope', scope); + + $('#' + field).parent().find('span > input').val(scope); + + // TODO: user loading/success feedback + this._config.save(); + this._setFieldScopeIcon(field, scope); + }, + + _showInputChangeSuccess: function(field) { + var $icon = this.$('#' + field + 'form > span'); + $icon.fadeIn(200); + setTimeout(function() { + $icon.fadeOut(300); + }, 2000); + }, + + _setFieldScopeIcon: function(field, scope) { + var $icon = this.$('#' + field + 'form > h2 > span'); + $icon.removeClass('icon-password'); + $icon.removeClass('icon-contacts-dark'); + $icon.removeClass('icon-link'); + switch (scope) { + case 'private': + $icon.addClass('icon-password'); + break; + case 'contacts': + $icon.addClass('icon-contacts-dark'); + break; + case 'public': + $icon.addClass('icon-link'); + break; + } + } + }); + + OC.Settings = OC.Settings || {}; + OC.Settings.FederationSettingsView = FederationSettingsView; +})(_, $, OC); diff --git a/settings/js/personal.js b/settings/js/personal.js index c2cb437bd13a8..9045851ba0c12 100644 --- a/settings/js/personal.js +++ b/settings/js/personal.js @@ -1,10 +1,15 @@ +/* global OC */ + /** * Copyright (c) 2011, Robin Appelman * 2013, Morris Jobke + * 2016, Christoph Wurst * This file is licensed under the Affero General Public License version 3 or later. * See the COPYING-README file. */ +OC.Settings = OC.Settings || {}; + /** * The callback will be fired as soon as enter is pressed by the * user or 1 second after the last data entry @@ -21,21 +26,21 @@ jQuery.fn.keyUpDelayedOrEnter = function (callback, allowEmptyValue) { return; } if (allowEmptyValue || that.val() !== '') { - cb(); + cb(event); } }, 1000)); this.keypress(function (event) { if (event.keyCode === 13 && (allowEmptyValue || that.val() !== '')) { event.preventDefault(); - cb(); + cb(event); } }); - this.bind('paste', null, function (e) { - if(!e.keyCode){ + this.bind('paste', null, function (event) { + if(!event.keyCode){ if (allowEmptyValue || that.val() !== '') { - cb(); + cb(event); } } }); @@ -187,7 +192,7 @@ function avatarResponseHandler (data) { if (typeof data === 'string') { data = JSON.parse(data); } - var $warning = $('#avatar .warning'); + var $warning = $('#avatarform .warning'); $warning.hide(); if (data.status === "success") { updateAvatar(); @@ -265,8 +270,10 @@ $(document).ready(function () { } }); - $('#displayName').keyUpDelayedOrEnter(changeDisplayName); - $('#email').keyUpDelayedOrEnter(changeEmailAddress, true); + var federationSettingsView = new OC.Settings.FederationSettingsView({ + el: '#personal-settings' + }); + federationSettingsView.render(); $("#languageinput").change(function () { // Serialize the data @@ -405,7 +412,7 @@ $(document).ready(function () { // Load the big avatar if (oc_config.enable_avatars) { - $('#avatar .avatardiv').avatar(OC.currentUser, 145); + $('#avatarform .avatardiv').avatar(OC.currentUser, 145); } // Show token views @@ -452,3 +459,5 @@ OC.Encryption.msg = { } } }; + +OC.Settings.updateAvatar = updateAvatar; diff --git a/settings/js/usersettings.js b/settings/js/usersettings.js new file mode 100644 index 0000000000000..fcfe556b1d9a6 --- /dev/null +++ b/settings/js/usersettings.js @@ -0,0 +1,50 @@ +/* global OC */ + +/** + * Copyright (c) 2016, Christoph Wurst + * + * This file is licensed under the Affero General Public License version 3 or later. + * See the COPYING-README file. + */ + +(function() { + 'use strict'; + + /** + * Model for storing and saving user settings + * + * @class UserSettings + */ + var UserSettings = OC.Backbone.Model.extend({ + url: OC.generateUrl('/settings/users/{id}/settings', {id: OC.currentUser}), + isNew: function() { + return false; // Force PUT on .save() + }, + parse: function(data) { + if (_.isUndefined(data)) { + return null; + } + if (_.isUndefined(data.data)) { + return null; + } + data = data.data; + + var ignored = [ + 'userId', + 'message' + ]; + + _.each(ignored, function(ign) { + if (!_.isUndefined(data[ign])) { + delete data[ign]; + } + }); + + return data; + } + }); + + OC.Settings = OC.Settings || {}; + + OC.Settings.UserSettings = UserSettings; +})(); \ No newline at end of file diff --git a/settings/personal.php b/settings/personal.php index 01c358de3aefa..24955c94071d8 100644 --- a/settings/personal.php +++ b/settings/personal.php @@ -40,6 +40,7 @@ $defaults = \OC::$server->getThemingDefaults(); $certificateManager = \OC::$server->getCertificateManager(); +$accountManager = new \OC\Accounts\AccountManager(\OC::$server->getDatabaseConnection(), \OC::$server->getEventDispatcher()); $config = \OC::$server->getConfig(); $urlGenerator = \OC::$server->getURLGenerator(); @@ -47,7 +48,10 @@ OC_Util::addScript('settings', 'authtoken'); OC_Util::addScript('settings', 'authtoken_collection'); OC_Util::addScript('settings', 'authtoken_view'); -OC_Util::addScript( 'settings', 'personal' ); +OC_Util::addScript('settings', 'usersettings'); +OC_Util::addScript('settings', 'federationsettingsview'); +OC_Util::addScript('settings', 'federationscopemenu'); +OC_Util::addScript('settings', 'personal'); OC_Util::addScript('settings', 'certificates'); OC_Util::addStyle( 'settings', 'settings' ); \OC_Util::addVendorScript('strengthify/jquery.strengthify'); @@ -66,7 +70,6 @@ $storageInfo=OC_Helper::getStorageInfo('/'); $user = OC::$server->getUserManager()->get(OC_User::getUser()); -$email = $user->getEMailAddress(); $userLang=$config->getUserValue( OC_User::getUser(), 'core', 'lang', \OC::$server->getL10NFactory()->findLanguage() ); $languageCodes = \OC::$server->getL10NFactory()->findAvailableLanguages(); @@ -152,16 +155,34 @@ } else { $totalSpace = OC_Helper::humanFileSize($storageInfo['total']); } + +$uid = $user->getUID(); +$userData = $accountManager->getUser($user); + $tmpl->assign('total_space', $totalSpace); $tmpl->assign('usage_relative', $storageInfo['relative']); $tmpl->assign('clients', $clients); -$tmpl->assign('email', $email); +$tmpl->assign('email', $userData[\OC\Accounts\AccountManager::PROPERTY_EMAIL]['value']); $tmpl->assign('languages', $languages); $tmpl->assign('commonlanguages', $commonLanguages); $tmpl->assign('activelanguage', $userLang); $tmpl->assign('passwordChangeSupported', OC_User::canUserChangePassword(OC_User::getUser())); $tmpl->assign('displayNameChangeSupported', OC_User::canUserChangeDisplayName(OC_User::getUser())); -$tmpl->assign('displayName', OC_User::getDisplayName()); +$tmpl->assign('displayName', $userData[\OC\Accounts\AccountManager::PROPERTY_DISPLAYNAME]['value']); + +$tmpl->assign('phone', $userData[\OC\Accounts\AccountManager::PROPERTY_PHONE]['value']); +$tmpl->assign('website', $userData[\OC\Accounts\AccountManager::PROPERTY_WEBSITE]['value']); +$tmpl->assign('twitter', $userData[\OC\Accounts\AccountManager::PROPERTY_TWITTER]['value']); +$tmpl->assign('address', $userData[\OC\Accounts\AccountManager::PROPERTY_ADDRESS]['value']); + +$tmpl->assign('avatarScope', $userData[\OC\Accounts\AccountManager::PROPERTY_AVATAR]['scope']); +$tmpl->assign('displayNameScope', $userData[\OC\Accounts\AccountManager::PROPERTY_DISPLAYNAME]['scope']); +$tmpl->assign('phoneScope', $userData[\OC\Accounts\AccountManager::PROPERTY_PHONE]['scope']); +$tmpl->assign('emailScope', $userData[\OC\Accounts\AccountManager::PROPERTY_EMAIL]['scope']); +$tmpl->assign('websiteScope', $userData[\OC\Accounts\AccountManager::PROPERTY_WEBSITE]['scope']); +$tmpl->assign('twitterScope', $userData[\OC\Accounts\AccountManager::PROPERTY_TWITTER]['scope']); +$tmpl->assign('addressScope', $userData[\OC\Accounts\AccountManager::PROPERTY_ADDRESS]['scope']); + $tmpl->assign('enableAvatars', $config->getSystemValue('enable_avatars', true) === true); $tmpl->assign('avatarChangeSupported', OC_User::canUserChangeAvatar(OC_User::getUser())); $tmpl->assign('certs', $certificateManager->listCertificates()); diff --git a/settings/routes.php b/settings/routes.php index 58a576063128d..62cfc398fdc99 100644 --- a/settings/routes.php +++ b/settings/routes.php @@ -51,7 +51,7 @@ ['name' => 'AppSettings#listApps', 'url' => '/settings/apps/list', 'verb' => 'GET'], ['name' => 'SecuritySettings#trustedDomains', 'url' => '/settings/admin/security/trustedDomains', 'verb' => 'POST'], ['name' => 'Users#setDisplayName', 'url' => '/settings/users/{username}/displayName', 'verb' => 'POST'], - ['name' => 'Users#setMailAddress', 'url' => '/settings/users/{id}/mailAddress', 'verb' => 'PUT'], + ['name' => 'Users#setUserSettings', 'url' => '/settings/users/{username}/settings', 'verb' => 'PUT'], ['name' => 'Users#stats', 'url' => '/settings/users/stats', 'verb' => 'GET'], ['name' => 'LogSettings#setLogLevel', 'url' => '/settings/admin/log/level', 'verb' => 'POST'], ['name' => 'LogSettings#getEntries', 'url' => '/settings/admin/log/entries', 'verb' => 'GET'], diff --git a/settings/templates/personal.php b/settings/templates/personal.php index 8f34d7b87b861..b81b19d906075 100644 --- a/settings/templates/personal.php +++ b/settings/templates/personal.php @@ -32,45 +32,128 @@ +
-
-

t('Profile picture')); ?>

-
-
- - - -
- - -

t('png or jpg, max. 20 MB')); ?>

- - t('Picture provided by original account')); ?> - -
+
+ +

+ + +

+
+
+ + + +
+ + +

t('png or jpg, max. 20 MB')); ?>

+ + t('Picture provided by original account')); ?> + +
- -
-

- -

- - - -
+
+
+
+

+ + +

+ +
+
+
+

+ + +

+ +
+ t('For password recovery and notifications')); ?> +
+
+
+

+ + +

+ +
+
+
+

+ + +

+ +
+
+
+

+ + +

+ +
+
+
+

+ + +

+ +
+ + +
@@ -102,10 +185,37 @@ class="password-confirm-required"

t('Email'); ?>

t('No email address set')); }?> +
+
+

t('Full name'));?>

+ t('No display name set')); } ?> +
+
+

t('Email')); ?>

+ t('No email address set')); }?> +
+
+

t('Phone')); ?>

+ t('No phone number set')); }?> +
+
+

t('Address')); ?>

+ t('No address set')); }?> +
+
+

t('Website')); ?>

+ t('No website set')); }?> +
+
+

t('Twitter')); ?>

+ t('No twitter handle set')); }?> +
+
+

t('Groups')); ?>

@@ -123,17 +233,17 @@ class="password-confirm-required"

t('Password'));?>


- + - + - +
userSession = $this->createMock(IUserSession::class); $this->userManager = $this->createMock(IUserManager::class); $this->throttler = $this->createMock(Throttler::class); + $this->keyManager = $this->createMock(Manager::class); $this->controller = new OCSController( 'core', @@ -66,7 +65,8 @@ public function setUp() { $this->capabilitiesManager, $this->userSession, $this->userManager, - $this->throttler + $this->throttler, + $this->keyManager ); } @@ -206,4 +206,39 @@ public function testPersonNoLogin() { $this->assertEquals($expected, $this->controller->personCheck('', '')); } + + public function testGetIdentityProofWithNotExistingUser() { + $this->userManager + ->expects($this->once()) + ->method('get') + ->with('NotExistingUser') + ->willReturn(null); + + $expected = new DataResponse('User not found', 404); + $this->assertEquals($expected, $this->controller->getIdentityProof('NotExistingUser')); + } + + public function testGetIdentityProof() { + $user = $this->createMock(IUser::class); + $key = $this->createMock(Key::class); + $this->userManager + ->expects($this->once()) + ->method('get') + ->with('ExistingUser') + ->willReturn($user); + $this->keyManager + ->expects($this->once()) + ->method('getKey') + ->with($user) + ->willReturn($key); + $key + ->expects($this->once()) + ->method('getPublic') + ->willReturn('Existing Users public key'); + + $expected = new DataResponse([ + 'public' => 'Existing Users public key', + ]); + $this->assertEquals($expected, $this->controller->getIdentityProof('ExistingUser')); + } } diff --git a/tests/Settings/Controller/UsersControllerTest.php b/tests/Settings/Controller/UsersControllerTest.php index 03c3a2e2ab464..ec92479b40e38 100644 --- a/tests/Settings/Controller/UsersControllerTest.php +++ b/tests/Settings/Controller/UsersControllerTest.php @@ -10,6 +10,7 @@ namespace Tests\Settings\Controller; +use OC\Accounts\AccountManager; use OC\Group\Manager; use OC\Settings\Controller\UsersController; use OCP\App\IAppManager; @@ -57,6 +58,8 @@ class UsersControllerTest extends \Test\TestCase { private $avatarManager; /** @var IL10N|\PHPUnit_Framework_MockObject_MockObject */ private $l; + /** @var AccountManager | \PHPUnit_Framework_MockObject_MockObject */ + private $accountManager; protected function setUp() { parent::setUp(); @@ -71,6 +74,7 @@ protected function setUp() { $this->urlGenerator = $this->createMock(IURLGenerator::class); $this->appManager = $this->createMock(IAppManager::class); $this->avatarManager = $this->createMock(IAvatarManager::class); + $this->accountManager = $this->createMock(AccountManager::class); $this->l = $this->createMock(IL10N::class); $this->l->method('t') ->will($this->returnCallback(function ($text, $parameters = []) { @@ -117,7 +121,8 @@ protected function getController($isAdmin) { 'no-reply@owncloud.com', $this->urlGenerator, $this->appManager, - $this->avatarManager + $this->avatarManager, + $this->accountManager ); } @@ -1760,74 +1765,6 @@ public function testNoAvatar() { $this->assertEquals($expectedResult, $result); } - /** - * @return array - */ - public function setEmailAddressData() { - return [ - /* mailAddress, isValid, expectsUpdate, canChangeDisplayName, responseCode */ - [ '', true, true, true, Http::STATUS_OK ], - [ 'foo@local', true, true, true, Http::STATUS_OK], - [ 'foo@bar@local', false, false, true, Http::STATUS_UNPROCESSABLE_ENTITY], - [ 'foo@local', true, false, false, Http::STATUS_FORBIDDEN], - ]; - } - - /** - * @dataProvider setEmailAddressData - * - * @param string $mailAddress - * @param bool $isValid - * @param bool $expectsUpdate - * @param bool $expectsDelete - */ - public function testSetEmailAddress($mailAddress, $isValid, $expectsUpdate, $canChangeDisplayName, $responseCode) { - $controller = $this->getController(true); - - $user = $this->getMockBuilder('\OC\User\User') - ->disableOriginalConstructor()->getMock(); - $user - ->expects($this->any()) - ->method('getUID') - ->will($this->returnValue('foo')); - $user - ->expects($this->any()) - ->method('canChangeDisplayName') - ->will($this->returnValue($canChangeDisplayName)); - $user - ->expects($expectsUpdate ? $this->once() : $this->never()) - ->method('setEMailAddress') - ->with( - $this->equalTo($mailAddress) - ); - - $this->userSession - ->expects($this->atLeastOnce()) - ->method('getUser') - ->will($this->returnValue($user)); - $this->mailer - ->expects($this->any()) - ->method('validateMailAddress') - ->with($mailAddress) - ->willReturn($isValid); - - if ($isValid) { - $user->expects($this->atLeastOnce()) - ->method('canChangeDisplayName') - ->willReturn(true); - - $this->userManager - ->expects($this->atLeastOnce()) - ->method('get') - ->with('foo') - ->will($this->returnValue($user)); - } - - $response = $controller->setMailAddress($user->getUID(), $mailAddress); - - $this->assertSame($responseCode, $response->getStatus()); - } - public function testStatsAdmin() { $controller = $this->getController(true); @@ -1976,6 +1913,7 @@ public function testSetDisplayName($currentUser, $editUser, $isAdmin, $isSubAdmi ->method('get') ->with($editUser->getUID()) ->willReturn($editUser); + $this->accountManager->expects($this->any())->method('getUser')->willReturn([]); $subadmin = $this->getMockBuilder('\OC\SubAdmin') ->disableOriginalConstructor() @@ -1994,10 +1932,6 @@ public function testSetDisplayName($currentUser, $editUser, $isAdmin, $isSubAdmi ->willReturn($isAdmin); if ($valid === true) { - $editUser->expects($this->once()) - ->method('setDisplayName') - ->with('newDisplayName') - ->willReturn(true); $expectedResponse = new DataResponse( [ 'status' => 'success', @@ -2009,7 +1943,6 @@ public function testSetDisplayName($currentUser, $editUser, $isAdmin, $isSubAdmi ] ); } else { - $editUser->expects($this->never())->method('setDisplayName'); $expectedResponse = new DataResponse( [ 'status' => 'error', @@ -2040,6 +1973,7 @@ public function testSetDisplayNameFails() { ->expects($this->once()) ->method('getUser') ->willReturn($user); + $this->userManager ->expects($this->once()) ->method('get') diff --git a/tests/lib/Accounts/AccountsManagerTest.php b/tests/lib/Accounts/AccountsManagerTest.php new file mode 100644 index 0000000000000..de88fbcab97fb --- /dev/null +++ b/tests/lib/Accounts/AccountsManagerTest.php @@ -0,0 +1,202 @@ + + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + + +namespace Test\Accounts; + + +use OC\Accounts\AccountManager; +use OC\Mail\Mailer; +use OCP\IUser; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Test\TestCase; + +/** + * Class AccountsManagerTest + * + * @group DB + * @package Test\Accounts + */ +class AccountsManagerTest extends TestCase { + + /** @var \OCP\IDBConnection */ + private $connection; + + /** @var EventDispatcherInterface | \PHPUnit_Framework_MockObject_MockObject */ + private $eventDispatcher; + + /** @var string accounts table name */ + private $table = 'accounts'; + + public function setUp() { + parent::setUp(); + $this->eventDispatcher = $this->getMockBuilder('Symfony\Component\EventDispatcher\EventDispatcherInterface') + ->disableOriginalConstructor()->getMock(); + $this->connection = \OC::$server->getDatabaseConnection(); + } + + public function tearDown() { + parent::tearDown(); + $query = $this->connection->getQueryBuilder(); + $query->delete($this->table)->execute(); + } + + /** + * get a instance of the accountManager + * + * @param array $mockedMethods list of methods which should be mocked + * @return \PHPUnit_Framework_MockObject_MockObject | AccountManager + */ + public function getInstance($mockedMethods = null) { + return $this->getMockBuilder('OC\Accounts\AccountManager') + ->setConstructorArgs([$this->connection, $this->eventDispatcher]) + ->setMethods($mockedMethods) + ->getMock(); + + } + + /** + * @dataProvider dataTrueFalse + * + * @param bool $userAlreadyExists + */ + public function testUpdateUser($userAlreadyExists) { + $accountManager = $this->getInstance(['getUser', 'insertNewUser', 'updateExistingUser']); + $user = $this->getMockBuilder('OCP\IUser')->getMock(); + + $accountManager->expects($this->once())->method('getUser')->with($user)->willReturn($userAlreadyExists); + + if ($userAlreadyExists) { + $accountManager->expects($this->once())->method('updateExistingUser') + ->with($user, 'data'); + $accountManager->expects($this->never())->method('insertNewUser'); + } else { + $accountManager->expects($this->once())->method('insertNewUser') + ->with($user, 'data'); + $accountManager->expects($this->never())->method('updateExistingUser'); + } + + $this->eventDispatcher->expects($this->once())->method('dispatch') + ->willReturnCallback(function($eventName, $event) use ($user) { + $this->assertSame('OC\AccountManager::userUpdated', $eventName); + $this->assertInstanceOf('Symfony\Component\EventDispatcher\GenericEvent', $event); + } + ); + + $accountManager->updateUser($user, 'data'); + } + + public function dataTrueFalse() { + return [ + [true], + [false] + ]; + } + + + /** + * @dataProvider dataTestGetUser + * + * @param string $setUser + * @param array $setData + * @param IUser $askUser + * @param array $expectedData + * @param book $userAlreadyExists + */ + public function testGetUser($setUser, $setData, $askUser, $expectedData, $userAlreadyExists) { + $accountManager = $this->getInstance(['buildDefaultUserRecord', 'insertNewUser']); + if (!$userAlreadyExists) { + $accountManager->expects($this->once())->method('buildDefaultUserRecord') + ->with($askUser)->willReturn($expectedData); + $accountManager->expects($this->once())->method('insertNewUser') + ->with($askUser, $expectedData); + } + $this->addDummyValuesToTable($setUser, $setData); + $this->assertEquals($expectedData, + $accountManager->getUser($askUser) + ); + } + + public function dataTestGetUser() { + $user1 = $this->getMockBuilder('OCP\IUser')->getMock(); + $user1->expects($this->any())->method('getUID')->willReturn('user1'); + $user2 = $this->getMockBuilder('OCP\IUser')->getMock(); + $user2->expects($this->any())->method('getUID')->willReturn('user2'); + return [ + ['user1', ['key' => 'value'], $user1, ['key' => 'value'], true], + ['user1', ['key' => 'value'], $user2, [], false], + ]; + } + + public function testUpdateExistingUser() { + $user = $this->getMockBuilder('OCP\IUser')->getMock(); + $user->expects($this->once())->method('getUID')->willReturn('uid'); + $oldData = ['key' => 'value']; + $newData = ['newKey' => 'newValue']; + + $accountManager = $this->getInstance(); + $this->addDummyValuesToTable('uid', $oldData); + $this->invokePrivate($accountManager, 'updateExistingUser', [$user, $newData]); + $newDataFromTable = $this->getDataFromTable('uid'); + $this->assertEquals($newData, $newDataFromTable); + } + + public function testInsertNewUser() { + $user = $this->getMockBuilder('OCP\IUser')->getMock(); + $uid = 'uid'; + $data = ['key' => 'value']; + + $accountManager = $this->getInstance(); + $user->expects($this->once())->method('getUID')->willReturn($uid); + $this->assertNull($this->getDataFromTable($uid)); + $this->invokePrivate($accountManager, 'insertNewUser', [$user, $data]); + + $dataFromDb = $this->getDataFromTable($uid); + $this->assertEquals($data, $dataFromDb); + } + + private function addDummyValuesToTable($uid, $data) { + + $query = $this->connection->getQueryBuilder(); + $query->insert($this->table) + ->values( + [ + 'uid' => $query->createNamedParameter($uid), + 'data' => $query->createNamedParameter(json_encode($data)), + ] + ) + ->execute(); + } + + private function getDataFromTable($uid) { + $query = $this->connection->getQueryBuilder(); + $query->select('data')->from($this->table) + ->where($query->expr()->eq('uid', $query->createParameter('uid'))) + ->setParameter('uid', $uid); + $query->execute(); + $result = $query->execute()->fetchAll(); + + if (!empty($result)) { + return json_decode($result[0]['data'], true); + } + } + +} diff --git a/tests/lib/App/ManagerTest.php b/tests/lib/App/ManagerTest.php index 3dbcb8a560988..e38f72b3d92fe 100644 --- a/tests/lib/App/ManagerTest.php +++ b/tests/lib/App/ManagerTest.php @@ -320,6 +320,7 @@ public function testGetInstalledApps() { 'dav', 'federatedfilesharing', 'files', + 'lookup_server_connector', 'provisioning_api', 'test1', 'test3', @@ -344,6 +345,7 @@ public function testGetAppsForUser() { 'dav', 'federatedfilesharing', 'files', + 'lookup_server_connector', 'provisioning_api', 'test1', 'test3', @@ -364,6 +366,7 @@ public function testGetAppsNeedingUpgrade() { 'files' => ['id' => 'files'], 'federatedfilesharing' => ['id' => 'federatedfilesharing'], 'provisioning_api' => ['id' => 'provisioning_api'], + 'lookup_server_connector' => ['id' => 'lookup_server_connector'], 'test1' => ['id' => 'test1', 'version' => '1.0.1', 'requiremax' => '9.0.0'], 'test2' => ['id' => 'test2', 'version' => '1.0.0', 'requiremin' => '8.2.0'], 'test3' => ['id' => 'test3', 'version' => '1.2.4', 'requiremin' => '9.0.0'], @@ -408,6 +411,7 @@ public function testGetIncompatibleApps() { 'files' => ['id' => 'files'], 'federatedfilesharing' => ['id' => 'federatedfilesharing'], 'provisioning_api' => ['id' => 'provisioning_api'], + 'lookup_server_connector' => ['id' => 'lookup_server_connector'], 'test1' => ['id' => 'test1', 'version' => '1.0.1', 'requiremax' => '8.0.0'], 'test2' => ['id' => 'test2', 'version' => '1.0.0', 'requiremin' => '8.2.0'], 'test3' => ['id' => 'test3', 'version' => '1.2.4', 'requiremin' => '9.0.0'], diff --git a/tests/lib/AppTest.php b/tests/lib/AppTest.php index 971d86cf6a480..575e32dd60c2c 100644 --- a/tests/lib/AppTest.php +++ b/tests/lib/AppTest.php @@ -351,6 +351,7 @@ function appConfigValuesProvider() { 'appforgroup12', 'dav', 'federatedfilesharing', + 'lookup_server_connector', 'provisioning_api', 'twofactor_backupcodes', 'workflowengine', @@ -368,6 +369,7 @@ function appConfigValuesProvider() { 'appforgroup2', 'dav', 'federatedfilesharing', + 'lookup_server_connector', 'provisioning_api', 'twofactor_backupcodes', 'workflowengine', @@ -386,6 +388,7 @@ function appConfigValuesProvider() { 'appforgroup2', 'dav', 'federatedfilesharing', + 'lookup_server_connector', 'provisioning_api', 'twofactor_backupcodes', 'workflowengine', @@ -404,6 +407,7 @@ function appConfigValuesProvider() { 'appforgroup2', 'dav', 'federatedfilesharing', + 'lookup_server_connector', 'provisioning_api', 'twofactor_backupcodes', 'workflowengine', @@ -422,6 +426,7 @@ function appConfigValuesProvider() { 'appforgroup2', 'dav', 'federatedfilesharing', + 'lookup_server_connector', 'provisioning_api', 'twofactor_backupcodes', 'workflowengine', @@ -502,11 +507,11 @@ public function testEnabledAppsCache() { ); $apps = \OC_App::getEnabledApps(); - $this->assertEquals(array('files', 'app3', 'dav', 'federatedfilesharing', 'provisioning_api', 'twofactor_backupcodes', 'workflowengine'), $apps); + $this->assertEquals(array('files', 'app3', 'dav', 'federatedfilesharing', 'lookup_server_connector', 'provisioning_api', 'twofactor_backupcodes', 'workflowengine'), $apps); // mock should not be called again here $apps = \OC_App::getEnabledApps(); - $this->assertEquals(array('files', 'app3', 'dav', 'federatedfilesharing', 'provisioning_api', 'twofactor_backupcodes', 'workflowengine'), $apps); + $this->assertEquals(array('files', 'app3', 'dav', 'federatedfilesharing', 'lookup_server_connector', 'provisioning_api', 'twofactor_backupcodes', 'workflowengine'), $apps); $this->restoreAppConfig(); \OC_User::setUserId(null); diff --git a/tests/lib/Security/IdentityProof/ManagerTest.php b/tests/lib/Security/IdentityProof/ManagerTest.php new file mode 100644 index 0000000000000..d93f675825b8d --- /dev/null +++ b/tests/lib/Security/IdentityProof/ManagerTest.php @@ -0,0 +1,166 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see . + * + */ + +namespace Test\Security; + +use OC\Security\IdentityProof\Key; +use OC\Security\IdentityProof\Manager; +use OCP\Files\IAppData; +use OCP\Files\SimpleFS\ISimpleFile; +use OCP\Files\SimpleFS\ISimpleFolder; +use OCP\IUser; +use OCP\Security\ICrypto; +use Test\TestCase; + +class ManagerTest extends TestCase { + /** @var IAppData|\PHPUnit_Framework_MockObject_MockObject */ + private $appData; + /** @var ICrypto|\PHPUnit_Framework_MockObject_MockObject */ + private $crypto; + /** @var Manager|\PHPUnit_Framework_MockObject_MockObject */ + private $manager; + + public function setUp() { + parent::setUp(); + $this->appData = $this->createMock(IAppData::class); + $this->crypto = $this->createMock(ICrypto::class); + $this->manager = $this->getMockBuilder(Manager::class) + ->setConstructorArgs([ + $this->appData, + $this->crypto + ]) + ->setMethods(['generateKeyPair']) + ->getMock(); + } + + public function testGetKeyWithExistingKey() { + $user = $this->createMock(IUser::class); + $user + ->expects($this->once()) + ->method('getUID') + ->willReturn('MyUid'); + $folder = $this->createMock(ISimpleFolder::class); + $privateFile = $this->createMock(ISimpleFile::class); + $privateFile + ->expects($this->once()) + ->method('getContent') + ->willReturn('EncryptedPrivateKey'); + $publicFile = $this->createMock(ISimpleFile::class); + $publicFile + ->expects($this->once()) + ->method('getContent') + ->willReturn('MyPublicKey'); + $this->crypto + ->expects($this->once()) + ->method('decrypt') + ->with('EncryptedPrivateKey') + ->willReturn('MyPrivateKey'); + $folder + ->expects($this->at(0)) + ->method('getFile') + ->with('private') + ->willReturn($privateFile); + $folder + ->expects($this->at(1)) + ->method('getFile') + ->with('public') + ->willReturn($publicFile); + $this->appData + ->expects($this->once()) + ->method('getFolder') + ->with('MyUid') + ->willReturn($folder); + + $expected = new Key('MyPublicKey', 'MyPrivateKey'); + $this->assertEquals($expected, $this->manager->getKey($user)); + } + + public function testGetKeyWithNotExistingKey() { + $user = $this->createMock(IUser::class); + $user + ->expects($this->exactly(3)) + ->method('getUID') + ->willReturn('MyUid'); + $this->appData + ->expects($this->at(0)) + ->method('getFolder') + ->with('MyUid') + ->willThrowException(new \Exception()); + $this->manager + ->expects($this->once()) + ->method('generateKeyPair') + ->willReturn(['MyNewPublicKey', 'MyNewPrivateKey']); + $this->appData + ->expects($this->at(1)) + ->method('newFolder') + ->with('MyUid'); + $folder = $this->createMock(ISimpleFolder::class); + $this->crypto + ->expects($this->once()) + ->method('encrypt') + ->with('MyNewPrivateKey') + ->willReturn('MyNewEncryptedPrivateKey'); + $privateFile = $this->createMock(ISimpleFile::class); + $privateFile + ->expects($this->once()) + ->method('putContent') + ->with('MyNewEncryptedPrivateKey'); + $publicFile = $this->createMock(ISimpleFile::class); + $publicFile + ->expects($this->once()) + ->method('putContent') + ->with('MyNewPublicKey'); + $folder + ->expects($this->at(0)) + ->method('newFile') + ->with('private') + ->willReturn($privateFile); + $folder + ->expects($this->at(1)) + ->method('newFile') + ->with('public') + ->willReturn($publicFile); + $this->appData + ->expects($this->at(2)) + ->method('getFolder') + ->with('MyUid') + ->willReturn($folder); + + + $expected = new Key('MyNewPublicKey', 'MyNewPrivateKey'); + $this->assertEquals($expected, $this->manager->getKey($user)); + } + + public function testGenerateKeyPair() { + $manager = new Manager( + $this->appData, + $this->crypto + ); + $data = 'MyTestData'; + + list($resultPublicKey, $resultPrivateKey) = $this->invokePrivate($manager, 'generateKeyPair'); + openssl_sign($data, $signature, $resultPrivateKey); + $details = openssl_pkey_get_details(openssl_pkey_get_public($resultPublicKey)); + + $this->assertSame(1, openssl_verify($data, $signature, $resultPublicKey)); + $this->assertSame(2048, $details['bits']); + } +} diff --git a/version.php b/version.php index d556386a848a0..0eb9f3de72a04 100644 --- a/version.php +++ b/version.php @@ -25,7 +25,8 @@ // We only can count up. The 4. digit is only for the internal patchlevel to trigger DB upgrades // between betas, final and RCs. This is _not_ the public version number. Reset minor/patchlevel // when updating major/minor version number. -$OC_Version = array(11, 0, 0, 1); + +$OC_Version = array(11, 0, 0, 2); // The human readable string $OC_VersionString = '11.0 alpha';