Skip to content

Commit

Permalink
feat(ldap): sync additional properties to profile and SAB
Browse files Browse the repository at this point in the history
Synced from LDAP to profile:
- Date of birth

Synced from LDAP to SAB (via the profile):
- Biography
- Date of birth

Original code by Jake Nabasny (GitHub: @slapcat)

Co-authored-by: Jake Nabasny <jake@nabasny.com>
Co-authored-by: Richard Steinmetz <richard@steinmetz.cloud>
Signed-off-by: Richard Steinmetz <richard@steinmetz.cloud>
  • Loading branch information
slapcat and st3iny committed May 28, 2024
1 parent 0eb74d7 commit e1b5855
Show file tree
Hide file tree
Showing 108 changed files with 531 additions and 172 deletions.
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@
- J0WI <J0WI@users.noreply.github.com>
- Jaakko Salo <jaakkos@gmail.com>
- Jacob Neplokh <me@jacobneplokh.com>
- Jake Nabasny <jake@nabasny.com>
- Jakob Sack <mail@jakobsack.de>
- Jakub Onderka <ahoj@jakubonderka.cz>
- James Guo <i@ze3kr.com>
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 @@ -39,6 +39,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 @@ -51,12 +52,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 @@ -108,7 +113,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 @@ -129,7 +134,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 @@ -217,7 +222,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
141 changes: 141 additions & 0 deletions apps/settings/src/components/PersonalInfo/BirthdaySection.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
<!--
- 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: initialValue ? new Date(initialValue) : new Date(),
}
},
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
this.showCheckmarkIcon = true
setTimeout(() => { this.showCheckmarkIcon = false }, 2000)
} else {
this.$emit('update:value', this.initialValue)
handleError(error, errorMessage)
this.showErrorIcon = true
setTimeout(() => { this.showErrorIcon = false }, 2000)
}
},
},
}
</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

0 comments on commit e1b5855

Please sign in to comment.