Skip to content

Commit f863290

Browse files
slapcatst3iny
andcommitted
feat(ldap): sync additional properties to profile and SAB
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 <[email protected]> Co-authored-by: Richard Steinmetz <[email protected]> Signed-off-by: Richard Steinmetz <[email protected]>
1 parent 57a7f09 commit f863290

108 files changed

Lines changed: 529 additions & 172 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@
183183
184184
- Jaakko Salo <[email protected]>
185185
- Jacob Neplokh <[email protected]>
186+
- Jake Nabasny <[email protected]>
186187
- Jakob Sack <[email protected]>
187188
- Jakub Onderka <[email protected]>
188189
- James Guo <[email protected]>

apps/dav/lib/CardDAV/Converter.php

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,17 @@
77
*/
88
namespace OCA\DAV\CardDAV;
99

10+
use DateTimeImmutable;
1011
use Exception;
1112
use OCP\Accounts\IAccountManager;
1213
use OCP\IImage;
1314
use OCP\IURLGenerator;
1415
use OCP\IUser;
1516
use OCP\IUserManager;
17+
use Psr\Log\LoggerInterface;
1618
use Sabre\VObject\Component\VCard;
1719
use Sabre\VObject\Property\Text;
20+
use Sabre\VObject\Property\VCard\Date;
1821

