Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ldap): sync additional properties to profile and SAB #45512

Merged
merged 1 commit into from
May 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@
- J0WI <[email protected]>
- Jaakko Salo <[email protected]>
- Jacob Neplokh <[email protected]>
- Jake Nabasny <[email protected]>
- Jakob Sack <[email protected]>
- Jakub Onderka <[email protected]>
- James Guo <[email protected]>
Expand Down
29 changes: 27 additions & 2 deletions apps/dav/lib/CardDAV/Converter.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,17 @@
*/
namespace OCA\DAV\CardDAV;

use DateTimeImmutable;
use Exception;
use OCP\Accounts\IAccountManager;
use OCP\IImage;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\IUserManager;
use Psr\Log\LoggerInterface;
use Sabre\VObject\Component\VCard;
use Sabre\VObject\Property\Text;
use Sabre\VObject\Property\VCard\Date;

class Converter {
/** @var IURLGenerator */
Expand All @@ -23,8 +26,12 @@ class Converter {
private $accountManager;
private IUserManager $userManager;

public function __construct(IAccountManager $accountManager,
IUserManager $userManager, IURLGenerator $urlGenerator) {
public function __construct(
IAccountManager $accountManager,
IUserManager $userManager,
IURLGenerator $urlGenerator,
private LoggerInterface $logger,
) {
$this->accountManager = $accountManager;
$this->userManager = $userManager;
$this->urlGenerator = $urlGenerator;
Expand Down Expand Up @@ -114,6 +121,24 @@ public function createCardFromUser(IUser $user): ?VCard {
case IAccountManager::PROPERTY_ROLE:
$vCard->add(new Text($vCard, 'TITLE', $property->getValue(), ['X-NC-SCOPE' => $scope]));
break;
case IAccountManager::PROPERTY_BIOGRAPHY:
$vCard->add(new Text($vCard, 'NOTE', $property->getValue(), ['X-NC-SCOPE' => $scope]));
break;
case IAccountManager::PROPERTY_BIRTHDATE:
try {
$birthdate = new DateTimeImmutable($property->getValue());
} catch (Exception $e) {
// Invalid date -> just skip the property
$this->logger->info("Failed to parse user's birthdate for the SAB: " . $property->getValue(), [
'exception' => $e,
'userId' => $user->getUID(),
]);
break;
}
$dateProperty = new Date($vCard, 'BDAY', null, ['X-NC-SCOPE' => $scope]);
$dateProperty->setDateTime($birthdate);
$vCard->add($dateProperty);
break;
}
}

Expand Down
11 changes: 8 additions & 3 deletions apps/dav/tests/unit/CardDAV/ConverterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use OCP\IUser;
use OCP\IUserManager;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
use Test\TestCase;

class ConverterTest extends TestCase {
Expand All @@ -30,12 +31,16 @@ class ConverterTest extends TestCase {
/** @var IURLGenerator */
private $urlGenerator;

/** @var LoggerInterface|\PHPUnit\Framework\MockObject\MockObject */
private $logger;

protected function setUp(): void {
parent::setUp();

$this->accountManager = $this->createMock(IAccountManager::class);
$this->userManager = $this->createMock(IUserManager::class);
$this->urlGenerator = $this->createMock(IURLGenerator::class);
$this->logger = $this->createMock(LoggerInterface::class);
}

/**
Expand Down Expand Up @@ -87,7 +92,7 @@ public function testCreation($expectedVCard, $displayName = null, $eMailAddress
$user = $this->getUserMock((string)$displayName, $eMailAddress, $cloudId);
$accountManager = $this->getAccountManager($user);

$converter = new Converter($accountManager, $this->userManager, $this->urlGenerator);
$converter = new Converter($accountManager, $this->userManager, $this->urlGenerator, $this->logger);
$vCard = $converter->createCardFromUser($user);
if ($expectedVCard !== null) {
$this->assertInstanceOf('Sabre\VObject\Component\VCard', $vCard);
Expand All @@ -108,7 +113,7 @@ public function testManagerProp(): void {
->willReturn('Manager');
$accountManager = $this->getAccountManager($user);

$converter = new Converter($accountManager, $this->userManager, $this->urlGenerator);
$converter = new Converter($accountManager, $this->userManager, $this->urlGenerator, $this->logger);
$vCard = $converter->createCardFromUser($user);

$this->compareData(
Expand Down Expand Up @@ -196,7 +201,7 @@ public function providesNewUsers() {
* @param $fullName
*/
public function testNameSplitter($expected, $fullName): void {
$converter = new Converter($this->accountManager, $this->userManager, $this->urlGenerator);
$converter = new Converter($this->accountManager, $this->userManager, $this->urlGenerator, $this->logger);
$r = $converter->splitFullName($fullName);
$r = implode(';', $r);
$this->assertEquals($expected, $r);
Expand Down
4 changes: 4 additions & 0 deletions apps/provisioning_api/lib/Controller/UsersController.php
Original file line number Diff line number Diff line change
Expand Up @@ -905,6 +905,7 @@ public function editUser(string $userId, string $key, string $value): DataRespon
$permittedFields[] = IAccountManager::PROPERTY_HEADLINE;
$permittedFields[] = IAccountManager::PROPERTY_BIOGRAPHY;
$permittedFields[] = IAccountManager::PROPERTY_PROFILE_ENABLED;
$permittedFields[] = IAccountManager::PROPERTY_BIRTHDATE;
$permittedFields[] = IAccountManager::PROPERTY_PHONE . self::SCOPE_SUFFIX;
$permittedFields[] = IAccountManager::PROPERTY_ADDRESS . self::SCOPE_SUFFIX;
$permittedFields[] = IAccountManager::PROPERTY_WEBSITE . self::SCOPE_SUFFIX;
Expand All @@ -915,6 +916,7 @@ public function editUser(string $userId, string $key, string $value): DataRespon
$permittedFields[] = IAccountManager::PROPERTY_HEADLINE . self::SCOPE_SUFFIX;
$permittedFields[] = IAccountManager::PROPERTY_BIOGRAPHY . self::SCOPE_SUFFIX;
$permittedFields[] = IAccountManager::PROPERTY_PROFILE_ENABLED . self::SCOPE_SUFFIX;
$permittedFields[] = IAccountManager::PROPERTY_BIRTHDATE . self::SCOPE_SUFFIX;

$permittedFields[] = IAccountManager::PROPERTY_AVATAR . self::SCOPE_SUFFIX;

Expand Down Expand Up @@ -1085,6 +1087,7 @@ public function editUser(string $userId, string $key, string $value): DataRespon
case IAccountManager::PROPERTY_ROLE:
case IAccountManager::PROPERTY_HEADLINE:
case IAccountManager::PROPERTY_BIOGRAPHY:
case IAccountManager::PROPERTY_BIRTHDATE:
$userAccount = $this->accountManager->getAccount($targetUser);
try {
$userProperty = $userAccount->getProperty($key);
Expand Down Expand Up @@ -1131,6 +1134,7 @@ public function editUser(string $userId, string $key, string $value): DataRespon
case IAccountManager::PROPERTY_HEADLINE . self::SCOPE_SUFFIX:
case IAccountManager::PROPERTY_BIOGRAPHY . self::SCOPE_SUFFIX:
case IAccountManager::PROPERTY_PROFILE_ENABLED . self::SCOPE_SUFFIX:
case IAccountManager::PROPERTY_BIRTHDATE . self::SCOPE_SUFFIX:
case IAccountManager::PROPERTY_AVATAR . self::SCOPE_SUFFIX:
$propertyName = substr($key, 0, strlen($key) - strlen(self::SCOPE_SUFFIX));
$userAccount = $this->accountManager->getAccount($targetUser);
Expand Down
9 changes: 8 additions & 1 deletion apps/settings/lib/Controller/UsersController.php
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,8 @@ protected function canAdminChangeUserPasswords(): bool {
* @param string|null $twitterScope
* @param string|null $fediverse
* @param string|null $fediverseScope
* @param string|null $birthdate
* @param string|null $birthdateScope
*
* @return DataResponse
*/
Expand All @@ -343,7 +345,9 @@ public function setUserSettings(?string $avatarScope = null,
?string $twitter = null,
?string $twitterScope = null,
?string $fediverse = null,
?string $fediverseScope = null
?string $fediverseScope = null,
?string $birthdate = null,
?string $birthdateScope = null,
) {
$user = $this->userSession->getUser();
if (!$user instanceof IUser) {
Expand Down Expand Up @@ -383,6 +387,7 @@ public function setUserSettings(?string $avatarScope = null,
IAccountManager::PROPERTY_PHONE => ['value' => $phone, 'scope' => $phoneScope],
IAccountManager::PROPERTY_TWITTER => ['value' => $twitter, 'scope' => $twitterScope],
IAccountManager::PROPERTY_FEDIVERSE => ['value' => $fediverse, 'scope' => $fediverseScope],
IAccountManager::PROPERTY_BIRTHDATE => ['value' => $birthdate, 'scope' => $birthdateScope],
];
$allowUserToChangeDisplayName = $this->config->getSystemValueBool('allow_user_to_change_display_name', true);
foreach ($updatable as $property => $data) {
Expand Down Expand Up @@ -424,6 +429,8 @@ public function setUserSettings(?string $avatarScope = null,
'twitterScope' => $userAccount->getProperty(IAccountManager::PROPERTY_TWITTER)->getScope(),
'fediverse' => $userAccount->getProperty(IAccountManager::PROPERTY_FEDIVERSE)->getValue(),
'fediverseScope' => $userAccount->getProperty(IAccountManager::PROPERTY_FEDIVERSE)->getScope(),
'birthdate' => $userAccount->getProperty(IAccountManager::PROPERTY_BIRTHDATE)->getValue(),
'birthdateScope' => $userAccount->getProperty(IAccountManager::PROPERTY_BIRTHDATE)->getScope(),
'message' => $this->l10n->t('Settings saved'),
],
],
Expand Down
1 change: 1 addition & 0 deletions apps/settings/lib/Settings/Personal/PersonalInfo.php
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ public function getForm(): TemplateResponse {
'role' => $this->getProperty($account, IAccountManager::PROPERTY_ROLE),
'headline' => $this->getProperty($account, IAccountManager::PROPERTY_HEADLINE),
'biography' => $this->getProperty($account, IAccountManager::PROPERTY_BIOGRAPHY),
'birthdate' => $this->getProperty($account, IAccountManager::PROPERTY_BIRTHDATE),
];

$accountParameters = [
Expand Down
137 changes: 137 additions & 0 deletions apps/settings/src/components/PersonalInfo/BirthdaySection.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<!--
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->

<template>
<section>
<HeaderBar :scope="birthdate.scope"
:input-id="inputId"
:readable="birthdate.readable" />

<template>
<NcDateTimePickerNative :id="inputId"
type="date"
label=""
:value="value"
@input="onInput" />
</template>

<p class="property__helper-text-message">
{{ t('settings', 'Enter your date of birth') }}
</p>
</section>
</template>

<script>
import HeaderBar from './shared/HeaderBar.vue'
import AccountPropertySection from './shared/AccountPropertySection.vue'
import { NAME_READABLE_ENUM } from '../../constants/AccountPropertyConstants.js'
import { NcDateTimePickerNative } from '@nextcloud/vue'
import debounce from 'debounce'
import { savePrimaryAccountProperty } from '../../service/PersonalInfo/PersonalInfoService'
import { handleError } from '../../utils/handlers'
import AlertCircle from 'vue-material-design-icons/AlertCircleOutline.vue'
import { loadState } from '@nextcloud/initial-state'
const { birthdate } = loadState('settings', 'personalInfoParameters', {})
export default {
name: 'BirthdaySection',
components: {
AlertCircle,
AccountPropertySection,
NcDateTimePickerNative,
HeaderBar,
},
data() {
let initialValue = null
if (birthdate.value) {
initialValue = new Date(birthdate.value)
}
return {
birthdate: {
...birthdate,
readable: NAME_READABLE_ENUM[birthdate.name],
},
initialValue,
}
},
computed: {
inputId() {
return `account-property-${birthdate.name}`
},
value: {
get() {
return new Date(this.birthdate.value)
},
/** @param {Date} value */
set(value) {
const day = value.getDate().toString().padStart(2, '0')
const month = (value.getMonth() + 1).toString().padStart(2, '0')
const year = value.getFullYear()
this.birthdate.value = `${year}-${month}-${day}`
}
},
},
methods: {
onInput(e) {
this.value = e
this.debouncePropertyChange(this.value)
},
debouncePropertyChange: debounce(async function(value) {
await this.updateProperty(value)
}, 500),
async updateProperty(value) {
try {
const responseData = await savePrimaryAccountProperty(
this.birthdate.name,
value,
)
this.handleResponse({
value,
status: responseData.ocs?.meta?.status,
})
} catch (error) {
this.handleResponse({
errorMessage: t('settings', 'Unable to update date of birth'),
error,
})
}
},
handleResponse({ value, status, errorMessage, error }) {
if (status === 'ok') {
this.initialValue = value
} else {
this.$emit('update:value', this.initialValue)
handleError(error, errorMessage)
}
},
},
}
</script>
<style lang="scss" scoped>
section {
padding: 10px 10px;
&::v-deep button:disabled {
cursor: default;
}
.property__helper-text-message {
color: var(--color-text-maxcontrast);
padding: 4px 0;
display: flex;
align-items: center;
}
}
</style>
6 changes: 6 additions & 0 deletions apps/settings/src/constants/AccountPropertyConstants.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export const ACCOUNT_PROPERTY_ENUM = Object.freeze({
ROLE: 'role',
TWITTER: 'twitter',
WEBSITE: 'website',
BIRTHDATE: 'birthdate',
})

/** Enum of account properties to human readable account property names */
Expand All @@ -62,6 +63,7 @@ export const ACCOUNT_PROPERTY_READABLE_ENUM = Object.freeze({
TWITTER: t('settings', 'X (formerly Twitter)'),
FEDIVERSE: t('settings', 'Fediverse (e.g. Mastodon)'),
WEBSITE: t('settings', 'Website'),
BIRTHDATE: t('settings', 'Date of birth'),
})

export const NAME_READABLE_ENUM = Object.freeze({
Expand All @@ -79,6 +81,7 @@ export const NAME_READABLE_ENUM = Object.freeze({
[ACCOUNT_PROPERTY_ENUM.TWITTER]: ACCOUNT_PROPERTY_READABLE_ENUM.TWITTER,
[ACCOUNT_PROPERTY_ENUM.FEDIVERSE]: ACCOUNT_PROPERTY_READABLE_ENUM.FEDIVERSE,
[ACCOUNT_PROPERTY_ENUM.WEBSITE]: ACCOUNT_PROPERTY_READABLE_ENUM.WEBSITE,
[ACCOUNT_PROPERTY_ENUM.BIRTHDATE]: ACCOUNT_PROPERTY_READABLE_ENUM.BIRTHDATE,
})

/** Enum of profile specific sections to human readable names */
Expand All @@ -102,6 +105,7 @@ export const PROPERTY_READABLE_KEYS_ENUM = Object.freeze({
[ACCOUNT_PROPERTY_READABLE_ENUM.TWITTER]: ACCOUNT_PROPERTY_ENUM.TWITTER,
[ACCOUNT_PROPERTY_READABLE_ENUM.FEDIVERSE]: ACCOUNT_PROPERTY_ENUM.FEDIVERSE,
[ACCOUNT_PROPERTY_READABLE_ENUM.WEBSITE]: ACCOUNT_PROPERTY_ENUM.WEBSITE,
[ACCOUNT_PROPERTY_READABLE_ENUM.BIRTHDATE]: ACCOUNT_PROPERTY_ENUM.BIRTHDATE,
})

/**
Expand Down Expand Up @@ -144,6 +148,7 @@ export const PROPERTY_READABLE_SUPPORTED_SCOPES_ENUM = Object.freeze({
[ACCOUNT_PROPERTY_READABLE_ENUM.TWITTER]: [SCOPE_ENUM.LOCAL, SCOPE_ENUM.PRIVATE],
[ACCOUNT_PROPERTY_READABLE_ENUM.FEDIVERSE]: [SCOPE_ENUM.LOCAL, SCOPE_ENUM.PRIVATE],
[ACCOUNT_PROPERTY_READABLE_ENUM.WEBSITE]: [SCOPE_ENUM.LOCAL, SCOPE_ENUM.PRIVATE],
[ACCOUNT_PROPERTY_READABLE_ENUM.BIRTHDATE]: [SCOPE_ENUM.LOCAL, SCOPE_ENUM.PRIVATE],
})

/** List of readable account properties which aren't published to the lookup server */
Expand All @@ -152,6 +157,7 @@ export const UNPUBLISHED_READABLE_PROPERTIES = Object.freeze([
ACCOUNT_PROPERTY_READABLE_ENUM.HEADLINE,
ACCOUNT_PROPERTY_READABLE_ENUM.ORGANISATION,
ACCOUNT_PROPERTY_READABLE_ENUM.ROLE,
ACCOUNT_PROPERTY_READABLE_ENUM.BIRTHDATE,
])

/** Scope suffix */
Expand Down
Loading
Loading