From 9df74aea0a2d38464af9b7e36df1b2d381d7c683 Mon Sep 17 00:00:00 2001 From: Christopher Ng Date: Wed, 9 Jun 2021 16:49:26 +0000 Subject: [PATCH] [WIP] Vuetiful emails Signed-off-by: Christopher Ng --- .../lib/Controller/UsersController.php | 15 +- apps/settings/js/federationsettingsview.js | 2 +- .../lib/Settings/Personal/PersonalInfo.php | 35 +++- .../components/PersonalInfo/EmailSection.vue | 158 ++++++++++++++++++ apps/settings/src/main-personal-info.js | 40 +++++ .../src/service/PersonalInfoService.js | 51 ++++++ .../settings/personal/personal.info.php | 48 +----- apps/settings/webpack.js | 1 + lib/private/Accounts/AccountManager.php | 19 ++- lib/private/Accounts/TAccountsHelper.php | 4 +- lib/public/Accounts/IAccountManager.php | 4 + 11 files changed, 320 insertions(+), 57 deletions(-) create mode 100644 apps/settings/src/components/PersonalInfo/EmailSection.vue create mode 100644 apps/settings/src/main-personal-info.js create mode 100644 apps/settings/src/service/PersonalInfoService.js diff --git a/apps/provisioning_api/lib/Controller/UsersController.php b/apps/provisioning_api/lib/Controller/UsersController.php index 256077e9ae99b..777d7a43d7531 100644 --- a/apps/provisioning_api/lib/Controller/UsersController.php +++ b/apps/provisioning_api/lib/Controller/UsersController.php @@ -740,12 +740,19 @@ public function editUser(string $userId, string $key, string $value): DataRespon $this->config->setUserValue($targetUser->getUID(), 'core', 'locale', $value); break; case IAccountManager::PROPERTY_EMAIL: - if (filter_var($value, FILTER_VALIDATE_EMAIL) || $value === '') { - $targetUser->setEMailAddress($value); - } else { - throw new OCSException('', 102); + $userAccount = $this->accountManager->getAccount($targetUser); + $userProperty = $userAccount->getProperty($key); + if ($userProperty->getValue() !== $value) { + try { + $userProperty->setValue($value); + $this->accountManager->updateAccount($userAccount); + } catch (\InvalidArgumentException $e) { + throw new OCSException('Invalid ' . $e->getMessage(), 102); + } } break; + case IAccountManager::COLLECTION_EMAIL: + break; case IAccountManager::PROPERTY_PHONE: case IAccountManager::PROPERTY_ADDRESS: case IAccountManager::PROPERTY_WEBSITE: diff --git a/apps/settings/js/federationsettingsview.js b/apps/settings/js/federationsettingsview.js index cf2865be1b0a1..b8d2b406fbad7 100644 --- a/apps/settings/js/federationsettingsview.js +++ b/apps/settings/js/federationsettingsview.js @@ -36,7 +36,7 @@ this._inputFields = [ 'displayname', 'phone', - 'email', + // 'email', 'website', 'twitter', 'address', diff --git a/apps/settings/lib/Settings/Personal/PersonalInfo.php b/apps/settings/lib/Settings/Personal/PersonalInfo.php index 94ff867dabac4..4db8c7dd2646b 100644 --- a/apps/settings/lib/Settings/Personal/PersonalInfo.php +++ b/apps/settings/lib/Settings/Personal/PersonalInfo.php @@ -48,6 +48,8 @@ use OCP\IUserManager; use OCP\L10N\IFactory; use OCP\Settings\ISettings; +use OCP\Accounts\IAccountProperty; +use OCP\AppFramework\Services\IInitialState; class PersonalInfo implements ISettings { @@ -65,6 +67,8 @@ class PersonalInfo implements ISettings { private $l10nFactory; /** @var IL10N */ private $l; + /** @var IInitialState */ + private $initialStateService; public function __construct( IConfig $config, @@ -73,7 +77,8 @@ public function __construct( IAccountManager $accountManager, IAppManager $appManager, IFactory $l10nFactory, - IL10N $l + IL10N $l, + IInitialState $initialStateService ) { $this->config = $config; $this->userManager = $userManager; @@ -82,6 +87,7 @@ public function __construct( $this->appManager = $appManager; $this->l10nFactory = $l10nFactory; $this->l = $l; + $this->initialStateService = $initialStateService; } public function getForm(): TemplateResponse { @@ -138,6 +144,33 @@ public function getForm(): TemplateResponse { 'groups' => $this->getGroups($user), ] + $messageParameters + $languageParameters + $localeParameters; + $primaryEmail = [ + 'value' => $account->getProperty(IAccountManager::PROPERTY_EMAIL)->getValue(), + 'scope' => $account->getProperty(IAccountManager::PROPERTY_EMAIL)->getScope(), + 'verified' => $account->getProperty(IAccountManager::PROPERTY_EMAIL)->getVerified(), + 'message' => $messageParameters[IAccountManager::PROPERTY_EMAIL . 'Message'], + ]; + + $additionalEmails = array_map( + fn(IAccountProperty $property) => [ + 'value' => $property->getValue(), + 'scope' => $property->getScope(), + 'verified' => $property->getVerified(), + ], + $account->getPropertyCollection(IAccountManager::COLLECTION_EMAIL)->getProperties() + ); + + $emails = [ + 'primaryEmail' => $primaryEmail, + 'additionalEmails' => $additionalEmails, + ]; + + $accountParameters = [ + 'displayNameChangeSupported' => $user->canChangeDisplayName(), + ]; + + $this->initialStateService->provideInitialState('emails', $emails); + $this->initialStateService->provideInitialState('accountParameters', $accountParameters); return new TemplateResponse('settings', 'settings/personal/personal.info', $parameters, ''); } diff --git a/apps/settings/src/components/PersonalInfo/EmailSection.vue b/apps/settings/src/components/PersonalInfo/EmailSection.vue new file mode 100644 index 0000000000000..0ac51afa9a739 --- /dev/null +++ b/apps/settings/src/components/PersonalInfo/EmailSection.vue @@ -0,0 +1,158 @@ + + + + + + + diff --git a/apps/settings/src/main-personal-info.js b/apps/settings/src/main-personal-info.js new file mode 100644 index 0000000000000..86fa63a5249a5 --- /dev/null +++ b/apps/settings/src/main-personal-info.js @@ -0,0 +1,40 @@ +/** + * @copyright 2021, Christopher Ng + * + * @author Christopher Ng + * + * @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 . + * + */ + +import Vue from 'vue' +import { loadState } from '@nextcloud/initial-state' + +import EmailSection from './components/PersonalInfo/EmailSection' + +// eslint-disable-next-line camelcase +__webpack_nonce__ = btoa(OC.requestToken) + +Vue.prototype.t = t + +const View = Vue.extend(EmailSection) +new View({ + propsData: { + initialEmails: loadState('settings', 'emails'), + accountParams: loadState('settings', 'accountParameters'), + VerificationEnum: Object.freeze({ NOT_VERIFIED: '0', VERIFICATION_IN_PROGRESS: '1', VERIFIED: '2' }), + }, +}).$mount('#vue-emailform') diff --git a/apps/settings/src/service/PersonalInfoService.js b/apps/settings/src/service/PersonalInfoService.js new file mode 100644 index 0000000000000..830f1735590bc --- /dev/null +++ b/apps/settings/src/service/PersonalInfoService.js @@ -0,0 +1,51 @@ +/** + * @copyright 2021, Christopher Ng + * + * @author Christopher Ng + * + * @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 . + * + */ + +import axios from '@nextcloud/axios' +import { getCurrentUser } from '@nextcloud/auth' +import { generateOcsUrl } from '@nextcloud/router' + +export const setPrimaryEmail = async(email) => { + const userId = getCurrentUser().uid + // TODO upgrade @nextcloud/router to v2.0 so we can remove the .slice() trailing slash hack + const url = generateOcsUrl(`cloud/users/${userId}`, 2).slice(0, -1) + + const res = await axios.put(url, { + key: 'email', + value: email, + }) + + return res.data +} + +export const setAdditionalEmails = async(emails) => { + const userId = getCurrentUser().uid + // TODO upgrade @nextcloud/router to v2.0 so we can remove the .slice() trailing slash hack + const url = generateOcsUrl(`cloud/users/${userId}`, 2).slice(0, -1) + + const res = await axios.put(url, { + key: 'additional_mail', + value: emails, + }) + + return res.data +} diff --git a/apps/settings/templates/settings/personal/personal.info.php b/apps/settings/templates/settings/personal/personal.info.php index 6f8516e6437f9..9aef8bc8d86e1 100644 --- a/apps/settings/templates/settings/personal/personal.info.php +++ b/apps/settings/templates/settings/personal/personal.info.php @@ -31,6 +31,7 @@ 'federationsettingsview', 'federationscopemenu', 'settings/personalInfo', + 'vue-settings-personal-info', ]); ?> @@ -126,52 +127,7 @@
-
-

- - - - - - -

-
- -
- - placeholder="t('Your email address')); ?>" - autocomplete="on" autocapitalize="none" autocorrect="off" /> - - - - t('No email address set')); - }?> - - - t('For password reset and notifications')); ?> - - -
+
diff --git a/apps/settings/webpack.js b/apps/settings/webpack.js index 756a748ae1b4b..12ac464fed740 100644 --- a/apps/settings/webpack.js +++ b/apps/settings/webpack.js @@ -32,6 +32,7 @@ module.exports = { 'settings-personal-security': path.join(__dirname, 'src', 'main-personal-security'), 'settings-personal-webauthn': path.join(__dirname, 'src', 'main-personal-webauth'), 'settings-nextcloud-pdf': path.join(__dirname, 'src', 'main-nextcloud-pdf'), + 'settings-personal-info': path.join(__dirname, 'src', 'main-personal-info'), }, output: { path: path.resolve(__dirname, './js'), diff --git a/lib/private/Accounts/AccountManager.php b/lib/private/Accounts/AccountManager.php index 4d75c94346b42..6440d18d6271a 100644 --- a/lib/private/Accounts/AccountManager.php +++ b/lib/private/Accounts/AccountManager.php @@ -405,6 +405,12 @@ protected function addMissingDefaultValues(array $userData) { } } + foreach ($this::COLLECTION_PROPERTIES as $property) { + if (!isset($userData[$property])) { + $userData[$property] = new AccountPropertyCollection($property); + } + } + return $userData; } @@ -566,6 +572,11 @@ protected function buildDefaultUserRecord(IUser $user) { 'scope' => self::SCOPE_FEDERATED, 'verified' => self::NOT_VERIFIED, ], + self::COLLECTION_EMAIL => + [ + // TODO implement the correct way + 'properties' => [], + ], self::PROPERTY_AVATAR => [ 'scope' => self::SCOPE_FEDERATED @@ -588,7 +599,11 @@ protected function buildDefaultUserRecord(IUser $user) { private function parseAccountData(IUser $user, $data): Account { $account = new Account($user); foreach ($data as $property => $accountData) { - $account->setProperty($property, $accountData['value'] ?? '', $accountData['scope'] ?? self::SCOPE_LOCAL, $accountData['verified'] ?? self::NOT_VERIFIED); + if (!$this->isCollection($property)) { + $account->setProperty($property, $accountData['value'] ?? '', $accountData['scope'] ?? self::SCOPE_LOCAL, $accountData['verified'] ?? self::NOT_VERIFIED); + } else { + $account->setPropertyCollection(new AccountPropertyCollection($property)); + } } return $account; } @@ -600,7 +615,7 @@ public function getAccount(IUser $user): IAccount { public function updateAccount(IAccount $account): void { $data = []; - foreach ($account->getProperties() as $property) { + foreach ($account->getAllProperties() as $property) { $data[$property->getName()] = [ 'value' => $property->getValue(), 'scope' => $property->getScope(), diff --git a/lib/private/Accounts/TAccountsHelper.php b/lib/private/Accounts/TAccountsHelper.php index 530204b451fb7..c6d9b9b0a5a59 100644 --- a/lib/private/Accounts/TAccountsHelper.php +++ b/lib/private/Accounts/TAccountsHelper.php @@ -31,9 +31,7 @@ trait TAccountsHelper { protected function isCollection(string $propertyName): bool { return in_array($propertyName, - [ - IAccountManager::COLLECTION_EMAIL, - ], + IAccountManager::COLLECTION_PROPERTIES, true ); } diff --git a/lib/public/Accounts/IAccountManager.php b/lib/public/Accounts/IAccountManager.php index 9418e07ec972f..b497cf5b054a1 100644 --- a/lib/public/Accounts/IAccountManager.php +++ b/lib/public/Accounts/IAccountManager.php @@ -98,6 +98,10 @@ interface IAccountManager { public const COLLECTION_EMAIL = 'additional_mail'; + public const COLLECTION_PROPERTIES = [ + self::COLLECTION_EMAIL, + ]; + public const NOT_VERIFIED = '0'; public const VERIFICATION_IN_PROGRESS = '1'; public const VERIFIED = '2';