Skip to content

Commit 3923eab

Browse files
nickvergessenAndyScherzinger
authored andcommitted
feat(profile): Add an API to get the profile field data
Signed-off-by: Joas Schilling <coding@schilljs.com>
1 parent b41ba29 commit 3923eab

File tree

8 files changed

+622
-31
lines changed

8 files changed

+622
-31
lines changed

build/integration/features/bootstrap/Provisioning.php

Lines changed: 56 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
55
* SPDX-License-Identifier: AGPL-3.0-or-later
66
*/
7+
8+
use Behat\Gherkin\Node\TableNode;
79
use GuzzleHttp\Client;
810
use GuzzleHttp\Message\ResponseInterface;
911
use PHPUnit\Framework\Assert;
@@ -124,7 +126,7 @@ public function creatingTheUser($user, $displayname = '') {
124126
* @Then /^user "([^"]*)" has$/
125127
*
126128
* @param string $user
127-
* @param \Behat\Gherkin\Node\TableNode|null $settings
129+
* @param TableNode|null $settings
128130
*/
129131
public function userHasSetting($user, $settings) {
130132
$fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/users/$user";
@@ -145,12 +147,43 @@ public function userHasSetting($user, $settings) {
145147
if (isset($value['element']) && in_array($setting[0], ['additional_mail', 'additional_mailScope'], true)) {
146148
$expectedValues = explode(';', $setting[1]);
147149
foreach ($expectedValues as $expected) {
148-
Assert::assertTrue(in_array($expected, $value['element'], true));
150+
Assert::assertTrue(in_array($expected, $value['element'], true), 'Data wrong for field: ' . $setting[0]);
149151
}
150152
} elseif (isset($value[0])) {
151-
Assert::assertEqualsCanonicalizing($setting[1], $value[0]);
153+
Assert::assertEqualsCanonicalizing($setting[1], $value[0], 'Data wrong for field: ' . $setting[0]);
152154
} else {
153-
Assert::assertEquals('', $setting[1]);
155+
Assert::assertEquals('', $setting[1], 'Data wrong for field: ' . $setting[0]);
156+
}
157+
}
158+
}
159+
160+
/**
161+
* @Then /^user "([^"]*)" has the following profile data$/
162+
*/
163+
public function userHasProfileData(string $user, ?TableNode $settings): void {
164+
$fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/profile/$user";
165+
$client = new Client();
166+
$options = [];
167+
if ($this->currentUser === 'admin') {
168+
$options['auth'] = $this->adminUser;
169+
} else {
170+
$options['auth'] = [$this->currentUser, $this->regularUser];
171+
}
172+
$options['headers'] = [
173+
'OCS-APIREQUEST' => 'true',
174+
'Accept' => 'application/json',
175+
];
176+
177+
$response = $client->get($fullUrl, $options);
178+
$body = $response->getBody()->getContents();
179+
$data = json_decode($body, true);
180+
$data = $data['ocs']['data'];
181+
foreach ($settings->getRows() as $setting) {
182+
Assert::assertArrayHasKey($setting[0], $data, 'Profile data field missing: ' . $setting[0]);
183+
if ($setting[1] === 'NULL') {
184+
Assert::assertNull($data[$setting[0]], 'Profile data wrong for field: ' . $setting[0]);
185+
} else {
186+
Assert::assertEquals($setting[1], $data[$setting[0]], 'Profile data wrong for field: ' . $setting[0]);
154187
}
155188
}
156189
}
@@ -159,7 +192,7 @@ public function userHasSetting($user, $settings) {
159192
* @Then /^group "([^"]*)" has$/
160193
*
161194
* @param string $user
162-
* @param \Behat\Gherkin\Node\TableNode|null $settings
195+
* @param TableNode|null $settings
163196
*/
164197
public function groupHasSetting($group, $settings) {
165198
$fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/groups/details?search=$group";
@@ -191,7 +224,7 @@ public function groupHasSetting($group, $settings) {
191224
* @Then /^user "([^"]*)" has editable fields$/
192225
*
193226
* @param string $user
194-
* @param \Behat\Gherkin\Node\TableNode|null $fields
227+
* @param TableNode|null $fields
195228
*/
196229
public function userHasEditableFields($user, $fields) {
197230
$fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/user/fields";
@@ -221,9 +254,9 @@ public function userHasEditableFields($user, $fields) {
221254
* @Then /^search users by phone for region "([^"]*)" with$/
222255
*
223256
* @param string $user
224-
* @param \Behat\Gherkin\Node\TableNode|null $settings
257+
* @param TableNode|null $settings
225258
*/
226-
public function searchUserByPhone($region, \Behat\Gherkin\Node\TableNode $searchTable) {
259+
public function searchUserByPhone($region, TableNode $searchTable) {
227260
$fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/users/search/by-phone";
228261
$client = new Client();
229262
$options = [];
@@ -624,10 +657,10 @@ public function userIsNotSubadminOfGroup($user, $group) {
624657

625658
/**
626659
* @Then /^users returned are$/
627-
* @param \Behat\Gherkin\Node\TableNode|null $usersList
660+
* @param TableNode|null $usersList
628661
*/
629662
public function theUsersShouldBe($usersList) {
630-
if ($usersList instanceof \Behat\Gherkin\Node\TableNode) {
663+
if ($usersList instanceof TableNode) {
631664
$users = $usersList->getRows();
632665
$usersSimplified = $this->simplifyArray($users);
633666
$respondedArray = $this->getArrayOfUsersResponded($this->response);
@@ -637,10 +670,10 @@ public function theUsersShouldBe($usersList) {
637670

638671
/**
639672
* @Then /^phone matches returned are$/
640-
* @param \Behat\Gherkin\Node\TableNode|null $usersList
673+
* @param TableNode|null $usersList
641674
*/
642675
public function thePhoneUsersShouldBe($usersList) {
643-
if ($usersList instanceof \Behat\Gherkin\Node\TableNode) {
676+
if ($usersList instanceof TableNode) {
644677
$users = $usersList->getRowsHash();
645678
$listCheckedElements = simplexml_load_string($this->response->getBody())->data;
646679
$respondedArray = json_decode(json_encode($listCheckedElements), true);
@@ -650,10 +683,10 @@ public function thePhoneUsersShouldBe($usersList) {
650683

651684
/**
652685
* @Then /^detailed users returned are$/
653-
* @param \Behat\Gherkin\Node\TableNode|null $usersList
686+
* @param TableNode|null $usersList
654687
*/
655688
public function theDetailedUsersShouldBe($usersList) {
656-
if ($usersList instanceof \Behat\Gherkin\Node\TableNode) {
689+
if ($usersList instanceof TableNode) {
657690
$users = $usersList->getRows();
658691
$usersSimplified = $this->simplifyArray($users);
659692
$respondedArray = $this->getArrayOfDetailedUsersResponded($this->response);
@@ -664,10 +697,10 @@ public function theDetailedUsersShouldBe($usersList) {
664697

665698
/**
666699
* @Then /^groups returned are$/
667-
* @param \Behat\Gherkin\Node\TableNode|null $groupsList
700+
* @param TableNode|null $groupsList
668701
*/
669702
public function theGroupsShouldBe($groupsList) {
670-
if ($groupsList instanceof \Behat\Gherkin\Node\TableNode) {
703+
if ($groupsList instanceof TableNode) {
671704
$groups = $groupsList->getRows();
672705
$groupsSimplified = $this->simplifyArray($groups);
673706
$respondedArray = $this->getArrayOfGroupsResponded($this->response);
@@ -677,10 +710,10 @@ public function theGroupsShouldBe($groupsList) {
677710

678711
/**
679712
* @Then /^subadmin groups returned are$/
680-
* @param \Behat\Gherkin\Node\TableNode|null $groupsList
713+
* @param TableNode|null $groupsList
681714
*/
682715
public function theSubadminGroupsShouldBe($groupsList) {
683-
if ($groupsList instanceof \Behat\Gherkin\Node\TableNode) {
716+
if ($groupsList instanceof TableNode) {
684717
$groups = $groupsList->getRows();
685718
$groupsSimplified = $this->simplifyArray($groups);
686719
$respondedArray = $this->getArrayOfSubadminsResponded($this->response);
@@ -690,10 +723,10 @@ public function theSubadminGroupsShouldBe($groupsList) {
690723

691724
/**
692725
* @Then /^apps returned are$/
693-
* @param \Behat\Gherkin\Node\TableNode|null $appList
726+
* @param TableNode|null $appList
694727
*/
695728
public function theAppsShouldBe($appList) {
696-
if ($appList instanceof \Behat\Gherkin\Node\TableNode) {
729+
if ($appList instanceof TableNode) {
697730
$apps = $appList->getRows();
698731
$appsSimplified = $this->simplifyArray($apps);
699732
$respondedArray = $this->getArrayOfAppsResponded($this->response);
@@ -703,7 +736,7 @@ public function theAppsShouldBe($appList) {
703736

704737
/**
705738
* @Then /^subadmin users returned are$/
706-
* @param \Behat\Gherkin\Node\TableNode|null $groupsList
739+
* @param TableNode|null $groupsList
707740
*/
708741
public function theSubadminUsersShouldBe($groupsList) {
709742
$this->theSubadminGroupsShouldBe($groupsList);
@@ -882,7 +915,7 @@ public function userIsEnabled($user) {
882915
* @param string $quota
883916
*/
884917
public function userHasAQuotaOf($user, $quota) {
885-
$body = new \Behat\Gherkin\Node\TableNode([
918+
$body = new TableNode([
886919
0 => ['key', 'quota'],
887920
1 => ['value', $quota],
888921
]);
@@ -950,7 +983,7 @@ public function cleanupGroups() {
950983
/**
951984
* @Then /^user "([^"]*)" has not$/
952985
*/
953-
public function userHasNotSetting($user, \Behat\Gherkin\Node\TableNode $settings) {
986+
public function userHasNotSetting($user, TableNode $settings) {
954987
$fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/users/$user";
955988
$client = new Client();
956989
$options = [];

build/integration/features/provisioning-v1.feature

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,23 @@ Feature: provisioning
169169
| address | Foo Bar Town |
170170
| website | https://nextcloud.com |
171171
| twitter | Nextcloud |
172+
And sending "PUT" to "/cloud/users/brand-new-user" with
173+
| key | organisation |
174+
| value | Nextcloud GmbH |
175+
And sending "PUT" to "/cloud/users/brand-new-user" with
176+
| key | role |
177+
| value | Engineer |
178+
And the OCS status code should be "100"
179+
And the HTTP status code should be "200"
180+
Then user "brand-new-user" has the following profile data
181+
| userId | brand-new-user |
182+
| displayname | Brand New User |
183+
| organisation | Nextcloud GmbH |
184+
| role | Engineer |
185+
| address | Foo Bar Town |
186+
| timezone | UTC |
187+
| timezoneOffset | 0 |
188+
| pronouns | NULL |
172189

173190
Scenario: Edit a user account properties scopes
174191
Given user "brand-new-user" exists

core/Controller/ProfileApiController.php

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@
1010
namespace OC\Core\Controller;
1111

1212
use OC\Core\Db\ProfileConfigMapper;
13+
use OC\Core\ResponseDefinitions;
1314
use OC\Profile\ProfileManager;
1415
use OCP\AppFramework\Http;
1516
use OCP\AppFramework\Http\Attribute\ApiRoute;
17+
use OCP\AppFramework\Http\Attribute\BruteForceProtection;
1618
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
1719
use OCP\AppFramework\Http\Attribute\PasswordConfirmationRequired;
1820
use OCP\AppFramework\Http\Attribute\UserRateLimit;
@@ -21,17 +23,27 @@
2123
use OCP\AppFramework\OCS\OCSForbiddenException;
2224
use OCP\AppFramework\OCS\OCSNotFoundException;
2325
use OCP\AppFramework\OCSController;
26+
use OCP\AppFramework\Utility\ITimeFactory;
27+
use OCP\IConfig;
2428
use OCP\IRequest;
29+
use OCP\IUser;
2530
use OCP\IUserManager;
2631
use OCP\IUserSession;
32+
use OCP\Share\IManager;
2733

34+
/**
35+
* @psalm-import-type CoreProfileData from ResponseDefinitions
36+
*/
2837
class ProfileApiController extends OCSController {
2938
public function __construct(
3039
IRequest $request,
40+
private IConfig $config,
41+
private ITimeFactory $timeFactory,
3142
private ProfileConfigMapper $configMapper,
3243
private ProfileManager $profileManager,
3344
private IUserManager $userManager,
3445
private IUserSession $userSession,
46+
private IManager $shareManager,
3547
) {
3648
parent::__construct('core', $request);
3749
}
@@ -57,14 +69,13 @@ public function __construct(
5769
#[ApiRoute(verb: 'PUT', url: '/{targetUserId}', root: '/profile')]
5870
public function setVisibility(string $targetUserId, string $paramId, string $visibility): DataResponse {
5971
$requestingUser = $this->userSession->getUser();
60-
$targetUser = $this->userManager->get($targetUserId);
61-
62-
if (!$this->userManager->userExists($targetUserId)) {
63-
throw new OCSNotFoundException('Account does not exist');
72+
if ($requestingUser->getUID() !== $targetUserId) {
73+
throw new OCSForbiddenException('People can only edit their own visibility settings');
6474
}
6575

66-
if ($requestingUser !== $targetUser) {
67-
throw new OCSForbiddenException('People can only edit their own visibility settings');
76+
$targetUser = $this->userManager->get($targetUserId);
77+
if (!$targetUser instanceof IUser) {
78+
throw new OCSNotFoundException('Account does not exist');
6879
}
6980

7081
// Ensure that a profile config is created in the database
@@ -80,4 +91,55 @@ public function setVisibility(string $targetUserId, string $paramId, string $vis
8091

8192
return new DataResponse();
8293
}
94+
95+
/**
96+
* Get profile fields for another user
97+
*
98+
* @param string $targetUserId ID of the user
99+
* @return DataResponse<Http::STATUS_OK, CoreProfileData, array{}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_NOT_FOUND, null, array{}>
100+
*
101+
* 200: Profile data returned successfully
102+
* 400: Profile is disabled
103+
* 404: Account not found or disabled
104+
*/
105+
#[NoAdminRequired]
106+
#[ApiRoute(verb: 'GET', url: '/{targetUserId}', root: '/profile')]
107+
#[BruteForceProtection(action: 'user')]
108+
#[UserRateLimit(limit: 30, period: 120)]
109+
public function getProfileFields(string $targetUserId): DataResponse {
110+
$targetUser = $this->userManager->get($targetUserId);
111+
if (!$targetUser instanceof IUser) {
112+
$response = new DataResponse(null, Http::STATUS_NOT_FOUND);
113+
$response->throttle();
114+
return $response;
115+
}
116+
if (!$targetUser->isEnabled()) {
117+
return new DataResponse(null, Http::STATUS_NOT_FOUND);
118+
}
119+
120+
if (!$this->profileManager->isProfileEnabled($targetUser)) {
121+
return new DataResponse(null, Http::STATUS_BAD_REQUEST);
122+
}
123+
124+
$requestingUser = $this->userSession->getUser();
125+
if ($targetUser !== $requestingUser) {
126+
if (!$this->shareManager->currentUserCanEnumerateTargetUser($requestingUser, $targetUser)) {
127+
return new DataResponse(null, Http::STATUS_NOT_FOUND);
128+
}
129+
}
130+
131+
$profileFields = $this->profileManager->getProfileFields($targetUser, $requestingUser);
132+
133+
// Extend the profile information with timezone of the user
134+
$timezoneStringTarget = $this->config->getUserValue($targetUser->getUID(), 'core', 'timezone') ?: $this->config->getSystemValueString('default_timezone', 'UTC');
135+
try {
136+
$timezoneTarget = new \DateTimeZone($timezoneStringTarget);
137+
} catch (\Throwable) {
138+
$timezoneTarget = new \DateTimeZone('UTC');
139+
}
140+
$profileFields['timezone'] = $timezoneTarget->getName(); // E.g. Europe/Berlin
141+
$profileFields['timezoneOffset'] = $timezoneTarget->getOffset($this->timeFactory->now()); // In seconds E.g. 7200
142+
143+
return new DataResponse($profileFields);
144+
}
83145
}

core/ResponseDefinitions.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,31 @@
202202
* endedAt: ?int,
203203
* }
204204
*
205+
* @psalm-type CoreProfileAction = array{
206+
* id: string,
207+
* icon: string,
208+
* title: string,
209+
* target: ?string,
210+
* }
211+
*
212+
* @psalm-type CoreProfileFields = array{
213+
* userId: string,
214+
* address?: string|null,
215+
* biography?: string|null,
216+
* displayname?: string|null,
217+
* headline?: string|null,
218+
* isUserAvatarVisible?: bool,
219+
* organisation?: string|null,
220+
* pronouns?: string|null,
221+
* role?: string|null,
222+
* actions: list<CoreProfileAction>,
223+
* }
224+
*
225+
* @psalm-type CoreProfileData = CoreProfileFields&array{
226+
* timezone: string,
227+
* timezoneOffset: int,
228+
* }
229+
*
205230
*/
206231
class ResponseDefinitions {
207232
}

0 commit comments

Comments
 (0)