From 9f313bbf1ba4084a626b4fe7b8931b18f11776f8 Mon Sep 17 00:00:00 2001 From: Christopher Ng Date: Tue, 7 Sep 2021 14:59:56 +0000 Subject: [PATCH] [WHIP] Workable Hack In Progress Signed-off-by: Christopher Ng --- apps/files_sharing/public.php | 2 +- .../Controller/LoginRedirectorController.php | 2 +- .../lib/Controller/AUserData.php | 1 + .../lib/Controller/UsersController.php | 40 +- .../tests/Controller/UsersControllerTest.php | 42 ++ apps/settings/css/settings.scss | 8 +- .../lib/Settings/Personal/PersonalInfo.php | 18 +- .../DisplayNameSection/DisplayName.vue | 2 + .../ProfileSection/ProfileCheckbox.vue | 113 ++++ .../ProfileSection/ProfileSection.vue | 126 +++++ .../PersonalInfo/shared/HeaderBar.vue | 4 +- .../src/constants/AccountPropertyConstants.js | 17 +- apps/settings/src/main-personal-info.js | 3 + .../service/PersonalInfo/ProfileService.js | 48 ++ apps/settings/src/utils/validate.js | 14 +- .../settings/personal/personal.info.php | 3 + .../BeforeTemplateRenderedListener.php | 48 +- apps/user_status/src/UserStatus.vue | 118 ++++- core/Controller/ProfileController.php | 161 ++++++ core/css/css-variables.scss | 2 + core/css/variables.scss | 4 + core/routes.php | 1 + core/src/components/UserMenu.js | 12 +- core/src/profile.js | 45 ++ core/src/views/Profile.vue | 490 ++++++++++++++++++ core/templates/{404.php => 404-file.php} | 0 core/templates/404-page.php | 26 + core/templates/profile.php | 5 + core/webpack.js | 1 + lib/private/Accounts/AccountManager.php | 28 + lib/public/Accounts/IAccountManager.php | 6 + 31 files changed, 1351 insertions(+), 39 deletions(-) create mode 100644 apps/settings/src/components/PersonalInfo/ProfileSection/ProfileCheckbox.vue create mode 100644 apps/settings/src/components/PersonalInfo/ProfileSection/ProfileSection.vue create mode 100644 apps/settings/src/service/PersonalInfo/ProfileService.js create mode 100644 core/Controller/ProfileController.php create mode 100644 core/src/profile.js create mode 100644 core/src/views/Profile.vue rename core/templates/{404.php => 404-file.php} (100%) create mode 100644 core/templates/404-page.php create mode 100644 core/templates/profile.php diff --git a/apps/files_sharing/public.php b/apps/files_sharing/public.php index 8734042d87797..49d85c07ff0c6 100644 --- a/apps/files_sharing/public.php +++ b/apps/files_sharing/public.php @@ -38,6 +38,6 @@ header('Location: ' . $urlGenerator->linkToRoute($route, ['token' => $token])); } else { http_response_code(404); - $tmpl = new OCP\Template('', '404', 'guest'); + $tmpl = new OCP\Template('', '404-file', 'guest'); print_unescaped($tmpl->fetchPage()); } diff --git a/apps/oauth2/lib/Controller/LoginRedirectorController.php b/apps/oauth2/lib/Controller/LoginRedirectorController.php index 57f18a97f85c5..48392d3c56958 100644 --- a/apps/oauth2/lib/Controller/LoginRedirectorController.php +++ b/apps/oauth2/lib/Controller/LoginRedirectorController.php @@ -88,7 +88,7 @@ public function authorize($client_id, $params = [ 'content' => $this->l->t('Your client is not authorized to connect. Please inform the administrator of your client.'), ]; - return new TemplateResponse('core', '404', $params, 'guest'); + return new TemplateResponse('core', '404-file', $params, 'guest'); } if ($response_type !== 'code') { diff --git a/apps/provisioning_api/lib/Controller/AUserData.php b/apps/provisioning_api/lib/Controller/AUserData.php index e358d28206186..01e04f0163fcb 100644 --- a/apps/provisioning_api/lib/Controller/AUserData.php +++ b/apps/provisioning_api/lib/Controller/AUserData.php @@ -174,6 +174,7 @@ protected function getUserData(string $userId, bool $includeScopes = false): arr IAccountManager::PROPERTY_ADDRESS, IAccountManager::PROPERTY_WEBSITE, IAccountManager::PROPERTY_TWITTER, + IAccountManager::PROPERTY_PROFILE_ENABLED, ] as $propertyName) { $property = $userAccount->getProperty($propertyName); $data[$propertyName] = $property->getValue(); diff --git a/apps/provisioning_api/lib/Controller/UsersController.php b/apps/provisioning_api/lib/Controller/UsersController.php index a0eda5848ec73..cba134b4f3fc6 100644 --- a/apps/provisioning_api/lib/Controller/UsersController.php +++ b/apps/provisioning_api/lib/Controller/UsersController.php @@ -594,6 +594,11 @@ public function getEditableFieldsForUser(string $userId): DataResponse { $permittedFields[] = IAccountManager::PROPERTY_ADDRESS; $permittedFields[] = IAccountManager::PROPERTY_WEBSITE; $permittedFields[] = IAccountManager::PROPERTY_TWITTER; + $permittedFields[] = IAccountManager::PROPERTY_COMPANY; + $permittedFields[] = IAccountManager::PROPERTY_JOB_TITLE; + $permittedFields[] = IAccountManager::PROPERTY_HEADLINE; + $permittedFields[] = IAccountManager::PROPERTY_BIOGRAPHY; + $permittedFields[] = IAccountManager::PROPERTY_PROFILE_ENABLED; return new DataResponse($permittedFields); } @@ -693,11 +698,11 @@ public function editUserMultiValue( * * @param string $userId * @param string $key - * @param string $value + * @param string|bool $value * @return DataResponse * @throws OCSException */ - public function editUser(string $userId, string $key, string $value): DataResponse { + public function editUser(string $userId, string $key, string|bool $value): DataResponse { $currentLoggedInUser = $this->userSession->getUser(); $targetUser = $this->userManager->get($userId); @@ -737,6 +742,11 @@ public function editUser(string $userId, string $key, string $value): DataRespon $permittedFields[] = IAccountManager::PROPERTY_ADDRESS; $permittedFields[] = IAccountManager::PROPERTY_WEBSITE; $permittedFields[] = IAccountManager::PROPERTY_TWITTER; + $permittedFields[] = IAccountManager::PROPERTY_COMPANY; + $permittedFields[] = IAccountManager::PROPERTY_JOB_TITLE; + $permittedFields[] = IAccountManager::PROPERTY_HEADLINE; + $permittedFields[] = IAccountManager::PROPERTY_BIOGRAPHY; + $permittedFields[] = IAccountManager::PROPERTY_PROFILE_ENABLED; $permittedFields[] = IAccountManager::PROPERTY_PHONE . self::SCOPE_SUFFIX; $permittedFields[] = IAccountManager::PROPERTY_ADDRESS . self::SCOPE_SUFFIX; $permittedFields[] = IAccountManager::PROPERTY_WEBSITE . self::SCOPE_SUFFIX; @@ -768,6 +778,11 @@ public function editUser(string $userId, string $key, string $value): DataRespon $permittedFields[] = IAccountManager::PROPERTY_ADDRESS; $permittedFields[] = IAccountManager::PROPERTY_WEBSITE; $permittedFields[] = IAccountManager::PROPERTY_TWITTER; + $permittedFields[] = IAccountManager::PROPERTY_COMPANY; + $permittedFields[] = IAccountManager::PROPERTY_JOB_TITLE; + $permittedFields[] = IAccountManager::PROPERTY_HEADLINE; + $permittedFields[] = IAccountManager::PROPERTY_BIOGRAPHY; + $permittedFields[] = IAccountManager::PROPERTY_PROFILE_ENABLED; $permittedFields[] = 'quota'; } else { // No rights @@ -863,6 +878,10 @@ public function editUser(string $userId, string $key, string $value): DataRespon case IAccountManager::PROPERTY_ADDRESS: case IAccountManager::PROPERTY_WEBSITE: case IAccountManager::PROPERTY_TWITTER: + case IAccountManager::PROPERTY_COMPANY: + case IAccountManager::PROPERTY_JOB_TITLE: + case IAccountManager::PROPERTY_HEADLINE: + case IAccountManager::PROPERTY_BIOGRAPHY: $userAccount = $this->accountManager->getAccount($targetUser); try { $userProperty = $userAccount->getProperty($key); @@ -881,6 +900,23 @@ public function editUser(string $userId, string $key, string $value): DataRespon } $this->accountManager->updateAccount($userAccount); break; + case IAccountManager::PROPERTY_PROFILE_ENABLED: + if (!is_bool($value)) { + throw new OCSException('Invalid value, value must be a boolean', 102); + } + $value = $value === true ? '1' : '0'; + + $userAccount = $this->accountManager->getAccount($targetUser); + try { + $userProperty = $userAccount->getProperty($key); + if ($userProperty->getValue() !== $value) { + $userProperty->setValue($value); + } + } catch (PropertyDoesNotExistException $e) { + $userAccount->setProperty($key, $value, IAccountManager::SCOPE_LOCAL, IAccountManager::NOT_VERIFIED); + } + $this->accountManager->updateAccount($userAccount); + break; case IAccountManager::PROPERTY_DISPLAYNAME . self::SCOPE_SUFFIX: case IAccountManager::PROPERTY_EMAIL . self::SCOPE_SUFFIX: case IAccountManager::PROPERTY_PHONE . self::SCOPE_SUFFIX: diff --git a/apps/provisioning_api/tests/Controller/UsersControllerTest.php b/apps/provisioning_api/tests/Controller/UsersControllerTest.php index cc638c89a6393..d44917c388208 100644 --- a/apps/provisioning_api/tests/Controller/UsersControllerTest.php +++ b/apps/provisioning_api/tests/Controller/UsersControllerTest.php @@ -998,6 +998,10 @@ public function testGetUserDataAsAdmin() { IAccountManager::PROPERTY_PHONE => ['value' => 'phone'], IAccountManager::PROPERTY_TWITTER => ['value' => 'twitter'], IAccountManager::PROPERTY_WEBSITE => ['value' => 'website'], + IAccountManager::PROPERTY_COMPANY => ['value' => 'company'], + IAccountManager::PROPERTY_JOB_TITLE => ['value' => 'job_title'], + IAccountManager::PROPERTY_HEADLINE => ['value' => 'headline'], + IAccountManager::PROPERTY_BIOGRAPHY => ['value' => 'biography'], ]); $this->config ->expects($this->at(0)) @@ -1067,6 +1071,11 @@ public function testGetUserDataAsAdmin() { 'setPassword' => true, ], 'additional_mail' => [], + 'company' => 'company', + 'job_title' => 'job_title', + 'headline' => 'headline', + 'biography' => 'biography', + 'profile_enabled' => '1', ]; $this->assertEquals($expected, $this->invokePrivate($this->api, 'getUserData', ['UID'])); } @@ -1165,6 +1174,10 @@ public function testGetUserDataAsSubAdminAndUserIsAccessible() { IAccountManager::PROPERTY_PHONE => ['value' => 'phone'], IAccountManager::PROPERTY_TWITTER => ['value' => 'twitter'], IAccountManager::PROPERTY_WEBSITE => ['value' => 'website'], + IAccountManager::PROPERTY_COMPANY => ['value' => 'company'], + IAccountManager::PROPERTY_JOB_TITLE => ['value' => 'job_title'], + IAccountManager::PROPERTY_HEADLINE => ['value' => 'headline'], + IAccountManager::PROPERTY_BIOGRAPHY => ['value' => 'biography'], ]); $this->l10nFactory @@ -1195,6 +1208,11 @@ public function testGetUserDataAsSubAdminAndUserIsAccessible() { 'setPassword' => true, ], 'additional_mail' => [], + 'company' => 'company', + 'job_title' => 'job_title', + 'headline' => 'headline', + 'biography' => 'biography', + 'profile_enabled' => '1', ]; $this->assertEquals($expected, $this->invokePrivate($this->api, 'getUserData', ['UID'])); } @@ -1332,6 +1350,10 @@ public function testGetUserDataAsSubAdminSelfLookup() { IAccountManager::PROPERTY_PHONE => ['value' => 'phone'], IAccountManager::PROPERTY_TWITTER => ['value' => 'twitter'], IAccountManager::PROPERTY_WEBSITE => ['value' => 'website'], + IAccountManager::PROPERTY_COMPANY => ['value' => 'company'], + IAccountManager::PROPERTY_JOB_TITLE => ['value' => 'job_title'], + IAccountManager::PROPERTY_HEADLINE => ['value' => 'headline'], + IAccountManager::PROPERTY_BIOGRAPHY => ['value' => 'biography'], ]); $this->l10nFactory @@ -1361,6 +1383,11 @@ public function testGetUserDataAsSubAdminSelfLookup() { 'setPassword' => false, ], 'additional_mail' => [], + 'company' => 'company', + 'job_title' => 'job_title', + 'headline' => 'headline', + 'biography' => 'biography', + 'profile_enabled' => '1', ]; $this->assertEquals($expected, $this->invokePrivate($this->api, 'getUserData', ['UID'])); } @@ -3875,6 +3902,11 @@ public function dataGetEditableFields() { IAccountManager::PROPERTY_ADDRESS, IAccountManager::PROPERTY_WEBSITE, IAccountManager::PROPERTY_TWITTER, + IAccountManager::PROPERTY_COMPANY, + IAccountManager::PROPERTY_JOB_TITLE, + IAccountManager::PROPERTY_HEADLINE, + IAccountManager::PROPERTY_BIOGRAPHY, + IAccountManager::PROPERTY_PROFILE_ENABLED, ]], [true, ISetDisplayNameBackend::class, [ IAccountManager::PROPERTY_DISPLAYNAME, @@ -3884,6 +3916,11 @@ public function dataGetEditableFields() { IAccountManager::PROPERTY_ADDRESS, IAccountManager::PROPERTY_WEBSITE, IAccountManager::PROPERTY_TWITTER, + IAccountManager::PROPERTY_COMPANY, + IAccountManager::PROPERTY_JOB_TITLE, + IAccountManager::PROPERTY_HEADLINE, + IAccountManager::PROPERTY_BIOGRAPHY, + IAccountManager::PROPERTY_PROFILE_ENABLED, ]], [true, UserInterface::class, [ IAccountManager::PROPERTY_EMAIL, @@ -3892,6 +3929,11 @@ public function dataGetEditableFields() { IAccountManager::PROPERTY_ADDRESS, IAccountManager::PROPERTY_WEBSITE, IAccountManager::PROPERTY_TWITTER, + IAccountManager::PROPERTY_COMPANY, + IAccountManager::PROPERTY_JOB_TITLE, + IAccountManager::PROPERTY_HEADLINE, + IAccountManager::PROPERTY_BIOGRAPHY, + IAccountManager::PROPERTY_PROFILE_ENABLED, ]], ]; } diff --git a/apps/settings/css/settings.scss b/apps/settings/css/settings.scss index 53a9a28c0800b..6a00519f797a7 100644 --- a/apps/settings/css/settings.scss +++ b/apps/settings/css/settings.scss @@ -114,7 +114,13 @@ input { .profile-settings-container { display: inline-grid; grid-template-columns: 1fr; - grid-template-rows: 1fr 2fr 1fr; + grid-template-rows: 1fr 1fr 2fr; + + #locale { + h3 { + height: 32px; + } + } } .personal-show-container { diff --git a/apps/settings/lib/Settings/Personal/PersonalInfo.php b/apps/settings/lib/Settings/Personal/PersonalInfo.php index 0a84f20f51352..c0a2b6a0a9fee 100644 --- a/apps/settings/lib/Settings/Personal/PersonalInfo.php +++ b/apps/settings/lib/Settings/Personal/PersonalInfo.php @@ -146,9 +146,11 @@ public function getForm(): TemplateResponse { ] + $messageParameters + $languageParameters + $localeParameters; $personalInfoParameters = [ + 'userId' => $uid, 'displayNames' => $this->getDisplayNames($account), 'emails' => $this->getEmails($account), 'languages' => $this->getLanguages($user), + 'profileEnabled' => $this->getProfileEnabled($account), ]; $accountParameters = [ @@ -245,7 +247,7 @@ function (IAccountProperty $property) { 'verified' => $property->getVerified(), ]; }, - $account->getPropertyCollection(IAccountManager::COLLECTION_EMAIL)->getProperties() + $account->getPropertyCollection(IAccountManager::COLLECTION_EMAIL)->getProperties(), ); $emails = [ @@ -359,4 +361,18 @@ private function getMessageParameters(IAccount $account): array { } return $messageParameters; } + + /** + * returns the profile enabled state + * + * @param IAccount $account + * @return bool + */ + private function getProfileEnabled(IAccount $account): bool { + return filter_var( + $account->getProperty(IAccountManager::PROPERTY_PROFILE_ENABLED)->getValue(), + FILTER_VALIDATE_BOOLEAN, + FILTER_NULL_ON_FAILURE, + ); + } } diff --git a/apps/settings/src/components/PersonalInfo/DisplayNameSection/DisplayName.vue b/apps/settings/src/components/PersonalInfo/DisplayNameSection/DisplayName.vue index c751d616a89ad..67b94dbe8e9ee 100644 --- a/apps/settings/src/components/PersonalInfo/DisplayNameSection/DisplayName.vue +++ b/apps/settings/src/components/PersonalInfo/DisplayNameSection/DisplayName.vue @@ -45,6 +45,7 @@ + + diff --git a/apps/settings/src/components/PersonalInfo/ProfileSection/ProfileSection.vue b/apps/settings/src/components/PersonalInfo/ProfileSection/ProfileSection.vue new file mode 100644 index 0000000000000..a5daf8400de03 --- /dev/null +++ b/apps/settings/src/components/PersonalInfo/ProfileSection/ProfileSection.vue @@ -0,0 +1,126 @@ + + + + + + + diff --git a/apps/settings/src/components/PersonalInfo/shared/HeaderBar.vue b/apps/settings/src/components/PersonalInfo/shared/HeaderBar.vue index 5388984494da6..67f84bfec2daa 100644 --- a/apps/settings/src/components/PersonalInfo/shared/HeaderBar.vue +++ b/apps/settings/src/components/PersonalInfo/shared/HeaderBar.vue @@ -83,7 +83,7 @@ export default { }, labelFor: { type: String, - required: true, + default: '', }, scope: { type: String, @@ -124,7 +124,7 @@ export default { color: var(--color-text-light); &.setting-property { - height: 38px; + height: 32px; } label { diff --git a/apps/settings/src/constants/AccountPropertyConstants.js b/apps/settings/src/constants/AccountPropertyConstants.js index 0288ee679ce66..34f93185a9112 100644 --- a/apps/settings/src/constants/AccountPropertyConstants.js +++ b/apps/settings/src/constants/AccountPropertyConstants.js @@ -21,7 +21,7 @@ */ /* - * SYNC to be kept in sync with lib/public/Accounts/IAccountManager.php + * SYNC to be kept in sync with `lib/public/Accounts/IAccountManager.php` */ import { translate as t } from '@nextcloud/l10n' @@ -30,10 +30,15 @@ import { translate as t } from '@nextcloud/l10n' export const ACCOUNT_PROPERTY_ENUM = Object.freeze({ ADDRESS: 'address', AVATAR: 'avatar', + BIOGRAPHY: 'biography', + COMPANY: 'company', DISPLAYNAME: 'displayname', + HEADLINE: 'headline', + JOB_TITLE: 'job_title', EMAIL: 'email', EMAIL_COLLECTION: 'additional_mail', PHONE: 'phone', + PROFILE_ENABLED: 'profile_enabled', TWITTER: 'twitter', WEBSITE: 'website', }) @@ -42,10 +47,15 @@ export const ACCOUNT_PROPERTY_ENUM = Object.freeze({ export const ACCOUNT_PROPERTY_READABLE_ENUM = Object.freeze({ ADDRESS: t('settings', 'Address'), AVATAR: t('settings', 'Avatar'), + BIOGRAPHY: t('settings', 'Biography'), + COMPANY: t('settings', 'Company'), DISPLAYNAME: t('settings', 'Full name'), + HEADLINE: t('settings', 'Headline'), + JOB_TITLE: t('settings', 'Job Title'), EMAIL: t('settings', 'Email'), EMAIL_COLLECTION: t('settings', 'Additional email'), PHONE: t('settings', 'Phone number'), + PROFILE_ENABLED: t('settings', 'Profile'), TWITTER: t('settings', 'Twitter'), WEBSITE: t('settings', 'Website'), }) @@ -72,10 +82,15 @@ export const SCOPE_ENUM = Object.freeze({ export const PROPERTY_READABLE_SUPPORTED_SCOPES_ENUM = Object.freeze({ [ACCOUNT_PROPERTY_READABLE_ENUM.ADDRESS]: [SCOPE_ENUM.LOCAL, SCOPE_ENUM.PRIVATE], [ACCOUNT_PROPERTY_READABLE_ENUM.AVATAR]: [SCOPE_ENUM.LOCAL, SCOPE_ENUM.PRIVATE], + [ACCOUNT_PROPERTY_READABLE_ENUM.BIOGRAPHY]: [SCOPE_ENUM.LOCAL], + [ACCOUNT_PROPERTY_READABLE_ENUM.COMPANY]: [SCOPE_ENUM.LOCAL], [ACCOUNT_PROPERTY_READABLE_ENUM.DISPLAYNAME]: [SCOPE_ENUM.LOCAL], + [ACCOUNT_PROPERTY_READABLE_ENUM.HEADLINE]: [SCOPE_ENUM.LOCAL], + [ACCOUNT_PROPERTY_READABLE_ENUM.JOB_TITLE]: [SCOPE_ENUM.LOCAL], [ACCOUNT_PROPERTY_READABLE_ENUM.EMAIL]: [SCOPE_ENUM.LOCAL], [ACCOUNT_PROPERTY_READABLE_ENUM.EMAIL_COLLECTION]: [SCOPE_ENUM.LOCAL], [ACCOUNT_PROPERTY_READABLE_ENUM.PHONE]: [SCOPE_ENUM.LOCAL, SCOPE_ENUM.PRIVATE], + [ACCOUNT_PROPERTY_READABLE_ENUM.PROFILE_ENABLED]: [SCOPE_ENUM.LOCAL], [ACCOUNT_PROPERTY_READABLE_ENUM.TWITTER]: [SCOPE_ENUM.LOCAL, SCOPE_ENUM.PRIVATE], [ACCOUNT_PROPERTY_READABLE_ENUM.WEBSITE]: [SCOPE_ENUM.LOCAL, SCOPE_ENUM.PRIVATE], }) diff --git a/apps/settings/src/main-personal-info.js b/apps/settings/src/main-personal-info.js index 78de03cf7cf4d..25a8d533590c4 100644 --- a/apps/settings/src/main-personal-info.js +++ b/apps/settings/src/main-personal-info.js @@ -30,6 +30,7 @@ import logger from './logger' import DisplayNameSection from './components/PersonalInfo/DisplayNameSection/DisplayNameSection' import EmailSection from './components/PersonalInfo/EmailSection/EmailSection' import LanguageSection from './components/PersonalInfo/LanguageSection/LanguageSection' +import ProfileSection from './components/PersonalInfo/ProfileSection/ProfileSection' __webpack_nonce__ = btoa(getRequestToken()) @@ -45,7 +46,9 @@ Vue.mixin({ const DisplayNameView = Vue.extend(DisplayNameSection) const EmailView = Vue.extend(EmailSection) const LanguageView = Vue.extend(LanguageSection) +const ProfileView = Vue.extend(ProfileSection) new DisplayNameView().$mount('#vue-displaynamesection') new EmailView().$mount('#vue-emailsection') new LanguageView().$mount('#vue-languagesection') +new ProfileView().$mount('#vue-profilesection') diff --git a/apps/settings/src/service/PersonalInfo/ProfileService.js b/apps/settings/src/service/PersonalInfo/ProfileService.js new file mode 100644 index 0000000000000..ac085ef2aa365 --- /dev/null +++ b/apps/settings/src/service/PersonalInfo/ProfileService.js @@ -0,0 +1,48 @@ +/** + * @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' +import confirmPassword from '@nextcloud/password-confirmation' + +import { ACCOUNT_PROPERTY_ENUM } from '../../constants/AccountPropertyConstants' + +/** + * Save the enabled state of the user profile + * + * @param {boolean} isEnabled whether the profile page is enabled or not + * @returns {object} + */ +export const saveEnableProfile = async(isEnabled) => { + const userId = getCurrentUser().uid + const url = generateOcsUrl('cloud/users/{userId}', { userId }) + + await confirmPassword() + + const res = await axios.put(url, { + key: ACCOUNT_PROPERTY_ENUM.PROFILE_ENABLED, + value: isEnabled, + }) + + return res.data +} diff --git a/apps/settings/src/utils/validate.js b/apps/settings/src/utils/validate.js index e2c7d82da2ff6..f3401678c73f8 100644 --- a/apps/settings/src/utils/validate.js +++ b/apps/settings/src/utils/validate.js @@ -59,10 +59,20 @@ export function validateEmail(input) { /** * Validate the language input * - * @param {string} input the input + * @param {object} input the input * @returns {boolean} */ export function validateLanguage(input) { return input.code !== '' - && input.name + && Boolean(input.name) +} + +/** + * Validate the enable profile input + * + * @param {boolean} input the input + * @returns {boolean} + */ +export function validateEnableProfile(input) { + return typeof input === 'boolean' } diff --git a/apps/settings/templates/settings/personal/personal.info.php b/apps/settings/templates/settings/personal/personal.info.php index 7484870a93677..359f7a2369719 100644 --- a/apps/settings/templates/settings/personal/personal.info.php +++ b/apps/settings/templates/settings/personal/personal.info.php @@ -244,6 +244,9 @@
+
+
+
diff --git a/apps/user_status/lib/Listener/BeforeTemplateRenderedListener.php b/apps/user_status/lib/Listener/BeforeTemplateRenderedListener.php index 86e151affdf6e..fc183500ed207 100644 --- a/apps/user_status/lib/Listener/BeforeTemplateRenderedListener.php +++ b/apps/user_status/lib/Listener/BeforeTemplateRenderedListener.php @@ -26,6 +26,9 @@ */ namespace OCA\UserStatus\Listener; +use OCP\Accounts\IAccount; +use OCP\Accounts\IAccountManager; +use OCP\IUserSession; use OCA\UserStatus\AppInfo\Application; use OCA\UserStatus\Service\JSDataService; use OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent; @@ -33,9 +36,18 @@ use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; use OCP\IInitialStateService; +use OCP\User\Events\BeforeUserLoggedInEvent; +use OCP\User\Events\PostLoginEvent; +use OCP\User\Events\UserLiveStatusEvent; class BeforeTemplateRenderedListener implements IEventListener { + /** @var IAccountManager */ + private $accountManager; + + /** @var IUserSession */ + private $userSession; + /** @var IInitialStateService */ private $initialState; @@ -45,11 +57,19 @@ class BeforeTemplateRenderedListener implements IEventListener { /** * BeforeTemplateRenderedListener constructor. * + * @param IAccountManager $accountManager + * @param IUserSession $userSession * @param IInitialStateService $initialState * @param JSDataService $jsDataService */ - public function __construct(IInitialStateService $initialState, - JSDataService $jsDataService) { + public function __construct( + IAccountManager $accountManager, + IUserSession $userSession, + IInitialStateService $initialState, + JSDataService $jsDataService + ) { + $this->accountManager = $accountManager; + $this->userSession = $userSession; $this->initialState = $initialState; $this->jsDataService = $jsDataService; } @@ -58,6 +78,12 @@ public function __construct(IInitialStateService $initialState, * @inheritDoc */ public function handle(Event $event): void { + $user = $this->userSession->getUser(); + if ($user === null) { + return; + } + $account = $this->accountManager->getAccount($user); + if (!($event instanceof BeforeTemplateRenderedEvent)) { // Unrelated return; @@ -71,7 +97,25 @@ public function handle(Event $event): void { return $this->jsDataService; }); + $this->initialState->provideLazyInitialState(Application::APP_ID, 'profileEnabled', function () use ($account) { + return ['profileEnabled' => $this->getProfileEnabled($account)]; + }); + \OCP\Util::addScript('user_status', 'user-status-menu'); \OCP\Util::addStyle('user_status', 'user-status-menu'); } + + /** + * returns the profile enabled state + * + * @param IAccount $account + * @return bool + */ + private function getProfileEnabled(IAccount $account): bool { + return filter_var( + $account->getProperty(IAccountManager::PROPERTY_PROFILE_ENABLED)->getValue(), + FILTER_VALIDATE_BOOLEAN, + FILTER_NULL_ON_FAILURE, + ); + } } diff --git a/apps/user_status/src/UserStatus.vue b/apps/user_status/src/UserStatus.vue index 05748532cd5bc..26b7483dff51f 100644 --- a/apps/user_status/src/UserStatus.vue +++ b/apps/user_status/src/UserStatus.vue @@ -23,12 +23,20 @@
  • - - {{ displayName }} - + :href="profilePageLink" + @click="loadProfilePage"> +
    +
    {{ displayName }}
    +
    +
    +
    +
    + {{ t('user_status', 'View profile') }} +
    + import { getCurrentUser } from '@nextcloud/auth' +import { subscribe, unsubscribe } from '@nextcloud/event-bus' +import { loadState } from '@nextcloud/initial-state' +import { generateUrl } from '@nextcloud/router' import debounce from 'debounce' import { sendHeartbeat } from './services/heartbeatService' import OnlineStatusMixin from './mixins/OnlineStatusMixin' +const { profileEnabled } = loadState('user_status', 'profileEnabled', false) + export default { name: 'UserStatus', @@ -72,21 +85,30 @@ export default { data() { return { - isModalOpen: false, + displayName: getCurrentUser().displayName, heartbeatInterval: null, - setAwayTimeout: null, - mouseMoveListener: null, isAway: false, + isModalOpen: false, + loadingProfilePage: false, + mouseMoveListener: null, + profileEnabled, + setAwayTimeout: null, } }, computed: { /** - * The display-name of the current user + * The profile page link * - * @returns {String} + * @returns {String|null} */ - displayName() { - return getCurrentUser().displayName + profilePageLink() { + if (this.profileEnabled) { + return generateUrl('/u/{userId}', { userId: getCurrentUser().uid }) + } + // Since an anchor element is used rather than a button, + // this hack removes href if the profile is disabled so that disabling pointer-events is not needed to prevent a click from opening a page + // and to allow the hover event for styling + return null }, }, @@ -95,6 +117,9 @@ export default { * and stores it in Vuex */ mounted() { + subscribe('settings:displayName:updated', this.handleDisplayNameUpdate) + subscribe('settings:profileEnabled:updated', this.handleProfileEnabledUpdate) + this.$store.dispatch('loadStatusFromInitialState') if (OC.config.session_keepalive) { @@ -130,11 +155,27 @@ export default { * Some housekeeping before destroying the component */ beforeDestroy() { + unsubscribe('settings:displayName:updated', this.handleDisplayNameUpdate) + unsubscribe('settings:profileEnabled:updated', this.handleProfileEnabledUpdate) window.removeEventListener('mouseMove', this.mouseMoveListener) clearInterval(this.heartbeatInterval) }, methods: { + handleDisplayNameUpdate(displayName) { + this.displayName = displayName + }, + + handleProfileEnabledUpdate(profileEnabled) { + this.profileEnabled = profileEnabled + }, + + loadProfilePage() { + if (this.profileEnabled) { + this.loadingProfilePage = true + } + }, + /** * Opens the modal to set a custom status */ @@ -171,20 +212,51 @@ export default { + + diff --git a/core/templates/404.php b/core/templates/404-file.php similarity index 100% rename from core/templates/404.php rename to core/templates/404-file.php diff --git a/core/templates/404-page.php b/core/templates/404-page.php new file mode 100644 index 0000000000000..7a8a7c04da073 --- /dev/null +++ b/core/templates/404-page.php @@ -0,0 +1,26 @@ +getURLGenerator(); + header('Location: ' . $urlGenerator->getAbsoluteURL('/')); + exit; +} +// @codeCoverageIgnoreEnd +?> + + + + + diff --git a/core/templates/profile.php b/core/templates/profile.php new file mode 100644 index 0000000000000..84d6eb0e38fd9 --- /dev/null +++ b/core/templates/profile.php @@ -0,0 +1,5 @@ +
    +
    diff --git a/core/webpack.js b/core/webpack.js index 9c501ede414d9..7cac738db538b 100644 --- a/core/webpack.js +++ b/core/webpack.js @@ -35,6 +35,7 @@ module.exports = [ install: path.join(__dirname, 'src/install.js'), login: path.join(__dirname, 'src/login.js'), main: path.join(__dirname, 'src/main.js'), + profile: path.join(__dirname, 'src/profile.js'), maintenance: path.join(__dirname, 'src/maintenance.js'), recommendedapps: path.join(__dirname, 'src/recommendedapps.js'), 'unified-search': path.join(__dirname, 'src/unified-search.js'), diff --git a/lib/private/Accounts/AccountManager.php b/lib/private/Accounts/AccountManager.php index 9fc5accfa08a4..cbdec1ff4e73e 100644 --- a/lib/private/Accounts/AccountManager.php +++ b/lib/private/Accounts/AccountManager.php @@ -574,6 +574,34 @@ protected function buildDefaultUserRecord(IUser $user) { 'verified' => self::NOT_VERIFIED, ], + [ + 'name' => self::PROPERTY_COMPANY, + 'value' => '', + 'scope' => self::SCOPE_LOCAL, + ], + + [ + 'name' => self::PROPERTY_JOB_TITLE, + 'value' => '', + 'scope' => self::SCOPE_LOCAL, + ], + + [ + 'name' => self::PROPERTY_HEADLINE, + 'value' => '', + 'scope' => self::SCOPE_LOCAL, + ], + + [ + 'name' => self::PROPERTY_BIOGRAPHY, + 'value' => '', + 'scope' => self::SCOPE_LOCAL, + ], + + [ + 'name' => self::PROPERTY_PROFILE_ENABLED, + 'value' => '1', + ], ]; } diff --git a/lib/public/Accounts/IAccountManager.php b/lib/public/Accounts/IAccountManager.php index 9418e07ec972f..54da24a64eb86 100644 --- a/lib/public/Accounts/IAccountManager.php +++ b/lib/public/Accounts/IAccountManager.php @@ -96,6 +96,12 @@ interface IAccountManager { public const PROPERTY_ADDRESS = 'address'; public const PROPERTY_TWITTER = 'twitter'; + public const PROPERTY_COMPANY = 'company'; + public const PROPERTY_JOB_TITLE = 'job_title'; + public const PROPERTY_HEADLINE = 'headline'; + public const PROPERTY_BIOGRAPHY = 'biography'; + public const PROPERTY_PROFILE_ENABLED = 'profile_enabled'; + public const COLLECTION_EMAIL = 'additional_mail'; public const NOT_VERIFIED = '0';