1922
class Converter {
2023
/** @var IURLGenerator */
@@ -23,8 +26,12 @@ class Converter {
2326
private $accountManager;
2427
private IUserManager $userManager;
2528

26-
public function __construct(IAccountManager $accountManager,
27-
IUserManager $userManager, IURLGenerator $urlGenerator) {
29+
public function __construct(
30+
IAccountManager $accountManager,
31+
IUserManager $userManager,
32+
IURLGenerator $urlGenerator,
33+
private LoggerInterface $logger,
34+
) {
2835
$this->accountManager = $accountManager;
2936
$this->userManager = $userManager;
3037
$this->urlGenerator = $urlGenerator;
@@ -114,6 +121,24 @@ public function createCardFromUser(IUser $user): ?VCard {
114121
case IAccountManager::PROPERTY_ROLE:
115122
$vCard->add(new Text($vCard, 'TITLE', $property->getValue(), ['X-NC-SCOPE' => $scope]));
116123
break;
124+
case IAccountManager::PROPERTY_BIOGRAPHY:
125+
$vCard->add(new Text($vCard, 'NOTE', $property->getValue(), ['X-NC-SCOPE' => $scope]));
126+
break;
127+
case IAccountManager::PROPERTY_BIRTHDATE:
128+
try {
129+
$birthdate = new DateTimeImmutable($property->getValue());
130+
} catch (Exception $e) {
131+
// Invalid date -> just skip the property
132+
$this->logger->info("Failed to parse user's birthdate for the SAB: " . $property->getValue(), [
133+
'exception' => $e,
134+
'userId' => $user->getUID(),
135+
]);
136+
break;
137+
}
138+
$dateProperty = new Date($vCard, 'BDAY', null, ['X-NC-SCOPE' => $scope]);
139+
$dateProperty->setDateTime($birthdate);
140+
$vCard->add($dateProperty);
141+
break;
117142
}
118143
}
119144

apps/dav/tests/unit/CardDAV/ConverterTest.php

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use OCP\IUser;
1919
use OCP\IUserManager;
2020
use PHPUnit\Framework\MockObject\MockObject;
21+
use Psr\Log\LoggerInterface;
2122
use Test\TestCase;
2223

2324
class ConverterTest extends TestCase {
@@ -30,12 +31,16 @@ class ConverterTest extends TestCase {
3031
/** @var IURLGenerator */
3132
private $urlGenerator;
3233

34+
/** @var LoggerInterface|\PHPUnit\Framework\MockObject\MockObject */
35+
private $logger;
36+
3337
protected function setUp(): void {
3438
parent::setUp();
3539

3640
$this->accountManager = $this->createMock(IAccountManager::class);
3741
$this->userManager = $this->createMock(IUserManager::class);
3842
$this->urlGenerator = $this->createMock(IURLGenerator::class);
43+
$this->logger = $this->createMock(LoggerInterface::class);
3944
}
4045

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

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

111-
$converter = new Converter($accountManager, $this->userManager, $this->urlGenerator);
116+
$converter = new Converter($accountManager, $this->userManager, $this->urlGenerator, $this->logger);
112117
$vCard = $converter->createCardFromUser($user);
113118

114119
$this->compareData(
@@ -196,7 +201,7 @@ public function providesNewUsers() {
196201
* @param $fullName
197202
*/
198203
public function testNameSplitter($expected, $fullName): void {
199-
$converter = new Converter($this->accountManager, $this->userManager, $this->urlGenerator);
204+
$converter = new Converter($this->accountManager, $this->userManager, $this->urlGenerator, $this->logger);
200205
$r = $converter->splitFullName($fullName);
201206
$r = implode(';', $r);
202207
$this->assertEquals($expected, $r);

apps/provisioning_api/lib/Controller/UsersController.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -905,6 +905,7 @@ public function editUser(string $userId, string $key, string $value): DataRespon
905905
$permittedFields[] = IAccountManager::PROPERTY_HEADLINE;
906906
$permittedFields[] = IAccountManager::PROPERTY_BIOGRAPHY;
907907
$permittedFields[] = IAccountManager::PROPERTY_PROFILE_ENABLED;
908+
$permittedFields[] = IAccountManager::PROPERTY_BIRTHDATE;
908909
$permittedFields[] = IAccountManager::PROPERTY_PHONE . self::SCOPE_SUFFIX;
909910
$permittedFields[] = IAccountManager::PROPERTY_ADDRESS . self::SCOPE_SUFFIX;
910911
$permittedFields[] = IAccountManager::PROPERTY_WEBSITE . self::SCOPE_SUFFIX;
@@ -915,6 +916,7 @@ public function editUser(string $userId, string $key, string $value): DataRespon
915916
$permittedFields[] = IAccountManager::PROPERTY_HEADLINE . self::SCOPE_SUFFIX;
916917
$permittedFields[] = IAccountManager::PROPERTY_BIOGRAPHY . self::SCOPE_SUFFIX;
917918
$permittedFields[] = IAccountManager::PROPERTY_PROFILE_ENABLED . self::SCOPE_SUFFIX;
919+
$permittedFields[] = IAccountManager::PROPERTY_BIRTHDATE . self::SCOPE_SUFFIX;
918920

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

@@ -1085,6 +1087,7 @@ public function editUser(string $userId, string $key, string $value): DataRespon
10851087
case IAccountManager::PROPERTY_ROLE:
10861088
case IAccountManager::PROPERTY_HEADLINE:
10871089
case IAccountManager::PROPERTY_BIOGRAPHY:
1090+
case IAccountManager::PROPERTY_BIRTHDATE:
10881091
$userAccount = $this->accountManager->getAccount($targetUser);
10891092
try {
10901093
$userProperty = $userAccount->getProperty($key);
@@ -1131,6 +1134,7 @@ public function editUser(string $userId, string $key, string $value): DataRespon
11311134
case IAccountManager::PROPERTY_HEADLINE . self::SCOPE_SUFFIX:
11321135
case IAccountManager::PROPERTY_BIOGRAPHY . self::SCOPE_SUFFIX:
11331136
case IAccountManager::PROPERTY_PROFILE_ENABLED . self::SCOPE_SUFFIX:
1137+
case IAccountManager::PROPERTY_BIRTHDATE . self::SCOPE_SUFFIX:
11341138
case IAccountManager::PROPERTY_AVATAR . self::SCOPE_SUFFIX:
11351139
$propertyName = substr($key, 0, strlen($key) - strlen(self::SCOPE_SUFFIX));
11361140
$userAccount = $this->accountManager->getAccount($targetUser);

apps/settings/lib/Controller/UsersController.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,8 @@ protected function canAdminChangeUserPasswords(): bool {
326326
* @param string|null $twitterScope
327327
* @param string|null $fediverse
328328
* @param string|null $fediverseScope
329+
* @param string|null $birthdate
330+
* @param string|null $birthdateScope
329331
*
330332
* @return DataResponse
331333
*/
@@ -343,7 +345,9 @@ public function setUserSettings(?string $avatarScope = null,
343345
?string $twitter = null,
344346
?string $twitterScope = null,
345347
?string $fediverse = null,
346-
?string $fediverseScope = null
348+
?string $fediverseScope = null,
349+
?string $birthdate = null,
350+
?string $birthdateScope = null,
347351
) {
348352
$user = $this->userSession->getUser();
349353
if (!$user instanceof IUser) {
@@ -383,6 +387,7 @@ public function setUserSettings(?string $avatarScope = null,
383387
IAccountManager::PROPERTY_PHONE => ['value' => $phone, 'scope' => $phoneScope],
384388
IAccountManager::PROPERTY_TWITTER => ['value' => $twitter, 'scope' => $twitterScope],
385389
IAccountManager::PROPERTY_FEDIVERSE => ['value' => $fediverse, 'scope' => $fediverseScope],
390+
IAccountManager::PROPERTY_BIRTHDATE => ['value' => $birthdate, 'scope' => $birthdateScope],
386391
];
387392
$allowUserToChangeDisplayName = $this->config->getSystemValueBool('allow_user_to_change_display_name', true);
388393
foreach ($updatable as $property => $data) {
@@ -424,6 +429,8 @@ public function setUserSettings(?string $avatarScope = null,
424429
'twitterScope' => $userAccount->getProperty(IAccountManager::PROPERTY_TWITTER)->getScope(),
425430
'fediverse' => $userAccount->getProperty(IAccountManager::PROPERTY_FEDIVERSE)->getValue(),
426431
'fediverseScope' => $userAccount->getProperty(IAccountManager::PROPERTY_FEDIVERSE)->getScope(),
432+
'birthdate' => $userAccount->getProperty(IAccountManager::PROPERTY_BIRTHDATE)->getValue(),
433+
'birthdateScope' => $userAccount->getProperty(IAccountManager::PROPERTY_BIRTHDATE)->getScope(),
427434
'message' => $this->l10n->t('Settings saved'),
428435
],
429436
],

apps/settings/lib/Settings/Personal/PersonalInfo.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ public function getForm(): TemplateResponse {
167167
'role' => $this->getProperty($account, IAccountManager::PROPERTY_ROLE),
168168
'headline' => $this->getProperty($account, IAccountManager::PROPERTY_HEADLINE),
169169
'biography' => $this->getProperty($account, IAccountManager::PROPERTY_BIOGRAPHY),
170+
'birthdate' => $this->getProperty($account, IAccountManager::PROPERTY_BIRTHDATE),
170171
];
171172

172173
$accountParameters = [
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
6+
<template>
7+
<section>
8+
<HeaderBar :scope="birthdate.scope"
9+
:input-id="inputId"
10+
:readable="birthdate.readable" />
11+
12+
<template>
13+
<NcDateTimePickerNative :id="inputId"
14+
type="date"
15+
label=""
16+
:value="value"
17+
@input="onInput" />
18+
</template>
19+
20+
<p class="property__helper-text-message">
21+
{{ t('settings', 'Enter your date of birth') }}
22+
</p>
23+
</section>
24+
</template>
25+
26+
<script>
27+
import HeaderBar from './shared/HeaderBar.vue'
28+
import AccountPropertySection from './shared/AccountPropertySection.vue'
29+
import { NAME_READABLE_ENUM } from '../../constants/AccountPropertyConstants.js'
30+
import { NcDateTimePickerNative } from '@nextcloud/vue'
31+
import debounce from 'debounce'
32+
import { savePrimaryAccountProperty } from '../../service/PersonalInfo/PersonalInfoService'
33+
import { handleError } from '../../utils/handlers'
34+
import AlertCircle from 'vue-material-design-icons/AlertCircleOutline.vue'
35+
import { loadState } from '@nextcloud/initial-state'
36+
37+
const { birthdate } = loadState('settings', 'personalInfoParameters', {})
38+
39+
export default {
40+
name: 'BirthdaySection',
41+
42+
components: {
43+
AlertCircle,
44+
AccountPropertySection,
45+
NcDateTimePickerNative,
46+
HeaderBar,
47+
},
48+
49+
data() {
50+
let initialValue = null
51+
if (birthdate.value) {
52+
initialValue = new Date(birthdate.value)
53+
}
54+
55+
return {
56+
birthdate: {
57+
...birthdate,
58+
readable: NAME_READABLE_ENUM[birthdate.name],
59+
},
60+
initialValue,
61+
}
62+
},
63+
64+
computed: {
65+
inputId() {
66+
return `account-property-${birthdate.name}`
67+
},
68+
value: {
69+
get() {
70+
return new Date(this.birthdate.value)
71+
},
72+
/** @param {Date} value */
73+
set(value) {
74+
const day = value.getDate().toString().padStart(2, '0')
75+
const month = (value.getMonth() + 1).toString().padStart(2, '0')
76+
const year = value.getFullYear()
77+
this.birthdate.value = `${year}-${month}-${day}`
78+
}
79+
},
80+
},
81+
82+
methods: {
83+
onInput(e) {
84+
this.value = e
85+
this.debouncePropertyChange(this.value)
86+
},
87+
88+
debouncePropertyChange: debounce(async function(value) {
89+
await this.updateProperty(value)
90+
}, 500),
91+
92+
async updateProperty(value) {
93+
try {
94+
const responseData = await savePrimaryAccountProperty(
95+
this.birthdate.name,
96+
value,
97+
)
98+
this.handleResponse({
99+
value,
100+
status: responseData.ocs?.meta?.status,
101+
})
102+
} catch (error) {
103+
this.handleResponse({
104+
errorMessage: t('settings', 'Unable to update date of birth'),
105+
error,
106+
})
107+
}
108+
},
109+
110+
handleResponse({ value, status, errorMessage, error }) {
111+
if (status === 'ok') {
112+
this.initialValue = value
113+
} else {
114+
this.$emit('update:value', this.initialValue)
115+
handleError(error, errorMessage)
116+
}
117+
},
118+
},
119+
}
120+
</script>
121+
122+
<style lang="scss" scoped>
123+
section {
124+
padding: 10px 10px;
125+
126+
&::v-deep button:disabled {
127+
cursor: default;
128+
}
129+
130+
.property__helper-text-message {
131+
color: var(--color-text-maxcontrast);
132+
padding: 4px 0;
133+
display: flex;
134+
align-items: center;
135+
}
136+
}
137+
</style>

apps/settings/src/constants/AccountPropertyConstants.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export const ACCOUNT_PROPERTY_ENUM = Object.freeze({
4444
ROLE: 'role',
4545
TWITTER: 'twitter',
4646
WEBSITE: 'website',
47+
BIRTHDATE: 'birthdate',
4748
})
4849

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

6769
export const NAME_READABLE_ENUM = Object.freeze({
@@ -79,6 +81,7 @@ export const NAME_READABLE_ENUM = Object.freeze({
7981
[ACCOUNT_PROPERTY_ENUM.TWITTER]: ACCOUNT_PROPERTY_READABLE_ENUM.TWITTER,
8082
[ACCOUNT_PROPERTY_ENUM.FEDIVERSE]: ACCOUNT_PROPERTY_READABLE_ENUM.FEDIVERSE,
8183
[ACCOUNT_PROPERTY_ENUM.WEBSITE]: ACCOUNT_PROPERTY_READABLE_ENUM.WEBSITE,
84+
[ACCOUNT_PROPERTY_ENUM.BIRTHDATE]: ACCOUNT_PROPERTY_READABLE_ENUM.BIRTHDATE,
8285
})
8386

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

107111
/**
@@ -144,6 +148,7 @@ export const PROPERTY_READABLE_SUPPORTED_SCOPES_ENUM = Object.freeze({
144148
[ACCOUNT_PROPERTY_READABLE_ENUM.TWITTER]: [SCOPE_ENUM.LOCAL, SCOPE_ENUM.PRIVATE],
145149
[ACCOUNT_PROPERTY_READABLE_ENUM.FEDIVERSE]: [SCOPE_ENUM.LOCAL, SCOPE_ENUM.PRIVATE],
146150
[ACCOUNT_PROPERTY_READABLE_ENUM.WEBSITE]: [SCOPE_ENUM.LOCAL, SCOPE_ENUM.PRIVATE],
151+
[ACCOUNT_PROPERTY_READABLE_ENUM.BIRTHDATE]: [SCOPE_ENUM.LOCAL, SCOPE_ENUM.PRIVATE],
147152
})
148153

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

157163
/** Scope suffix */

0 commit comments

Comments
 (0)