diff options
author | Morris Jobke <hey@morrisjobke.de> | 2017-04-29 00:38:02 -0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-04-29 00:38:02 -0300 |
commit | 130780056109d8b65e7b9abe40c89e26a75c5e35 (patch) | |
tree | 488ad0c7d7c9bf6339100c1628b3b12433d8e0f3 | |
parent | 2a773310dc58adcd299c1f7ae37e834cbae3b027 (diff) | |
parent | a0bf706983007a69aa2adb0a0af35e265ff1b0d8 (diff) | |
download | nextcloud-server-130780056109d8b65e7b9abe40c89e26a75c5e35.tar.gz nextcloud-server-130780056109d8b65e7b9abe40c89e26a75c5e35.zip |
Merge pull request #3869 from nextcloud/verify-personal-data
Verify personal data
23 files changed, 957 insertions, 56 deletions
diff --git a/apps/federatedfilesharing/js/settings-personal.js b/apps/federatedfilesharing/js/settings-personal.js index 04096cb0416..c954f74f323 100644 --- a/apps/federatedfilesharing/js/settings-personal.js +++ b/apps/federatedfilesharing/js/settings-personal.js @@ -20,6 +20,9 @@ $(document).ready(function() { } }); + /* Verification icon tooltip */ + $('#personal-settings-container .verify img').tooltip({placement: 'bottom', trigger: 'hover'}); + $('#fileSharingSettings .clipboardButton').tooltip({placement: 'bottom', title: t('core', 'Copy'), trigger: 'hover'}); // Clipboard! diff --git a/apps/files_sharing/lib/Controller/ShareesAPIController.php b/apps/files_sharing/lib/Controller/ShareesAPIController.php index eb65727c770..7d345efb3eb 100644 --- a/apps/files_sharing/lib/Controller/ShareesAPIController.php +++ b/apps/files_sharing/lib/Controller/ShareesAPIController.php @@ -654,13 +654,15 @@ class ShareesAPIController extends OCSController { protected function getLookup($search) { $isEnabled = $this->config->getAppValue('files_sharing', 'lookupServerEnabled', 'no'); + $lookupServerUrl = $this->config->getSystemValue('lookup_server', 'https://lookup.nextcloud.com'); + $lookupServerUrl = rtrim($lookupServerUrl, '/'); $result = []; if($isEnabled === 'yes') { try { $client = $this->clientService->newClient(); $response = $client->get( - 'https://lookup.nextcloud.com/users?search=' . urlencode($search), + $lookupServerUrl . '/users?search=' . urlencode($search), [ 'timeout' => 10, 'connect_timeout' => 3, diff --git a/apps/lookup_server_connector/appinfo/app.php b/apps/lookup_server_connector/appinfo/app.php index 639eeafcf3f..f0d624d5f3a 100644 --- a/apps/lookup_server_connector/appinfo/app.php +++ b/apps/lookup_server_connector/appinfo/app.php @@ -28,18 +28,24 @@ $dispatcher->addListener('OC\AccountManager::userUpdated', function(\Symfony\Com \OC::$server->getAppDataDir('identityproof'), \OC::$server->getCrypto() ); + + $config = \OC::$server->getConfig(); + $lookupServer = $config->getSystemValue('lookup_server', ''); + $updateLookupServer = new \OCA\LookupServerConnector\UpdateLookupServer( - new \OC\Accounts\AccountManager(\OC::$server->getDatabaseConnection(), \OC::$server->getEventDispatcher()), - \OC::$server->getConfig(), - \OC::$server->getSecureRandom(), + new \OC\Accounts\AccountManager( + \OC::$server->getDatabaseConnection(), + \OC::$server->getEventDispatcher(), + \OC::$server->getJobList() + ), \OC::$server->getHTTPClientService(), - $keyManager, new \OC\Security\IdentityProof\Signer( $keyManager, new \OC\AppFramework\Utility\TimeFactory(), \OC::$server->getUserManager() ), - \OC::$server->getJobList() + \OC::$server->getJobList(), + $lookupServer ); $updateLookupServer->userUpdated($user); }); diff --git a/apps/lookup_server_connector/lib/BackgroundJobs/RetryJob.php b/apps/lookup_server_connector/lib/BackgroundJobs/RetryJob.php index f33323b2d4f..faeef05da17 100644 --- a/apps/lookup_server_connector/lib/BackgroundJobs/RetryJob.php +++ b/apps/lookup_server_connector/lib/BackgroundJobs/RetryJob.php @@ -26,6 +26,7 @@ namespace OCA\LookupServerConnector\BackgroundJobs; use OC\BackgroundJob\Job; use OCP\BackgroundJob\IJobList; use OCP\Http\Client\IClientService; +use OCP\ILogger; class RetryJob extends Job { /** @var IClientService */ @@ -36,21 +37,28 @@ class RetryJob extends Job { private $lookupServer = 'https://lookup.nextcloud.com/users'; /** - * @param IClientService|null $clientService - * @param IJobList|null $jobList + * @param IClientService $clientService + * @param IJobList $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(); + public function __construct(IClientService $clientService, + IJobList $jobList) { + $this->clientService = $clientService; + $this->jobList = $jobList; + } + + /** + * run the job, then remove it from the jobList + * + * @param JobList $jobList + * @param ILogger $logger + */ + public function execute($jobList, ILogger $logger = null) { + + if ($this->shouldRun($this->argument)) { + parent::execute($jobList, $logger); + $jobList->remove($this, $this->argument); } + } protected function run($argument) { diff --git a/apps/lookup_server_connector/lib/UpdateLookupServer.php b/apps/lookup_server_connector/lib/UpdateLookupServer.php index 86865311725..3a7c2fa7236 100644 --- a/apps/lookup_server_connector/lib/UpdateLookupServer.php +++ b/apps/lookup_server_connector/lib/UpdateLookupServer.php @@ -23,14 +23,11 @@ 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 @@ -40,44 +37,36 @@ use OCP\Security\ISecureRandom; 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'; + private $lookupServer = 'https://lookup.nextcloud.com'; /** * @param AccountManager $accountManager - * @param IConfig $config - * @param ISecureRandom $secureRandom * @param IClientService $clientService - * @param Manager $manager * @param Signer $signer * @param IJobList $jobList + * @param string $lookupServer if nothing is given we use the default lookup server */ public function __construct(AccountManager $accountManager, - IConfig $config, - ISecureRandom $secureRandom, IClientService $clientService, - Manager $manager, Signer $signer, - IJobList $jobList) { + IJobList $jobList, + $lookupServer = '') { $this->accountManager = $accountManager; - $this->config = $config; - $this->secureRandom = $secureRandom; $this->clientService = $clientService; - $this->keyManager = $manager; $this->signer = $signer; $this->jobList = $jobList; + if ($lookupServer !== '') { + $this->lookupServer = $lookupServer; + } + $this->lookupServer = rtrim($this->lookupServer, '/'); + $this->lookupServer .= '/users'; } /** @@ -113,6 +102,13 @@ class UpdateLookupServer { $dataArray['website'] = isset($publicData[AccountManager::PROPERTY_WEBSITE]) ? $publicData[AccountManager::PROPERTY_WEBSITE]['value'] : ''; $dataArray['twitter'] = isset($publicData[AccountManager::PROPERTY_TWITTER]) ? $publicData[AccountManager::PROPERTY_TWITTER]['value'] : ''; $dataArray['phone'] = isset($publicData[AccountManager::PROPERTY_PHONE]) ? $publicData[AccountManager::PROPERTY_PHONE]['value'] : ''; + $dataArray['twitter_signature'] = isset($publicData[AccountManager::PROPERTY_TWITTER]['signature']) ? $publicData[AccountManager::PROPERTY_TWITTER]['signature'] : ''; + $dataArray['website_signature'] = isset($publicData[AccountManager::PROPERTY_WEBSITE]['signature']) ? $publicData[AccountManager::PROPERTY_WEBSITE]['signature'] : ''; + $dataArray['verificationStatus'] = + [ + AccountManager::PROPERTY_WEBSITE => isset($publicData[AccountManager::PROPERTY_WEBSITE]) ? $publicData[AccountManager::PROPERTY_WEBSITE]['verified'] : '', + AccountManager::PROPERTY_TWITTER => isset($publicData[AccountManager::PROPERTY_TWITTER]) ? $publicData[AccountManager::PROPERTY_TWITTER]['verified'] : '', + ]; } $dataArray = $this->signer->sign('lookupserver', $dataArray, $user); diff --git a/config/config.sample.php b/config/config.sample.php index 84b98550fb0..4646de33082 100644 --- a/config/config.sample.php +++ b/config/config.sample.php @@ -1515,4 +1515,9 @@ $CONFIG = array( */ 'copied_sample_config' => true, +/** + * use a custom lookup server to publish user data + */ +'lookup_server' => 'https://lookup.nextcloud.com', + ); diff --git a/core/img/actions/verified.svg b/core/img/actions/verified.svg new file mode 100644 index 00000000000..2f9e34e2394 --- /dev/null +++ b/core/img/actions/verified.svg @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1"> + <path d="m8 0a3 3 0 0 0 -2.8281 2.0098 3 3 0 0 0 -0.1719 -0.0098 3 3 0 0 0 -3 3 3 3 0 0 0 0.0059 0.1719 3 3 0 0 0 -2.0059 2.8281 3 3 0 0 0 2.0098 2.828 3 3 0 0 0 -0.0098 0.172 3 3 0 0 0 3 3 3 3 0 0 0 0.1719 -0.006 3 3 0 0 0 2.8281 2.006 3 3 0 0 0 2.828 -2.01 3 3 0 0 0 0.172 0.01 3 3 0 0 0 3 -3 3 3 0 0 0 -0.006 -0.172 3 3 0 0 0 2.006 -2.828 3 3 0 0 0 -2.01 -2.8281 3 3 0 0 0 0.01 -0.1719 3 3 0 0 0 -3 -3 3 3 0 0 0 -0.172 0.0059 3 3 0 0 0 -2.828 -2.0059zm2.934 4.5625 1.433 1.4336-5.7772 5.7789-2.9511-2.9508 1.414-1.414 1.5371 1.5351 4.3442-4.3828z" fill-rule="evenodd" fill="#0082c9"/> +</svg> diff --git a/core/img/actions/verify.svg b/core/img/actions/verify.svg new file mode 100644 index 00000000000..5ad11481055 --- /dev/null +++ b/core/img/actions/verify.svg @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1"> + <path d="m8 0a3 3 0 0 0 -2.8281 2.0098 3 3 0 0 0 -0.1719 -0.0098 3 3 0 0 0 -3 3 3 3 0 0 0 0.0059 0.1719 3 3 0 0 0 -2.0059 2.8281 3 3 0 0 0 2.0098 2.828 3 3 0 0 0 -0.0098 0.172 3 3 0 0 0 3 3 3 3 0 0 0 0.1719 -0.006 3 3 0 0 0 2.8281 2.006 3 3 0 0 0 2.828 -2.01 3 3 0 0 0 0.172 0.01 3 3 0 0 0 3 -3 3 3 0 0 0 -0.006 -0.172 3 3 0 0 0 2.006 -2.828 3 3 0 0 0 -2.01 -2.8281 3 3 0 0 0 0.01 -0.1719 3 3 0 0 0 -3 -3 3 3 0 0 0 -0.172 0.0059 3 3 0 0 0 -2.828 -2.0059zm2.934 4.5625 1.433 1.4336-5.7772 5.7789-2.9511-2.9508 1.414-1.414 1.5371 1.5351 4.3442-4.3828z" fill-rule="evenodd" fill="#969696"/> +</svg> diff --git a/core/img/actions/verifying.svg b/core/img/actions/verifying.svg new file mode 100644 index 00000000000..beb824b7eec --- /dev/null +++ b/core/img/actions/verifying.svg @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1"> + <path d="m8 0a3 3 0 0 0 -2.8281 2.0098 3 3 0 0 0 -0.1719 -0.0098 3 3 0 0 0 -3 3 3 3 0 0 0 0.0059 0.1719 3 3 0 0 0 -2.0059 2.8281 3 3 0 0 0 2.0098 2.828 3 3 0 0 0 -0.0098 0.172 3 3 0 0 0 3 3 3 3 0 0 0 0.1719 -0.006 3 3 0 0 0 2.8281 2.006 3 3 0 0 0 2.828 -2.01 3 3 0 0 0 0.172 0.01 3 3 0 0 0 3 -3 3 3 0 0 0 -0.006 -0.172 3 3 0 0 0 2.006 -2.828 3 3 0 0 0 -2.01 -2.8281 3 3 0 0 0 0.01 -0.1719 3 3 0 0 0 -3 -3 3 3 0 0 0 -0.172 0.0059 3 3 0 0 0 -2.828 -2.0059zm-0.0352 3.4922c0.58455-0.00435 1.1821 0.096216 1.7559 0.33398 0.69638 0.28822 1.2735 0.7423 1.7246 1.2832l1.055-1.0547v3.375h-3.375l1.125-1.125c-0.2925-0.3924-0.6924-0.7131-1.1777-0.9141-1.4351-0.5944-3.0794 0.0942-3.6739 1.5293l-1.5644-0.6504c0.7133-1.7221 2.3772-2.7643 4.1308-2.7773zm-4.4648 5.3437h3.375l-0.98438 0.98438c0.2773 0.3207 0.6189 0.5997 1.0371 0.7737 1.4351 0.594 3.0793-0.095 3.6743-1.5295l1.5625 0.65039c-0.951 2.2961-3.5905 3.3941-5.8867 2.4431-0.6318-0.261-1.1678-0.651-1.5996-1.125l-1.1777 1.178v-3.3751z" fill-rule="evenodd" fill="#0082c9"/> +</svg> diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 8a883938b55..2151aeff33b 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -789,6 +789,7 @@ return array( 'OC\\Settings\\Admin\\Sharing' => $baseDir . '/lib/private/Settings/Admin/Sharing.php', 'OC\\Settings\\Admin\\TipsTricks' => $baseDir . '/lib/private/Settings/Admin/TipsTricks.php', 'OC\\Settings\\Application' => $baseDir . '/settings/Application.php', + 'OC\\Settings\\BackgroundJobs\\VerifyUserData' => $baseDir . '/settings/BackgroundJobs/VerifyUserData.php', 'OC\\Settings\\Controller\\AdminSettingsController' => $baseDir . '/settings/Controller/AdminSettingsController.php', 'OC\\Settings\\Controller\\AppSettingsController' => $baseDir . '/settings/Controller/AppSettingsController.php', 'OC\\Settings\\Controller\\AuthSettingsController' => $baseDir . '/settings/Controller/AuthSettingsController.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 70761572620..ec5190bc71d 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -819,6 +819,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Settings\\Admin\\Sharing' => __DIR__ . '/../../..' . '/lib/private/Settings/Admin/Sharing.php', 'OC\\Settings\\Admin\\TipsTricks' => __DIR__ . '/../../..' . '/lib/private/Settings/Admin/TipsTricks.php', 'OC\\Settings\\Application' => __DIR__ . '/../../..' . '/settings/Application.php', + 'OC\\Settings\\BackgroundJobs\\VerifyUserData' => __DIR__ . '/../../..' . '/settings/BackgroundJobs/VerifyUserData.php', 'OC\\Settings\\Controller\\AdminSettingsController' => __DIR__ . '/../../..' . '/settings/Controller/AdminSettingsController.php', 'OC\\Settings\\Controller\\AppSettingsController' => __DIR__ . '/../../..' . '/settings/Controller/AppSettingsController.php', 'OC\\Settings\\Controller\\AuthSettingsController' => __DIR__ . '/../../..' . '/settings/Controller/AuthSettingsController.php', diff --git a/lib/private/Accounts/AccountManager.php b/lib/private/Accounts/AccountManager.php index 2eb518d4f04..41fdad148aa 100644 --- a/lib/private/Accounts/AccountManager.php +++ b/lib/private/Accounts/AccountManager.php @@ -23,6 +23,7 @@ namespace OC\Accounts; +use OCP\BackgroundJob\IJobList; use OCP\IDBConnection; use OCP\IUser; use Symfony\Component\EventDispatcher\EventDispatcherInterface; @@ -53,6 +54,10 @@ class AccountManager { const PROPERTY_ADDRESS = 'address'; const PROPERTY_TWITTER = 'twitter'; + const NOT_VERIFIED = '0'; + const VERIFICATION_IN_PROGRESS = '1'; + const VERIFIED = '2'; + /** @var IDBConnection database connection */ private $connection; @@ -62,15 +67,22 @@ class AccountManager { /** @var EventDispatcherInterface */ private $eventDispatcher; + /** @var IJobList */ + private $jobList; + /** * AccountManager constructor. * * @param IDBConnection $connection * @param EventDispatcherInterface $eventDispatcher + * @param IJobList $jobList */ - public function __construct(IDBConnection $connection, EventDispatcherInterface $eventDispatcher) { + public function __construct(IDBConnection $connection, + EventDispatcherInterface $eventDispatcher, + IJobList $jobList) { $this->connection = $connection; $this->eventDispatcher = $eventDispatcher; + $this->jobList = $jobList; } /** @@ -85,6 +97,8 @@ class AccountManager { if (empty($userData)) { $this->insertNewUser($user, $data); } elseif ($userData !== $data) { + $data = $this->checkEmailVerification($userData, $data, $user); + $data = $this->updateVerifyStatus($userData, $data); $this->updateExistingUser($user, $data); } else { // nothing needs to be done if new and old data set are the same @@ -120,7 +134,110 @@ class AccountManager { return $userData; } - return json_decode($result[0]['data'], true); + $userDataArray = json_decode($result[0]['data'], true); + + $userDataArray = $this->addMissingDefaultValues($userDataArray); + + return $userDataArray; + } + + /** + * check if we need to ask the server for email verification, if yes we create a cronjob + * + * @param $oldData + * @param $newData + * @param IUser $user + * @return array + */ + protected function checkEmailVerification($oldData, $newData, IUser $user) { + if ($oldData[self::PROPERTY_EMAIL]['value'] !== $newData[self::PROPERTY_EMAIL]['value']) { + $this->jobList->add('OC\Settings\BackgroundJobs\VerifyUserData', + [ + 'verificationCode' => '', + 'data' => $newData[self::PROPERTY_EMAIL]['value'], + 'type' => self::PROPERTY_EMAIL, + 'uid' => $user->getUID(), + 'try' => 0, + 'lastRun' => time() + ] + ); + $newData[AccountManager::PROPERTY_EMAIL]['verified'] = AccountManager::VERIFICATION_IN_PROGRESS; + } + + return $newData; + } + + /** + * make sure that all expected data are set + * + * @param array $userData + * @return array + */ + protected function addMissingDefaultValues(array $userData) { + + foreach ($userData as $key => $value) { + if (!isset($userData[$key]['verified'])) { + $userData[$key]['verified'] = self::NOT_VERIFIED; + } + } + + return $userData; + } + + /** + * reset verification status if personal data changed + * + * @param array $oldData + * @param array $newData + * @return array + */ + protected function updateVerifyStatus($oldData, $newData) { + + // which account was already verified successfully? + $twitterVerified = isset($oldData[self::PROPERTY_TWITTER]['verified']) && $oldData[self::PROPERTY_TWITTER]['verified'] === self::VERIFIED; + $websiteVerified = isset($oldData[self::PROPERTY_WEBSITE]['verified']) && $oldData[self::PROPERTY_WEBSITE]['verified'] === self::VERIFIED; + $emailVerified = isset($oldData[self::PROPERTY_EMAIL]['verified']) && $oldData[self::PROPERTY_EMAIL]['verified'] === self::VERIFIED; + + // keep old verification status if we don't have a new one + if(!isset($newData[self::PROPERTY_TWITTER]['verified'])) { + // keep old verification status if value didn't changed and an old value exists + $keepOldStatus = $newData[self::PROPERTY_TWITTER]['value'] === $oldData[self::PROPERTY_TWITTER]['value'] && isset($oldData[self::PROPERTY_TWITTER]['verified']); + $newData[self::PROPERTY_TWITTER]['verified'] = $keepOldStatus ? $oldData[self::PROPERTY_TWITTER]['verified'] : self::NOT_VERIFIED; + } + + if(!isset($newData[self::PROPERTY_WEBSITE]['verified'])) { + // keep old verification status if value didn't changed and an old value exists + $keepOldStatus = $newData[self::PROPERTY_WEBSITE]['value'] === $oldData[self::PROPERTY_WEBSITE]['value'] && isset($oldData[self::PROPERTY_WEBSITE]['verified']); + $newData[self::PROPERTY_WEBSITE]['verified'] = $keepOldStatus ? $oldData[self::PROPERTY_WEBSITE]['verified'] : self::NOT_VERIFIED; + } + + if(!isset($newData[self::PROPERTY_EMAIL]['verified'])) { + // keep old verification status if value didn't changed and an old value exists + $keepOldStatus = $newData[self::PROPERTY_EMAIL]['value'] === $oldData[self::PROPERTY_EMAIL]['value'] && isset($oldData[self::PROPERTY_EMAIL]['verified']); + $newData[self::PROPERTY_EMAIL]['verified'] = $keepOldStatus ? $oldData[self::PROPERTY_EMAIL]['verified'] : self::VERIFICATION_IN_PROGRESS; + } + + // reset verification status if a value from a previously verified data was changed + if($twitterVerified && + $oldData[self::PROPERTY_TWITTER]['value'] !== $newData[self::PROPERTY_TWITTER]['value'] + ) { + $newData[self::PROPERTY_TWITTER]['verified'] = self::NOT_VERIFIED; + } + + if($websiteVerified && + $oldData[self::PROPERTY_WEBSITE]['value'] !== $newData[self::PROPERTY_WEBSITE]['value'] + ) { + $newData[self::PROPERTY_WEBSITE]['verified'] = self::NOT_VERIFIED; + } + + if($emailVerified && + $oldData[self::PROPERTY_EMAIL]['value'] !== $newData[self::PROPERTY_EMAIL]['value'] + ) { + $newData[self::PROPERTY_EMAIL]['verified'] = self::NOT_VERIFIED; + } + + return $newData; + } /** @@ -171,21 +288,25 @@ class AccountManager { [ 'value' => $user->getDisplayName(), 'scope' => self::VISIBILITY_CONTACTS_ONLY, + 'verified' => self::NOT_VERIFIED, ], self::PROPERTY_ADDRESS => [ 'value' => '', 'scope' => self::VISIBILITY_PRIVATE, + 'verified' => self::NOT_VERIFIED, ], self::PROPERTY_WEBSITE => [ 'value' => '', 'scope' => self::VISIBILITY_PRIVATE, + 'verified' => self::NOT_VERIFIED, ], self::PROPERTY_EMAIL => [ 'value' => $user->getEMailAddress(), 'scope' => self::VISIBILITY_CONTACTS_ONLY, + 'verified' => self::NOT_VERIFIED, ], self::PROPERTY_AVATAR => [ @@ -195,11 +316,13 @@ class AccountManager { [ 'value' => '', 'scope' => self::VISIBILITY_PRIVATE, + 'verified' => self::NOT_VERIFIED, ], self::PROPERTY_TWITTER => [ 'value' => '', 'scope' => self::VISIBILITY_PRIVATE, + 'verified' => self::NOT_VERIFIED, ], ]; } diff --git a/lib/private/Accounts/Hooks.php b/lib/private/Accounts/Hooks.php index 38e7e20485b..eca56913fbd 100644 --- a/lib/private/Accounts/Hooks.php +++ b/lib/private/Accounts/Hooks.php @@ -89,7 +89,8 @@ class Hooks { if (is_null($this->accountManager)) { $this->accountManager = new AccountManager( \OC::$server->getDatabaseConnection(), - \OC::$server->getEventDispatcher() + \OC::$server->getEventDispatcher(), + \OC::$server->getJobList() ); } return $this->accountManager; diff --git a/settings/BackgroundJobs/VerifyUserData.php b/settings/BackgroundJobs/VerifyUserData.php new file mode 100644 index 00000000000..4a32398f6c4 --- /dev/null +++ b/settings/BackgroundJobs/VerifyUserData.php @@ -0,0 +1,273 @@ +<?php +/** + * @copyright Copyright (c) 2017 Bjoern Schiessle <bjoern@schiessle.org> + * + * @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 <http://www.gnu.org/licenses/>. + * + */ + + +namespace OC\Settings\BackgroundJobs; + + +use OC\Accounts\AccountManager; +use OC\BackgroundJob\Job; +use OC\BackgroundJob\JobList; +use OCP\AppFramework\Http; +use OCP\BackgroundJob\IJobList; +use OCP\Http\Client\IClientService; +use OCP\IConfig; +use OCP\ILogger; +use OCP\IUserManager; + +class VerifyUserData extends Job { + + /** @var bool */ + private $retainJob = true; + + /** @var int max number of attempts to send the request */ + private $maxTry = 24; + + /** @var int how much time should be between two tries (1 hour) */ + private $interval = 3600; + + /** @var AccountManager */ + private $accountManager; + + /** @var IUserManager */ + private $userManager; + + /** @var IClientService */ + private $httpClientService; + + /** @var ILogger */ + private $logger; + + /** @var string */ + private $lookupServerUrl; + + /** + * VerifyUserData constructor. + * + * @param AccountManager $accountManager + * @param IUserManager $userManager + * @param IClientService $clientService + * @param ILogger $logger + * @param IConfig $config + */ + public function __construct(AccountManager $accountManager, + IUserManager $userManager, + IClientService $clientService, + ILogger $logger, + IConfig $config + ) { + $this->accountManager = $accountManager; + $this->userManager = $userManager; + $this->httpClientService = $clientService; + $this->logger = $logger; + + $lookupServerUrl = $config->getSystemValue('lookup_server', 'https://lookup.nextcloud.com'); + $this->lookupServerUrl = rtrim($lookupServerUrl, '/'); + } + + /** + * run the job, then remove it from the jobList + * + * @param JobList $jobList + * @param ILogger $logger + */ + public function execute($jobList, ILogger $logger = null) { + + if ($this->shouldRun($this->argument)) { + parent::execute($jobList, $logger); + $jobList->remove($this, $this->argument); + if ($this->retainJob) { + $this->reAddJob($jobList, $this->argument); + } + } + + } + + protected function run($argument) { + + $try = (int)$argument['try'] + 1; + + switch($argument['type']) { + case AccountManager::PROPERTY_WEBSITE: + $result = $this->verifyWebsite($argument); + break; + case AccountManager::PROPERTY_TWITTER: + case AccountManager::PROPERTY_EMAIL: + $result = $this->verifyViaLookupServer($argument, $argument['type']); + break; + default: + // no valid type given, no need to retry + $this->logger->error($argument['type'] . ' is no valid type for user account data.'); + $result = true; + } + + if ($result === true || $try > $this->maxTry) { + $this->retainJob = false; + } + } + + /** + * verify web page + * + * @param array $argument + * @return bool true if we could check the verification code, otherwise false + */ + protected function verifyWebsite(array $argument) { + + $result = false; + + $url = rtrim($argument['data'], '/') . '/.well-known/' . 'CloudIdVerificationCode.txt'; + + $client = $this->httpClientService->newClient(); + try { + $response = $client->get($url); + } catch (\Exception $e) { + return false; + } + + if ($response->getStatusCode() === Http::STATUS_OK) { + $result = true; + $publishedCode = $response->getBody(); + // remove new lines and spaces + $publishedCodeSanitized = trim(preg_replace('/\s\s+/', ' ', $publishedCode)); + $user = $this->userManager->get($argument['uid']); + // we don't check a valid user -> give up + if ($user === null) { + $this->logger->error($argument['uid'] . ' doesn\'t exist, can\'t verify user data.'); + return $result; + } + $userData = $this->accountManager->getUser($user); + + if ($publishedCodeSanitized === $argument['verificationCode']) { + $userData[AccountManager::PROPERTY_WEBSITE]['verified'] = AccountManager::VERIFIED; + } else { + $userData[AccountManager::PROPERTY_WEBSITE]['verified'] = AccountManager::NOT_VERIFIED; + } + + $this->accountManager->updateUser($user, $userData); + } + + return $result; + } + + /** + * verify email address + * + * @param array $argument + * @param string $dataType + * @return bool true if we could check the verification code, otherwise false + */ + protected function verifyViaLookupServer(array $argument, $dataType) { + + $user = $this->userManager->get($argument['uid']); + + // we don't check a valid user -> give up + if ($user === null) { + $this->logger->error($argument['uid'] . ' doesn\'t exist, can\'t verify user data.'); + return true; + } + + $localUserData = $this->accountManager->getUser($user); + $cloudId = $user->getCloudId(); + + // ask lookup-server for user data + $lookupServerData = $this->queryLookupServer($cloudId); + + // for some reasons we couldn't read any data from the lookup server, try again later + if (empty($lookupServerData)) { + return false; + } + + // lookup server has verification data for wrong user data (e.g. email address), try again later + if ($lookupServerData[$dataType]['value'] !== $argument['data']) { + return false; + } + + // lookup server hasn't verified the email address so far, try again later + if ($lookupServerData[$dataType]['verified'] === AccountManager::NOT_VERIFIED) { + return false; + } + + $localUserData[$dataType]['verified'] = AccountManager::VERIFIED; + $this->accountManager->updateUser($user, $localUserData); + + return true; + } + + /** + * @param string $cloudId + * @return array + */ + protected function queryLookupServer($cloudId) { + try { + $client = $this->httpClientService->newClient(); + $response = $client->get( + $this->lookupServerUrl . '/users?search=' . urlencode($cloudId) . '&exactCloudId=1', + [ + 'timeout' => 10, + 'connect_timeout' => 3, + ] + ); + + $body = json_decode($response->getBody(), true); + + if ($body['federationId'] === $cloudId) { + return $body; + } + + } catch (\Exception $e) { + // do nothing, we will just re-try later + } + + return []; + } + + /** + * re-add background job with new arguments + * + * @param IJobList $jobList + * @param array $argument + */ + protected function reAddJob(IJobList $jobList, array $argument) { + $jobList->add('OC\Settings\BackgroundJobs\VerifyUserData', + [ + 'verificationCode' => $argument['verificationCode'], + 'data' => $argument['data'], + 'type' => $argument['type'], + 'uid' => $argument['uid'], + 'try' => (int)$argument['try'] + 1, + 'lastRun' => time() + ] + ); + } + + /** + * test if it is time for the next run + * + * @param array $argument + * @return bool + */ + protected function shouldRun(array $argument) { + $lastRun = (int)$argument['lastRun']; + return ((time() - $lastRun) > $this->interval); + } + +} diff --git a/settings/Controller/UsersController.php b/settings/Controller/UsersController.php index b42d4faa569..7f6602a510c 100644 --- a/settings/Controller/UsersController.php +++ b/settings/Controller/UsersController.php @@ -34,9 +34,12 @@ use OC\Accounts\AccountManager; use OC\AppFramework\Http; use OC\ForbiddenException; use OC\Settings\Mailer\NewUserMailHelper; +use OC\Security\IdentityProof\Manager; use OCP\App\IAppManager; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\IJobList; use OCP\IConfig; use OCP\IGroupManager; use OCP\IL10N; @@ -48,6 +51,7 @@ use OCP\IUserManager; use OCP\IUserSession; use OCP\Mail\IMailer; use OCP\IAvatarManager; +use OCP\Security\ICrypto; use OCP\Security\ISecureRandom; /** @@ -82,6 +86,14 @@ class UsersController extends Controller { private $secureRandom; /** @var NewUserMailHelper */ private $newUserMailHelper; + /** @var ITimeFactory */ + private $timeFactory; + /** @var ICrypto */ + private $crypto; + /** @var Manager */ + private $keyManager; + /** @var IJobList */ + private $jobList; /** * @param string $appName @@ -100,6 +112,10 @@ class UsersController extends Controller { * @param AccountManager $accountManager * @param ISecureRandom $secureRandom * @param NewUserMailHelper $newUserMailHelper + * @param ITimeFactory $timeFactory + * @param ICrypto $crypto + * @param Manager $keyManager + * @param IJobList $jobList */ public function __construct($appName, IRequest $request, @@ -116,7 +132,11 @@ class UsersController extends Controller { IAvatarManager $avatarManager, AccountManager $accountManager, ISecureRandom $secureRandom, - NewUserMailHelper $newUserMailHelper) { + NewUserMailHelper $newUserMailHelper, + ITimeFactory $timeFactory, + ICrypto $crypto, + Manager $keyManager, + IJobList $jobList) { parent::__construct($appName, $request); $this->userManager = $userManager; $this->groupManager = $groupManager; @@ -130,6 +150,10 @@ class UsersController extends Controller { $this->accountManager = $accountManager; $this->secureRandom = $secureRandom; $this->newUserMailHelper = $newUserMailHelper; + $this->timeFactory = $timeFactory; + $this->crypto = $crypto; + $this->keyManager = $keyManager; + $this->jobList = $jobList; // check for encryption state - TODO see formatUserForIndex $this->isEncryptionAppEnabled = $appManager->isEnabledForUser('encryption'); @@ -493,6 +517,94 @@ class UsersController extends Controller { * @NoSubadminRequired * @PasswordConfirmationRequired * + * @param string $account + * @param bool $onlyVerificationCode only return verification code without updating the data + * @return DataResponse + */ + public function getVerificationCode($account, $onlyVerificationCode) { + + $user = $this->userSession->getUser(); + + if ($user === null) { + return new DataResponse([], Http::STATUS_BAD_REQUEST); + } + + $accountData = $this->accountManager->getUser($user); + $cloudId = $user->getCloudId(); + $message = "Use my Federated Cloud ID to share with me: " . $cloudId; + $signature = $this->signMessage($user, $message); + + $code = $message . ' ' . $signature; + $codeMd5 = $message . ' ' . md5($signature); + + switch ($account) { + case 'verify-twitter': + $accountData[AccountManager::PROPERTY_TWITTER]['verified'] = AccountManager::VERIFICATION_IN_PROGRESS; + $msg = $this->l10n->t('In order to verify your Twitter account post following tweet on Twitter (please make sure to post it without any line breaks):'); + $code = $codeMd5; + $type = AccountManager::PROPERTY_TWITTER; + $data = $accountData[AccountManager::PROPERTY_TWITTER]['value']; + $accountData[AccountManager::PROPERTY_TWITTER]['signature'] = $signature; + break; + case 'verify-website': + $accountData[AccountManager::PROPERTY_WEBSITE]['verified'] = AccountManager::VERIFICATION_IN_PROGRESS; + $msg = $this->l10n->t('In order to verify your Website store following content in your web-root at \'.well-known/CloudIdVerificationCode.txt\' (please make sure that the complete text is in one line):'); + $type = AccountManager::PROPERTY_WEBSITE; + $data = $accountData[AccountManager::PROPERTY_WEBSITE]['value']; + $accountData[AccountManager::PROPERTY_WEBSITE]['signature'] = $signature; + break; + default: + return new DataResponse([], Http::STATUS_BAD_REQUEST); + } + + if ($onlyVerificationCode === false) { + $this->accountManager->updateUser($user, $accountData); + + $this->jobList->add('OC\Settings\BackgroundJobs\VerifyUserData', + [ + 'verificationCode' => $code, + 'data' => $data, + 'type' => $type, + 'uid' => $user->getUID(), + 'try' => 0, + 'lastRun' => $this->getCurrentTime() + ] + ); + } + + return new DataResponse(['msg' => $msg, 'code' => $code]); + } + + /** + * get current timestamp + * + * @return int + */ + protected function getCurrentTime() { + return time(); + } + + /** + * sign message with users private key + * + * @param IUser $user + * @param string $message + * + * @return string base64 encoded signature + */ + protected function signMessage(IUser $user, $message) { + $privateKey = $this->keyManager->getKey($user)->getPrivate(); + openssl_sign(json_encode($message), $signature, $privateKey, OPENSSL_ALGO_SHA512); + $signatureBase64 = base64_encode($signature); + + return $signatureBase64; + } + + /** + * @NoAdminRequired + * @NoSubadminRequired + * @PasswordConfirmationRequired + * * @param string $avatarScope * @param string $displayname * @param string $displaynameScope diff --git a/settings/css/settings.css b/settings/css/settings.css index 65709c9578a..5693224b94c 100644 --- a/settings/css/settings.css +++ b/settings/css/settings.css @@ -138,6 +138,35 @@ input#openid, input#webdav { width:20em; } top: 82px; pointer-events: none; } + +/* verify accounts */ +#personal-settings-container .verify { + position: absolute; + right: 14px; + top: 70px; +} +#personal-settings-container .verify img { + padding: 12px 7px 6px; +} +/* only show pointer cursor when popup will be there */ +#personal-settings-container .verify-action { + cursor: pointer; +} +.verification-dialog { + display: none; + right: -9px; + top: 40px; + width: 275px; +} +.verification-dialog p { + padding: 10px; +} +.verification-dialog .verificationCode { + font-family: monospace; + display: block; + overflow-wrap: break-word; +} + .federationScopeMenu { top: 44px; margin: -5px 0px 0; diff --git a/settings/js/federationsettingsview.js b/settings/js/federationsettingsview.js index 2715c1e1e08..1a0a3dcb4d1 100644 --- a/settings/js/federationsettingsview.js +++ b/settings/js/federationsettingsview.js @@ -130,14 +130,52 @@ // TODO: user loading/success feedback this._config.save(); this._setFieldScopeIcon(field, scope); + this._updateVerifyButton(field, scope); + }, + + _updateVerifyButton: function(field, scope) { + // show verification button if the value is set and the scope is 'public' + if (field === 'twitter' || field === 'website'|| field === 'email') { + var verify = this.$('#' + field + 'form > .verify'); + var scope = this.$('#' + field + 'scope').val(); + var value = this.$('#' + field).val(); + + if (scope === 'public' && value !== '') { + verify.removeClass('hidden'); + return true; + } else { + verify.addClass('hidden'); + } + } + + return false; }, _showInputChangeSuccess: function(field) { - var $icon = this.$('#' + field + 'form > span'); + var $icon = this.$('#' + field + 'form > .icon-checkmark'); $icon.fadeIn(200); setTimeout(function() { $icon.fadeOut(300); }, 2000); + + var scope = this.$('#' + field + 'scope').val(); + var verifyAvailable = this._updateVerifyButton(field, scope); + + // change verification buttons from 'verify' to 'verifying...' on value change + if (verifyAvailable) { + if (field === 'twitter' || field === 'website') { + var verifyStatus = this.$('#' + field + 'form > .verify > #verify-' + field); + verifyStatus.attr('data-origin-title', t('core', 'Verify')); + verifyStatus.attr('src', OC.imagePath('core', 'actions/verify.svg')); + verifyStatus.data('status', '0'); + verifyStatus.addClass('verify-action'); + } else if (field === 'email') { + var verifyStatus = this.$('#' + field + 'form > .verify > #verify-' + field); + verifyStatus.attr('data-origin-title', t('core', 'Verifying …')); + verifyStatus.data('status', '1'); + verifyStatus.attr('src', OC.imagePath('core', 'actions/verifying.svg')); + } + } }, _setFieldScopeIcon: function(field, scope) { diff --git a/settings/js/personal.js b/settings/js/personal.js index 52ab2f23f87..254ee8f415b 100644 --- a/settings/js/personal.js +++ b/settings/js/personal.js @@ -201,6 +201,58 @@ $(document).ready(function () { } }); + var showVerifyDialog = function(dialog, howToVerify, verificationCode) { + var dialogContent = dialog.children('.verification-dialog-content'); + dialogContent.children(".explainVerification").text(howToVerify); + dialogContent.children(".verificationCode").text(verificationCode); + dialog.css('display', 'block'); + }; + + $(".verify").click(function (event) { + + event.stopPropagation(); + + var verify = $(this); + var indicator = $(this).children('img'); + var accountId = indicator.attr('id'); + var status = indicator.data('status'); + + var onlyVerificationCode = false; + if (parseInt(status) === 1) { + onlyVerificationCode = true; + } + + if (indicator.hasClass('verify-action')) { + $.ajax( + OC.generateUrl('/settings/users/{account}/verify', {account: accountId}), + { + method: 'GET', + data: {onlyVerificationCode: onlyVerificationCode} + } + ).done(function (data) { + var dialog = verify.children('.verification-dialog'); + showVerifyDialog($(dialog), data.msg, data.code); + indicator.attr('data-origin-title', t('core', 'Verifying …')); + indicator.attr('src', OC.imagePath('core', 'actions/verifying.svg')); + indicator.data('status', '1'); + }); + } + + }); + + // When the user clicks anywhere outside of the verification dialog we close it + $(document).click(function(event){ + var element = event.target; + var isDialog = $(element).hasClass('verificationCode') + || $(element).hasClass('explainVerification') + || $(element).hasClass('verification-dialog-content') + || $(element).hasClass('verification-dialog'); + if (!isDialog) { + $(document).find('.verification-dialog').css('display', 'none'); + } + }); + + var federationSettingsView = new OC.Settings.FederationSettingsView({ el: '#personal-settings' }); @@ -334,7 +386,7 @@ $(document).ready(function () { $('#removeavatar').removeClass('hidden').addClass('inlineblock'); } }); - + // Show token views var collection = new OC.Settings.AuthTokenCollection(); diff --git a/settings/personal.php b/settings/personal.php index 6cbcc330cd9..86ac4f753f4 100644 --- a/settings/personal.php +++ b/settings/personal.php @@ -40,7 +40,11 @@ OC_Util::checkLoggedIn(); $defaults = \OC::$server->getThemingDefaults(); $certificateManager = \OC::$server->getCertificateManager(); -$accountManager = new \OC\Accounts\AccountManager(\OC::$server->getDatabaseConnection(), \OC::$server->getEventDispatcher()); +$accountManager = new \OC\Accounts\AccountManager( + \OC::$server->getDatabaseConnection(), + \OC::$server->getEventDispatcher(), + \OC::$server->getJobList() +); $config = \OC::$server->getConfig(); $urlGenerator = \OC::$server->getURLGenerator(); @@ -181,6 +185,28 @@ $tmpl->assign('websiteScope', $userData[\OC\Accounts\AccountManager::PROPERTY_WE $tmpl->assign('twitterScope', $userData[\OC\Accounts\AccountManager::PROPERTY_TWITTER]['scope']); $tmpl->assign('addressScope', $userData[\OC\Accounts\AccountManager::PROPERTY_ADDRESS]['scope']); +$tmpl->assign('websiteVerification', $userData[\OC\Accounts\AccountManager::PROPERTY_WEBSITE]['verified']); +$tmpl->assign('twitterVerification', $userData[\OC\Accounts\AccountManager::PROPERTY_TWITTER]['verified']); +$tmpl->assign('emailVerification', $userData[\OC\Accounts\AccountManager::PROPERTY_EMAIL]['verified']); + +$needVerifyMessage = [\OC\Accounts\AccountManager::PROPERTY_EMAIL, \OC\Accounts\AccountManager::PROPERTY_WEBSITE, \OC\Accounts\AccountManager::PROPERTY_TWITTER]; + +foreach ($needVerifyMessage as $property) { + + switch ($userData[$property]['verified']) { + case \OC\Accounts\AccountManager::VERIFIED: + $message = $l->t('Verifying'); + break; + case \OC\Accounts\AccountManager::VERIFICATION_IN_PROGRESS: + $message = $l->t('Verifying …'); + break; + default: + $message = $l->t('Verify'); + } + + $tmpl->assign($property . 'Message', $message); +} + $tmpl->assign('avatarChangeSupported', OC_User::canUserChangeAvatar(OC_User::getUser())); $tmpl->assign('certs', $certificateManager->listCertificates()); $tmpl->assign('showCertificates', $enableCertImport); diff --git a/settings/routes.php b/settings/routes.php index b76bb213d0c..ba0761856d4 100644 --- a/settings/routes.php +++ b/settings/routes.php @@ -52,6 +52,7 @@ $application->registerRoutes($this, [ ['name' => 'Users#setDisplayName', 'url' => '/settings/users/{username}/displayName', 'verb' => 'POST'], ['name' => 'Users#setEMailAddress', 'url' => '/settings/users/{id}/mailAddress', 'verb' => 'PUT'], ['name' => 'Users#setUserSettings', 'url' => '/settings/users/{username}/settings', 'verb' => 'PUT'], + ['name' => 'Users#getVerificationCode', 'url' => '/settings/users/{account}/verify', 'verb' => 'GET'], ['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 24a78b07853..3e30d775395 100644 --- a/settings/templates/personal.php +++ b/settings/templates/personal.php @@ -99,6 +99,21 @@ <label for="email"><?php p($l->t('Email')); ?></label> <span class="icon-password"/> </h2> + <div class="verify <?php if ($_['email'] === '' || $_['emailScope'] !== 'public') p('hidden'); ?>"> + <img id="verify-email" title="<?php p($_['emailMessage']); ?>" data-status="<?php p($_['emailVerification']) ?>" src=" + <?php + switch($_['emailVerification']) { + case \OC\Accounts\AccountManager::VERIFICATION_IN_PROGRESS: + p(image_path('core', 'actions/verifying.svg')); + break; + case \OC\Accounts\AccountManager::VERIFIED: + p(image_path('core', 'actions/verified.svg')); + break; + default: + p(image_path('core', 'actions/verify.svg')); + } + ?>"> + </div> <input type="email" name="email" id="email" value="<?php p($_['email']); ?>" <?php if(!$_['displayNameChangeSupported']) { print_unescaped('class="hidden"'); } ?> placeholder="<?php p($l->t('Your email address')); ?>" @@ -151,8 +166,32 @@ <label for="website"><?php p($l->t('Website')); ?></label> <span class="icon-password"/> </h2> + <div class="verify <?php if ($_['website'] === '' || $_['websiteScope'] !== 'public') p('hidden'); ?>"> + <img id="verify-website" title="<?php p($_['websiteMessage']); ?>" data-status="<?php p($_['websiteVerification']) ?>" src=" + <?php + switch($_['websiteVerification']) { + case \OC\Accounts\AccountManager::VERIFICATION_IN_PROGRESS: + p(image_path('core', 'actions/verifying.svg')); + break; + case \OC\Accounts\AccountManager::VERIFIED: + p(image_path('core', 'actions/verified.svg')); + break; + default: + p(image_path('core', 'actions/verify.svg')); + } + ?>" + <?php if($_['websiteVerification'] === \OC\Accounts\AccountManager::VERIFICATION_IN_PROGRESS || $_['websiteVerification'] === \OC\Accounts\AccountManager::NOT_VERIFIED) print_unescaped(' class="verify-action"') ?> + > + <div class="verification-dialog popovermenu bubble menu"> + <div class="verification-dialog-content"> + <p class="explainVerification"></p> + <p class="verificationCode"></p> + <p><?php p($l->t('It can take up to 24 hours before the account is displayed as verified.'));?></p> + </div> + </div> + </div> <input type="text" name="website" id="website" value="<?php p($_['website']); ?>" - placeholder="<?php p($l->t('Your website')); ?>" + placeholder="<?php p($l->t('Link https://…')); ?>" autocomplete="on" autocapitalize="none" autocorrect="off" /> <span class="icon-checkmark hidden"/> <input type="hidden" id="websitescope" value="<?php p($_['websiteScope']) ?>"> @@ -164,8 +203,32 @@ <label for="twitter"><?php p($l->t('Twitter')); ?></label> <span class="icon-password"/> </h2> + <div class="verify <?php if ($_['twitter'] === '' || $_['twitterScope'] !== 'public') p('hidden'); ?>"> + <img id="verify-twitter" title="<?php p($_['twitterMessage']); ?>" data-status="<?php p($_['twitterVerification']) ?>" src=" + <?php + switch($_['twitterVerification']) { + case \OC\Accounts\AccountManager::VERIFICATION_IN_PROGRESS: + p(image_path('core', 'actions/verifying.svg')); + break; + case \OC\Accounts\AccountManager::VERIFIED: + p(image_path('core', 'actions/verified.svg')); + break; + default: + p(image_path('core', 'actions/verify.svg')); + } + ?>" + <?php if($_['twitterVerification'] === \OC\Accounts\AccountManager::VERIFICATION_IN_PROGRESS || $_['twitterVerification'] === \OC\Accounts\AccountManager::NOT_VERIFIED) print_unescaped(' class="verify-action"') ?> + > + <div class="verification-dialog popovermenu bubble menu"> + <div class="verification-dialog-content"> + <p class="explainVerification"></p> + <p class="verificationCode"></p> + <p><?php p($l->t('It can take up to 24 hours before the account is displayed as verified.'));?></p> + </div> + </div> + </div> <input type="text" name="twitter" id="twitter" value="<?php p($_['twitter']); ?>" - placeholder="<?php p($l->t('Your Twitter handle')); ?>" + placeholder="<?php p($l->t('Twitter handle @…')); ?>" autocomplete="on" autocapitalize="none" autocorrect="off" /> <span class="icon-checkmark hidden"/> <input type="hidden" id="twitterscope" value="<?php p($_['twitterScope']) ?>"> diff --git a/tests/Settings/Controller/UsersControllerTest.php b/tests/Settings/Controller/UsersControllerTest.php index d659d812b0d..589c5e97ee0 100644 --- a/tests/Settings/Controller/UsersControllerTest.php +++ b/tests/Settings/Controller/UsersControllerTest.php @@ -18,6 +18,7 @@ use OCP\App\IAppManager; use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\IJobList; use OCP\IAvatar; use OCP\IAvatarManager; use OCP\IConfig; @@ -74,6 +75,10 @@ class UsersControllerTest extends \Test\TestCase { private $newUserMailHelper; /** @var ICrypto | \PHPUnit_Framework_MockObject_MockObject */ private $crypto; + /** @var IJobList | \PHPUnit_Framework_MockObject_MockObject */ + private $jobList; + /** @var \OC\Security\IdentityProof\Manager |\PHPUnit_Framework_MockObject_MockObject */ + private $securityManager; protected function setUp() { parent::setUp(); @@ -92,6 +97,10 @@ class UsersControllerTest extends \Test\TestCase { $this->timeFactory = $this->createMock(ITimeFactory::class); $this->crypto = $this->createMock(ICrypto::class); $this->newUserMailHelper = $this->createMock(NewUserMailHelper::class); + $this->timeFactory = $this->createMock(ITimeFactory::class); + $this->crypto = $this->createMock(ICrypto::class); + $this->securityManager = $this->getMockBuilder(\OC\Security\IdentityProof\Manager::class)->disableOriginalConstructor()->getMock(); + $this->jobList = $this->createMock(IJobList::class); $this->l = $this->createMock(IL10N::class); $this->l->method('t') ->will($this->returnCallback(function ($text, $parameters = []) { @@ -136,7 +145,12 @@ class UsersControllerTest extends \Test\TestCase { $this->avatarManager, $this->accountManager, $this->secureRandom, - $this->newUserMailHelper + $this->newUserMailHelper, + $this->timeFactory, + $this->crypto, + $this->securityManager, + $this->jobList + ); } else { return $this->getMockBuilder(UsersController::class) @@ -157,7 +171,11 @@ class UsersControllerTest extends \Test\TestCase { $this->avatarManager, $this->accountManager, $this->secureRandom, - $this->newUserMailHelper + $this->newUserMailHelper, + $this->timeFactory, + $this->crypto, + $this->securityManager, + $this->jobList ] )->setMethods($mockedMethods)->getMock(); } @@ -2267,4 +2285,91 @@ class UsersControllerTest extends \Test\TestCase { $response = $controller->create('foo', '', array(), 'abc@example.org'); $this->assertEquals($expectedResponse, $response); } + + /** + * @param string $account + * @param string $type + * @param array $dataBefore + * @param array $expectedData + * + * @dataProvider dataTestGetVerificationCode + */ + public function testGetVerificationCode($account, $type, $dataBefore, $expectedData, $onlyVerificationCode) { + + $message = 'Use my Federated Cloud ID to share with me: user@nextcloud.com'; + $signature = 'theSignature'; + + $code = $message . ' ' . $signature; + if($type === AccountManager::PROPERTY_TWITTER) { + $code = $message . ' ' . md5($signature); + } + + $controller = $this->getController(false, ['signMessage', 'getCurrentTime']); + + $user = $this->createMock(IUser::class); + $this->userSession->expects($this->once())->method('getUser')->willReturn($user); + $this->accountManager->expects($this->once())->method('getUser')->with($user)->willReturn($dataBefore); + $user->expects($this->any())->method('getCloudId')->willReturn('user@nextcloud.com'); + $user->expects($this->any())->method('getUID')->willReturn('uid'); + $controller->expects($this->once())->method('signMessage')->with($user, $message)->willReturn($signature); + $controller->expects($this->any())->method('getCurrentTime')->willReturn(1234567); + + if ($onlyVerificationCode === false) { + $this->accountManager->expects($this->once())->method('updateUser')->with($user, $expectedData); + $this->jobList->expects($this->once())->method('add') + ->with('OC\Settings\BackgroundJobs\VerifyUserData', + [ + 'verificationCode' => $code, + 'data' => $dataBefore[$type]['value'], + 'type' => $type, + 'uid' => 'uid', + 'try' => 0, + 'lastRun' => 1234567 + ]); + } + + $result = $controller->getVerificationCode($account, $onlyVerificationCode); + + $data = $result->getData(); + $this->assertSame(Http::STATUS_OK, $result->getStatus()); + $this->assertSame($code, $data['code']); + } + + public function dataTestGetVerificationCode() { + + $accountDataBefore = [ + AccountManager::PROPERTY_WEBSITE => ['value' => 'https://nextcloud.com', 'verified' => AccountManager::NOT_VERIFIED], + AccountManager::PROPERTY_TWITTER => ['value' => '@nextclouders', 'verified' => AccountManager::NOT_VERIFIED, 'signature' => 'theSignature'], + ]; + + $accountDataAfterWebsite = [ + AccountManager::PROPERTY_WEBSITE => ['value' => 'https://nextcloud.com', 'verified' => AccountManager::VERIFICATION_IN_PROGRESS, 'signature' => 'theSignature'], + AccountManager::PROPERTY_TWITTER => ['value' => '@nextclouders', 'verified' => AccountManager::NOT_VERIFIED, 'signature' => 'theSignature'], + ]; + + $accountDataAfterTwitter = [ + AccountManager::PROPERTY_WEBSITE => ['value' => 'https://nextcloud.com', 'verified' => AccountManager::NOT_VERIFIED], + AccountManager::PROPERTY_TWITTER => ['value' => '@nextclouders', 'verified' => AccountManager::VERIFICATION_IN_PROGRESS, 'signature' => 'theSignature'], + ]; + + return [ + ['verify-twitter', AccountManager::PROPERTY_TWITTER, $accountDataBefore, $accountDataAfterTwitter, false], + ['verify-website', AccountManager::PROPERTY_WEBSITE, $accountDataBefore, $accountDataAfterWebsite, false], + ['verify-twitter', AccountManager::PROPERTY_TWITTER, $accountDataBefore, $accountDataAfterTwitter, true], + ['verify-website', AccountManager::PROPERTY_WEBSITE, $accountDataBefore, $accountDataAfterWebsite, true], + ]; + } + + /** + * test get verification code in case no valid user was given + */ + public function testGetVerificationCodeInvalidUser() { + + $controller = $this->getController(); + $this->userSession->expects($this->once())->method('getUser')->willReturn(null); + $result = $controller->getVerificationCode('account', false); + + $this->assertSame(Http::STATUS_BAD_REQUEST ,$result->getStatus()); + + } } diff --git a/tests/lib/Accounts/AccountsManagerTest.php b/tests/lib/Accounts/AccountsManagerTest.php index e6c1552fdc0..6cefebdea86 100644 --- a/tests/lib/Accounts/AccountsManagerTest.php +++ b/tests/lib/Accounts/AccountsManagerTest.php @@ -24,6 +24,7 @@ namespace Test\Accounts; use OC\Accounts\AccountManager; +use OCP\BackgroundJob\IJobList; use OCP\IUser; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\GenericEvent; @@ -43,6 +44,9 @@ class AccountsManagerTest extends TestCase { /** @var EventDispatcherInterface | \PHPUnit_Framework_MockObject_MockObject */ private $eventDispatcher; + /** @var IJobList | \PHPUnit_Framework_MockObject_MockObject */ + private $jobList; + /** @var string accounts table name */ private $table = 'accounts'; @@ -51,6 +55,7 @@ class AccountsManagerTest extends TestCase { $this->eventDispatcher = $this->getMockBuilder('Symfony\Component\EventDispatcher\EventDispatcherInterface') ->disableOriginalConstructor()->getMock(); $this->connection = \OC::$server->getDatabaseConnection(); + $this->jobList = $this->getMockBuilder(IJobList::class)->getMock(); } public function tearDown() { @@ -67,7 +72,7 @@ class AccountsManagerTest extends TestCase { */ public function getInstance($mockedMethods = null) { return $this->getMockBuilder('OC\Accounts\AccountManager') - ->setConstructorArgs([$this->connection, $this->eventDispatcher]) + ->setConstructorArgs([$this->connection, $this->eventDispatcher, $this->jobList]) ->setMethods($mockedMethods) ->getMock(); @@ -75,15 +80,24 @@ class AccountsManagerTest extends TestCase { /** * @dataProvider dataTrueFalse + * + * @param array $newData + * @param array $oldData + * @param bool $insertNew + * @param bool $updateExisting */ - public function testUpdateUser($newData, $oldData, $insertNew, $updateExisitng) { - $accountManager = $this->getInstance(['getUser', 'insertNewUser', 'updateExistingUser']); + public function testUpdateUser($newData, $oldData, $insertNew, $updateExisting) { + $accountManager = $this->getInstance(['getUser', 'insertNewUser', 'updateExistingUser', 'updateVerifyStatus', 'checkEmailVerification']); /** @var IUser $user */ $user = $this->createMock(IUser::class); $accountManager->expects($this->once())->method('getUser')->with($user)->willReturn($oldData); - if ($updateExisitng) { + if ($updateExisting) { + $accountManager->expects($this->once())->method('checkEmailVerification') + ->with($oldData, $newData, $user)->willReturn($newData); + $accountManager->expects($this->once())->method('updateVerifyStatus') + ->with($oldData, $newData)->willReturn($newData); $accountManager->expects($this->once())->method('updateExistingUser') ->with($user, $newData); $accountManager->expects($this->never())->method('insertNewUser'); @@ -94,8 +108,10 @@ class AccountsManagerTest extends TestCase { $accountManager->expects($this->never())->method('updateExistingUser'); } - if (!$insertNew && !$updateExisitng) { + if (!$insertNew && !$updateExisting) { $accountManager->expects($this->never())->method('updateExistingUser'); + $accountManager->expects($this->never())->method('checkEmailVerification'); + $accountManager->expects($this->never())->method('updateVerifyStatus'); $accountManager->expects($this->never())->method('insertNewUser'); $this->eventDispatcher->expects($this->never())->method('dispatch'); } else { @@ -133,13 +149,22 @@ class AccountsManagerTest extends TestCase { * @param book $userAlreadyExists */ public function testGetUser($setUser, $setData, $askUser, $expectedData, $userAlreadyExists) { - $accountManager = $this->getInstance(['buildDefaultUserRecord', 'insertNewUser']); + $accountManager = $this->getInstance(['buildDefaultUserRecord', 'insertNewUser', 'addMissingDefaultValues']); if (!$userAlreadyExists) { $accountManager->expects($this->once())->method('buildDefaultUserRecord') ->with($askUser)->willReturn($expectedData); $accountManager->expects($this->once())->method('insertNewUser') ->with($askUser, $expectedData); } + + if(empty($expectedData)) { + $accountManager->expects($this->never())->method('addMissingDefaultValues'); + + } else { + $accountManager->expects($this->once())->method('addMissingDefaultValues')->with($expectedData) + ->willReturn($expectedData); + } + $this->addDummyValuesToTable($setUser, $setData); $this->assertEquals($expectedData, $accountManager->getUser($askUser) @@ -184,6 +209,25 @@ class AccountsManagerTest extends TestCase { $this->assertEquals($data, $dataFromDb); } + public function testAddMissingDefaultValues() { + + $accountManager = $this->getInstance(); + + $input = [ + 'key1' => ['value' => 'value1', 'verified' => '0'], + 'key2' => ['value' => 'value1'], + ]; + + $expected = [ + 'key1' => ['value' => 'value1', 'verified' => '0'], + 'key2' => ['value' => 'value1', 'verified' => '0'], + ]; + + $result = $this->invokePrivate($accountManager, 'addMissingDefaultValues', [$input]); + + $this->assertSame($expected, $result); + } + private function addDummyValuesToTable($uid, $data) { $query = $this->connection->getQueryBuilder(); |