summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMorris Jobke <hey@morrisjobke.de>2017-04-29 00:38:02 -0300
committerGitHub <noreply@github.com>2017-04-29 00:38:02 -0300
commit130780056109d8b65e7b9abe40c89e26a75c5e35 (patch)
tree488ad0c7d7c9bf6339100c1628b3b12433d8e0f3
parent2a773310dc58adcd299c1f7ae37e834cbae3b027 (diff)
parenta0bf706983007a69aa2adb0a0af35e265ff1b0d8 (diff)
downloadnextcloud-server-130780056109d8b65e7b9abe40c89e26a75c5e35.tar.gz
nextcloud-server-130780056109d8b65e7b9abe40c89e26a75c5e35.zip
Merge pull request #3869 from nextcloud/verify-personal-data
Verify personal data
-rw-r--r--apps/federatedfilesharing/js/settings-personal.js3
-rw-r--r--apps/files_sharing/lib/Controller/ShareesAPIController.php4
-rw-r--r--apps/lookup_server_connector/appinfo/app.php16
-rw-r--r--apps/lookup_server_connector/lib/BackgroundJobs/RetryJob.php34
-rw-r--r--apps/lookup_server_connector/lib/UpdateLookupServer.php36
-rw-r--r--config/config.sample.php5
-rw-r--r--core/img/actions/verified.svg4
-rw-r--r--core/img/actions/verify.svg4
-rw-r--r--core/img/actions/verifying.svg4
-rw-r--r--lib/composer/composer/autoload_classmap.php1
-rw-r--r--lib/composer/composer/autoload_static.php1
-rw-r--r--lib/private/Accounts/AccountManager.php127
-rw-r--r--lib/private/Accounts/Hooks.php3
-rw-r--r--settings/BackgroundJobs/VerifyUserData.php273
-rw-r--r--settings/Controller/UsersController.php114
-rw-r--r--settings/css/settings.css29
-rw-r--r--settings/js/federationsettingsview.js40
-rw-r--r--settings/js/personal.js54
-rw-r--r--settings/personal.php28
-rw-r--r--settings/routes.php1
-rw-r--r--settings/templates/personal.php67
-rw-r--r--tests/Settings/Controller/UsersControllerTest.php109
-rw-r--r--tests/lib/Accounts/AccountsManagerTest.php56
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();